"""
tests/test_v10_locomo_storage.py — Tests for v10 LoCoMo storage plumbing (#516).

Verifies:
1. Migration adds columns to semantic_claim
2. Round-trip: create claim with LoCoMo fields, read back
3. Default fallback: claim without LoCoMo fields gets defaults
4. Reconciliation forwards LoCoMo fields from CandidateClaim
5. Status mapping: CandidateClaim status="completed" stores correctly
6. Pre-v10 compatibility: _row_to_claim handles missing columns
"""
from __future__ import annotations

import pytest
import pytest_asyncio
import aiosqlite
from unittest.mock import AsyncMock, MagicMock

from cortex.config import CortexConfig
from cortex.storage import SQLiteAdapter
from cortex.types import CandidateClaim, SemanticClaim


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------

@pytest_asyncio.fixture
async def adapter():
    """Fresh in-memory SQLiteAdapter with all migrations applied."""
    a = SQLiteAdapter(db_path=":memory:", busy_timeout_ms=1000, enable_wal=False)
    await a.initialize()
    yield a
    await a.close()


async def _create_test_session(adapter: SQLiteAdapter, session_id: str = "test-session-id", owner_id: str = "test-owner"):
    """Insert a minimal session_record row so FK constraints pass."""
    conn = await adapter._get_conn()
    await conn.execute(
        "INSERT OR IGNORE INTO session_record (id, owner_id, session_id, started_at, created_at) VALUES (?, ?, ?, datetime('now'), datetime('now'))",
        (session_id, owner_id, session_id),
    )
    await conn.commit()


# ---------------------------------------------------------------------------
# 1. Migration test
# ---------------------------------------------------------------------------

class TestV10Migration:
    async def test_columns_exist(self, adapter: SQLiteAdapter):
        """v10 migration should add memory_class, event_date, sequence_order, salience."""
        conn = await adapter._get_conn()
        async with conn.execute("PRAGMA table_info(semantic_claim)") as cur:
            rows = await cur.fetchall()
        col_names = {row[1] for row in rows}
        assert "memory_class" in col_names
        assert "event_date" in col_names
        assert "sequence_order" in col_names
        assert "salience" in col_names

    async def test_schema_version_is_10(self, adapter: SQLiteAdapter):
        # v11 (contradictions table, #512) has been applied; schema_version is now 11
        version = await adapter.get_config("schema_version")
        assert int(version) >= 10

    async def test_indexes_exist(self, adapter: SQLiteAdapter):
        conn = await adapter._get_conn()
        async with conn.execute(
            "SELECT name FROM sqlite_master WHERE type='index' AND name LIKE 'idx_claim_%'"
        ) as cur:
            rows = await cur.fetchall()
        idx_names = {row[0] for row in rows}
        assert "idx_claim_memory_class" in idx_names
        assert "idx_claim_event_date" in idx_names
        assert "idx_claim_salience" in idx_names


# ---------------------------------------------------------------------------
# 2. Round-trip test
# ---------------------------------------------------------------------------

class TestLoCoMoRoundTrip:
    async def test_create_and_read_with_all_fields(self, adapter: SQLiteAdapter):
        """Create a claim with all LoCoMo fields, read back, verify."""
        claim = await adapter.create_claim(
            subject="Andrew",
            predicate="visited",
            object_value="Tokyo last summer",
            owner_id="test-owner",
            memory_class="episodic",
            event_date="2025-07-15",
            sequence_order=3,
            salience="high",
        )
        assert claim.memory_class == "episodic"
        assert claim.event_date == "2025-07-15"
        assert claim.sequence_order == 3
        assert claim.salience == "high"

        # Read back via get_claim
        fetched = await adapter.get_claim(claim.id, owner_id="test-owner")
        assert fetched is not None
        assert fetched.memory_class == "episodic"
        assert fetched.event_date == "2025-07-15"
        assert fetched.sequence_order == 3
        assert fetched.salience == "high"

    async def test_sequence_order_zero_is_valid(self, adapter: SQLiteAdapter):
        """sequence_order=0 should be stored, not treated as falsy."""
        claim = await adapter.create_claim(
            subject="User",
            predicate="said",
            object_value="hello",
            owner_id="test-owner",
            sequence_order=0,
        )
        fetched = await adapter.get_claim(claim.id, owner_id="test-owner")
        assert fetched is not None
        assert fetched.sequence_order == 0


# ---------------------------------------------------------------------------
# 3. Default fallback test
# ---------------------------------------------------------------------------

class TestLoCoMoDefaults:
    async def test_defaults_when_not_provided(self, adapter: SQLiteAdapter):
        """Claim created without LoCoMo fields should get correct defaults."""
        claim = await adapter.create_claim(
            subject="Eva",
            predicate="likes",
            object_value="engineering",
            owner_id="test-owner",
        )
        assert claim.memory_class == "semantic"
        assert claim.event_date is None
        assert claim.sequence_order is None
        assert claim.salience == "medium"


# ---------------------------------------------------------------------------
# 4. Reconciliation forwarding test
# ---------------------------------------------------------------------------

class TestReconciliationForwarding:
    async def test_locomo_fields_forwarded(self, adapter: SQLiteAdapter):
        """CandidateClaim with LoCoMo fields should have them reach storage via reconciliation."""
        from cortex.core.reconciliation import ReconciliationEngine

        await _create_test_session(adapter)
        config = CortexConfig()
        engine = ReconciliationEngine(storage=adapter, config=config, llm=MagicMock(), entity_resolver=MagicMock())

        candidate = CandidateClaim(
            subject="Andrew",
            predicate="enjoys",
            object_value="hiking in the mountains",
            confidence=0.9,
            memory_class="episodic",
            event_date="2025-03-10",
            sequence_order=1,
            salience="high",
        )

        # Use the internal _add_claim method
        claim = await engine._add_claim(
            candidate=candidate,
            session_id="test-session-id",
            owner_id="test-owner",
        )

        assert claim.memory_class == "episodic"
        assert claim.event_date == "2025-03-10"
        assert claim.sequence_order == 1
        assert claim.salience == "high"

        # Also verify from DB read
        fetched = await adapter.get_claim(claim.id, owner_id="test-owner")
        assert fetched is not None
        assert fetched.memory_class == "episodic"
        assert fetched.event_date == "2025-03-10"

    async def test_locomo_defaults_when_candidate_lacks_fields(self, adapter: SQLiteAdapter):
        """CandidateClaim without LoCoMo fields should still store with defaults."""
        from cortex.core.reconciliation import ReconciliationEngine

        await _create_test_session(adapter)
        config = CortexConfig()
        engine = ReconciliationEngine(storage=adapter, config=config, llm=MagicMock(), entity_resolver=MagicMock())

        candidate = CandidateClaim(
            subject="Eva",
            predicate="knows",
            object_value="Python well",
            confidence=0.8,
        )

        claim = await engine._add_claim(
            candidate=candidate,
            session_id="test-session-id",
            owner_id="test-owner",
        )

        assert claim.memory_class == "semantic"
        assert claim.event_date is None
        assert claim.sequence_order is None
        assert claim.salience == "medium"


# ---------------------------------------------------------------------------
# 5. Status mapping test
# ---------------------------------------------------------------------------

class TestStatusMapping:
    async def test_completed_status_from_candidate(self, adapter: SQLiteAdapter):
        """CandidateClaim with status='completed' should store as status='completed'."""
        from cortex.core.reconciliation import ReconciliationEngine

        await _create_test_session(adapter)
        config = CortexConfig()
        engine = ReconciliationEngine(storage=adapter, config=config, llm=MagicMock(), entity_resolver=MagicMock())

        candidate = CandidateClaim(
            subject="Task",
            predicate="is",
            object_value="done",
            confidence=0.9,
            status="completed",
        )

        claim = await engine._add_claim(
            candidate=candidate,
            session_id="test-session-id",
            owner_id="test-owner",
        )

        assert claim.status == "completed"

        fetched = await adapter.get_claim(claim.id, owner_id="test-owner")
        assert fetched is not None
        assert fetched.status == "completed"

    async def test_active_status_not_overridden(self, adapter: SQLiteAdapter):
        """CandidateClaim with status='active' should not override (it's the default)."""
        from cortex.core.reconciliation import ReconciliationEngine

        await _create_test_session(adapter)
        config = CortexConfig()
        engine = ReconciliationEngine(storage=adapter, config=config, llm=MagicMock(), entity_resolver=MagicMock())

        candidate = CandidateClaim(
            subject="Fact",
            predicate="is",
            object_value="true",
            confidence=0.8,
            status="active",
        )

        claim = await engine._add_claim(
            candidate=candidate,
            session_id="test-session-id",
            owner_id="test-owner",
        )

        assert claim.status == "active"


# ---------------------------------------------------------------------------
# 6. Pre-v10 compatibility test
# ---------------------------------------------------------------------------

class TestPreV10Compatibility:
    async def test_row_to_claim_without_new_columns(self):
        """_row_to_claim should handle rows missing LoCoMo columns gracefully."""
        from cortex.storage._helpers import _row_to_claim

        # Simulate a pre-v10 row by creating a minimal DB without migration
        async with aiosqlite.connect(":memory:") as conn:
            conn.row_factory = aiosqlite.Row
            # Create a minimal semantic_claim table WITHOUT LoCoMo columns
            await conn.execute("""
                CREATE TABLE semantic_claim (
                    id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
                    owner_id TEXT NOT NULL DEFAULT 'default',
                    subject TEXT NOT NULL,
                    predicate TEXT NOT NULL,
                    object_value TEXT NOT NULL,
                    memory_type TEXT DEFAULT 'session',
                    subject_entity_id TEXT,
                    object_entity_id TEXT,
                    datatype TEXT DEFAULT 'text',
                    confidence REAL DEFAULT 0.5,
                    sensitivity TEXT DEFAULT 'normal',
                    scope TEXT DEFAULT 'user',
                    valid_from TEXT,
                    valid_to TEXT,
                    status TEXT DEFAULT 'active',
                    superseded_by TEXT,
                    last_accessed TEXT,
                    access_count INTEGER DEFAULT 0,
                    source_session TEXT,
                    created_at TEXT,
                    updated_at TEXT
                )
            """)
            await conn.execute("""
                INSERT INTO semantic_claim (subject, predicate, object_value)
                VALUES ('Test', 'is', 'valid')
            """)
            await conn.commit()

            async with conn.execute("SELECT * FROM semantic_claim LIMIT 1") as cur:
                row = await cur.fetchone()

            claim = _row_to_claim(row)
            # Should use dataclass defaults for missing columns
            assert claim.memory_class == "semantic"
            assert claim.event_date is None
            assert claim.sequence_order is None
            assert claim.salience == "medium"
            # Existing fields should be correct
            assert claim.subject == "Test"
            assert claim.predicate == "is"
            assert claim.object_value == "valid"
