# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.

import dataclasses
from dataclasses import dataclass
import datetime
import enum
from typing import Dict, Any, List, Set, Optional, Tuple

import pytest
import typing

import pyfory
from pyfory import Fory
from pyfory.error import TypeUnregisteredError
from pyfory.struct import DataClassSerializer, build_default_values_factory
from pyfory.types import TypeId


def ser_de(fory, obj):
    binary = fory.serialize(obj)
    return fory.deserialize(binary)


@dataclass
class SimpleObject:
    f1: Optional[Dict[pyfory.int32, pyfory.float64]] = None


@dataclass
class ComplexObject:
    f1: Optional[Any] = None
    f2: Optional[Any] = None
    f3: pyfory.int8 = 0
    f4: pyfory.int16 = 0
    f5: pyfory.int32 = 0
    f6: pyfory.int64 = 0
    f7: pyfory.float32 = 0
    f8: pyfory.float64 = 0
    f9: Optional[List[pyfory.int16]] = None
    f10: Optional[Dict[pyfory.int32, pyfory.float64]] = None


def test_struct():
    fory = Fory(xlang=True, ref=True)
    fory.register_type(SimpleObject, typename="SimpleObject")
    fory.register_type(ComplexObject, typename="example.ComplexObject")
    o = SimpleObject(f1={1: 1.0 / 3})
    assert ser_de(fory, o) == o

    o = ComplexObject(
        f1="str",
        f2={"k1": -1, "k2": [1, 2]},
        f3=2**7 - 1,
        f4=2**15 - 1,
        f5=2**31 - 1,
        f6=2**63 - 1,
        f7=1.0 / 2,
        f8=2.0 / 3,
        f9=[1, 2],
        f10={1: 1.0 / 3, 100: 2 / 7.0},
    )
    assert ser_de(fory, o) == o
    with pytest.raises(AssertionError):
        assert ser_de(fory, ComplexObject(f7=1.0 / 3)) == ComplexObject(f7=1.0 / 3)
    with pytest.raises(OverflowError):
        assert ser_de(fory, ComplexObject(f3=2**8)) == ComplexObject(f3=2**8)
    with pytest.raises(OverflowError):
        assert ser_de(fory, ComplexObject(f4=2**16)) == ComplexObject(f4=2**16)
    with pytest.raises(OverflowError):
        assert ser_de(fory, ComplexObject(f5=2**32)) == ComplexObject(f5=2**32)
    with pytest.raises(OverflowError):
        assert ser_de(fory, ComplexObject(f6=2**64)) == ComplexObject(f6=2**64)


@dataclass
class SuperClass1:
    f1: Optional[Any] = None
    f2: pyfory.int8 = 0


@dataclass
class ChildClass1(SuperClass1):
    f3: Optional[Dict[str, pyfory.float64]] = None


def test_strict():
    fory = Fory(xlang=False, ref=True)
    obj = ChildClass1(f1="a", f2=-10, f3={"a": -10.0, "b": 1 / 3})
    with pytest.raises(TypeUnregisteredError):
        fory.serialize(obj)


def test_inheritance():
    type_hints = typing.get_type_hints(ChildClass1)
    print(type_hints)
    assert type_hints.keys() == {"f1", "f2", "f3"}
    fory = Fory(xlang=False, ref=True, strict=False)
    obj = ChildClass1(f1="a", f2=-10, f3={"a": -10.0, "b": 1 / 3})
    assert ser_de(fory, obj) == obj
    assert type(fory.type_resolver.get_serializer(ChildClass1)) is pyfory.DataClassSerializer


@dataclass
class DataClassObject:
    f_int: int
    f_float: float
    f_str: str
    f_bool: bool
    f_list: List[int]
    f_dict: Dict[str, float]
    f_any: Optional[Any]
    f_complex: Optional[ComplexObject] = None

    @classmethod
    def create(cls):
        return cls(
            f_int=42,
            f_float=3.14159,
            f_str="test_codegen",
            f_bool=True,
            f_list=[1, 2, 3],
            f_dict={"key": 1.5},
            f_any="any_data",
            f_complex=None,
        )


@dataclass
class BoolCoercionObject:
    b: bool


@dataclass(frozen=True)
class TupleFieldObject:
    bar: Tuple[str, int]


@dataclass(frozen=True)
class XlangTupleFieldObject:
    bar: Tuple[str, int]


@dataclass(frozen=True)
class XlangNestedTupleObject:
    tuple_field: Tuple[List[int], Dict[str, int]]
    list_of_tuples: List[Tuple[str, int]]
    map_of_tuples: Dict[str, Tuple[str, int]]
    set_of_tuples: Set[Tuple[str, int]]
    tuple_of_tuples: Tuple[Tuple[str, int], Tuple[str, int]]


def test_sort_fields():
    @dataclass
    class TestClass:
        f1: pyfory.int32
        f2: List[pyfory.int16]
        f3: Dict[str, pyfory.float64]
        f4: str
        f5: pyfory.float32
        f6: bytes
        f7: bool
        f8: Any
        f9: Dict[pyfory.int32, pyfory.float64]
        f10: List[str]
        f11: pyfory.int8
        f12: pyfory.int64
        f13: pyfory.float64
        f14: Set[pyfory.int32]
        f15: datetime.datetime

    fory = Fory(xlang=True, ref=True)
    serializer = DataClassSerializer(fory.type_resolver, TestClass)
    # Sorting order:
    # 1. Non-compressed primitives (compress=0) by -size, then name:
    #    float64(8), float32(4), bool(1), int8(1) => f13, f5, f11, f7
    #    (f11 < f7 alphabetically since '1' < '7')
    # 2. Compressed primitives (compress=1) by -size, then name:
    #    int64(8), int32(4) => f12, f1
    # 3. Internal types by type_id, then name: str, datetime, bytes => f4, f15, f6
    # 4. Collection types by type_id, then name: list => f10, f2
    # 5. Set types by type_id, then name: set => f14
    # 6. Map types by type_id, then name: dict => f3, f9
    # 7. Other types (polymorphic/any) by name: any => f8
    assert serializer._field_names == ["f13", "f5", "f11", "f7", "f12", "f1", "f4", "f15", "f6", "f10", "f2", "f14", "f3", "f9", "f8"]


@pytest.mark.parametrize(
    "value, expected",
    [
        (1, True),
        (0, False),
    ],
)
def test_bool_field_coercion(value, expected):
    fory = Fory(xlang=False, ref=True, strict=False)
    result = ser_de(fory, BoolCoercionObject(value))
    assert result.b is expected


def test_bool_field_coercion_numpy_bool():
    np = pytest.importorskip("numpy")
    fory = Fory(xlang=False, ref=True, strict=False)

    result_true = ser_de(fory, BoolCoercionObject(np.bool_(True)))
    assert result_true.b is True

    result_false = ser_de(fory, BoolCoercionObject(np.bool_(False)))
    assert result_false.b is False


@pytest.mark.parametrize(
    "numeric_type",
    [
        pyfory.int8,
        pyfory.int16,
        pyfory.int32,
        pyfory.fixed_int32,
        pyfory.int64,
        pyfory.fixed_int64,
        pyfory.tagged_int64,
        pyfory.uint8,
        pyfory.uint16,
        pyfory.uint32,
        pyfory.fixed_uint32,
        pyfory.uint64,
        pyfory.fixed_uint64,
        pyfory.tagged_uint64,
        pyfory.float32,
        pyfory.float64,
    ],
)
def test_numeric_serializer_need_to_write_ref_disabled(numeric_type):
    fory = Fory(xlang=False, ref=True, strict=False)
    serializer = fory.type_resolver.get_serializer(numeric_type)
    assert serializer.need_to_write_ref is False


def test_data_class_serializer_xlang():
    fory = Fory(xlang=True, ref=True)
    fory.register_type(ComplexObject, typename="example.ComplexObject")
    fory.register_type(DataClassObject, typename="example.TestDataClassObject")

    complex_data = ComplexObject(
        f1="nested_str",
        f5=100,
        f8=3.14,
        f10={10: 1.0, 20: 2.0},
    )
    obj_original = DataClassObject(
        f_int=123,
        f_float=45.67,
        f_str="hello xlang",
        f_bool=True,
        f_list=[1, 2, 3, 4, 5],
        f_dict={"a": 1.1, "b": 2.2},
        f_any="any_value",
        f_complex=complex_data,
    )

    obj_deserialized = ser_de(fory, obj_original)

    assert obj_deserialized == obj_original
    assert obj_deserialized.f_int == obj_original.f_int
    assert obj_deserialized.f_float == obj_original.f_float
    assert obj_deserialized.f_str == obj_original.f_str
    assert obj_deserialized.f_bool == obj_original.f_bool
    assert obj_deserialized.f_list == obj_original.f_list
    assert obj_deserialized.f_dict == obj_original.f_dict
    assert obj_deserialized.f_any == obj_original.f_any
    assert obj_deserialized.f_complex == obj_original.f_complex
    assert type(fory.type_resolver.get_serializer(DataClassObject)) is pyfory.DataClassSerializer
    # Ensure it's using xlang mode indirectly, by checking no JIT methods if possible,
    # or by ensuring it was registered with _register_xtype which now uses DataClassSerializer.
    # For now, the registration path check is implicit via xlang=True usage.
    # We can also check if the hash is non-zero if it was computed,
    # or if the _serializers attribute exists.
    serializer_instance = fory.type_resolver.get_serializer(DataClassObject)
    assert hasattr(serializer_instance, "_serializers")
    assert not hasattr(serializer_instance, "_xlang")

    # Test with None for a complex field
    obj_with_none_complex = DataClassObject(
        f_int=789,
        f_float=12.34,
        f_str="another string",
        f_bool=False,
        f_list=[10, 20],
        f_dict={"x": 7.7, "y": 8.8},
        f_any=None,
        f_complex=None,
    )
    obj_deserialized_none = ser_de(fory, obj_with_none_complex)
    assert obj_deserialized_none == obj_with_none_complex


@pytest.mark.parametrize("track_ref", [False, True])
def test_dataclass_with_typed_tuple_field(track_ref):
    fory = Fory(xlang=False, ref=track_ref, strict=False)
    obj = TupleFieldObject(bar=("a", 1))
    assert ser_de(fory, obj) == obj


@pytest.mark.parametrize("track_ref", [False, True])
def test_xlang_dataclass_tuple_field(track_ref):
    fory = Fory(xlang=True, ref=track_ref, strict=False)
    fory.register_type(XlangTupleFieldObject, typename="example.XlangTupleFieldObject")
    obj = XlangTupleFieldObject(bar=("a", 1))
    result = ser_de(fory, obj)
    assert result == obj
    assert isinstance(result.bar, tuple)


@pytest.mark.parametrize("track_ref", [False, True])
def test_xlang_nested_tuple_container_fields(track_ref):
    fory = Fory(xlang=True, ref=track_ref, strict=False)
    fory.register_type(XlangNestedTupleObject, typename="example.XlangNestedTupleObject")
    obj = XlangNestedTupleObject(
        tuple_field=([1, 2], {"a": 1, "b": 2}),
        list_of_tuples=[("a", 1), ("b", 2)],
        map_of_tuples={"left": ("c", 3), "right": ("d", 4)},
        set_of_tuples={("e", 5), ("f", 6)},
        tuple_of_tuples=(("g", 7), ("h", 8)),
    )
    result = ser_de(fory, obj)
    assert result == obj
    assert isinstance(result.tuple_field, tuple)
    assert all(isinstance(value, tuple) for value in result.list_of_tuples)
    assert all(isinstance(value, tuple) for value in result.map_of_tuples.values())
    assert all(isinstance(value, tuple) for value in result.set_of_tuples)
    assert isinstance(result.tuple_of_tuples, tuple)
    assert all(isinstance(value, tuple) for value in result.tuple_of_tuples)


def test_struct_evolving_override():
    @pyfory.dataclass
    class EvolvingStruct:
        f1: pyfory.int32 = 0

    @pyfory.dataclass(evolving=False)
    class FixedStruct:
        f1: pyfory.int32 = 0

    fory = Fory(xlang=True, compatible=True)
    fory.register_type(EvolvingStruct, namespace="test", typename="EvolvingStruct")
    fory.register_type(FixedStruct, namespace="test", typename="FixedStruct")
    evolving_info = fory.type_resolver.get_type_info(EvolvingStruct)
    fixed_info = fory.type_resolver.get_type_info(FixedStruct)
    assert evolving_info.type_id == TypeId.NAMED_COMPATIBLE_STRUCT
    assert fixed_info.type_id == TypeId.NAMED_STRUCT

    evolving = EvolvingStruct(f1=123)
    fixed = FixedStruct(f1=123)
    evolving_bytes = fory.serialize(evolving)
    fixed_bytes = fory.serialize(fixed)

    assert len(fixed_bytes) < len(evolving_bytes)
    assert fory.deserialize(evolving_bytes) == evolving
    assert fory.deserialize(fixed_bytes) == fixed


def test_data_class_serializer_xlang_serializer():
    """Test DataClassSerializer round-trip behavior in xlang mode."""
    fory = Fory(xlang=True, ref=True)

    # Register types first
    fory.register_type(ComplexObject, typename="example.ComplexObject")
    fory.register_type(DataClassObject, typename="example.TestDataClassObject")

    # trigger lazy serializer replace
    fory.serialize(DataClassObject.create())
    # Get the serializer that was created during registration
    serializer = fory.type_resolver.get_serializer(DataClassObject)

    # Serializer API is unified: no mode-specific serializer attribute.
    assert not hasattr(serializer, "_xlang")
    assert hasattr(serializer, "_serializers")
    assert len(serializer._serializers) == len(serializer._field_names)

    # Test that the generated methods work correctly through the normal serialization flow
    test_obj = DataClassObject(
        f_int=42,
        f_float=3.14159,
        f_str="test_codegen",
        f_bool=True,
        f_list=[1, 2, 3],
        f_dict={"key": 1.5},
        f_any="any_data",
        f_complex=None,
    )

    # Test serialization and deserialization using the normal fory flow
    binary = fory.serialize(test_obj)
    deserialized_obj = fory.deserialize(binary)

    # Verify the results
    assert deserialized_obj.f_int == test_obj.f_int
    assert deserialized_obj.f_float == test_obj.f_float
    assert deserialized_obj.f_str == test_obj.f_str
    assert deserialized_obj.f_bool == test_obj.f_bool
    assert deserialized_obj.f_list == test_obj.f_list
    assert deserialized_obj.f_dict == test_obj.f_dict
    assert deserialized_obj.f_any == test_obj.f_any
    assert deserialized_obj.f_complex == test_obj.f_complex


def test_data_class_serializer_xlang_vs_non_xlang():
    """Test that xlang and non-xlang modes use the same dataclass serializer behavior."""
    fory_xlang = Fory(xlang=True, ref=True)
    fory_python = Fory(xlang=False, ref=True, strict=False)

    # Register types for xlang
    fory_xlang.register_type(ComplexObject, typename="example.ComplexObject")
    fory_xlang.register_type(DataClassObject, typename="example.TestDataClassObject")

    # trigger lazy serializer replace
    fory_xlang.serialize(DataClassObject.create())
    # For Python mode, we can create the serializer directly since it doesn't require registration
    serializer_xlang = fory_xlang.type_resolver.get_serializer(DataClassObject)
    serializer_python = DataClassSerializer(fory_python.type_resolver, DataClassObject)

    assert not hasattr(serializer_xlang, "_xlang")
    assert not hasattr(serializer_python, "_xlang")

    # Unified serializer metadata should be mode-independent.
    assert serializer_xlang._field_names == serializer_python._field_names
    assert serializer_xlang._nullable_fields == serializer_python._nullable_fields
    assert serializer_xlang._dynamic_fields == serializer_python._dynamic_fields
    assert serializer_xlang._hash == serializer_python._hash


class MissingDefaultEnum(enum.Enum):
    A = 1
    B = 2


@dataclass
class MissingDefaultFactoryFields:
    required: int
    required_float: float
    required_str: str
    required_bytes: bytes
    required_list: List[int]
    required_set: Set[int]
    required_dict: Dict[str, int]
    plain_default: int = 7
    list_default: List[int] = dataclasses.field(default_factory=list)
    enum_default_none: MissingDefaultEnum = None


def test_build_default_values_factory():
    fory = Fory(xlang=False, ref=True, strict=False)
    type_hints = typing.get_type_hints(MissingDefaultFactoryFields)
    default_factories = build_default_values_factory(
        fory,
        type_hints,
        dataclasses.fields(MissingDefaultFactoryFields),
    )

    assert callable(default_factories["required"])
    assert callable(default_factories["required_float"])
    assert callable(default_factories["required_str"])
    assert callable(default_factories["required_bytes"])
    assert callable(default_factories["required_list"])
    assert callable(default_factories["required_set"])
    assert callable(default_factories["required_dict"])
    assert callable(default_factories["plain_default"])
    assert callable(default_factories["list_default"])
    assert callable(default_factories["enum_default_none"])

    assert default_factories["required"]() == 0
    assert default_factories["required_float"]() == 0.0
    assert default_factories["required_str"]() == ""
    assert default_factories["required_bytes"]() == b""
    list_required_one = default_factories["required_list"]()
    list_required_two = default_factories["required_list"]()
    assert list_required_one == []
    assert list_required_two == []
    assert list_required_one is not list_required_two
    set_required_one = default_factories["required_set"]()
    set_required_two = default_factories["required_set"]()
    assert set_required_one == set()
    assert set_required_two == set()
    assert set_required_one is not set_required_two
    dict_required_one = default_factories["required_dict"]()
    dict_required_two = default_factories["required_dict"]()
    assert dict_required_one == {}
    assert dict_required_two == {}
    assert dict_required_one is not dict_required_two
    assert default_factories["plain_default"]() == 7
    assert default_factories["enum_default_none"]() is MissingDefaultEnum.A
    list_one = default_factories["list_default"]()
    list_two = default_factories["list_default"]()
    assert list_one == []
    assert list_two == []
    assert list_one is not list_two


@dataclass
class OptionalFieldsObject:
    f1: Optional[int] = None
    f2: Optional[str] = None
    f3: Optional[List[int]] = None
    f4: int = 0
    f5: str = ""


@pytest.mark.parametrize("xlang", [False, True])
@pytest.mark.parametrize("compatible", [False, True])
def test_optional_fields(xlang, compatible):
    fory = Fory(xlang=xlang, ref=True, compatible=compatible, strict=False)
    if xlang:
        fory.register_type(OptionalFieldsObject, typename="example.OptionalFieldsObject")

    obj_with_none = OptionalFieldsObject(f1=None, f2=None, f3=None, f4=42, f5="test")
    result = ser_de(fory, obj_with_none)
    assert result.f1 is None
    assert result.f2 is None
    assert result.f3 is None
    assert result.f4 == 42
    assert result.f5 == "test"

    obj_with_values = OptionalFieldsObject(f1=100, f2="hello", f3=[1, 2, 3], f4=42, f5="test")
    result = ser_de(fory, obj_with_values)
    assert result.f1 == 100
    assert result.f2 == "hello"
    assert result.f3 == [1, 2, 3]
    assert result.f4 == 42
    assert result.f5 == "test"

    obj_mixed = OptionalFieldsObject(f1=100, f2=None, f3=[1, 2, 3], f4=42, f5="test")
    result = ser_de(fory, obj_mixed)
    assert result.f1 == 100
    assert result.f2 is None
    assert result.f3 == [1, 2, 3]
    assert result.f4 == 42
    assert result.f5 == "test"


@dataclass
class NestedOptionalObject:
    f1: Optional[ComplexObject] = None
    f2: Optional[Dict[str, int]] = None
    f3: str = ""


@pytest.mark.parametrize("xlang", [False, True])
@pytest.mark.parametrize("compatible", [False, True])
def test_nested_optional_fields(xlang, compatible):
    fory = Fory(xlang=xlang, ref=True, compatible=compatible, strict=False)
    if xlang:
        fory.register_type(ComplexObject, typename="example.ComplexObject")
        fory.register_type(NestedOptionalObject, typename="example.NestedOptionalObject")

    obj_with_none = NestedOptionalObject(f1=None, f2=None, f3="test")
    result = ser_de(fory, obj_with_none)
    assert result.f1 is None
    assert result.f2 is None
    assert result.f3 == "test"

    complex_obj = ComplexObject(f1="nested", f5=100, f8=3.14)
    obj_with_values = NestedOptionalObject(f1=complex_obj, f2={"a": 1, "b": 2}, f3="test")
    result = ser_de(fory, obj_with_values)
    assert result.f1.f1 == "nested"
    assert result.f1.f5 == 100
    assert result.f2 == {"a": 1, "b": 2}
    assert result.f3 == "test"


@dataclass
class OptionalV1:
    f1: Optional[int] = None
    f2: str = ""
    f3: Optional[List[int]] = None


@dataclass
class OptionalV2:
    f1: Optional[int] = None
    f2: str = ""
    f3: Optional[List[int]] = None
    f4: Optional[str] = None


@dataclass
class OptionalV3:
    f1: Optional[int] = None
    f2: str = ""


@dataclass
class CompatibleV1:
    f1: int = 0
    f2: str = ""
    f3: float = 0.0


@dataclass
class CompatibleV2:
    f1: int = 0
    f2: str = ""
    f3: float = 0.0
    f4: bool = False


@dataclass
class CompatibleV3:
    f1: int = 0
    f2: str = ""


@dataclass
class CompatibleRequiredFieldV1:
    f1: int


@dataclass
class CompatibleRequiredFieldV2:
    f1: int
    f2: int


@dataclass
class CompatibleRequiredDefaultsV1:
    f1: int


@dataclass
class CompatibleRequiredDefaultsV2:
    f1: int
    f_int: int
    f_float: float
    f_str: str
    f_bytes: bytes
    f_list: List[int]
    f_set: Set[int]
    f_dict: Dict[str, int]


@pytest.mark.parametrize("xlang", [False, True])
def test_compatible_mode_add_field(xlang):
    """Test that adding a field with default value works in compatible mode."""
    fory_v1 = Fory(xlang=xlang, ref=True, compatible=True, strict=False)
    fory_v2 = Fory(xlang=xlang, ref=True, compatible=True, strict=False)

    fory_v1.register_type(CompatibleV1, typename="example.Compatible")
    fory_v2.register_type(CompatibleV2, typename="example.Compatible")

    # V1 object serialized
    v1_obj = CompatibleV1(f1=100, f2="test", f3=3.14)
    v1_binary = fory_v1.serialize(v1_obj)

    # V2 can read V1 data, new field gets default value
    v2_result = fory_v2.deserialize(v1_binary)
    assert v2_result.f1 == 100
    assert v2_result.f2 == "test"
    assert v2_result.f3 == 3.14
    assert v2_result.f4 is False  # Default value


@pytest.mark.parametrize("xlang", [False, True])
def test_compatible_mode_remove_field(xlang):
    """Test that removing a field works in compatible mode."""
    fory_v2 = Fory(xlang=xlang, ref=True, compatible=True, strict=False)
    fory_v3 = Fory(xlang=xlang, ref=True, compatible=True, strict=False)

    fory_v2.register_type(CompatibleV2, typename="example.Compatible")
    fory_v3.register_type(CompatibleV3, typename="example.Compatible")

    # V2 object with all fields
    v2_obj = CompatibleV2(f1=200, f2="hello", f3=2.71, f4=True)
    v2_binary = fory_v2.serialize(v2_obj)

    # V3 can read V2 data, extra fields are ignored
    v3_result = fory_v3.deserialize(v2_binary)
    assert v3_result.f1 == 200
    assert v3_result.f2 == "hello"
    # f3 and f4 from V2 are ignored


@pytest.mark.parametrize("xlang", [False, True])
def test_compatible_mode_bidirectional(xlang):
    """Test bidirectional compatible serialization."""
    fory_v1 = Fory(xlang=xlang, ref=True, compatible=True, strict=False)
    fory_v2 = Fory(xlang=xlang, ref=True, compatible=True, strict=False)

    fory_v1.register_type(CompatibleV1, typename="example.Compatible")
    fory_v2.register_type(CompatibleV2, typename="example.Compatible")

    # V1 -> V2
    v1_obj = CompatibleV1(f1=100, f2="test", f3=3.14)
    v1_binary = fory_v1.serialize(v1_obj)
    v2_result = fory_v2.deserialize(v1_binary)
    assert v2_result.f1 == 100
    assert v2_result.f2 == "test"
    assert v2_result.f3 == 3.14
    assert v2_result.f4 is False

    # V2 -> V1
    v2_obj = CompatibleV2(f1=200, f2="hello", f3=2.71, f4=True)
    v2_binary = fory_v2.serialize(v2_obj)
    v1_result = fory_v1.deserialize(v2_binary)
    assert v1_result.f1 == 200
    assert v1_result.f2 == "hello"
    assert v1_result.f3 == 2.71


@pytest.mark.parametrize("xlang", [False, True])
def test_compatible_mode_add_required_field_without_default_uses_zero_value(xlang):
    fory_v1 = Fory(xlang=xlang, ref=True, compatible=True, strict=False)
    fory_v2 = Fory(xlang=xlang, ref=True, compatible=True, strict=False)

    fory_v1.register_type(CompatibleRequiredFieldV1, typename="example.CompatibleRequiredField")
    fory_v2.register_type(CompatibleRequiredFieldV2, typename="example.CompatibleRequiredField")

    v1_binary = fory_v1.serialize(CompatibleRequiredFieldV1(f1=321))
    v2_result = fory_v2.deserialize(v1_binary)

    assert v2_result.f1 == 321
    assert hasattr(v2_result, "f2")
    assert v2_result.f2 == 0

    serializer_v2 = fory_v2.type_resolver.get_serializer(CompatibleRequiredFieldV2)
    assert hasattr(serializer_v2, "_default_values_factory")
    assert callable(serializer_v2._default_values_factory["f2"])
    assert serializer_v2._default_values_factory["f2"]() == 0
    assert ser_de(fory_v2, v2_result) == v2_result


@pytest.mark.parametrize("xlang", [False, True])
def test_compatible_mode_add_required_fields_use_type_defaults(xlang):
    fory_v1 = Fory(xlang=xlang, ref=True, compatible=True, strict=False)
    fory_v2 = Fory(xlang=xlang, ref=True, compatible=True, strict=False)

    fory_v1.register_type(CompatibleRequiredDefaultsV1, typename="example.CompatibleRequiredDefaults")
    fory_v2.register_type(CompatibleRequiredDefaultsV2, typename="example.CompatibleRequiredDefaults")

    v1_binary = fory_v1.serialize(CompatibleRequiredDefaultsV1(f1=11))
    v2_result = fory_v2.deserialize(v1_binary)

    assert v2_result.f1 == 11
    assert v2_result.f_int == 0
    assert v2_result.f_float == 0.0
    assert v2_result.f_str == ""
    assert v2_result.f_bytes == b""
    assert v2_result.f_list == []
    assert v2_result.f_set == set()
    assert v2_result.f_dict == {}
    assert ser_de(fory_v2, v2_result) == v2_result


@dataclass
class CompatibleWithOptional:
    f1: Optional[int] = None
    f2: str = ""
    f3: Optional[List[int]] = None


@dataclass
class CompatibleWithOptionalV2:
    f1: Optional[int] = None
    f2: str = ""
    f3: Optional[List[int]] = None
    f4: Optional[str] = None


@pytest.mark.parametrize("xlang", [False, True])
def test_compatible_mode_with_optional_fields(xlang):
    """Test compatible mode with optional fields."""
    fory_v1 = Fory(xlang=xlang, ref=True, compatible=True, strict=False)
    fory_v2 = Fory(xlang=xlang, ref=True, compatible=True, strict=False)

    fory_v1.register_type(CompatibleWithOptional, typename="example.CompatibleOptional")
    fory_v2.register_type(CompatibleWithOptionalV2, typename="example.CompatibleOptional")

    # V1 with None values
    v1_obj = CompatibleWithOptional(f1=None, f2="test", f3=None)
    v1_binary = fory_v1.serialize(v1_obj)
    v2_result = fory_v2.deserialize(v1_binary)
    assert v2_result.f1 is None
    assert v2_result.f2 == "test"
    assert v2_result.f3 is None
    assert v2_result.f4 is None

    # V1 with values
    v1_obj2 = CompatibleWithOptional(f1=100, f2="test", f3=[1, 2, 3])
    v1_binary2 = fory_v1.serialize(v1_obj2)
    v2_result2 = fory_v2.deserialize(v1_binary2)
    assert v2_result2.f1 == 100
    assert v2_result2.f2 == "test"
    assert v2_result2.f3 == [1, 2, 3]
    assert v2_result2.f4 is None


@dataclass
class CompatibleAllTypes:
    f_int: int = 0
    f_str: str = ""
    f_float: float = 0.0
    f_bool: bool = False
    f_list: Optional[List[int]] = None
    f_dict: Optional[Dict[str, int]] = None


@dataclass
class CompatibleAllTypesV2:
    f_int: int = 0
    f_str: str = ""
    f_float: float = 0.0
    f_bool: bool = False
    f_list: Optional[List[int]] = None
    f_dict: Optional[Dict[str, int]] = None
    f_new: str = "default"


@pytest.mark.parametrize("xlang", [False, True])
def test_compatible_mode_all_basic_types(xlang):
    """Test compatible mode with all basic types."""
    fory_v1 = Fory(xlang=xlang, ref=True, compatible=True, strict=False)
    fory_v2 = Fory(xlang=xlang, ref=True, compatible=True, strict=False)

    fory_v1.register_type(CompatibleAllTypes, typename="example.CompatibleAllTypes")
    fory_v2.register_type(CompatibleAllTypesV2, typename="example.CompatibleAllTypes")

    v1_obj = CompatibleAllTypes(f_int=42, f_str="hello", f_float=3.14, f_bool=True, f_list=[1, 2, 3], f_dict={"a": 1, "b": 2})
    v1_binary = fory_v1.serialize(v1_obj)
    v2_result = fory_v2.deserialize(v1_binary)

    assert v2_result.f_int == 42
    assert v2_result.f_str == "hello"
    assert v2_result.f_float == 3.14
    assert v2_result.f_bool is True
    assert v2_result.f_list == [1, 2, 3]
    assert v2_result.f_dict == {"a": 1, "b": 2}
    assert v2_result.f_new == "default"


def test_optional_compatible_mode_evolution():
    fory_v1 = Fory(xlang=True, ref=True, compatible=True)
    fory_v2 = Fory(xlang=True, ref=True, compatible=True)
    fory_v3 = Fory(xlang=True, ref=True, compatible=True)

    fory_v1.register_type(OptionalV1, typename="example.OptionalVersioned")
    fory_v2.register_type(OptionalV2, typename="example.OptionalVersioned")
    fory_v3.register_type(OptionalV3, typename="example.OptionalVersioned")

    v1_obj = OptionalV1(f1=100, f2="test", f3=[1, 2, 3])
    v1_binary = fory_v1.serialize(v1_obj)

    v2_result = fory_v2.deserialize(v1_binary)
    assert v2_result.f1 == 100
    assert v2_result.f2 == "test"
    assert v2_result.f3 == [1, 2, 3]
    assert v2_result.f4 is None

    v1_obj_with_none = OptionalV1(f1=None, f2="test", f3=None)
    v1_binary_with_none = fory_v1.serialize(v1_obj_with_none)

    v2_result_with_none = fory_v2.deserialize(v1_binary_with_none)
    assert v2_result_with_none.f1 is None
    assert v2_result_with_none.f2 == "test"
    assert v2_result_with_none.f3 is None
    assert v2_result_with_none.f4 is None

    v2_obj = OptionalV2(f1=200, f2="test2", f3=[4, 5], f4="extra")
    v2_binary = fory_v2.serialize(v2_obj)

    v3_result = fory_v3.deserialize(v2_binary)
    assert v3_result.f1 == 200
    assert v3_result.f2 == "test2"

    v2_obj_partial_none = OptionalV2(f1=None, f2="test2", f3=None, f4=None)
    v2_binary_partial_none = fory_v2.serialize(v2_obj_partial_none)

    v3_result_partial_none = fory_v3.deserialize(v2_binary_partial_none)
    assert v3_result_partial_none.f1 is None
    assert v3_result_partial_none.f2 == "test2"

    v3_obj = OptionalV3(f1=300, f2="test3")
    v3_binary = fory_v3.serialize(v3_obj)

    v1_result = fory_v1.deserialize(v3_binary)
    assert v1_result.f1 == 300
    assert v1_result.f2 == "test3"
    assert v1_result.f3 is None


# ============================================================================
# Tests for dynamic field configuration
# ============================================================================


@dataclass
class Animal:
    name: str = pyfory.field(id=0, default="")


@dataclass
class Dog(Animal):
    breed: str = pyfory.field(id=1, default="")


@dataclass
class Zoo:
    # dynamic=True: can hold Dog instance in Animal field
    animal: Animal = pyfory.field(id=0, dynamic=True)
    # dynamic=False: use declared type's serializer, subclass info lost
    animal2: Animal = pyfory.field(id=1, dynamic=False)


def test_dynamic_with_inheritance():
    """Test dynamic=True allows polymorphic serialization with inheritance."""
    fory = Fory(xlang=False, ref=True, strict=False)
    fory.register_type(Animal)
    fory.register_type(Dog)
    fory.register_type(Zoo)

    dog1 = Dog(name="Buddy", breed="Labrador")
    dog2 = Dog(name="Rex", breed="German Shepherd")
    zoo = Zoo(animal=dog1, animal2=dog2)

    result = ser_de(fory, zoo)
    # dynamic=True: Dog type preserved
    assert isinstance(result.animal, Dog)
    assert result.animal.name == "Buddy"
    assert result.animal.breed == "Labrador"
    # dynamic=False: subclass info lost, only Animal fields deserialized
    assert isinstance(result.animal2, Animal)
    assert not isinstance(result.animal2, Dog)
    assert result.animal2.name == "Rex"
    assert not hasattr(result.animal2, "breed") or getattr(result.animal2, "breed", None) != "German Shepherd"


def test_dynamic_with_inheritance_xlang():
    """Test dynamic=True allows polymorphic serialization in xlang mode."""
    fory = Fory(xlang=True, ref=True)
    fory.register_type(Animal, typename="example.Animal")
    fory.register_type(Dog, typename="example.Dog")
    fory.register_type(Zoo, typename="example.Zoo")

    dog1 = Dog(name="Max", breed="Husky")
    dog2 = Dog(name="Luna", breed="Poodle")
    zoo = Zoo(animal=dog1, animal2=dog2)

    result = ser_de(fory, zoo)
    # dynamic=True: Dog type preserved
    assert isinstance(result.animal, Dog)
    assert result.animal.name == "Max"
    assert result.animal.breed == "Husky"
    # dynamic=False: subclass info lost, only Animal fields deserialized
    assert isinstance(result.animal2, Animal)
    assert not isinstance(result.animal2, Dog)
    assert result.animal2.name == "Luna"
    assert not hasattr(result.animal2, "breed") or getattr(result.animal2, "breed", None) != "Poodle"
