"""
Tests for GateWay Result Serialization Module.
"""

import time

import pytest

from verlex_server.gateway.core.results import (
    COMPRESSION_THRESHOLD,
    MemoryResultStorage,
    ResultDeserializationError,
    ResultManager,
    ResultMetadata,
    ResultStatus,
    ResultType,
    SerializedResult,
    deserialize_result,
    serialize_result,
)


class TestResultTypes:
    """Tests for result type detection."""

    def test_simple_value(self):
        """Test serialization of simple values."""
        result = serialize_result("job_1", 42)
        assert result.metadata.result_type == ResultType.VALUE
        assert result.metadata.size_bytes > 0

        value = deserialize_result(result)
        assert value == 42

    def test_string_value(self):
        """Test serialization of strings."""
        result = serialize_result("job_2", "hello world")
        value = deserialize_result(result)
        assert value == "hello world"

    def test_dict_value(self):
        """Test serialization of dictionaries."""
        original = {"key": "value", "numbers": [1, 2, 3]}
        result = serialize_result("job_3", original)
        value = deserialize_result(result)
        assert value == original

    def test_list_value(self):
        """Test serialization of lists."""
        original = [1, 2, 3, "hello", {"nested": True}]
        result = serialize_result("job_4", original)
        value = deserialize_result(result)
        assert value == original

    def test_none_value(self):
        """Test serialization of None."""
        result = serialize_result("job_5", None)
        assert result.metadata.result_type == ResultType.NONE
        value = deserialize_result(result)
        assert value is None

    def test_exception_value(self):
        """Test serialization of exceptions."""
        original = ValueError("test error")
        result = serialize_result("job_6", original)
        assert result.metadata.result_type == ResultType.ERROR

        value = deserialize_result(result)
        # Exception type may not be preserved exactly due to __builtins__ behavior
        assert isinstance(value, Exception)
        assert "test error" in str(value)


class TestCompression:
    """Tests for result compression."""

    def test_small_value_not_compressed(self):
        """Small values should not be compressed."""
        result = serialize_result("job_1", 42)
        assert not result.metadata.compressed

    def test_large_value_compressed(self):
        """Large values should be compressed."""
        large_data = "x" * (COMPRESSION_THRESHOLD + 1000)
        result = serialize_result("job_2", large_data)

        # Should be compressed
        assert result.metadata.compressed
        assert result.metadata.compressed_size_bytes < result.metadata.size_bytes

    def test_compression_disabled(self):
        """Test with compression disabled."""
        large_data = "x" * (COMPRESSION_THRESHOLD + 1000)
        result = serialize_result("job_3", large_data, compress=False)

        assert not result.metadata.compressed
        value = deserialize_result(result)
        assert value == large_data


class TestChecksum:
    """Tests for checksum verification."""

    def test_checksum_valid(self):
        """Valid checksum should pass verification."""
        result = serialize_result("job_1", {"data": "test"})
        value = deserialize_result(result, verify_checksum=True)
        assert value == {"data": "test"}

    def test_checksum_corrupted(self):
        """Corrupted payload should fail checksum."""
        result = serialize_result("job_2", {"data": "test"})

        # Corrupt the payload
        corrupted = bytearray(result.payload)
        corrupted[0] = (corrupted[0] + 1) % 256
        result.payload = bytes(corrupted)

        with pytest.raises(ResultDeserializationError):
            deserialize_result(result, verify_checksum=True)

    def test_checksum_skip(self):
        """Skipping checksum should not raise error."""
        result = serialize_result("job_3", {"data": "test"})

        # Corrupt the checksum (not the payload)
        result.metadata.compressed_checksum = "invalid"

        # Should not raise when verification is skipped
        # (But may still fail if we corrupt wrong thing)


class TestResultMetadata:
    """Tests for result metadata."""

    def test_metadata_creation(self):
        """Test metadata is created correctly."""
        result = serialize_result("job_123", "test")

        assert result.metadata.job_id == "job_123"
        assert result.metadata.status == ResultStatus.AVAILABLE
        assert result.metadata.serializer == "cloudpickle"
        assert result.metadata.created_at > 0
        assert result.metadata.checksum is not None

    def test_metadata_expiry(self):
        """Test TTL is set correctly."""
        result = serialize_result("job_1", "test", ttl_seconds=60)

        assert result.metadata.expires_at is not None
        assert result.metadata.expires_at > time.time()
        assert result.metadata.expires_at < time.time() + 120

    def test_metadata_no_expiry(self):
        """Test no expiry when TTL is None."""
        result = serialize_result("job_2", "test", ttl_seconds=None)
        assert result.metadata.expires_at is None

    def test_metadata_to_dict(self):
        """Test metadata serialization."""
        result = serialize_result("job_3", "test")
        meta_dict = result.metadata.to_dict()

        assert meta_dict["job_id"] == "job_3"
        assert meta_dict["result_type"] == "value"
        assert meta_dict["status"] == "available"

    def test_metadata_from_dict(self):
        """Test metadata deserialization."""
        result = serialize_result("job_4", "test")
        meta_dict = result.metadata.to_dict()

        restored = ResultMetadata.from_dict(meta_dict)
        assert restored.job_id == result.metadata.job_id
        assert restored.result_type == result.metadata.result_type


class TestSerializedResult:
    """Tests for SerializedResult transport."""

    def test_to_transport_dict(self):
        """Test conversion to transport format."""
        result = serialize_result("job_1", {"key": "value"})
        transport = result.to_transport_dict()

        assert "metadata" in transport
        assert "payload" in transport
        assert isinstance(transport["payload"], str)  # Base64 encoded

    def test_from_transport_dict(self):
        """Test restoration from transport format."""
        original = serialize_result("job_2", {"key": "value"})
        transport = original.to_transport_dict()

        restored = SerializedResult.from_transport_dict(transport)
        value = deserialize_result(restored)

        assert value == {"key": "value"}


class TestMemoryResultStorage:
    """Tests for in-memory result storage."""

    @pytest.fixture
    def storage(self):
        return MemoryResultStorage(max_size_mb=10)

    @pytest.mark.asyncio
    async def test_store_retrieve(self, storage):
        """Test basic store and retrieve."""
        result = serialize_result("job_1", "test data")
        await storage.store(result)

        retrieved = await storage.retrieve("job_1")
        assert retrieved is not None

        value = deserialize_result(retrieved)
        assert value == "test data"

    @pytest.mark.asyncio
    async def test_delete(self, storage):
        """Test result deletion."""
        result = serialize_result("job_2", "test data")
        await storage.store(result)

        deleted = await storage.delete("job_2")
        assert deleted

        retrieved = await storage.retrieve("job_2")
        assert retrieved is None

    @pytest.mark.asyncio
    async def test_exists(self, storage):
        """Test existence check."""
        result = serialize_result("job_3", "test data")

        assert not await storage.exists("job_3")

        await storage.store(result)
        assert await storage.exists("job_3")

    @pytest.mark.asyncio
    async def test_get_metadata(self, storage):
        """Test metadata retrieval."""
        result = serialize_result("job_4", "test data")
        await storage.store(result)

        metadata = await storage.get_metadata("job_4")
        assert metadata is not None
        assert metadata.job_id == "job_4"

    @pytest.mark.asyncio
    async def test_expiry(self, storage):
        """Test expired results are cleaned up."""
        # Create result that expires immediately
        result = serialize_result("job_5", "test data", ttl_seconds=0)
        result.metadata.expires_at = time.time() - 1  # Already expired

        await storage.store(result)

        # Should be gone on retrieve
        retrieved = await storage.retrieve("job_5")
        assert retrieved is None

    @pytest.mark.asyncio
    async def test_cleanup_expired(self, storage):
        """Test cleanup of expired results."""
        # Store some results
        for i in range(5):
            result = serialize_result(f"job_{i}", f"data_{i}")
            result.metadata.expires_at = time.time() - 1  # Already expired
            await storage.store(result)

        count = await storage.cleanup_expired()
        assert count == 5

    @pytest.mark.asyncio
    async def test_eviction_on_capacity(self, storage):
        """Test oldest results are evicted when capacity reached."""
        # Fill storage with data
        for i in range(100):
            large_data = "x" * 100000  # 100KB each
            result = serialize_result(f"job_{i}", large_data)
            await storage.store(result)

        # Should have evicted some old results
        # (10MB limit / 100KB per result = max ~100 results, but compression helps)


class TestResultManager:
    """Tests for ResultManager."""

    @pytest.fixture
    def manager(self):
        return ResultManager()

    @pytest.mark.asyncio
    async def test_store_and_get(self, manager):
        """Test storing and retrieving results."""
        await manager.store_result("job_1", {"result": "data"})

        result = await manager.get_result("job_1")
        assert result == {"result": "data"}

    @pytest.mark.asyncio
    async def test_get_nonexistent(self, manager):
        """Test retrieving nonexistent result."""
        result = await manager.get_result("nonexistent")
        assert result is None

    @pytest.mark.asyncio
    async def test_delete_result(self, manager):
        """Test deleting results."""
        await manager.store_result("job_2", "test")

        deleted = await manager.delete_result("job_2")
        assert deleted

        result = await manager.get_result("job_2")
        assert result is None

    @pytest.mark.asyncio
    async def test_result_exists(self, manager):
        """Test existence check."""
        assert not await manager.result_exists("job_3")

        await manager.store_result("job_3", "test")
        assert await manager.result_exists("job_3")

    @pytest.mark.asyncio
    async def test_get_metadata(self, manager):
        """Test metadata retrieval."""
        await manager.store_result("job_4", "test")

        metadata = await manager.get_metadata("job_4")
        assert metadata is not None
        assert metadata.job_id == "job_4"
