"""
tests/test_storage.py — Integration tests for SQLiteAdapter.

All tests use an in-memory SQLite DB (':memory:') to keep them fast
and isolated. Each test function gets a fresh adapter via the fixture.
"""
from __future__ import annotations

import pytest
import pytest_asyncio

from cortex.config import CortexConfig
from cortex.storage import SQLiteAdapter
from cortex.types import (
    CornerstoneMemory,
    Entity,
    EpisodeEvent,
    FTSResult,
    MemoryEdge,
    MemoryFeedback,
    SemanticClaim,
    SessionRecord,
    VectorResult,
)


# ---------------------------------------------------------------------------
# Fixture
# ---------------------------------------------------------------------------

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


# ---------------------------------------------------------------------------
# Schema smoke test
# ---------------------------------------------------------------------------

class TestSchema:
    async def test_schema_version(self, adapter: SQLiteAdapter):
        version = await adapter.get_config("schema_version")
        assert version == "12"  # v12 adds temporal + temporal_marker columns (#573)

    async def test_tables_exist(self, adapter: SQLiteAdapter):
        """All v3 tables should exist after initialize()."""
        conn = await adapter._get_conn()
        expected_tables = {
            "entity", "entity_alias", "cornerstone_memory", "session_record",
            "episode_event", "semantic_claim", "claim_provenance",
            "procedure_memory", "brain_graph_index", "brain_graph_node",
            "brain_graph_edge", "memory_edge", "embedding_index",
            "retrieval_log", "drift_log", "deprecation_log",
            "memory_feedback", "cortex_config",
            "memory_activation", "memory_evidence", "handoff_packet",
            "open_loop", "commitment",
        }
        async with conn.execute(
            "SELECT name FROM sqlite_master WHERE type='table'"
        ) as cur:
            rows = await cur.fetchall()
        actual = {r[0] for r in rows}
        missing = expected_tables - actual
        assert not missing, f"Missing tables: {missing}"

    async def test_fts_tables_exist(self, adapter: SQLiteAdapter):
        conn = await adapter._get_conn()
        async with conn.execute(
            "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'fts_%'"
        ) as cur:
            rows = await cur.fetchall()
        names = {r[0] for r in rows}
        assert "fts_claims" in names
        assert "fts_episodes" in names

    async def test_idempotent_initialize(self, adapter: SQLiteAdapter):
        """Calling initialize() twice must not raise."""
        await adapter._run_migration()  # should be a no-op


# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------

class TestConfig:
    async def test_get_config_default_values(self, adapter: SQLiteAdapter):
        dim = await adapter.get_config("embedding_dim")
        assert dim == "1536"

    async def test_set_and_get_config(self, adapter: SQLiteAdapter):
        await adapter.set_config("test_key", "test_value")
        val = await adapter.get_config("test_key")
        assert val == "test_value"

    async def test_set_config_upsert(self, adapter: SQLiteAdapter):
        await adapter.set_config("owner_id", "alice")
        await adapter.set_config("owner_id", "bob")
        val = await adapter.get_config("owner_id")
        assert val == "bob"

    async def test_get_missing_key_returns_none(self, adapter: SQLiteAdapter):
        val = await adapter.get_config("nonexistent_key_xyz")
        assert val is None


# ---------------------------------------------------------------------------
# Entity
# ---------------------------------------------------------------------------

class TestEntity:
    async def test_create_entity(self, adapter: SQLiteAdapter):
        e = await adapter.create_entity("Andrew", entity_type="person")
        assert isinstance(e, Entity)
        assert e.canonical_name == "Andrew"
        assert e.entity_type == "person"
        assert e.id is not None

    async def test_get_entity(self, adapter: SQLiteAdapter):
        e = await adapter.create_entity("Eva", entity_type="agent")
        fetched = await adapter.get_entity(e.id)
        assert fetched is not None
        assert fetched.canonical_name == "Eva"

    async def test_get_entity_not_found(self, adapter: SQLiteAdapter):
        result = await adapter.get_entity("nonexistent-id")
        assert result is None

    async def test_find_entity_by_alias(self, adapter: SQLiteAdapter):
        e = await adapter.create_entity("OpenClaw", entity_type="product")
        await adapter.add_entity_alias(e.id, "oc", alias_type="abbreviation")
        found = await adapter.find_entity_by_alias("oc")
        assert found is not None
        assert found.id == e.id

    async def test_find_entity_by_alias_case_insensitive(self, adapter: SQLiteAdapter):
        e = await adapter.create_entity("OpenClaw", entity_type="product")
        await adapter.add_entity_alias(e.id, "OpenClaw", alias_type="name")
        found = await adapter.find_entity_by_alias("OPENCLAW")
        assert found is not None
        assert found.id == e.id

    async def test_find_entity_by_alias_not_found(self, adapter: SQLiteAdapter):
        result = await adapter.find_entity_by_alias("nobody")
        assert result is None

    async def test_merge_entities(self, adapter: SQLiteAdapter):
        e1 = await adapter.create_entity("E1", entity_type="concept")
        e2 = await adapter.create_entity("E2", entity_type="concept")
        await adapter.add_entity_alias(e2.id, "e2alias")

        merged = await adapter.merge_entities(keep_id=e1.id, merge_id=e2.id)
        assert merged.id == e1.id

        # e2 should be gone
        gone = await adapter.get_entity(e2.id)
        assert gone is None

        # alias should now point to e1
        found = await adapter.find_entity_by_alias("e2alias")
        assert found is not None
        assert found.id == e1.id


# ---------------------------------------------------------------------------
# Cornerstone
# ---------------------------------------------------------------------------

class TestCornerstone:
    async def test_seal_cornerstone(self, adapter: SQLiteAdapter):
        cs = await adapter.seal_cornerstone(
            label="identity", content="Eva is a cognitive assistant."
        )
        assert isinstance(cs, CornerstoneMemory)
        assert cs.label == "identity"
        assert cs.golden_hash == cs.current_hash

    async def test_get_cornerstones_empty(self, adapter: SQLiteAdapter):
        results = await adapter.get_cornerstones()
        assert results == []

    async def test_get_cornerstones(self, adapter: SQLiteAdapter):
        await adapter.seal_cornerstone("c1", "Content 1", injection_order=2)
        await adapter.seal_cornerstone("c2", "Content 2", injection_order=1)
        cornerstones = await adapter.get_cornerstones()
        assert len(cornerstones) == 2
        # Should be ordered by injection_order
        assert cornerstones[0].label == "c2"
        assert cornerstones[1].label == "c1"

    async def test_update_cornerstone_drift(self, adapter: SQLiteAdapter):
        cs = await adapter.seal_cornerstone("cs", "Golden content")
        await adapter.update_cornerstone_drift(cs.id, drift_score=0.4)

        conn = await adapter._get_conn()
        async with conn.execute(
            "SELECT drift_score FROM cornerstone_memory WHERE id = ?", (cs.id,)
        ) as cur:
            row = await cur.fetchone()
        assert row is not None
        assert abs(row[0] - 0.4) < 1e-6

    async def test_restore_cornerstone(self, adapter: SQLiteAdapter):
        cs = await adapter.seal_cornerstone("cs", "Original content")
        # Simulate drift by directly updating content
        conn = await adapter._get_conn()
        await conn.execute(
            "UPDATE cornerstone_memory SET content = 'Drifted content', drift_score = 0.9 WHERE id = ?",
            (cs.id,),
        )
        await conn.commit()

        restored = await adapter.restore_cornerstone(cs.id)
        assert restored.content == "Original content"
        assert restored.drift_score == 0.0

    async def test_update_cornerstone_content_sets_drift_on_divergence(self, adapter: SQLiteAdapter):
        cs = await adapter.seal_cornerstone("identity", "I am Eva and I help Andrew.")

        revised = await adapter.update_cornerstone_content(
            cs.id,
            content="I help.",
            reason="compression test",
        )

        assert revised.drift_score > 0.0
        assert revised.last_drift_check is not None

    async def test_update_cornerstone_content_resets_drift_when_restored_text(self, adapter: SQLiteAdapter):
        cs = await adapter.seal_cornerstone("identity", "I am Eva and I help Andrew.")

        await adapter.update_cornerstone_content(cs.id, content="I help.", reason="compression test")
        revised = await adapter.update_cornerstone_content(cs.id, content=cs.golden_copy, reason="restore text")

        assert revised.drift_score == 0.0


# ---------------------------------------------------------------------------
# Session
# ---------------------------------------------------------------------------

class TestSession:
    async def test_create_session(self, adapter: SQLiteAdapter):
        s = await adapter.create_session("sess-001")
        assert isinstance(s, SessionRecord)
        assert s.session_id == "sess-001"
        assert s.status == "awake"

    async def test_get_active_session(self, adapter: SQLiteAdapter):
        await adapter.create_session("sess-active")
        s = await adapter.get_active_session()
        assert s is not None
        assert s.session_id == "sess-active"

    async def test_get_active_session_none_when_sleeping(self, adapter: SQLiteAdapter):
        s = await adapter.create_session("sess-sleep")
        await adapter.update_session("sess-sleep", status="sleeping")
        active = await adapter.get_active_session()
        assert active is None

    async def test_update_session(self, adapter: SQLiteAdapter):
        await adapter.create_session("sess-update")
        updated = await adapter.update_session(
            "sess-update", turn_count=5, fatigue_score=0.3
        )
        assert updated.turn_count == 5
        assert abs(updated.fatigue_score - 0.3) < 1e-6


# ---------------------------------------------------------------------------
# Episode
# ---------------------------------------------------------------------------

class TestEpisode:
    async def test_log_episode(self, adapter: SQLiteAdapter):
        s = await adapter.create_session("sess-ep")
        ev = await adapter.log_episode(
            session_id=s.id,
            actor_id="user",
            kind="message",
            raw_text="Hello world",
        )
        assert isinstance(ev, EpisodeEvent)
        assert ev.raw_text == "Hello world"
        assert ev.actor_id == "user"

    async def test_episode_synced_to_fts(self, adapter: SQLiteAdapter):
        s = await adapter.create_session("sess-fts-ep")
        await adapter.log_episode(
            session_id=s.id,
            actor_id="agent",
            kind="message",
            raw_text="The quick brown fox",
        )
        results = await adapter.fts_search("quick fox", table="episodes")
        assert len(results) >= 1
        assert results[0].item_id is not None


# ---------------------------------------------------------------------------
# Semantic Claim (CRUD)
# ---------------------------------------------------------------------------

class TestSemanticClaim:
    async def test_create_claim(self, adapter: SQLiteAdapter):
        c = await adapter.create_claim(
            subject="Andrew",
            predicate="prefers",
            object_value="dark mode",
        )
        assert isinstance(c, SemanticClaim)
        assert c.subject == "Andrew"
        assert c.status == "active"

    async def test_get_claim(self, adapter: SQLiteAdapter):
        c = await adapter.create_claim("A", "is", "B")
        fetched = await adapter.get_claim(c.id)
        assert fetched is not None
        assert fetched.id == c.id

    async def test_update_claim(self, adapter: SQLiteAdapter):
        c = await adapter.create_claim("X", "likes", "Y")
        updated = await adapter.update_claim(c.id, confidence=0.9, status="cooling")
        assert abs(updated.confidence - 0.9) < 1e-6
        assert updated.status == "cooling"

    async def test_get_claims_by_subject(self, adapter: SQLiteAdapter):
        await adapter.create_claim("Eva", "knows", "Python")
        await adapter.create_claim("Eva", "uses", "OpenClaw")
        await adapter.create_claim("Andrew", "likes", "coffee")

        claims = await adapter.get_claims_by_subject("Eva")
        assert len(claims) == 2
        subjects = {c.subject for c in claims}
        assert subjects == {"Eva"}

    async def test_get_claims_by_status(self, adapter: SQLiteAdapter):
        c1 = await adapter.create_claim("A", "p1", "v1")
        c2 = await adapter.create_claim("B", "p2", "v2")
        await adapter.update_claim(c2.id, status="deprecated")

        active = await adapter.get_claims_by_status("active")
        deprecated = await adapter.get_claims_by_status("deprecated")
        assert any(c.id == c1.id for c in active)
        assert any(c.id == c2.id for c in deprecated)

    async def test_get_claims_by_entity(self, adapter: SQLiteAdapter):
        e = await adapter.create_entity("Lucca", entity_type="character")
        c = await adapter.create_claim(
            "Lucca", "is", "engineer", subject_entity_id=e.id
        )
        claims = await adapter.get_claims_by_entity(e.id)
        assert len(claims) == 1
        assert claims[0].id == c.id

    async def test_get_session_claims(self, adapter: SQLiteAdapter):
        s = await adapter.create_session("sess-sc")
        c = await adapter.create_claim(
            "Eva", "session_note", "hello", source_session=s.id
        )
        claims = await adapter.get_session_claims(s.id)
        assert len(claims) == 1
        assert claims[0].id == c.id

    async def test_claim_synced_to_fts(self, adapter: SQLiteAdapter):
        await adapter.create_claim("Eva", "speaks", "Python fluently")
        results = await adapter.fts_search("Python fluently", table="claims")
        assert len(results) >= 1


# ---------------------------------------------------------------------------
# Provenance
# ---------------------------------------------------------------------------

class TestProvenance:
    async def test_add_provenance(self, adapter: SQLiteAdapter):
        s = await adapter.create_session("sess-prov")
        ev = await adapter.log_episode(s.id, "user", "message", "raw text")
        c = await adapter.create_claim("sub", "pred", "obj")
        prov = await adapter.add_provenance(
            claim_id=c.id,
            episode_event_id=ev.id,
            extraction_method="llm_extract",
        )
        assert prov.claim_id == c.id
        assert prov.episode_event_id == ev.id
        assert prov.extraction_method == "llm_extract"


# ---------------------------------------------------------------------------
# Embeddings + vector search
# ---------------------------------------------------------------------------

class TestEmbeddings:
    async def test_store_and_retrieve_embedding(self, adapter: SQLiteAdapter):
        c = await adapter.create_claim("sub", "pred", "obj")
        embedding = [0.1] * 8
        await adapter.store_embedding("claim", c.id, embedding, "test-model")

        results = await adapter.vector_search(embedding, item_type="claim", top_k=5)
        assert len(results) >= 1
        assert results[0].item_id == c.id
        assert results[0].score > 0.99  # identical vector → cosine ≈ 1

    async def test_vector_search_top_k(self, adapter: SQLiteAdapter):
        for i in range(5):
            c = await adapter.create_claim(f"sub{i}", "pred", "obj")
            vec = [float(i) / 10] * 8
            await adapter.store_embedding("claim", c.id, vec, "test-model")

        query = [0.4] * 8
        results = await adapter.vector_search(query, item_type="claim", top_k=3)
        assert len(results) == 3

    async def test_vector_search_no_type_filter(self, adapter: SQLiteAdapter):
        c = await adapter.create_claim("sub", "pred", "obj")
        s = await adapter.create_session("sess-vec")
        ev = await adapter.log_episode(s.id, "user", "message", "text")

        await adapter.store_embedding("claim", c.id, [1.0, 0.0], "m")
        await adapter.store_embedding("event", ev.id, [0.0, 1.0], "m")

        results = await adapter.vector_search([1.0, 0.0], top_k=10)
        types = {r.item_type for r in results}
        assert "claim" in types

    async def test_vector_search_owner_isolation(self, adapter: SQLiteAdapter):
        a = await adapter.create_claim("Alice", "likes", "sushi", owner_id="owner-a")
        b = await adapter.create_claim("Bob", "likes", "sushi", owner_id="owner-b")
        emb = [0.3, 0.2, 0.1]
        await adapter.store_embedding("claim", a.id, emb, "m")
        await adapter.store_embedding("claim", b.id, emb, "m")

        results = await adapter.vector_search(emb, item_type="claim", top_k=10, owner_id="owner-a")
        ids = {r.item_id for r in results}
        assert a.id in ids
        assert b.id not in ids


# ---------------------------------------------------------------------------
# FTS search
# ---------------------------------------------------------------------------

class TestFTS:
    async def test_fts_search_claims(self, adapter: SQLiteAdapter):
        await adapter.create_claim("Eva", "remembers", "memorable moments from Tokyo")
        results = await adapter.fts_search("Tokyo moments", table="claims")
        assert len(results) >= 1

    async def test_fts_search_episodes(self, adapter: SQLiteAdapter):
        s = await adapter.create_session("sess-fts2")
        await adapter.log_episode(s.id, "user", "message", "Let's visit Tokyo again")
        results = await adapter.fts_search("visit Tokyo", table="episodes")
        assert len(results) >= 1

    async def test_fts_search_claims_owner_isolation(self, adapter: SQLiteAdapter):
        a = await adapter.create_claim("Alice", "lives_in", "Bangkok", owner_id="owner-a")
        b = await adapter.create_claim("Bob", "lives_in", "Bangkok", owner_id="owner-b")
        results = await adapter.fts_search("Bangkok", table="claims", owner_id="owner-a")
        ids = {r.item_id for r in results}
        assert a.id in ids
        assert b.id not in ids

    async def test_fts_invalid_table(self, adapter: SQLiteAdapter):
        with pytest.raises(ValueError, match="Unknown FTS table"):
            await adapter.fts_search("anything", table="invalid_table")

    async def test_fts_no_results(self, adapter: SQLiteAdapter):
        results = await adapter.fts_search("xyzzy_nonexistent_term_zzz")
        assert results == []

    async def test_fts_claim_update_syncs_content(self, adapter: SQLiteAdapter):
        """Updating object_value must refresh the FTS index."""
        c = await adapter.create_claim("Eva", "speaks", "Python fluently")

        # Old content is findable
        old_results = await adapter.fts_search("Python fluently", table="claims")
        assert any(r.item_id == c.id for r in old_results)

        # Update the object_value
        await adapter.update_claim(c.id, object_value="Rust and Python fluently")

        # New content is now findable
        new_results = await adapter.fts_search("Rust fluently", table="claims")
        assert any(r.item_id == c.id for r in new_results)

    async def test_fts_claim_hard_delete_removes_entry(self, adapter: SQLiteAdapter):
        """Hard-deleting a claim must remove its FTS entry."""
        c = await adapter.create_claim("Eva", "knows", "secret unicorn lore")

        # Confirm it's indexed
        before = await adapter.fts_search("unicorn lore", table="claims")
        assert any(r.item_id == c.id for r in before)

        await adapter.delete_claim(c.id, owner_id="default", hard=True)

        after = await adapter.fts_search("unicorn lore", table="claims")
        assert not any(r.item_id == c.id for r in after)

    async def test_fts_claim_soft_delete_removes_entry(self, adapter: SQLiteAdapter):
        """Soft-deleting (archiving) a claim must remove its FTS entry."""
        c = await adapter.create_claim("Eva", "knows", "dragon breath mechanics")

        # Confirm it's indexed
        before = await adapter.fts_search("dragon breath", table="claims")
        assert any(r.item_id == c.id for r in before)

        await adapter.delete_claim(c.id, owner_id="default", hard=False)

        after = await adapter.fts_search("dragon breath", table="claims")
        assert not any(r.item_id == c.id for r in after)


# ---------------------------------------------------------------------------
# Memory edges
# ---------------------------------------------------------------------------

class TestMemoryEdge:
    async def test_create_edge(self, adapter: SQLiteAdapter):
        c1 = await adapter.create_claim("A", "p", "v1")
        c2 = await adapter.create_claim("A", "p", "v2")
        edge = await adapter.create_edge(
            source_type="claim", source_id=c1.id,
            target_type="claim", target_id=c2.id,
            edge_type="supersedes",
        )
        assert isinstance(edge, MemoryEdge)
        assert edge.edge_type == "supersedes"

    async def test_get_neighbors(self, adapter: SQLiteAdapter):
        c1 = await adapter.create_claim("N1", "p", "v")
        c2 = await adapter.create_claim("N2", "p", "v")
        await adapter.create_edge("claim", c1.id, "claim", c2.id, "related_to")

        neighbors = await adapter.get_neighbors("claim", c1.id)
        assert len(neighbors) == 1
        assert neighbors[0].target_id == c2.id

    async def test_get_neighbors_filtered_by_type(self, adapter: SQLiteAdapter):
        c1 = await adapter.create_claim("N", "p", "v")
        c2 = await adapter.create_claim("N2", "p", "v")
        c3 = await adapter.create_claim("N3", "p", "v")
        await adapter.create_edge("claim", c1.id, "claim", c2.id, "supports")
        await adapter.create_edge("claim", c1.id, "claim", c3.id, "contradicts")

        filtered = await adapter.get_neighbors("claim", c1.id, edge_types=["supports"])
        assert len(filtered) == 1
        assert filtered[0].edge_type == "supports"


# ---------------------------------------------------------------------------
# Brain Graph
# ---------------------------------------------------------------------------

class TestBrainGraph:
    async def test_create_brain_graph(self, adapter: SQLiteAdapter):
        bg = await adapter.create_brain_graph("api-docs", "api_doc")
        assert bg.name == "api-docs"
        assert bg.status == "building"

    async def test_add_node_and_edge(self, adapter: SQLiteAdapter):
        bg = await adapter.create_brain_graph("graph1", "codebase")
        n1 = await adapter.add_brain_graph_node(bg.id, "concept", "Authentication")
        n2 = await adapter.add_brain_graph_node(bg.id, "endpoint", "/auth/login")
        e = await adapter.add_brain_graph_edge(
            bg.id, n1.id, n2.id, "related_to"
        )
        assert e.source_id == n1.id
        assert e.target_id == n2.id

    async def test_get_brain_graph_nodes(self, adapter: SQLiteAdapter):
        bg = await adapter.create_brain_graph("g2", "domain_knowledge")
        await adapter.add_brain_graph_node(bg.id, "concept", "C1")
        await adapter.add_brain_graph_node(bg.id, "concept", "C2")
        await adapter.add_brain_graph_node(bg.id, "rule", "R1")

        all_nodes = await adapter.get_brain_graph_nodes(bg.id)
        assert len(all_nodes) == 3

        concepts = await adapter.get_brain_graph_nodes(bg.id, node_type="concept")
        assert len(concepts) == 2

    async def test_node_count_increments(self, adapter: SQLiteAdapter):
        bg = await adapter.create_brain_graph("g3", "api_doc")
        await adapter.add_brain_graph_node(bg.id, "concept", "N1")
        await adapter.add_brain_graph_node(bg.id, "concept", "N2")

        conn = await adapter._get_conn()
        async with conn.execute(
            "SELECT node_count FROM brain_graph_index WHERE id = ?", (bg.id,)
        ) as cur:
            row = await cur.fetchone()
        assert row[0] == 2


# ---------------------------------------------------------------------------
# Feedback
# ---------------------------------------------------------------------------

class TestFeedback:
    async def test_create_feedback(self, adapter: SQLiteAdapter):
        c = await adapter.create_claim("sub", "pred", "obj")
        fb = await adapter.create_feedback(
            target_type="claim",
            target_id=c.id,
            feedback_type="wrong",
            feedback_value="Actually it's different",
        )
        assert isinstance(fb, MemoryFeedback)
        assert fb.status == "pending"

    async def test_get_pending_feedback(self, adapter: SQLiteAdapter):
        c = await adapter.create_claim("sub", "pred", "obj")
        fb = await adapter.create_feedback("claim", c.id, "outdated")
        pending = await adapter.get_pending_feedback()
        assert any(f.id == fb.id for f in pending)

    async def test_apply_feedback(self, adapter: SQLiteAdapter):
        c = await adapter.create_claim("sub", "pred", "obj")
        fb = await adapter.create_feedback("claim", c.id, "wrong")
        applied = await adapter.apply_feedback(fb.id)
        assert applied.status == "applied"
        assert applied.applied_at is not None

        pending = await adapter.get_pending_feedback()
        assert not any(f.id == fb.id for f in pending)


# ---------------------------------------------------------------------------
# Logs
# ---------------------------------------------------------------------------

class TestLogs:
    async def test_log_retrieval(self, adapter: SQLiteAdapter):
        await adapter.log_retrieval(
            query_text="What does Eva remember?",
            mode="hybrid",
            token_budget=3000,
            tokens_used=150,
        )
        conn = await adapter._get_conn()
        async with conn.execute("SELECT COUNT(*) FROM retrieval_log") as cur:
            row = await cur.fetchone()
        assert row[0] == 1

    async def test_log_drift(self, adapter: SQLiteAdapter):
        cs = await adapter.seal_cornerstone("cs", "Golden text")
        await adapter.log_drift(
            cornerstone_id=cs.id,
            drift_score=0.42,
            action_taken="warn",
            current_text="Slightly different",
            golden_text="Golden text",
        )
        conn = await adapter._get_conn()
        async with conn.execute("SELECT COUNT(*) FROM drift_log") as cur:
            row = await cur.fetchone()
        assert row[0] == 1

    async def test_log_deprecation(self, adapter: SQLiteAdapter):
        c = await adapter.create_claim("sub", "pred", "obj")
        await adapter.log_deprecation(
            claim_id=c.id,
            action="promoted",
            reason="high confidence",
            classifier="llm_judge",
        )
        conn = await adapter._get_conn()
        async with conn.execute("SELECT COUNT(*) FROM deprecation_log") as cur:
            row = await cur.fetchone()
        assert row[0] == 1


# ---------------------------------------------------------------------------
# CortexConfig loader
# ---------------------------------------------------------------------------

class TestCortexConfig:
    def test_defaults(self):
        cfg = CortexConfig.defaults()
        assert cfg.storage_backend == "sqlite"
        assert cfg.storage_enable_wal is True
        assert cfg.storage_busy_timeout_ms == 5000
        assert cfg.embedding_dim == 1024

    def test_overlay(self):
        cfg = CortexConfig.from_toml(overlay={"cortex": {"storage": {"db_path": "/tmp/test.db"}}})
        assert cfg.storage_db_path == "/tmp/test.db"

    def test_db_path_resolved_absolute(self):
        cfg = CortexConfig()
        cfg.storage_db_path = "/absolute/cortex.db"
        assert str(cfg.db_path_resolved()) == "/absolute/cortex.db"

    def test_db_path_resolved_relative(self, tmp_path):
        cfg = CortexConfig()
        cfg.storage_db_path = "data/cortex.db"
        resolved = cfg.db_path_resolved(base=tmp_path)
        assert resolved == tmp_path / "data" / "cortex.db"
