###############################################################################
#
# (C) Copyright 2025 EVERYSK TECHNOLOGIES
#
# This is an unpublished work containing confidential and proprietary
# information of EVERYSK TECHNOLOGIES. Disclosure, use, or reproduction
# without authorization of EVERYSK TECHNOLOGIES is prohibited.
#
###############################################################################
from everysk.core.mapping import BaseMapping
from everysk.core.object import BaseDict, BaseObject
from everysk.core.unittests import TestCase


class TestClass(BaseMapping):
    __invalid_keys__: frozenset[str] = frozenset({'field6'})

    field1: int = 10
    field2: int
    field3 = 3.0
    field6: str = 'value'


class BaseMappingTestCase(TestCase):
    def tearDown(self) -> None:
        # Reset TestClass to initial state after each test
        TestClass.field1 = 10
        TestClass.field2 = None
        TestClass.field3 = 3.0
        TestClass._generate_attributes()  # noqa: SLF001

    def test_attributes(self) -> None:
        self.assertSetEqual(TestClass.__attributes__, {'field1', 'field2', 'field3', 'field6'})

    def test_attributes_inheritance(self) -> None:
        class SubTestClass(TestClass):
            field4: str = 'test'
            field5: float

        self.assertSetEqual(SubTestClass.__attributes__, {'field1', 'field2', 'field3', 'field4', 'field5', 'field6'})

    def test_attributes_add(self) -> None:
        class SubTestClass(TestClass):
            field4: str = 'test'

        SubTestClass.field5 = 5.0  # Adding attribute after class creation
        self.assertSetEqual(SubTestClass.__attributes__, {'field1', 'field2', 'field3', 'field4', 'field5', 'field6'})

    def test_changed_attributes(self) -> None:
        obj = TestClass()
        obj.field2 = 20
        obj.field1 = 10
        obj.field3 = 5.0
        self.assertDictEqual(obj, {'field1': 10, 'field2': 20, 'field3': 5.0})

    def test_changed_keys(self) -> None:
        obj = TestClass()
        obj['field2'] = 25
        obj['field1'] = 10
        obj['field3'] = 4.0
        self.assertDictEqual(obj, {'field1': 10, 'field2': 25, 'field3': 4.0})

    def test_delete_attribute(self) -> None:
        obj = TestClass(field2=30)
        self.assertEqual(obj.field2, 30)
        self.assertEqual(obj['field2'], 30)
        del obj.field2
        with self.assertRaises(AttributeError) as context:
            _ = obj.field2
        self.assertEqual(str(context.exception), "'TestClass' object has no attribute 'field2'.")
        with self.assertRaises(KeyError) as context:
            _ = obj['field2']
        self.assertEqual(str(context.exception), "'field2'")

    def test_delete_key(self) -> None:
        obj = TestClass(field2=30)
        self.assertEqual(obj.field2, 30)
        self.assertEqual(obj['field2'], 30)
        del obj['field2']
        with self.assertRaises(AttributeError) as context:
            _ = obj.field2
        self.assertEqual(str(context.exception), "'TestClass' object has no attribute 'field2'.")
        with self.assertRaises(KeyError) as context:
            _ = obj['field2']
        self.assertEqual(str(context.exception), "'field2'")

    def test_equal(self) -> None:
        obj1 = TestClass()
        obj2 = TestClass()
        self.assertDictEqual(obj1, obj2)

        obj1.field2 = 20
        self.assertNotEqual(obj1, obj2)

        obj2.field2 = 20
        self.assertDictEqual(obj1, obj2)

    def test_equal_changed_class_value(self) -> None:
        obj1 = TestClass()
        obj2 = TestClass()
        self.assertDictEqual(obj1, obj2)

        TestClass.field1 = 15
        self.assertDictEqual(obj1, obj2)
        self.assertEqual(obj1.field1, 10)
        self.assertEqual(obj1['field1'], 10)
        self.assertEqual(obj2.field1, 10)
        self.assertEqual(obj2['field1'], 10)

    def test_field_type(self) -> None:
        self.assertDictEqual(
            TestClass.__annotations__,
            {'__invalid_keys__': frozenset[str], 'field1': int, 'field2': int, 'field6': str, 'field3': float},
        )
        self.assertEqual(TestClass.__dict__['field1'].field_type, int)
        self.assertEqual(TestClass.__dict__['field2'].field_type, int)
        self.assertEqual(TestClass.__dict__['field3'].field_type, float)
        self.assertEqual(TestClass.__dict__['field6'].field_type, str)

    def test_field_type_generic_alias(self) -> None:
        class GenericTestClass(BaseMapping):
            field1: list[int] = None

        self.assertEqual(GenericTestClass.__dict__['field1'].field_type, list)

    def test_field_type_union(self) -> None:
        class UnionTestClass(BaseMapping):
            field1: int | list[str] | None = None

        self.assertEqual(UnionTestClass.__dict__['field1'].field_type, int | list)

    def test_inherited_changed(self) -> None:
        class SubTestClass(TestClass):
            field4: str = 'test'

        obj = SubTestClass()
        obj2 = SubTestClass()

        obj.field2 = 20
        obj.field4 = 'changed'
        self.assertDictEqual(obj, {'field1': 10, 'field2': 20, 'field3': 3.0, 'field4': 'changed'})
        self.assertDictEqual(obj2, {'field1': 10, 'field2': None, 'field3': 3.0, 'field4': 'test'})

    def test_inherited_attribute(self) -> None:
        class SubTestClass(TestClass):
            pass

        obj = SubTestClass()
        self.assertEqual(obj.field1, 10)
        self.assertEqual(obj.field2, None)
        self.assertEqual(obj.field3, 3.0)
        self.assertEqual(obj.field6, 'value')
        # Fields that are keys don't have inherited values
        TestClass.field1 = 20
        self.assertEqual(obj.field1, 10)
        self.assertEqual(obj['field1'], 10)
        # Fields that are attributes only have inherited values
        TestClass.field6 = 'new_value'
        self.assertEqual(obj.field6, 'new_value')

    def test_init(self) -> None:
        obj = TestClass(field2=25, field3=4.5)
        self.assertEqual(obj.field1, 10)
        self.assertEqual(obj['field1'], 10)
        self.assertEqual(obj.field2, 25)
        self.assertEqual(obj['field2'], 25)
        self.assertEqual(obj.field3, 4.5)
        self.assertEqual(obj['field3'], 4.5)
        self.assertDictEqual(obj, {'field1': 10, 'field2': 25, 'field3': 4.5})

    def test_init_new_attribute(self) -> None:
        obj = BaseMapping(key='value')
        self.assertEqual(obj.key, 'value')
        self.assertEqual(obj['key'], 'value')

    def test_isinstance(self) -> None:
        self.assertIsInstance(BaseMapping(), dict)
        self.assertIsInstance(BaseMapping(), BaseDict)
        self.assertIsInstance(BaseMapping(), BaseMapping)
        self.assertNotIsInstance(BaseDict(), dict)
        self.assertIsInstance(BaseDict(), BaseDict)
        self.assertNotIsInstance(BaseDict(), BaseMapping)

    def test_key(self) -> None:
        obj = TestClass()
        self.assertEqual(obj['field1'], 10)
        self.assertEqual(obj['field2'], None)
        self.assertEqual(obj['field3'], 3.0)

    def test_key_changed(self) -> None:
        obj = TestClass()
        obj['field2'] = 30
        self.assertEqual(obj.field2, 30)
        self.assertEqual(obj['field2'], 30)

    def test_key_invalid(self) -> None:
        class SubTestClass(TestClass):
            __invalid_keys__ = frozenset({'field3'})

        obj = SubTestClass()
        with self.assertRaises(KeyError):
            _ = obj['field3']

        self.assertEqual(obj.field3, 3.0)
        obj.field3 = 5.0
        self.assertEqual(obj.field3, 5.0)
        with self.assertRaises(KeyError):
            _ = obj['field3']

        del obj.field3
        with self.assertRaises(AttributeError):
            _ = obj.field3

        with self.assertRaises(KeyError):
            _ = obj['field3']

    def test_new_attribute(self) -> None:
        obj = TestClass()
        obj.new = 'new_value'
        self.assertEqual(obj.new, 'new_value')
        self.assertEqual(obj['new'], 'new_value')

    def test_value_class(self) -> None:
        obj = TestClass()
        self.assertEqual(TestClass.field1, 10)
        self.assertEqual(obj.field1, 10)
        self.assertEqual(obj['field1'], 10)
        TestClass.field1 = 15
        self.assertEqual(TestClass.field1, 15)
        # Values that are fields don't change in the instance only in the class
        self.assertEqual(obj.field1, 10)
        self.assertEqual(obj['field1'], 10)

    def test_value_instance(self) -> None:
        obj = TestClass()
        self.assertEqual(TestClass.field1, 10)
        self.assertEqual(obj.field1, 10)
        self.assertEqual(obj['field1'], 10)
        obj.field1 = 20
        self.assertEqual(TestClass.field1, 10)
        self.assertEqual(obj.field1, 20)
        self.assertEqual(obj['field1'], 20)

    def test_value_instances(self) -> None:
        obj1 = TestClass()
        obj2 = TestClass()
        self.assertEqual(TestClass.field1, 10)
        self.assertEqual(obj1.field1, 10)
        self.assertEqual(obj1['field1'], 10)
        self.assertEqual(obj2.field1, 10)
        self.assertEqual(obj2['field1'], 10)

        obj1.field1 = 20
        self.assertEqual(TestClass.field1, 10)
        self.assertEqual(obj1.field1, 20)
        self.assertEqual(obj1['field1'], 20)
        self.assertEqual(obj2.field1, 10)
        self.assertEqual(obj2['field1'], 10)

    def test_to_dict_defaults(self) -> None:
        obj = TestClass()
        expected_dict = {'field1': 10, 'field2': None, 'field3': 3.0}
        self.assertDictEqual(obj.to_dict(), expected_dict)

    def test_to_dict_defaults_sub_object(self) -> None:
        class SubTestClass(TestClass):
            field4: str = 'default'

        obj = SubTestClass()
        expected_dict = {'field1': 10, 'field2': None, 'field3': 3.0, 'field4': 'default'}
        self.assertDictEqual(obj.to_dict(), expected_dict)

    def test_to_dict_add_class_path(self) -> None:
        obj = TestClass()
        expected_dict = {
            'field1': 10,
            'field2': None,
            'field3': 3.0,
            '__class_path__': obj.get_full_dotted_class_path(),
        }
        self.assertDictEqual(obj.to_dict(add_class_path=True), expected_dict)

    def test_to_dict_exclude_keys(self) -> None:
        class SubTestClass(TestClass):
            def _to_dict_excluded_keys(self) -> set[str]:
                return {'field2', 'field3'}

        obj = SubTestClass()
        expected_dict = {'field1': 10}
        self.assertDictEqual(obj.to_dict(), expected_dict)

    def test_to_dict_recursive(self) -> None:
        class SubTestClass(BaseMapping):
            field1: BaseMapping = BaseMapping(field1=10)
            field2: BaseDict = BaseDict(key='value')
            field3: BaseObject = BaseObject(attr='value')

        obj = SubTestClass()
        expected_dict = {'field1': {'field1': 10}, 'field2': {'key': 'value'}, 'field3': {'attr': 'value'}}
        self.assertDictEqual(obj.to_dict(recursion=True), expected_dict)

    def test_to_native(self) -> None:
        obj = TestClass()
        result = obj.to_native()
        expected_dict = {'field1': 10, 'field2': None, 'field3': 3.0}
        self.assertDictEqual(result, expected_dict)
