"""Tests for BaseModel serialization functionality."""

from __future__ import annotations

import json

import msgspec

from pinecone.models._base import BaseModel


# ===== Test Models =====


class SimpleModel(BaseModel, kw_only=True):
    """Simple test model with basic fields."""

    name: str
    value: int
    optional: str | None = None


class NestedModel(BaseModel, kw_only=True):
    """Model with nested BaseModel."""

    inner: SimpleModel
    label: str


class ListModel(BaseModel, kw_only=True):
    """Model with list fields."""

    items: list[SimpleModel]
    tags: list[str]


# ===== Serialization Tests (from PR #104) =====


class TestSerialization:
    """Test .to_dict() and .to_json() methods."""

    def test_to_dict_basic(self):
        """to_dict() converts basic fields correctly."""
        model = SimpleModel(name="test", value=42)
        data = model.to_dict()

        assert isinstance(data, dict)
        assert data["name"] == "test"
        assert data["value"] == 42
        assert data["optional"] is None

    def test_to_dict_with_optional(self):
        """to_dict() includes optional fields."""
        model = SimpleModel(name="test", value=42, optional="opt")
        data = model.to_dict()

        assert data["optional"] == "opt"

    def test_to_dict_nested(self):
        """to_dict() recursively converts nested models."""
        inner = SimpleModel(name="inner", value=10)
        outer = NestedModel(inner=inner, label="outer")
        data = outer.to_dict()

        assert isinstance(data, dict)
        assert isinstance(data["inner"], dict)
        assert data["inner"]["name"] == "inner"
        assert data["inner"]["value"] == 10
        assert data["label"] == "outer"

    def test_to_dict_with_lists(self):
        """to_dict() handles lists of models."""
        items = [
            SimpleModel(name="item1", value=1),
            SimpleModel(name="item2", value=2),
        ]
        model = ListModel(items=items, tags=["a", "b"])
        data = model.to_dict()

        assert isinstance(data["items"], list)
        assert len(data["items"]) == 2
        assert data["items"][0]["name"] == "item1"
        assert data["items"][1]["value"] == 2
        assert data["tags"] == ["a", "b"]

    def test_to_json_basic(self):
        """to_json() returns valid JSON string."""
        model = SimpleModel(name="test", value=42)
        json_str = model.to_json()

        assert isinstance(json_str, str)

        # Verify it's valid JSON
        parsed = json.loads(json_str)
        assert parsed["name"] == "test"
        assert parsed["value"] == 42

    def test_to_json_nested(self):
        """to_json() handles nested models."""
        inner = SimpleModel(name="inner", value=10)
        outer = NestedModel(inner=inner, label="outer")
        json_str = outer.to_json()

        parsed = json.loads(json_str)
        assert parsed["inner"]["name"] == "inner"
        assert parsed["label"] == "outer"

    def test_to_json_utf8(self):
        """to_json() handles UTF-8 characters."""
        model = SimpleModel(name="test-√", value=42)
        json_str = model.to_json()

        parsed = json.loads(json_str)
        assert parsed["name"] == "test-√"

    def test_serialization_roundtrip(self):
        """Model can be serialized and deserialized."""
        original = SimpleModel(name="test", value=42, optional="opt")

        # to_dict roundtrip
        data = original.to_dict()
        restored = msgspec.convert(data, SimpleModel)
        assert restored.name == original.name
        assert restored.value == original.value
        assert restored.optional == original.optional

        # to_json roundtrip
        json_str = original.to_json()
        restored2 = msgspec.json.decode(json_str.encode("utf-8"), type=SimpleModel)
        assert restored2.name == original.name
        assert restored2.value == original.value


# ===== Dict Operations Via to_dict() =====


class TestDictOperations:
    """Test dict operations using to_dict()."""

    def test_dict_access_via_to_dict(self):
        """Access fields as dict by calling to_dict() first."""
        model = SimpleModel(name="test", value=42, optional="opt")

        # Convert to dict for dict operations
        data = model.to_dict()

        # Standard dict access
        assert data["name"] == "test"
        assert data["value"] == 42
        assert data["optional"] == "opt"

    def test_dynamic_field_access(self):
        """Dynamic field access via to_dict()."""
        model = SimpleModel(name="test", value=42)

        # Dynamic field name from config/user input
        field_name = "name"
        data = model.to_dict()
        assert data[field_name] == "test"

    def test_check_field_exists(self):
        """Check if field exists using 'in' on to_dict()."""
        model = SimpleModel(name="test", value=42)

        data = model.to_dict()
        assert "name" in data
        assert "value" in data
        assert "nonexistent" not in data

    def test_safe_access_with_get(self):
        """Safe field access with default using dict.get()."""
        model = SimpleModel(name="test", value=42)

        data = model.to_dict()
        assert data.get("name") == "test"
        assert data.get("nonexistent", "default") == "default"
        assert data.get("nonexistent") is None

    def test_iterate_fields(self):
        """Iterate over fields using to_dict()."""
        model = SimpleModel(name="test", value=42, optional="opt")

        # Iterate over dict
        data = model.to_dict()
        for key, value in data.items():
            # Can access original model too
            assert getattr(model, key) == value


# ===== Serialization + Field Name Handling =====


class TestFieldNameHandling:
    """Test serialization with renamed fields."""

    def test_to_dict_with_renamed_fields(self):
        """to_dict() respects rename='camel' and field(name=...)."""
        # This test documents expected behavior with field renaming

        class RenamedModel(BaseModel, kw_only=True, rename="camel"):
            user_name: str
            user_id: int

        model = RenamedModel(user_name="alice", user_id=123)

        # Attribute access uses Python names
        assert model.user_name == "alice"
        assert model.user_id == 123

        # to_dict() uses serialized names (camelCase)
        data = model.to_dict()
        assert data == {"userName": "alice", "userId": 123}

        # Access dict via serialized names
        assert data["userName"] == "alice"
        assert data["userId"] == 123

    def test_to_dict_with_field_name_override(self):
        """to_dict() respects field(name=...)."""
        from msgspec import field

        class FieldNameModel(BaseModel, kw_only=True):
            id: str = field(name="_id")
            score: float = field(name="_score")

        model = FieldNameModel(id="123", score=0.95)

        # Attribute access uses Python names
        assert model.id == "123"
        assert model.score == 0.95

        # to_dict() uses serialized names
        data = model.to_dict()
        assert data == {"_id": "123", "_score": 0.95}

        # Access dict via serialized names
        assert data["_id"] == "123"
        assert data["_score"] == 0.95

    def test_dynamic_access_via_to_dict(self):
        """Dynamic field access works via to_dict()."""
        model = SimpleModel(name="test", value=42, optional="opt")

        # Convert to dict for dynamic access
        data = model.to_dict()

        # Pattern 1: Extract specific fields dynamically
        fields_to_extract = ["name", "value"]
        extracted = {field: data[field] for field in fields_to_extract}
        assert extracted == {"name": "test", "value": 42}

        # Pattern 2: Check field exists
        has_optional = "optional" in data
        assert has_optional is True

        # Pattern 3: Safe access with fallback
        tags = data.get("tags", [])
        assert tags == []


# ===== Backward Compatibility Tests =====


class TestBackwardCompatibility:
    """Ensure BaseModel doesn't break existing functionality."""

    def test_attribute_access_unchanged(self):
        """Attribute access (primary interface) still works."""
        model = SimpleModel(name="test", value=42)

        # Direct attribute access (not dict-style)
        assert model.name == "test"
        assert model.value == 42
        assert model.optional is None

    def test_msgspec_features_preserved(self):
        """msgspec.Struct features still work."""
        model = SimpleModel(name="test", value=42, optional="opt")

        # msgspec.structs.astuple
        as_tuple = msgspec.structs.astuple(model)
        assert as_tuple == ("test", 42, "opt")

        # msgspec.structs.replace
        model2 = msgspec.structs.replace(model, value=100)
        assert model2.value == 100
        assert model2.name == "test"  # Unchanged

        # msgspec.structs.asdict (different from .to_dict())
        as_dict = msgspec.structs.asdict(model)
        assert as_dict == {"name": "test", "value": 42, "optional": "opt"}

    def test_isinstance_checks(self):
        """isinstance checks still work."""
        model = SimpleModel(name="test", value=42)

        assert isinstance(model, BaseModel)
        assert isinstance(model, msgspec.Struct)
        assert isinstance(model, SimpleModel)

    def test_type_hints_preserved(self):
        """Type hints work correctly."""
        model: SimpleModel = SimpleModel(name="test", value=42)

        # Type checkers should accept this
        name: str = model.name
        value: int = model.value
        optional: str | None = model.optional

        assert name == "test"
        assert value == 42
        assert optional is None


# ===== Edge Cases =====


class TestEdgeCases:
    """Test edge cases and corner scenarios."""

    def test_empty_model(self):
        """Model with no fields works."""

        class EmptyModel(BaseModel):
            pass

        model = EmptyModel()

        # Serialization works
        assert model.to_dict() == {}
        assert model.to_json() == "{}"

    def test_model_with_none_values(self):
        """Model with None values serializes correctly."""
        model = SimpleModel(name="test", value=0, optional=None)

        # Attribute access
        assert model.optional is None

        # Serialization preserves None
        data = model.to_dict()
        assert data["optional"] is None

        json_str = model.to_json()
        assert "null" in json_str

    def test_model_with_empty_collections(self):
        """Model with empty lists works."""
        model = ListModel(items=[], tags=[])

        # Attribute access
        assert model.items == []
        assert model.tags == []

        # Dict access via to_dict()
        data = model.to_dict()
        assert data["items"] == []
        assert data["tags"] == []

    def test_to_dict_includes_all_fields(self):
        """to_dict() includes all struct fields."""
        model = SimpleModel(name="test", value=42, optional="opt")

        data = model.to_dict()

        # All fields present
        assert set(data.keys()) == {"name", "value", "optional"}

        # Dict matches attribute values
        assert data["name"] == model.name
        assert data["value"] == model.value
