from typing import Literal

import pytest
from ovld import Medley

from serieux import Serieux, dump, load, serialize
from serieux.ctx import Context, ModifyContext, OmitDefaults, Trail
from serieux.exc import ValidationError

from .common import has_312_features, one_test_per_assert
from .definitions import (
    Color,
    Defaults,
    DIDHolder,
    DotDict,
    File,
    Level,
    LTHolder,
    Point,
    Thingies,
)


@one_test_per_assert
def test_serialize_scalars():
    assert serialize(0) == 0
    assert serialize(12) == 12
    assert serialize(-3.25) == -3.25
    assert serialize("flagada") == "flagada"
    assert serialize(True) is True
    assert serialize(False) is False
    assert serialize(None) is None


@one_test_per_assert
def test_serialize_scalars_conversion():
    assert serialize(float, 10) == 10.0


def test_serialize_point():
    pt = Point(1, 2)
    assert serialize(Point, pt) == {"x": 1, "y": 2}


def test_serialize_list_of_points():
    pts = [Point(1, 2), Point(3, 4)]
    assert serialize(list[Point], pts) == [
        {"x": 1, "y": 2},
        {"x": 3, "y": 4},
    ]


def test_serialize_dict_of_points():
    pts = {"p1": Point(1, 2), "p2": Point(3, 4)}
    assert serialize(dict[str, Point], pts) == {
        "p1": {"x": 1, "y": 2},
        "p2": {"x": 3, "y": 4},
    }


@has_312_features
def test_serialize_tree():
    from .definitions_py312 import Tree

    tree = Tree(
        left=Tree(
            left=1,
            right=Tree(left=Tree(left=2, right=3), right=Tree(left=4, right=5)),
        ),
        right=Tree(left=Tree(left=6, right=7), right=8),
    )
    assert serialize(Tree[int], tree) == {
        "left": {
            "left": 1,
            "right": {
                "left": {"left": 2, "right": 3},
                "right": {"left": 4, "right": 5},
            },
        },
        "right": {"left": {"left": 6, "right": 7}, "right": 8},
    }


def test_serialize_set():
    nums = {1, 2, 3, 4}
    assert serialize(set[int], nums) == [1, 2, 3, 4]


def test_serialize_frozenset():
    nums = frozenset({1, 2, 3, 4})
    assert serialize(frozenset[int], nums) == [1, 2, 3, 4]


def test_serialize_dict_subclass():
    data = DotDict(apple=3, banana=7)
    assert serialize(DotDict[str, int], data) == {"apple": 3, "banana": 7}


class Special(Medley):
    def serialize(self, typ: type[int], value: int, ctx: Context):
        return value * 10

    def serialize(self, typ: type[int], value: str, ctx: Context):
        return value * 2


def test_override():
    ss = (Serieux + Special)()
    assert ss.serialize(int, 3) == 30
    assert ss.serialize(int, "quack") == "quackquack"
    assert ss.serialize(list[int], [1, 2, 3]) == [10, 20, 30]
    assert ss.serialize(list[int], [1, "2", 3]) == [10, "22", 30]
    assert ss.serialize(Point, Point(8, 9)) == {"x": 80, "y": 90}
    assert ss.serialize(3) == 30


class quirkint(int):
    pass


class Quirky(Medley):
    def serialize(self, typ: type[int], value: quirkint, ctx: Context):
        return value * 10


def test_override_quirkint():
    ss = (Serieux + Quirky)()
    assert ss.serialize(int, 3) == 3
    assert ss.serialize(int, quirkint(3)) == 30
    assert ss.serialize(Point, Point(8, 9)) == {"x": 8, "y": 9}
    assert ss.serialize(Point, Point(quirkint(8), 9)) == {"x": 80, "y": 9}


class ExtraWeight(Context):
    weight: int


class WeightedImpl(Medley):
    def serialize(self, typ: type[int], value: int, ctx: ExtraWeight):
        return value + ctx.weight


def test_override_state():
    ss = (Serieux + WeightedImpl)()
    assert ss.serialize(int, 3) == 3
    assert ss.serialize(int, 3, ExtraWeight(10)) == 13
    assert ss.serialize(Point, Point(7, 8)) == {"x": 7, "y": 8}
    assert ss.serialize(Point, Point(7, 8), ExtraWeight(10)) == {"x": 17, "y": 18}


def test_serialize_enum():
    assert serialize(Color, Color.RED) == "red"
    assert serialize(list[Color], [Color.GREEN, Color.BLUE, Color.GREEN]) == [
        "green",
        "blue",
        "green",
    ]


def test_serialize_enum_int():
    assert serialize(Level, Level.MED) == 1
    assert serialize(list[Level], [Level.HI, Level.LO, Level.HI]) == [2, 0, 2]


def test_serialize_literal_enum():
    assert serialize(Literal["red", "green", "blue"], "red") == "red"
    with pytest.raises(
        ValidationError,
        match=r"'yellow' is not a valid option for typing.Literal\['red', 'green', 'blue'\]",
    ):
        serialize(Literal["red", "green", "blue"], "yellow")


def test_serialize_literal_int_enum():
    assert serialize(Literal[1, 2, 7], 2) == 2
    with pytest.raises(
        ValidationError,
        match=r"'3' is not a valid option for typing.Literal\[1, 2, 7\]",
    ):
        serialize(Literal[1, 2, 7], 3)


def test_serialize_literal_mixed_enum():
    lit = Literal[1, True, "quack"]
    assert serialize(lit, 1) == 1
    assert serialize(lit, True) is True
    assert serialize(lit, "quack") == "quack"
    with pytest.raises(ValidationError):
        serialize(lit, 3)
    with pytest.raises(ValidationError):
        serialize(lit, "boop")
    with pytest.raises(ValidationError):
        serialize(lit, False)


def test_serialize_ignore(datapath):
    pth = datapath / "job.yaml"
    f = File(pth)
    assert serialize(File, f) == {"path": str(pth)}


###############
# Error tests #
###############


def test_error_basic():
    with pytest.raises(
        ValidationError, match=r"Cannot serialize object of type 'str' into expected type 'int'"
    ):
        serialize(int, "oh no")


def test_error_dataclass():
    with pytest.raises(
        ValidationError, match=r"Cannot serialize object of type 'str' into expected type 'int'"
    ):
        serialize(Point, Point(x=1, y="oops"), Trail())


@has_312_features
def test_error_serialize_tree():
    from .definitions_py312 import Tree

    tree = Tree(Tree("a", 2), "b")

    with pytest.raises(ValidationError, match=r"At path \.left\.right"):
        serialize(Tree[str], tree, Trail())


def test_error_serialize_list():
    li = [0, 1, 2, 3, "oops", 5, 6]

    with pytest.raises(ValidationError, match=r"At path .4"):
        serialize(list[int], li, Trail())


def test_error_serialize_list_of_lists():
    li = [[0, 1], [2, 3, "oops", 5, 6]]

    with pytest.raises(ValidationError, match=r"At path .1.2"):
        serialize(list[list[int]], li, Trail())


def test_dump_no_dest():
    pt = Point(1, 2)
    assert serialize(Point, pt) == dump(Point, pt)


def test_dump(tmp_path):
    dest = tmp_path / "point.yaml"
    pt = Point(1, 2)
    dump(Point, pt, dest=dest)
    assert load(Point, dest) == pt


def test_dump_str_with_format():
    pt = Point(1, 2)
    s = dump(Point, pt, format="yaml")
    assert s == "x: 1\ny: 2\n"


def test_serialize_recursive_type():
    data = DIDHolder(
        did={
            "a": 1,
            "b": {
                "c": 2,
                "d": 3,
            },
        }
    )
    ser = serialize(DIDHolder, data)
    assert ser["did"] == data.did


def test_serialize_recursive_type_2():
    def p(x):
        return Point(x, x)

    data = LTHolder(lt=[p(1), [p(2), [p(3)], p(4)]])
    ser = serialize(LTHolder, data)
    assert ser["lt"] == [
        {"x": 1, "y": 1},
        [{"x": 2, "y": 2}, [{"x": 3, "y": 3}], {"x": 4, "y": 4}],
    ]


def test_error_serialize_list_modelizable():
    th = Thingies(["hello", "world"])
    assert serialize(Thingies, th) == ["hello", "world"]


def test_serialize_defaults_with_omit_defaults_context():
    # Test with default values - serialize normally includes all fields
    obj_with_defaults = Defaults("Alice")
    assert serialize(Defaults, obj_with_defaults) == {
        "name": "Alice",
        "aliases": [],
        "cool": False,
    }

    # Test with OmitDefaults context - should omit fields that have default values
    assert serialize(Defaults, obj_with_defaults, OmitDefaults()) == {
        "name": "Alice",
    }

    # Test with non-default values - should serialize normally
    obj_with_custom_values = Defaults("Bob", aliases=["Robert", "Bobby"], cool=True)
    assert serialize(Defaults, obj_with_custom_values) == {
        "name": "Bob",
        "aliases": ["Robert", "Bobby"],
        "cool": True,
    }

    # Test with OmitDefaults context but non-default values - should include all fields
    assert serialize(Defaults, obj_with_custom_values, OmitDefaults()) == {
        "name": "Bob",
        "aliases": ["Robert", "Bobby"],
        "cool": True,
    }

    # Test partial default values with OmitDefaults
    obj_partial = Defaults("Charlie", cool=True)
    assert serialize(Defaults, obj_partial, OmitDefaults()) == {
        "name": "Charlie",
        "cool": True,
    }


def test_serialize_modify_context():
    alice = Defaults("Alice")
    assert serialize(Defaults @ ModifyContext(OmitDefaults()), alice) == {
        "name": "Alice",
    }
    assert serialize(Defaults @ ModifyContext(sub=OmitDefaults()), alice, OmitDefaults()) == {
        "name": "Alice",
        "aliases": [],
        "cool": False,
    }
