# 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.

from pyfory import Fory
from pyfory.serializer import ReduceSerializer


class BasicReduceObject:
    """Object that implements __reduce__ returning (callable, args)"""

    def __init__(self, value, multiplier=1):
        self.value = value
        self.multiplier = multiplier

    def __reduce__(self):
        return self.__class__, (self.value, self.multiplier)

    def __eq__(self, other):
        return isinstance(other, self.__class__) and self.value == other.value and self.multiplier == other.multiplier


class ReduceWithStateObject:
    """Object that implements __reduce__ returning (callable, args, state)"""

    def __init__(self, name, data=None):
        self.name = name
        self.data = data or {}
        self.secret = "hidden"

    def __reduce__(self):
        # Return (callable, args, state)
        return self.__class__, (self.name,), {"data": self.data, "secret": self.secret}

    def __setstate__(self, state):
        self.data = state["data"]
        self.secret = state["secret"]

    def __eq__(self, other):
        return isinstance(other, self.__class__) and self.name == other.name and self.data == other.data and self.secret == other.secret


class ReduceExObject:
    """Object that implements __reduce_ex__"""

    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.computed = x * y

    def __reduce_ex__(self, protocol):
        return self.__class__, (self.x, self.y)

    def __eq__(self, other):
        return isinstance(other, self.__class__) and self.x == other.x and self.y == other.y and self.computed == other.computed


class ReduceWithListItems:
    """Object that implements __reduce__ with list items"""

    def __init__(self, initial_items=None):
        self.items = list(initial_items or [])
        self.metadata = "test"

    def __reduce__(self):
        # Return (callable, args, state, listitems)
        return self.__class__, (), {"metadata": self.metadata}, iter(self.items)

    def __setstate__(self, state):
        self.metadata = state["metadata"]

    def extend(self, items):
        self.items.extend(items)

    def __eq__(self, other):
        return isinstance(other, self.__class__) and self.items == other.items and self.metadata == other.metadata


class ReduceWithDictItems:
    """Object that implements __reduce__ with dict items"""

    def __init__(self, initial_dict=None):
        self.data = dict(initial_dict or {})
        self.name = "dict_obj"

    def __reduce__(self):
        # Return (callable, args, state, listitems, dictitems)
        return self.__class__, (), {"name": self.name}, None, iter(self.data.items())

    def __setstate__(self, state):
        self.name = state["name"]

    def __setitem__(self, key, value):
        self.data[key] = value

    def __eq__(self, other):
        return isinstance(other, self.__class__) and self.data == other.data and self.name == other.name


class BothReduceAndStateful:
    """Object that has both __reduce__ and __getstate__/__setstate__
    Should use ReduceSerializer due to a higher precedence"""

    def __init__(self, value):
        self.value = value
        self.reduce_used = False
        self.state_used = False

    def __reduce__(self):
        self.reduce_used = True
        return self.__class__, (self.value,)

    def __getstate__(self):
        self.state_used = True
        return {"value": self.value, "state_used": True}

    def __setstate__(self, state):
        self.value = state["value"]
        self.state_used = state.get("state_used", False)

    def __eq__(self, other):
        return isinstance(other, self.__class__) and self.value == other.value


def test_basic_reduce_object():
    """Test basic __reduce__ functionality"""
    fory = Fory(xlang=False, ref=True, strict=False)

    obj = BasicReduceObject(42, 3)

    # Verify ReduceSerializer is used
    serializer = fory.type_resolver.get_serializer(BasicReduceObject)
    assert isinstance(serializer, ReduceSerializer)

    # Test serialization/deserialization
    serialized = fory.serialize(obj)
    deserialized = fory.deserialize(serialized)

    assert deserialized == obj
    assert deserialized.value == 42
    assert deserialized.multiplier == 3


def test_reduce_with_state_object():
    """Test __reduce__ with state"""
    fory = Fory(xlang=False, ref=True, strict=False)

    obj = ReduceWithStateObject("test", {"key": "value"})

    # Verify ReduceSerializer is used
    serializer = fory.type_resolver.get_serializer(ReduceWithStateObject)
    assert isinstance(serializer, ReduceSerializer)

    # Test serialization/deserialization
    serialized = fory.serialize(obj)
    deserialized = fory.deserialize(serialized)

    assert deserialized == obj
    assert deserialized.name == "test"
    assert deserialized.data == {"key": "value"}
    assert deserialized.secret == "hidden"


def test_reduce_ex_object():
    """Test __reduce_ex__ functionality"""
    fory = Fory(xlang=False, ref=True, strict=False)

    obj = ReduceExObject(5, 7)

    # Verify ReduceSerializer is used
    serializer = fory.type_resolver.get_serializer(ReduceExObject)
    assert isinstance(serializer, ReduceSerializer)

    # Test serialization/deserialization
    serialized = fory.serialize(obj)
    deserialized = fory.deserialize(serialized)

    assert deserialized == obj
    assert deserialized.x == 5
    assert deserialized.y == 7
    assert deserialized.computed == 35


def test_reduce_with_list_items():
    """Test __reduce__ with list items"""
    fory = Fory(xlang=False, ref=True, strict=False)

    obj = ReduceWithListItems([1, 2, 3, 4])

    # Verify ReduceSerializer is used
    serializer = fory.type_resolver.get_serializer(ReduceWithListItems)
    assert isinstance(serializer, ReduceSerializer)

    # Test serialization/deserialization
    serialized = fory.serialize(obj)
    deserialized = fory.deserialize(serialized)

    assert deserialized == obj
    assert deserialized.items == [1, 2, 3, 4]
    assert deserialized.metadata == "test"


def test_reduce_with_dict_items():
    """Test __reduce__ with dict items"""
    fory = Fory(xlang=False, ref=True, strict=False)

    obj = ReduceWithDictItems({"a": 1, "b": 2})

    # Verify ReduceSerializer is used
    serializer = fory.type_resolver.get_serializer(ReduceWithDictItems)
    assert isinstance(serializer, ReduceSerializer)

    # Test serialization/deserialization
    serialized = fory.serialize(obj)
    deserialized = fory.deserialize(serialized)

    assert deserialized == obj
    assert deserialized.data == {"a": 1, "b": 2}
    assert deserialized.name == "dict_obj"


def test_reduce_precedence_over_stateful():
    """Test that ReduceSerializer has higher precedence than StatefulSerializer"""
    fory = Fory(xlang=False, ref=True, strict=False)

    obj = BothReduceAndStateful(100)

    # Verify ReduceSerializer is used, not StatefulSerializer
    serializer = fory.type_resolver.get_serializer(BothReduceAndStateful)
    assert isinstance(serializer, ReduceSerializer)

    # Test serialization/deserialization
    serialized = fory.serialize(obj)
    deserialized = fory.deserialize(serialized)

    assert deserialized == obj
    assert deserialized.value == 100
    # The reduce method should have been used during serialization
    # (though we can't directly test this since it's called on the original object)


def test_reference_tracking():
    """Test that reference tracking works with ReduceSerializer"""
    fory = Fory(xlang=False, ref=True, strict=False)

    obj1 = BasicReduceObject(42)
    obj2 = BasicReduceObject(42)
    container = [obj1, obj1, obj2]  # obj1 appears twice

    serialized = fory.serialize(container)
    deserialized = fory.deserialize(serialized)

    assert len(deserialized) == 3
    assert deserialized[0] == obj1
    assert deserialized[1] == obj1
    assert deserialized[2] == obj2
    # Check that the first two references point to the same object
    assert deserialized[0] is deserialized[1]
    assert deserialized[0] is not deserialized[2]


def test_nested_reduce_objects():
    """Test nested objects with __reduce__"""
    fory = Fory(xlang=False, ref=True, strict=False)

    inner = BasicReduceObject(10, 2)
    outer = ReduceWithStateObject("outer", {"inner": inner})

    serialized = fory.serialize(outer)
    deserialized = fory.deserialize(serialized)

    assert deserialized == outer
    assert deserialized.name == "outer"
    assert deserialized.data["inner"] == inner
    assert deserialized.data["inner"].value == 10
    assert deserialized.data["inner"].multiplier == 2


def test_cross_language_compatibility():
    """Test cross-language compatibility"""
    fory = Fory(xlang=False, ref=True, strict=False)

    obj = BasicReduceObject(123, 4)

    # Serialize with Python
    serialized = fory.serialize(obj)

    # Should be able to deserialize (basic test)
    deserialized = fory.deserialize(serialized)
    assert deserialized == obj

    # The serialized data should use Fory's native format, not pickle
    # This is verified by the fact that we're using write_ref/read_ref
    # in the ReduceSerializer implementation
