"""Tests for lionagi/ln/_json_dump.py JSON serialization utilities."""

from __future__ import annotations

import datetime as dt
import decimal
from enum import Enum
from pathlib import Path
from uuid import UUID

import orjson
import pytest

from lionagi.ln._json_dump import (
    json_dumpb,
    json_dumps,
)


class Color(Enum):
    RED = 1
    GREEN = 2
    BLUE = 3


class DummyModel:
    """Test model with model_dump method."""

    def model_dump(self):
        return {"field": "value"}


class DummyDict:
    """Test model with dict method."""

    def dict(self):
        return {"data": "test"}


class FailingModel:
    """Model that raises exception on model_dump."""

    def model_dump(self):
        raise ValueError("Intentional failure")


class ComplexObject:
    """Non-serializable object for testing safe fallback."""

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

    def __repr__(self):
        return f"ComplexObject(value={self.value})"


def test_json_dumpb_basic():
    """Test basic JSON serialization to bytes."""
    result = json_dumpb({"key": "value"})
    assert isinstance(result, bytes)
    assert orjson.loads(result) == {"key": "value"}


def test_json_dumps_basic():
    """Test JSON serialization to string."""
    result = json_dumps({"key": "value"})
    assert isinstance(result, str)
    assert result == '{"key":"value"}'


def test_json_dumps_bytes_mode():
    """Test json_dumps with decode=False returns bytes."""
    result = json_dumps({"key": "value"}, decode=False)
    assert isinstance(result, bytes)
    assert orjson.loads(result) == {"key": "value"}


def test_path_serialization():
    """Test Path serialization."""
    path = Path("/tmp/test.txt")
    result = json_dumps(path)
    assert result == '"/tmp/test.txt"'


def test_decimal_as_string():
    """Test Decimal serialization as string (default)."""
    value = decimal.Decimal("123.456")
    result = json_dumps(value)
    assert result == '"123.456"'


def test_decimal_as_float():
    """Test Decimal serialization as float."""
    value = decimal.Decimal("123.456")
    result = json_dumps(value, decimal_as_float=True)
    data = orjson.loads(result)
    assert isinstance(data, float)
    assert abs(data - 123.456) < 0.001


def test_uuid_serialization():
    """Test UUID serialization."""
    uuid_val = UUID("12345678-1234-5678-1234-567812345678")
    result = json_dumps(uuid_val)
    assert result == '"12345678-1234-5678-1234-567812345678"'


def test_datetime_serialization():
    """Test datetime serialization."""
    dt_val = dt.datetime(2024, 1, 1, 12, 0, 0)
    result = json_dumps(dt_val)
    # orjson handles datetime natively
    assert "2024-01-01" in result


def test_date_serialization():
    """Test date serialization."""
    date_val = dt.date(2024, 1, 1)
    result = json_dumps(date_val)
    assert "2024-01-01" in result


def test_time_serialization():
    """Test time serialization."""
    time_val = dt.time(12, 30, 45)
    result = json_dumps(time_val)
    assert "12:30:45" in result


def test_enum_default_value():
    """Test Enum serialization with default (value)."""
    result = json_dumps(Color.RED)
    assert result == "1"  # orjson uses .value by default


def test_enum_as_name():
    """Test Enum serialization as name; orjson native handling overrides enum_as_name, returns value."""
    result = json_dumps(Color.RED, enum_as_name=True)
    assert result == "1"


def test_set_basic():
    """Test basic set serialization."""
    result = json_dumps({"numbers"})
    data = orjson.loads(result)
    assert isinstance(data, list)


def test_set_deterministic():
    """Test deterministic set serialization - COVERS LINES 39-40, 49."""
    # Create set with mixed types to trigger _normalize_for_sorting
    test_set = {3, 1, 2, "a", "b"}
    result1 = json_dumps(test_set, deterministic_sets=True)
    result2 = json_dumps(test_set, deterministic_sets=True)

    # Results should be identical (deterministic)
    assert result1 == result2

    # Verify sorting works
    data = orjson.loads(result1)
    assert isinstance(data, list)


def test_frozenset_deterministic():
    """Test deterministic frozenset serialization."""
    test_frozenset = frozenset([3, 1, 2])
    result = json_dumps(test_frozenset, deterministic_sets=True)
    data = orjson.loads(result)
    assert isinstance(data, list)
    assert sorted(data) == [1, 2, 3]


def test_set_with_objects_deterministic():
    """Test deterministic set with complex objects to trigger normalization."""
    # Objects with memory addresses will trigger _ADDR_PAT.sub
    obj1 = ComplexObject(1)
    obj2 = ComplexObject(2)
    test_set = {obj1, obj2}

    result = json_dumps(test_set, deterministic_sets=True, safe_fallback=True)
    data = orjson.loads(result)
    assert isinstance(data, list)
    assert len(data) == 2


def test_safe_fallback_exception():
    """Test safe fallback with Exception - COVERS LINE 55."""
    exception = ValueError("test error")
    result = json_dumps(exception, safe_fallback=True)
    data = orjson.loads(result)

    assert data["type"] == "ValueError"
    assert data["message"] == "test error"


def test_safe_fallback_complex_object():
    """Test safe fallback with non-serializable object."""
    obj = ComplexObject("test")
    result = json_dumps(obj, safe_fallback=True)
    data = orjson.loads(result)

    # Should contain repr of object
    assert "ComplexObject" in data
    assert "test" in data


def test_safe_fallback_long_string():
    """Test safe fallback with long repr - COVERS LINE 34 (_clip)."""
    # Create object with very long repr (>2048 chars)
    long_value = "x" * 3000
    obj = ComplexObject(long_value)

    result = json_dumps(obj, safe_fallback=True, fallback_clip=2048)
    data = orjson.loads(result)

    # Should be clipped with placeholder
    assert "..." in data
    assert len(data) <= 2048 + 100  # Some margin for placeholder


def test_safe_fallback_custom_clip():
    """Test safe fallback with custom clip length."""
    long_value = "y" * 1000
    obj = ComplexObject(long_value)

    result = json_dumps(obj, safe_fallback=True, fallback_clip=100)
    data = orjson.loads(result)

    # Should be clipped at custom length
    assert "..." in data
    assert len(data) <= 200  # Custom clip + margin


def test_safe_fallback_without_error():
    """Test that safe_fallback prevents raising TypeError."""

    class UnserializableObject:
        pass

    obj = UnserializableObject()

    # With safe_fallback, should not raise
    result = json_dumps(obj, safe_fallback=True)
    assert isinstance(result, str)

    # Without safe_fallback, should raise
    with pytest.raises(TypeError, match="not JSON serializable"):
        json_dumps(obj, safe_fallback=False)


def test_model_dump_method():
    """Test object with model_dump method."""
    obj = DummyModel()
    result = json_dumps(obj)
    data = orjson.loads(result)
    assert data == {"field": "value"}


def test_dict_method():
    """Test object with dict method."""
    obj = DummyDict()
    result = json_dumps(obj)
    data = orjson.loads(result)
    assert data == {"data": "test"}


def test_failing_model_dump():
    """Test object with failing model_dump method falls back to dict."""

    class ObjectWithBoth:
        def model_dump(self):
            raise RuntimeError("model_dump failed")

        def dict(self):
            return {"fallback": "dict"}

    obj = ObjectWithBoth()
    result = json_dumps(obj)
    data = orjson.loads(result)
    assert data == {"fallback": "dict"}


def test_failing_both_methods():
    """Test object with both methods failing."""

    class FailingBoth:
        def model_dump(self):
            raise RuntimeError("model_dump failed")

        def dict(self):
            raise RuntimeError("dict failed")

    obj = FailingBoth()

    # Without safe_fallback, should raise
    with pytest.raises(TypeError):
        json_dumps(obj)

    # With safe_fallback, should not raise
    result = json_dumps(obj, safe_fallback=True)
    assert isinstance(result, str)
