# Copyright (c) 2021 CloudZero - ALL RIGHTS RESERVED - PROPRIETARY AND CONFIDENTIAL
# Unauthorized copying of this file and/or project, via any medium is strictly prohibited.
# Direct all questions to legal@cloudzero.com
import dataclasses
from dataclasses import dataclass
from datetime import date, datetime
from decimal import Decimal
from enum import Enum

import freezegun
import hypothesis.strategies as st
import pytest
from hypothesis import given, settings
from hypothesis.strategies import from_type
from voluptuous import All, Any, Coerce, Length, MultipleInvalid, Optional, Required, Schema, SetTo

from czc import type_util
from czc.type_util import serializable
from czt.unittest import maybe_expect_exception


class SizeType(str, Enum):
    small = 'small'
    big = 'big'
    enormous = 'enormous'

    def __str__(self):
        return self.value


@type_util.deserializable()
@dataclass
class FloatTypes:
    float_value: float
    decimal_value: Decimal


@type_util.deserializable()
@dataclass
class SimpleTypes:
    integer_value: int
    string_value: str
    enum_value: SizeType
    datetime_value: datetime
    date_value: date
    string_field: str = dataclasses.field(init=True)


@type_util.deserializable(
    schema=Schema(
        {
            Required(All(Any('i', 'x'), SetTo('i'))): int,
            Required('ls'): All([str], Length(min=1, max=3)),
            Optional('s'): All(str, lambda x: x.replace('fizz', 'buzz')),
        }
    )
)
@dataclass
class WithSchema:
    i: int
    ls: list[str]
    s: str = 'DEFAULT'


@type_util.deserializable()
@dataclass
class NestedWithSchema:
    x: WithSchema


@type_util.deserializable()
@dataclass
class NestedTypes:
    simple_types: SimpleTypes
    dict_str: dict[str, str]
    dict_int: dict[int, int]
    dict_enum: dict[SizeType, str]
    dict_struct: dict[str, SimpleTypes]
    list_str: list[str]
    list_int: list[int]
    list_enum: list[SizeType]
    list_struct: list[SimpleTypes]
    set_str: set[str]
    set_int: set[int]
    set_enum: set[SizeType]


@type_util.deserializable()
@dataclass
class ComplexTypes:
    simple_types: SimpleTypes
    nested_types: NestedTypes
    complex_dict: dict[SizeType, NestedTypes]


@type_util.deserializable()
@dataclass
class SimpleTypesWithDefaults:
    integer_value: int = 999
    string_value: str = 'A String Value'
    enum_value: SizeType = SizeType.big
    datetime_value: datetime = datetime(2021, 10, 11, 1, 2, 3)
    date_value: date = date(2021, 10, 11)
    string_value_from_field: str = dataclasses.field(default='A Default Field')
    string_value_from_factory: str = dataclasses.field(default_factory=lambda: 'A Runtime Value')
    some_dictionary_mutable_default: dict[str, str] = dataclasses.field(default_factory=dict)


@st.composite
def float_type_strategy(draw):
    return FloatTypes(
        float_value=draw(st.floats(allow_nan=False, allow_infinity=False)),
        decimal_value=draw(st.decimals(allow_nan=False, allow_infinity=False)),
    )


@pytest.mark.unit
@settings(max_examples=100)
@given(data=float_type_strategy())
def test_deserializable_supports_dataclass_with_float_types(data: FloatTypes):
    result = Coerce(FloatTypes)(serializable(data))

    def assert_equal(getter):
        assert getter(data) == pytest.approx(getter(result), nan_ok=True)

    assert_equal(lambda x: x.float_value)
    assert_equal(lambda x: x.decimal_value)


@pytest.mark.unit
@settings(max_examples=100)
@given(data=from_type(SimpleTypes))
def test_deserializable_supports_dataclass_with_simple_types(data: SimpleTypes):
    result = Coerce(SimpleTypes)(serializable(data))
    assert result == data


EXPLICIT_SCHEMA_EXAMPLES = [
    (dict(i=1, ls=['a'], s='x'), WithSchema(i=1, ls=['a'], s='x')),
    (dict(i=1, ls=['a']), WithSchema(i=1, ls=['a'], s='DEFAULT')),
    (dict(x=99, ls=['a', 'b', 'c']), WithSchema(i=99, ls=['a', 'b', 'c'], s='DEFAULT')),
    (dict(i=42, ls=['a'], s='fizz'), WithSchema(i=42, ls=['a'], s='buzz')),
    (dict(i=99, ls=[]), None),
    (dict(i=99, ls=['a', 'b', 'c', 'd']), None),
    (dict(ls=['a'], s='foo'), None),
]


@pytest.mark.unit
@pytest.mark.parametrize('data,expected_result', EXPLICIT_SCHEMA_EXAMPLES)
def test_deserializable_supports_explicit_schema(data, expected_result):
    def run_test():
        result = Coerce(WithSchema)(data)
        assert result == expected_result

    maybe_expect_exception(run_test, expected_result is None, expected_exception=MultipleInvalid)


@pytest.mark.unit
@pytest.mark.parametrize('data,expected_result', EXPLICIT_SCHEMA_EXAMPLES)
def test_deserializable_supports_nested_explicit_schema(data, expected_result):
    def run_test():
        result = Coerce(NestedWithSchema)(dict(x=data))
        assert result.x == expected_result

    maybe_expect_exception(run_test, expected_result is None, expected_exception=MultipleInvalid)


@pytest.mark.unit
@settings(max_examples=100)
@given(data=from_type(SimpleTypes))
def test_deserializable_dataclass_can_be_created_with_normal_args(data: SimpleTypes):
    result = SimpleTypes(
        integer_value=data.integer_value,
        string_value=data.string_value,
        enum_value=data.enum_value,
        datetime_value=data.datetime_value,
        date_value=data.date_value,
        string_field=data.string_field,
    )
    assert result == data


@st.composite
def simple_type_optional_keys_strategy(draw):
    fields = dataclasses.fields(SimpleTypesWithDefaults)
    keys = draw(st.sets(st.sampled_from(fields), min_size=0, max_size=len(fields)))

    val = draw(from_type(SimpleTypesWithDefaults))
    return {k: v for k, v in dataclasses.asdict(val).items() if k in keys}


@pytest.mark.unit
@settings(max_examples=100)
@given(data=simple_type_optional_keys_strategy())
def test_deserializable_supports_dataclass_with_optional_values(data: dict):
    result = Coerce(SimpleTypesWithDefaults)(serializable(data))

    defaults = dataclasses.asdict(SimpleTypesWithDefaults())
    expected_values = {**defaults, **data}
    assert result == SimpleTypesWithDefaults(**expected_values)


@pytest.mark.unit
@settings(max_examples=50)
@given(data=from_type(NestedTypes))
def test_deserializable_supports_dataclass_with_nested_types(data: NestedTypes):
    result = Coerce(NestedTypes)(serializable(data))
    assert result == data


@pytest.mark.unit
@settings(max_examples=30)
@given(data=from_type(ComplexTypes))
def test_deserializable_supports_dataclass_with_deep_nested_types(data: ComplexTypes):
    result = Coerce(ComplexTypes)(serializable(data))
    assert result == data


@pytest.mark.unit
@settings(max_examples=10)
@given(data=from_type(SimpleTypes))
def test_deserializable_is_compatible_with_freezegun(data: SimpleTypes):
    with freezegun.freeze_time(datetime.utcnow()):
        result = Coerce(SimpleTypes)(serializable(data))
        assert result == data
