from __future__ import annotations

from datetime import datetime, timezone

import pytest

import tests.models as models
import uniserde


def test_serialize_exact_variant_1() -> None:
    value_fresh = models.TestClass.create_variant_1()

    serde = uniserde.BsonSerde()
    value_bson = serde.as_bson(value_fresh)

    assert value_bson == {
        "_id": 1,
        "val_bool": value_fresh.val_bool,
        "val_int": value_fresh.val_int,
        "val_float": value_fresh.val_float,
        "val_bytes": value_fresh.val_bytes,
        "val_str": value_fresh.val_str,
        "val_datetime": value_fresh.val_datetime,
        "val_timedelta": value_fresh.val_timedelta.total_seconds(),
        "val_tuple": list(value_fresh.val_tuple),
        "val_list": value_fresh.val_list,
        "val_set": list(value_fresh.val_set),
        "val_dict": value_fresh.val_dict,
        "val_optional": value_fresh.val_optional,
        "val_old_union_optional_1": value_fresh.val_old_union_optional_1,
        "val_old_union_optional_2": value_fresh.val_old_union_optional_2,
        "val_new_union_optional_1": value_fresh.val_new_union_optional_1,
        "val_new_union_optional_2": value_fresh.val_new_union_optional_2,
        "val_any": value_fresh.val_any,
        "val_object_id": value_fresh.val_object_id,
        "val_literal": value_fresh.val_literal,
        "val_enum": "one",
        "val_flag": ["one", "two"],
        "val_path": str(value_fresh.val_path),
        "val_uuid": value_fresh.val_uuid,
        "val_class": {
            "foo": value_fresh.val_class.foo,
            "bar": value_fresh.val_class.bar,
        },
    }


@pytest.mark.parametrize("lazy", [False, True])
def test_round_trip_variant_1(lazy: bool) -> None:
    serde = uniserde.BsonSerde(lazy=lazy)

    value_fresh = models.TestClass.create_variant_1()

    value_bson = serde.as_bson(value_fresh)

    value_round_trip = serde.from_bson(value_bson, as_type=models.TestClass)

    assert value_fresh == value_round_trip


@pytest.mark.parametrize("lazy", [False, True])
def test_round_trip_variant_2(lazy: bool) -> None:
    serde = uniserde.BsonSerde(lazy=lazy)

    value_fresh = models.TestClass.create_variant_2()

    value_bson = serde.as_bson(value_fresh)

    value_round_trip = serde.from_bson(value_bson, as_type=models.TestClass)

    assert value_fresh == value_round_trip


@pytest.mark.parametrize("lazy", [False, True])
def test_round_trip_parent(lazy: bool) -> None:
    serde = uniserde.BsonSerde(lazy=lazy)

    value_fresh = models.ParentClass.create_parent_variant_1()

    value_bson = serde.as_bson(value_fresh)

    value_round_trip = serde.from_bson(value_bson, as_type=models.ParentClass)

    assert value_fresh == value_round_trip


@pytest.mark.parametrize("lazy", [False, True])
def test_round_trip_child(lazy: bool) -> None:
    serde = uniserde.BsonSerde(lazy=lazy)

    value_fresh = models.ChildClass.create_child_variant_1()

    value_bson = serde.as_bson(value_fresh)

    value_round_trip = serde.from_bson(value_bson, as_type=models.ChildClass)

    assert value_fresh == value_round_trip


@pytest.mark.parametrize("lazy", [False, True])
def test_round_trip_child_as_parent(lazy: bool) -> None:
    serde = uniserde.BsonSerde(lazy=lazy)

    value_fresh = models.ChildClass.create_child_variant_1()

    value_bson = serde.as_bson(value_fresh)

    value_round_trip = serde.from_bson(value_bson, as_type=models.ParentClass)

    assert isinstance(value_round_trip, models.ChildClass)
    assert value_fresh == value_round_trip


def test_rename_id() -> None:
    serde = uniserde.BsonSerde()

    value_fresh = models.ClassWithId(1, 2)

    value_bson = serde.as_bson(value_fresh)

    assert isinstance(value_bson, dict)
    assert "_id" in value_bson
    assert "id" not in value_bson
    assert "foo" in value_bson
    assert len(value_bson) == 2


def test_kw_only() -> None:
    serde = uniserde.BsonSerde()

    value_fresh = models.ClassWithKwOnly(1, bar=2)

    value_bson = serde.as_bson(value_fresh)

    assert isinstance(value_bson, dict)
    assert "foo" in value_bson
    assert "bar" in value_bson
    assert "_" not in value_bson
    assert len(value_bson) == 2
    assert value_bson["foo"] == 1
    assert value_bson["bar"] == 2


@pytest.mark.parametrize("lazy", [False, True])
def test_datetime_imputes_timezone(lazy: bool) -> None:
    # MongoDB does not explicitly store timezone information, but rather
    # converts everything to UTC. Make sure uniserde understands this and
    # imputes UTC.
    serde = uniserde.BsonSerde(lazy=lazy)

    value_parsed = serde.from_bson(
        datetime(2020, 1, 1, 1, 2, 3, 4),
        as_type=datetime,
    )

    assert isinstance(value_parsed, datetime)
    assert value_parsed.tzinfo is not None
    assert value_parsed == datetime(2020, 1, 1, 1, 2, 3, 4, timezone.utc)


def test_int_is_float() -> None:
    serde = uniserde.BsonSerde()

    serde.from_bson(1, as_type=float)


def test_catch_superfluous_value() -> None:
    serde = uniserde.BsonSerde()

    with pytest.raises(uniserde.SerdeError, match="Object contains superfluous fields"):
        serde.from_bson(
            {
                "foo": 1,
                "bar": "one",
                "invalidKey": True,
            },
            as_type=models.SimpleClass,
        )


def test_overridden_as_bson() -> None:
    serde = uniserde.BsonSerde()

    value_fresh = models.ClassWithStaticmethodOverrides.create()

    value_bson = serde.as_bson(value_fresh)

    assert value_bson == {
        "value": "overridden during serialization",
        "format": "bson",
    }


@pytest.mark.parametrize("lazy", [False, True])
def test_overridden_from_bson_staticmethod(lazy: bool) -> None:
    serde = uniserde.BsonSerde(lazy=lazy)

    value_bson = {
        "value": "stored value",
        "format": "bson",
    }

    value_parsed = serde.from_bson(
        value_bson,
        as_type=models.ClassWithStaticmethodOverrides,
    )

    assert isinstance(value_parsed, models.ClassWithStaticmethodOverrides)
    assert value_parsed.value == "overridden during deserialization"
    assert value_parsed.format == "bson"


@pytest.mark.parametrize("lazy", [False, True])
def test_overridden_from_bson_classmethod(lazy: bool) -> None:
    serde = uniserde.BsonSerde(lazy=lazy)

    value_bson = {
        "value": "stored value",
        "format": "bson",
    }

    value_parsed = serde.from_bson(
        value_bson,
        as_type=models.ClassWithClassmethodOverrides,
    )

    assert isinstance(value_parsed, models.ClassWithClassmethodOverrides)
    assert value_parsed.value == "overridden during deserialization"
    assert value_parsed.format == "bson"


def test_serialize_with_custom_handlers() -> None:
    """
    Provides custom handlers for some types during serialization.
    """
    serde = uniserde.BsonSerde(
        custom_serializers={
            int: lambda serde, val, as_type: val + 1,
        }
    )

    value_fresh = models.SimpleClass(1, "one")

    value_bson = serde.as_bson(value_fresh, as_type=models.SimpleClass)

    assert value_bson == {
        "foo": 2,
        "bar": "one",
    }


@pytest.mark.parametrize("lazy", [False, True])
def test_deserialize_with_custom_handlers(lazy: bool) -> None:
    """
    Provides custom handlers for some types during deserialization.
    """
    serde = uniserde.BsonSerde(
        custom_deserializers={
            int: lambda serde, val, as_type: val + 1,
        },
        lazy=lazy,
    )

    value_bson = {
        "foo": 1,
        "bar": "one",
    }

    value_parsed = serde.from_bson(
        value_bson,
        as_type=models.SimpleClass,
    )

    assert value_parsed == models.SimpleClass(2, "one")


def test_is_not_lazy_if_not_requested() -> None:
    """
    Make sure that when asking for eager deserialization, we get eager
    deserialization.
    """
    serde = uniserde.BsonSerde(lazy=False)

    value_fresh = models.TestClass.create_variant_1()

    value_bson = serde.as_bson(value_fresh)

    value_parsed = serde.from_bson(value_bson, as_type=models.TestClass)

    assert "_uniserde_remaining_fields_" not in vars(value_parsed)


def test_is_lazy_if_requested() -> None:
    """
    Make sure that when asking for eager deserialization, we get eager
    deserialization.
    """
    serde = uniserde.BsonSerde(lazy=True)

    value_fresh = models.TestClass.create_variant_1()

    value_bson = serde.as_bson(value_fresh)

    value_parsed = serde.from_bson(value_bson, as_type=models.TestClass)

    assert "_uniserde_remaining_fields_" in vars(value_parsed)
