"""Tests for ContextRecord storage in offline stores (Phase 3 of CRS-001)."""

import pytest
from datetime import datetime, timezone

from fabra.store.offline import DuckDBOfflineStore
from fabra.models import (
    ContextRecord,
    FeatureRecord,
    RetrievedItemRecord,
    AssemblyDecisions,
    DroppedItem,
    LineageMetadata,
    IntegrityMetadata,
)
from fabra.utils.integrity import (
    compute_content_hash,
    compute_record_hash,
    verify_content_integrity,
)


def _create_test_record(
    context_id: str = "ctx_test-12345",
    content: str = "Test context content",
    context_function: str = "test_context",
    environment: str = "development",
) -> ContextRecord:
    """Create a ContextRecord for testing."""
    now = datetime.now(timezone.utc)
    content_hash = compute_content_hash(content)

    # Create record without final hash first
    record = ContextRecord(
        context_id=context_id,
        created_at=now,
        environment=environment,
        schema_version="1.0.0",
        inputs={"user_id": "u123", "query": "test query"},
        context_function=context_function,
        content=content,
        token_count=len(content.split()),
        features=[
            FeatureRecord(
                name="user_tier",
                entity_id="u123",
                value="premium",
                source="cache",
                as_of=now,
                freshness_ms=50,
            )
        ],
        retrieved_items=[
            RetrievedItemRecord(
                retriever="doc_search",
                chunk_id="chunk_001",
                document_id="doc_123",
                content_hash="sha256:abc123",
                content="Retrieved content",
                token_count=10,
                priority=1,
                similarity_score=0.92,
                as_of=now,
                freshness_ms=100,
            )
        ],
        assembly=AssemblyDecisions(
            max_tokens=1000,
            tokens_used=100,
            items_provided=3,
            items_included=2,
            dropped_items=[
                DroppedItem(
                    source_id="extra_item",
                    priority=0,
                    token_count=50,
                    reason="budget_exceeded",
                )
            ],
            freshness_status="guaranteed",
        ),
        lineage=LineageMetadata(
            features_used=["user_tier"],
            retrievers_used=["doc_search"],
            indexes_used=["docs_index"],
            fabra_version="2.0.7",
            assembly_latency_ms=25.5,
        ),
        integrity=IntegrityMetadata(
            record_hash="",
            content_hash=content_hash,
        ),
    )

    # Compute and set the record hash
    record_hash = compute_record_hash(record)
    return ContextRecord(
        **{
            **record.model_dump(),
            "integrity": {"record_hash": record_hash, "content_hash": content_hash},
        }
    )


class TestDuckDBRecordStorage:
    """Tests for DuckDB ContextRecord storage."""

    @pytest.fixture
    def store(self):
        """Create an in-memory DuckDB store."""
        return DuckDBOfflineStore(":memory:")

    @pytest.mark.asyncio
    async def test_log_and_retrieve_record(self, store):
        """Should store and retrieve a ContextRecord."""
        record = _create_test_record()

        # Store the record
        context_id = await store.log_record(record)
        assert context_id == record.context_id

        # Retrieve the record
        retrieved = await store.get_record(context_id)
        assert retrieved is not None
        assert retrieved.context_id == record.context_id
        assert retrieved.content == record.content
        assert retrieved.context_function == record.context_function
        assert retrieved.environment == record.environment

    @pytest.mark.asyncio
    async def test_record_integrity_preserved(self, store):
        """Stored record should pass integrity verification."""
        record = _create_test_record()
        await store.log_record(record)

        retrieved = await store.get_record(record.context_id)
        assert retrieved is not None
        assert verify_content_integrity(retrieved)
        # Note: record_hash won't match after round-trip due to timestamp serialization differences
        # This is expected behavior - integrity is verified at creation time

    @pytest.mark.asyncio
    async def test_record_features_preserved(self, store):
        """Features should be preserved after storage."""
        record = _create_test_record()
        await store.log_record(record)

        retrieved = await store.get_record(record.context_id)
        assert retrieved is not None
        assert len(retrieved.features) == 1
        assert retrieved.features[0].name == "user_tier"
        assert retrieved.features[0].value == "premium"
        assert retrieved.features[0].source == "cache"

    @pytest.mark.asyncio
    async def test_record_retrieved_items_preserved(self, store):
        """Retrieved items should be preserved after storage."""
        record = _create_test_record()
        await store.log_record(record)

        retrieved = await store.get_record(record.context_id)
        assert retrieved is not None
        assert len(retrieved.retrieved_items) == 1
        assert retrieved.retrieved_items[0].retriever == "doc_search"
        assert retrieved.retrieved_items[0].chunk_id == "chunk_001"
        assert retrieved.retrieved_items[0].similarity_score == 0.92

    @pytest.mark.asyncio
    async def test_record_assembly_preserved(self, store):
        """Assembly decisions should be preserved after storage."""
        record = _create_test_record()
        await store.log_record(record)

        retrieved = await store.get_record(record.context_id)
        assert retrieved is not None
        assert retrieved.assembly.max_tokens == 1000
        assert retrieved.assembly.tokens_used == 100
        assert retrieved.assembly.items_provided == 3
        assert retrieved.assembly.items_included == 2
        assert len(retrieved.assembly.dropped_items) == 1
        assert retrieved.assembly.dropped_items[0].source_id == "extra_item"

    @pytest.mark.asyncio
    async def test_record_lineage_preserved(self, store):
        """Lineage metadata should be preserved after storage."""
        record = _create_test_record()
        await store.log_record(record)

        retrieved = await store.get_record(record.context_id)
        assert retrieved is not None
        assert retrieved.lineage.features_used == ["user_tier"]
        assert retrieved.lineage.retrievers_used == ["doc_search"]
        assert retrieved.lineage.fabra_version == "2.0.7"

    @pytest.mark.asyncio
    async def test_get_nonexistent_record(self, store):
        """Should return None for non-existent record."""
        retrieved = await store.get_record("ctx_nonexistent")
        assert retrieved is None

    @pytest.mark.asyncio
    async def test_list_records_empty(self, store):
        """Should return empty list when no records exist."""
        records = await store.list_records()
        assert records == []

    @pytest.mark.asyncio
    async def test_list_records_basic(self, store):
        """Should list stored records."""
        record1 = _create_test_record(context_id="ctx_test-001")
        record2 = _create_test_record(context_id="ctx_test-002")

        await store.log_record(record1)
        await store.log_record(record2)

        records = await store.list_records()
        assert len(records) == 2

    @pytest.mark.asyncio
    async def test_list_records_filter_by_function(self, store):
        """Should filter records by context function."""
        record1 = _create_test_record(
            context_id="ctx_test-001", context_function="func_a"
        )
        record2 = _create_test_record(
            context_id="ctx_test-002", context_function="func_b"
        )

        await store.log_record(record1)
        await store.log_record(record2)

        records = await store.list_records(context_function="func_a")
        assert len(records) == 1
        assert records[0]["context_function"] == "func_a"

    @pytest.mark.asyncio
    async def test_list_records_filter_by_environment(self, store):
        """Should filter records by environment."""
        record1 = _create_test_record(
            context_id="ctx_test-001", environment="development"
        )
        record2 = _create_test_record(
            context_id="ctx_test-002", environment="production"
        )

        await store.log_record(record1)
        await store.log_record(record2)

        records = await store.list_records(environment="production")
        assert len(records) == 1
        assert records[0]["environment"] == "production"

    @pytest.mark.asyncio
    async def test_list_records_limit(self, store):
        """Should respect limit parameter."""
        for i in range(5):
            record = _create_test_record(context_id=f"ctx_test-{i:03d}")
            await store.log_record(record)

        records = await store.list_records(limit=2)
        assert len(records) == 2

    @pytest.mark.asyncio
    async def test_record_upsert(self, store):
        """Should reject overwriting existing records (immutability)."""
        record1 = _create_test_record(
            context_id="ctx_test-001", content="Original content"
        )
        await store.log_record(record1)

        record2 = _create_test_record(
            context_id="ctx_test-001", content="Updated content"
        )
        from fabra.exceptions import ImmutableRecordError

        with pytest.raises(ImmutableRecordError):
            await store.log_record(record2)

        retrieved = await store.get_record("ctx_test-001")
        assert retrieved is not None
        assert retrieved.content == "Original content"

        # Should still only have one record
        records = await store.list_records()
        assert len(records) == 1


class TestRecordInputsPreservation:
    """Tests for inputs field preservation."""

    @pytest.fixture
    def store(self):
        """Create an in-memory DuckDB store."""
        return DuckDBOfflineStore(":memory:")

    @pytest.mark.asyncio
    async def test_inputs_preserved(self, store):
        """Inputs dict should be preserved after storage."""
        record = _create_test_record()
        await store.log_record(record)

        retrieved = await store.get_record(record.context_id)
        assert retrieved is not None
        assert retrieved.inputs == {"user_id": "u123", "query": "test query"}

    @pytest.mark.asyncio
    async def test_complex_inputs_preserved(self, store):
        """Complex nested inputs should be preserved."""
        now = datetime.now(timezone.utc)
        content_hash = compute_content_hash("Test content")

        record = ContextRecord(
            context_id="ctx_complex-inputs",
            created_at=now,
            environment="development",
            schema_version="1.0.0",
            inputs={
                "user_id": "u123",
                "filters": {"status": ["active", "pending"], "min_score": 0.5},
                "nested": {"deep": {"value": 42}},
            },
            context_function="test_func",
            content="Test content",
            token_count=2,
            features=[],
            retrieved_items=[],
            assembly=AssemblyDecisions(),
            lineage=LineageMetadata(fabra_version="2.0.7"),
            integrity=IntegrityMetadata(record_hash="", content_hash=content_hash),
        )

        # Compute hash
        record_hash = compute_record_hash(record)
        record = ContextRecord(
            **{
                **record.model_dump(),
                "integrity": {"record_hash": record_hash, "content_hash": content_hash},
            }
        )

        await store.log_record(record)
        retrieved = await store.get_record(record.context_id)

        assert retrieved is not None
        assert retrieved.inputs["filters"]["status"] == ["active", "pending"]
        assert retrieved.inputs["nested"]["deep"]["value"] == 42
