import json
import unittest

from pydantic import BaseModel

from amqp.adapter.type_descriptor import TypeDescriptor, _instantiate


class TestClass(BaseModel):
    value: str
    extra: str


class TestTypeDescriptor(unittest.TestCase):
    def test_annotated_with_json(self):
        type_str = (
            "typing.Optional[typing.Annotated[core.base.providers.ingestion.IngestionConfig, Json]]"
        )
        td = TypeDescriptor(type_str, {})

        self.assertEqual(td.type, "core.base.providers.ingestion.IngestionConfig")
        self.assertTrue(td.is_json)

    def test_annotated_without_json(self):
        type_str = "typing.Optional[typing.Annotated[str, SomethingElse]]"
        td = TypeDescriptor(type_str, {})

        self.assertEqual(td.type, "str")
        self.assertFalse(td.is_json)

    def test_annotated_multiple_args(self):
        type_str = "typing.Optional[typing.Annotated[list[int], Json, more_args]]"
        td = TypeDescriptor(type_str, {})

        self.assertEqual(td.type, "list[int]")
        self.assertTrue(td.is_json)

    def test_simple_optional(self):
        type_str = "typing.Optional[uuid.UUID]"
        td = TypeDescriptor(type_str, {})

        self.assertEqual(td.type, "uuid.UUID")
        self.assertFalse(td.is_json)

    def test_whitespace_variations(self):
        test_cases = [
            ("typing.Optional[typing.Annotated[  str  ,  Json  ]]", "str", True),
            ("typing.Optional[  typing.Annotated[dict,Json]]", "dict", True),
            ("typing.Optional[  int  ]", "int", False),
        ]

        for type_str, expected_type, expected_json in test_cases:
            with self.subTest(type_str=type_str):
                td = TypeDescriptor(type_str, {})
                self.assertEqual(td.type, expected_type)
                self.assertEqual(td.is_json, expected_json)

    def test_invalid_input(self):
        test_cases = [
            "typing.Optional[missing_bracket",
            "typing.Optional[]",
            "[without.optional]",
        ]

        for type_str in test_cases:
            with self.subTest(type_str=type_str):
                td = TypeDescriptor(type_str, {})
                self.assertEqual(td.type, "")
                self.assertFalse(td.is_json)

    def test_repr(self):
        type_str = "typing.Optional[typing.Annotated[str, Json]]"
        td = TypeDescriptor(type_str, {})

        self.assertEqual(repr(td), "TypeDescriptor(type='str', is_json=True)")

    def test_instantiate_basic_types(self):
        test_cases = [
            ("str", "hello", "hello"),
            ("int", "42", 42),
            ("float", "3.14", 3.14),
            ("bool", "true", True),
        ]

        for type_str, input_value, expected in test_cases:
            with self.subTest(type_str=type_str, input_value=input_value):
                result = _instantiate(type_str, input_value)
                self.assertEqual(result, expected)

    def test_instantiate_list_types(self):
        test_cases = [
            ("typing.List[str]", "a,b,c", ["a", "b", "c"]),
            ("typing.List[int]", "1,2,3", [1, 2, 3]),
            ("typing.List[str]", json.dumps(["a", "b", "c"]), ["a", "b", "c"], True),
            ("typing.List[int]", json.dumps([1, 2, 3]), [1, 2, 3], True),
        ]

        for type_str, input_value, expected, *args in test_cases:
            is_json = args[0] if args else False
            with self.subTest(type_str=type_str, input_value=input_value, is_json=is_json):
                result = _instantiate(type_str, input_value, is_json=is_json)
                self.assertEqual(result, expected)

    def test_instantiate_dict_types(self):
        test_cases = [
            ("typing.Dict", {"a": 1, "b": 2}, {"a": 1, "b": 2}),
            ("typing.Dict", json.dumps({"a": 1, "b": 2}), {"a": 1, "b": 2}, True),
            ("typing.Mapping", {"a": 1, "b": 2}, {"a": 1, "b": 2}),
        ]

        for type_str, input_value, expected, *args in test_cases:
            is_json = args[0] if args else False
            with self.subTest(type_str=type_str, input_value=input_value, is_json=is_json):
                result = _instantiate(type_str, input_value, is_json=is_json)
                self.assertEqual(result, expected)

    def test_instantiate_set_types(self):
        test_cases = [
            ("typing.Set[str]", "a,b,c", {"a", "b", "c"}),
            ("typing.Set[int]", "1,2,3", {1, 2, 3}),
            ("typing.Set[str]", json.dumps(["a", "b", "c"]), {"a", "b", "c"}, True),
        ]

        for type_str, input_value, expected, *args in test_cases:
            is_json = args[0] if args else False
            with self.subTest(type_str=type_str, input_value=input_value, is_json=is_json):
                result = _instantiate(type_str, input_value, is_json=is_json)
                self.assertEqual(result, expected)

    def test_instantiate_tuple_types(self):
        test_cases = [
            ("typing.Tuple[str, int]", ["hello", 42], ("hello", 42)),
            ("typing.Tuple[str, int]", json.dumps(["hello", 42]), ("hello", 42), True),
        ]

        for type_str, input_value, expected, *args in test_cases:
            is_json = args[0] if args else False
            with self.subTest(type_str=type_str, input_value=input_value, is_json=is_json):
                result = _instantiate(type_str, input_value, is_json=is_json)
                self.assertEqual(result, expected)

    def test_instantiate_optional_types(self):
        test_cases = [
            ("typing.Optional[str]", "hello", "hello"),
            ("typing.Optional[typing.List[str]]", "a,b,c", ["a", "b", "c"]),
            (
                "typing.Optional[typing.List[str]]",
                json.dumps(["a", "b", "c"]),
                ["a", "b", "c"],
                True,
            ),
        ]

        for type_str, input_value, expected, *args in test_cases:
            is_json = args[0] if args else False
            with self.subTest(type_str=type_str, input_value=input_value, is_json=is_json):
                result = _instantiate(type_str, input_value, is_json=is_json)
                self.assertEqual(result, expected)

    def test_instantiate_nested_types(self):
        test_cases = [
            (
                "typing.List[typing.Dict]",
                json.dumps([{"a": 1}, {"b": 2}]),
                [{"a": 1}, {"b": 2}],
                True,
            ),
            (
                "typing.Optional[typing.List[typing.Dict]]",
                json.dumps([{"a": 1}, {"b": 2}]),
                [{"a": 1}, {"b": 2}],
                True,
            ),
        ]

        for type_str, input_value, expected, *args in test_cases:
            is_json = args[0] if args else False
            with self.subTest(type_str=type_str, input_value=input_value, is_json=is_json):
                result = _instantiate(type_str, input_value, is_json=is_json)
                self.assertEqual(result, expected)

    def test_instantiate_error_handling(self):
        test_cases = [
            ("typing.List[str]", "invalid json", True, json.JSONDecodeError),
            ("typing.NonExistentType", "value", False, ImportError),
        ]

        for type_str, input_value, is_json, expected_exception in test_cases:
            with self.subTest(type_str=type_str, input_value=input_value, is_json=is_json):
                with self.assertRaises(expected_exception):
                    _instantiate(type_str, input_value, is_json=is_json)

    def test_instantiate_with_extra_init_data(self):

        test_cases = [
            (
                "tests.adapter.test_type_descriptor.TestClass",
                {"value": "test"},
                {"extra": "extra_value"},
                lambda x: x.value == "test" and x.extra == "extra_value",
            ),
        ]

        for type_str, input_value, extra_init_data, validator in test_cases:
            with self.subTest(type_str=type_str, input_value=input_value):
                result = _instantiate(type_str, input_value, extra_init_data=extra_init_data)
                self.assertTrue(validator(result))


if __name__ == "__main__":
    unittest.main()
