"""Tests for schema migrations (v1→v2, v2→v3, ..., v7→v8)."""

from __future__ import annotations

import sqlite3

import pytest

from claude_engram.migration import _migrate_v1_to_v2, get_schema_version, migrate_schema


@pytest.fixture()
def v1_db(tmp_path) -> sqlite3.Connection:
    """Create a v1 database with the original memories table (no new columns)."""
    db_path = tmp_path / "test.db"
    conn = sqlite3.connect(str(db_path))
    conn.executescript("""
        CREATE TABLE memories (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            content TEXT NOT NULL,
            category TEXT DEFAULT 'general',
            metadata TEXT DEFAULT '{}',
            created_at TEXT NOT NULL,
            updated_at TEXT NOT NULL
        );
        CREATE INDEX idx_memories_category ON memories(category);
        CREATE INDEX idx_memories_created ON memories(created_at);
    """)
    conn.commit()
    return conn


@pytest.fixture()
def empty_db(tmp_path) -> sqlite3.Connection:
    """Create an empty database with no tables."""
    db_path = tmp_path / "empty.db"
    conn = sqlite3.connect(str(db_path))
    return conn


class TestGetSchemaVersion:
    """Tests for get_schema_version()."""

    def test_fresh_db_returns_zero(self, empty_db: sqlite3.Connection) -> None:
        assert get_schema_version(empty_db) == 0

    def test_v1_db_returns_zero(self, v1_db: sqlite3.Connection) -> None:
        # v1 didn't set user_version, so it stays 0
        assert get_schema_version(v1_db) == 0

    def test_after_migration_returns_latest(self, v1_db: sqlite3.Connection) -> None:
        migrate_schema(v1_db)
        assert get_schema_version(v1_db) == 10

    def test_manual_version_set(self, empty_db: sqlite3.Connection) -> None:
        empty_db.execute("PRAGMA user_version = 8")
        assert get_schema_version(empty_db) == 8


class TestMigrateSchema:
    """Tests for migrate_schema()."""

    def test_v1_to_v2_adds_columns(self, v1_db: sqlite3.Connection) -> None:
        migrate_schema(v1_db)

        # Verify new columns exist by querying pragma
        columns = v1_db.execute("PRAGMA table_info(memories)").fetchall()
        col_names = {c[1] for c in columns}

        assert "proj_tag" in col_names
        assert "ttl_tier" in col_names
        assert "expires_at" in col_names
        assert "content_hash" in col_names

    def test_v1_to_v2_adds_indexes(self, v1_db: sqlite3.Connection) -> None:
        migrate_schema(v1_db)

        indexes = v1_db.execute("PRAGMA index_list(memories)").fetchall()
        index_names = {idx[1] for idx in indexes}

        assert "idx_memories_proj_tag" in index_names
        assert "idx_memories_expires_at" in index_names
        assert "idx_memories_content_hash" in index_names

    def test_idempotent_run_twice(self, v1_db: sqlite3.Connection) -> None:
        migrate_schema(v1_db)
        version_1 = get_schema_version(v1_db)

        # Second run should be a no-op
        migrate_schema(v1_db)
        version_2 = get_schema_version(v1_db)

        assert version_1 == version_2 == 10

    def test_preserves_existing_data(self, v1_db: sqlite3.Connection) -> None:
        # Insert a row before migration
        v1_db.execute(
            "INSERT INTO memories (content, category, created_at, updated_at) VALUES (?, ?, ?, ?)",
            ("test content", "general", "2024-01-01T00:00:00", "2024-01-01T00:00:00"),
        )
        v1_db.commit()

        migrate_schema(v1_db)

        row = v1_db.execute("SELECT * FROM memories WHERE id = 1").fetchone()
        assert row is not None
        # Content should still be there
        assert row[1] == "test content"

    def test_new_columns_have_defaults(self, v1_db: sqlite3.Connection) -> None:
        # Insert before migration
        v1_db.execute(
            "INSERT INTO memories (content, category, created_at, updated_at) VALUES (?, ?, ?, ?)",
            ("test", "general", "2024-01-01T00:00:00", "2024-01-01T00:00:00"),
        )
        v1_db.commit()

        migrate_schema(v1_db)

        row = v1_db.execute(
            "SELECT proj_tag, ttl_tier, expires_at, content_hash FROM memories WHERE id = 1"
        ).fetchone()
        assert row[0] == ""  # proj_tag normalized to '' (was NULL, backfilled)
        assert row[1] == "permanent"  # ttl_tier default 'permanent'
        assert row[2] is None  # expires_at default NULL
        assert row[3] is not None  # content_hash backfilled by migration

    def test_sets_user_version_to_latest(self, v1_db: sqlite3.Connection) -> None:
        migrate_schema(v1_db)
        version = v1_db.execute("PRAGMA user_version").fetchone()[0]
        assert version == 10

    def test_original_columns_preserved(self, v1_db: sqlite3.Connection) -> None:
        migrate_schema(v1_db)
        columns = v1_db.execute("PRAGMA table_info(memories)").fetchall()
        col_names = {c[1] for c in columns}

        # Original v1 columns
        assert "id" in col_names
        assert "content" in col_names
        assert "category" in col_names
        assert "metadata" in col_names
        assert "created_at" in col_names
        assert "updated_at" in col_names


# ============================================================================
# NEW TESTS: H7 - Migration backfill content_hash
# ============================================================================


class TestMigrationBackfillContentHash:
    """H7: v1->v2 migration should backfill content_hash for existing rows."""

    def test_migration_backfills_content_hash(self, v1_db: sqlite3.Connection) -> None:
        """Existing memories should get content_hash populated after migration."""
        # Insert rows before migration (no content_hash column yet)
        v1_db.execute(
            "INSERT INTO memories (content, category, created_at, updated_at) VALUES (?, ?, ?, ?)",
            ("test content one", "lesson", "2024-01-01T00:00:00", "2024-01-01T00:00:00"),
        )
        v1_db.execute(
            "INSERT INTO memories (content, category, created_at, updated_at) VALUES (?, ?, ?, ?)",
            ("test content two", "decision", "2024-01-02T00:00:00", "2024-01-02T00:00:00"),
        )
        v1_db.commit()

        migrate_schema(v1_db)

        # Both rows should now have content_hash
        rows = v1_db.execute("SELECT id, content, content_hash FROM memories").fetchall()
        assert len(rows) == 2
        for row in rows:
            assert row[2] is not None, f"Row {row[0]} should have content_hash"
            assert len(row[2]) == 64, "content_hash should be SHA-256 hex (64 chars)"

    def test_migration_backfill_hash_is_correct(self, v1_db: sqlite3.Connection) -> None:
        """Backfilled content_hash should match SHA-256 of content."""
        import hashlib

        content = "verify hash correctness"
        v1_db.execute(
            "INSERT INTO memories (content, category, created_at, updated_at) VALUES (?, ?, ?, ?)",
            (content, "lesson", "2024-01-01T00:00:00", "2024-01-01T00:00:00"),
        )
        v1_db.commit()

        migrate_schema(v1_db)

        row = v1_db.execute("SELECT content_hash FROM memories WHERE id = 1").fetchone()
        expected = hashlib.sha256(content.encode("utf-8")).hexdigest()
        assert row[0] == expected

    def test_migration_backfill_no_rows_is_safe(self, v1_db: sqlite3.Connection) -> None:
        """Migration with no existing rows should not error."""
        migrate_schema(v1_db)
        rows = v1_db.execute("SELECT COUNT(*) FROM memories").fetchone()
        assert rows[0] == 0

    def test_migration_backfill_null_proj_tag_becomes_empty_string(
        self, v1_db: sqlite3.Connection
    ) -> None:
        """Existing rows with NULL proj_tag should get empty string after migration."""
        v1_db.execute(
            "INSERT INTO memories (content, category, created_at, updated_at) VALUES (?, ?, ?, ?)",
            ("old memory", "lesson", "2024-01-01T00:00:00", "2024-01-01T00:00:00"),
        )
        v1_db.commit()

        migrate_schema(v1_db)

        row = v1_db.execute("SELECT proj_tag FROM memories WHERE id = 1").fetchone()
        # After migration backfill, NULL proj_tag should become empty string
        assert row[0] == ""


class TestMigrationDedupIndex:
    """C5: Migration should add the UNIQUE dedup index."""

    def test_migration_adds_dedup_index(self, v1_db: sqlite3.Connection) -> None:
        """v1->v2 migration should create idx_memories_dedup."""
        migrate_schema(v1_db)
        indexes = v1_db.execute("PRAGMA index_list(memories)").fetchall()
        index_names = [idx[1] for idx in indexes]
        assert "idx_memories_dedup" in index_names

    def test_migration_dedup_index_is_unique(self, v1_db: sqlite3.Connection) -> None:
        """idx_memories_dedup should be a UNIQUE index."""
        migrate_schema(v1_db)
        indexes = v1_db.execute("PRAGMA index_list(memories)").fetchall()
        for idx in indexes:
            if idx[1] == "idx_memories_dedup":
                assert idx[2] == 1, "idx_memories_dedup should be UNIQUE"
                break
        else:
            raise AssertionError("idx_memories_dedup not found")

    def test_migration_with_duplicate_rows_succeeds(self, v1_db: sqlite3.Connection) -> None:
        """Fix #2: Migration must dedup BEFORE creating UNIQUE index."""
        # Insert duplicate rows (same content, same category, proj_tag will be NULL→"")
        for _ in range(3):
            v1_db.execute(
                "INSERT INTO memories (content, category, created_at, updated_at) "
                "VALUES (?, ?, ?, ?)",
                ("duplicate content", "lesson", "2024-01-01T00:00:00", "2024-01-01T00:00:00"),
            )
        v1_db.commit()
        assert v1_db.execute("SELECT COUNT(*) FROM memories").fetchone()[0] == 3

        # Migration should NOT raise — it deduplicates before creating UNIQUE index
        migrate_schema(v1_db)

        # Only 1 row should remain (lowest id kept)
        rows = v1_db.execute("SELECT id FROM memories ORDER BY id").fetchall()
        assert len(rows) == 1
        assert rows[0][0] == 1  # Kept the earliest insertion

    def test_migration_dedup_preserves_unique_rows(self, v1_db: sqlite3.Connection) -> None:
        """Dedup should only remove actual duplicates, not unique content."""
        v1_db.execute(
            "INSERT INTO memories (content, category, created_at, updated_at) VALUES (?, ?, ?, ?)",
            ("unique A", "lesson", "2024-01-01T00:00:00", "2024-01-01T00:00:00"),
        )
        v1_db.execute(
            "INSERT INTO memories (content, category, created_at, updated_at) VALUES (?, ?, ?, ?)",
            ("unique B", "lesson", "2024-01-02T00:00:00", "2024-01-02T00:00:00"),
        )
        # Duplicate of A
        v1_db.execute(
            "INSERT INTO memories (content, category, created_at, updated_at) VALUES (?, ?, ?, ?)",
            ("unique A", "lesson", "2024-01-03T00:00:00", "2024-01-03T00:00:00"),
        )
        v1_db.commit()

        migrate_schema(v1_db)

        rows = v1_db.execute("SELECT content FROM memories ORDER BY id").fetchall()
        assert len(rows) == 2
        assert rows[0][0] == "unique A"
        assert rows[1][0] == "unique B"


# ============================================================================
# NEW TESTS: v2→v3 migration — counters table
# ============================================================================


@pytest.fixture()
def v2_db(v1_db: sqlite3.Connection) -> sqlite3.Connection:
    """Create a v2 database by running only the v1→v2 migration (stops at version 2)."""
    _migrate_v1_to_v2(v1_db)
    assert get_schema_version(v1_db) == 2
    return v1_db


class TestMigrateV2ToV3:
    """Tests for v2→v3 migration: counters table."""

    def test_migration_creates_counters_table(self, v2_db: sqlite3.Connection) -> None:
        migrate_schema(v2_db)
        tables = v2_db.execute(
            "SELECT name FROM sqlite_master WHERE type='table' AND name='counters'"
        ).fetchall()
        assert len(tables) == 1

    def test_migration_sets_version_4(self, v2_db: sqlite3.Connection) -> None:
        migrate_schema(v2_db)
        assert get_schema_version(v2_db) == 10

    def test_counters_table_has_correct_columns(self, v2_db: sqlite3.Connection) -> None:
        migrate_schema(v2_db)
        columns = v2_db.execute("PRAGMA table_info(counters)").fetchall()
        col_names = {c[1] for c in columns}
        assert col_names == {"id", "key", "value", "proj_tag", "updated_at"}

    def test_counters_has_proj_tag_index(self, v2_db: sqlite3.Connection) -> None:
        migrate_schema(v2_db)
        indexes = v2_db.execute("PRAGMA index_list(counters)").fetchall()
        index_names = {idx[1] for idx in indexes}
        assert "idx_counters_proj_tag" in index_names

    def test_counters_has_unique_constraint(self, v2_db: sqlite3.Connection) -> None:
        """UNIQUE(key, proj_tag) should exist for atomic upserts."""
        migrate_schema(v2_db)
        indexes = v2_db.execute("PRAGMA index_list(counters)").fetchall()
        # Find the unique index on (key, proj_tag)
        found_unique = False
        for idx in indexes:
            if idx[2] == 1:  # unique flag
                cols = v2_db.execute(f"PRAGMA index_info({idx[1]})").fetchall()
                col_names = {c[2] for c in cols}
                if col_names == {"key", "proj_tag"}:
                    found_unique = True
                    break
        assert found_unique, "UNIQUE(key, proj_tag) index not found on counters table"

    def test_idempotent_v3_migration(self, v2_db: sqlite3.Connection) -> None:
        migrate_schema(v2_db)
        assert get_schema_version(v2_db) == 10
        migrate_schema(v2_db)
        assert get_schema_version(v2_db) == 10

    def test_v1_to_v3_full_migration(self, v1_db: sqlite3.Connection) -> None:
        """v1 db should migrate through v2 to v3 in one call."""
        assert get_schema_version(v1_db) == 0
        migrate_schema(v1_db)
        assert get_schema_version(v1_db) == 10
        # Both memories columns and counters table should exist
        tables = v1_db.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()
        table_names = {t[0] for t in tables}
        assert "memories" in table_names
        assert "counters" in table_names

    def test_memories_table_preserved_after_v3(self, v2_db: sqlite3.Connection) -> None:
        """v2→v3 migration should not touch the memories table."""
        v2_db.execute(
            "INSERT INTO memories (content, category, metadata, created_at, updated_at, "
            "proj_tag, ttl_tier, content_hash) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
            ("test", "lesson", "{}", "2024-01-01", "2024-01-01", "", "permanent", "abc123"),
        )
        v2_db.commit()

        migrate_schema(v2_db)
        row = v2_db.execute("SELECT content FROM memories").fetchone()
        assert row[0] == "test"


# ============================================================================
# v6→v7 migration — bi-temporal valid_from/valid_to columns
# ============================================================================


def _create_v6_db(tmp_path) -> sqlite3.Connection:
    """Create a v6 database with all columns up to v6 (including superseded_by, is_active)."""
    import hashlib

    db_path = tmp_path / "v6_test.db"
    conn = sqlite3.connect(str(db_path))
    conn.row_factory = sqlite3.Row
    conn.executescript("""
        CREATE TABLE memories (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            content TEXT NOT NULL,
            category TEXT DEFAULT 'general',
            metadata TEXT DEFAULT '{}',
            created_at TEXT NOT NULL,
            updated_at TEXT NOT NULL,
            proj_tag TEXT DEFAULT '',
            ttl_tier TEXT DEFAULT 'permanent',
            expires_at TEXT DEFAULT NULL,
            content_hash TEXT DEFAULT NULL,
            summary TEXT DEFAULT NULL,
            token_count INTEGER DEFAULT NULL,
            superseded_by INTEGER DEFAULT NULL,
            is_active INTEGER DEFAULT 1
        );
        CREATE TABLE IF NOT EXISTS counters (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            key TEXT NOT NULL,
            value INTEGER NOT NULL DEFAULT 0,
            proj_tag TEXT NOT NULL DEFAULT '',
            updated_at TEXT NOT NULL,
            UNIQUE(key, proj_tag)
        );
        CREATE INDEX IF NOT EXISTS idx_memories_is_active ON memories(is_active);
        PRAGMA user_version = 6;
    """)

    # Insert some test memories
    for i, (content, cat, is_active, superseded_by) in enumerate(
        [
            ("Active decision about auth", "decision", 1, None),
            ("Old decision replaced", "decision", 0, 1),
            ("A permanent lesson", "lesson", 1, None),
        ],
        start=1,
    ):
        h = hashlib.sha256(content.encode()).hexdigest()
        conn.execute(
            "INSERT INTO memories (content, category, created_at, updated_at, "
            "content_hash, is_active, superseded_by, proj_tag) "
            "VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
            (
                content,
                cat,
                f"2025-01-{i:02d}T00:00:00+00:00",
                f"2025-01-{i + 5:02d}T00:00:00+00:00",
                h,
                is_active,
                superseded_by,
                "",
            ),
        )
    conn.commit()
    return conn


@pytest.fixture()
def v6_db(tmp_path) -> sqlite3.Connection:
    """Fixture for a v6 database."""
    return _create_v6_db(tmp_path)


class TestMigrateV6ToV7:
    """Tests for v6→v7 migration: bi-temporal valid_from/valid_to."""

    def test_migration_adds_temporal_columns(self, v6_db: sqlite3.Connection) -> None:
        migrate_schema(v6_db)
        columns = v6_db.execute("PRAGMA table_info(memories)").fetchall()
        col_names = {c[1] for c in columns}
        assert "valid_from" in col_names
        assert "valid_to" in col_names

    def test_migration_adds_temporal_index(self, v6_db: sqlite3.Connection) -> None:
        migrate_schema(v6_db)
        indexes = v6_db.execute("PRAGMA index_list(memories)").fetchall()
        index_names = {idx[1] for idx in indexes}
        assert "idx_memories_temporal" in index_names

    def test_migration_sets_version_7(self, v6_db: sqlite3.Connection) -> None:
        migrate_schema(v6_db)
        assert get_schema_version(v6_db) == 10

    def test_backfill_valid_from_equals_created_at(self, v6_db: sqlite3.Connection) -> None:
        """All existing memories should get valid_from = created_at."""
        migrate_schema(v6_db)
        rows = v6_db.execute("SELECT id, created_at, valid_from FROM memories").fetchall()
        for row in rows:
            assert row["valid_from"] == row["created_at"], (
                f"Memory {row['id']}: valid_from should equal created_at"
            )

    def test_backfill_active_memories_valid_to_null(self, v6_db: sqlite3.Connection) -> None:
        """Active memories should have valid_to = NULL (still valid)."""
        migrate_schema(v6_db)
        rows = v6_db.execute("SELECT id, valid_to FROM memories WHERE is_active = 1").fetchall()
        for row in rows:
            assert row["valid_to"] is None, f"Active memory {row['id']} should have NULL valid_to"

    def test_backfill_superseded_memories_valid_to_set(self, v6_db: sqlite3.Connection) -> None:
        """Superseded memories should get valid_to = updated_at."""
        migrate_schema(v6_db)
        rows = v6_db.execute(
            "SELECT id, updated_at, valid_to FROM memories WHERE is_active = 0"
        ).fetchall()
        assert len(rows) > 0, "Should have at least one superseded memory"
        for row in rows:
            assert row["valid_to"] == row["updated_at"], (
                f"Superseded memory {row['id']}: valid_to should equal updated_at"
            )

    def test_idempotent_v7_migration(self, v6_db: sqlite3.Connection) -> None:
        migrate_schema(v6_db)
        assert get_schema_version(v6_db) == 10
        # Second run should be a no-op
        migrate_schema(v6_db)
        assert get_schema_version(v6_db) == 10

    def test_full_v1_to_v8_migration(self, v1_db: sqlite3.Connection) -> None:
        """v1 database should migrate all the way to v10."""
        assert get_schema_version(v1_db) == 0
        migrate_schema(v1_db)
        assert get_schema_version(v1_db) == 10
        columns = v1_db.execute("PRAGMA table_info(memories)").fetchall()
        col_names = {c[1] for c in columns}
        assert "valid_from" in col_names
        assert "valid_to" in col_names
        # v8: memory_relations table should exist
        tables = v1_db.execute(
            "SELECT name FROM sqlite_master WHERE type='table' AND name='memory_relations'"
        ).fetchall()
        assert len(tables) == 1


# ============================================================================
# v7→v8 migration — memory_relations graph table
# ============================================================================


def _create_v7_db(tmp_path) -> sqlite3.Connection:
    """Create a v7 database with all columns up to v7."""
    import hashlib

    db_path = tmp_path / "v7_test.db"
    conn = sqlite3.connect(str(db_path))
    conn.row_factory = sqlite3.Row
    conn.executescript("""
        CREATE TABLE memories (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            content TEXT NOT NULL,
            category TEXT DEFAULT 'general',
            metadata TEXT DEFAULT '{}',
            created_at TEXT NOT NULL,
            updated_at TEXT NOT NULL,
            proj_tag TEXT DEFAULT '',
            ttl_tier TEXT DEFAULT 'permanent',
            expires_at TEXT DEFAULT NULL,
            content_hash TEXT DEFAULT NULL,
            summary TEXT DEFAULT NULL,
            token_count INTEGER DEFAULT NULL,
            superseded_by INTEGER DEFAULT NULL,
            is_active INTEGER DEFAULT 1,
            valid_from TEXT DEFAULT NULL,
            valid_to TEXT DEFAULT NULL
        );
        CREATE TABLE IF NOT EXISTS counters (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            key TEXT NOT NULL,
            value INTEGER NOT NULL DEFAULT 0,
            proj_tag TEXT NOT NULL DEFAULT '',
            updated_at TEXT NOT NULL,
            UNIQUE(key, proj_tag)
        );
        CREATE INDEX IF NOT EXISTS idx_memories_is_active ON memories(is_active);
        CREATE INDEX IF NOT EXISTS idx_memories_temporal ON memories(valid_from, valid_to);
        PRAGMA user_version = 7;
    """)

    # Insert test memories: one active, one superseded by id=1
    for i, (content, cat, is_active, superseded_by) in enumerate(
        [
            ("Current auth decision: use JWT", "decision", 1, None),
            ("Old auth decision: use sessions", "decision", 0, 1),
            ("A permanent lesson about testing", "lesson", 1, None),
        ],
        start=1,
    ):
        h = hashlib.sha256(content.encode()).hexdigest()
        conn.execute(
            "INSERT INTO memories (content, category, created_at, updated_at, "
            "content_hash, is_active, superseded_by, proj_tag, valid_from) "
            "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
            (
                content,
                cat,
                f"2025-01-{i:02d}T00:00:00+00:00",
                f"2025-01-{i + 5:02d}T00:00:00+00:00",
                h,
                is_active,
                superseded_by,
                "",
                f"2025-01-{i:02d}T00:00:00+00:00",
            ),
        )
    conn.commit()
    return conn


@pytest.fixture()
def v7_db(tmp_path) -> sqlite3.Connection:
    """Fixture for a v7 database."""
    return _create_v7_db(tmp_path)


class TestMigrateV7ToV8:
    """Tests for v7→v8 migration: memory_relations graph table."""

    def test_migration_creates_relations_table(self, v7_db: sqlite3.Connection) -> None:
        migrate_schema(v7_db)
        tables = v7_db.execute(
            "SELECT name FROM sqlite_master WHERE type='table' AND name='memory_relations'"
        ).fetchall()
        assert len(tables) == 1

    def test_migration_sets_version_8(self, v7_db: sqlite3.Connection) -> None:
        migrate_schema(v7_db)
        assert get_schema_version(v7_db) == 10

    def test_relations_table_has_correct_columns(self, v7_db: sqlite3.Connection) -> None:
        migrate_schema(v7_db)
        columns = v7_db.execute("PRAGMA table_info(memory_relations)").fetchall()
        col_names = {c["name"] for c in columns}
        expected = {
            "id",
            "source_id",
            "target_id",
            "relation_type",
            "metadata",
            "created_at",
            "valid_from",
            "valid_to",
            "invalidated_at",
        }
        assert expected == col_names

    def test_relations_has_source_index(self, v7_db: sqlite3.Connection) -> None:
        migrate_schema(v7_db)
        indexes = v7_db.execute("PRAGMA index_list(memory_relations)").fetchall()
        index_names = {idx["name"] for idx in indexes}
        assert "idx_relations_source" in index_names

    def test_relations_has_target_index(self, v7_db: sqlite3.Connection) -> None:
        migrate_schema(v7_db)
        indexes = v7_db.execute("PRAGMA index_list(memory_relations)").fetchall()
        index_names = {idx["name"] for idx in indexes}
        assert "idx_relations_target" in index_names

    def test_relations_has_unique_constraint(self, v7_db: sqlite3.Connection) -> None:
        """UNIQUE(source_id, target_id, relation_type) should exist."""
        migrate_schema(v7_db)
        indexes = v7_db.execute("PRAGMA index_list(memory_relations)").fetchall()
        found_unique = False
        for idx in indexes:
            if idx["unique"] == 1:
                cols = v7_db.execute(f"PRAGMA index_info({idx['name']})").fetchall()
                col_names = {c["name"] for c in cols}
                if col_names == {"source_id", "target_id", "relation_type"}:
                    found_unique = True
                    break
        assert found_unique, "UNIQUE(source_id, target_id, relation_type) not found"

    def test_backfill_creates_supersedes_edges(self, v7_db: sqlite3.Connection) -> None:
        """Memories with superseded_by should get 'supersedes' edges."""
        migrate_schema(v7_db)
        edges = v7_db.execute(
            "SELECT source_id, target_id, relation_type FROM memory_relations"
        ).fetchall()
        assert len(edges) == 1
        # Memory 2 was superseded_by=1, so edge: source=1 (new) supersedes target=2 (old)
        assert edges[0]["source_id"] == 1
        assert edges[0]["target_id"] == 2
        assert edges[0]["relation_type"] == "supersedes"

    def test_backfill_no_superseded_memories(self, v7_db: sqlite3.Connection) -> None:
        """If no memories have superseded_by, no edges are created."""
        # Clear superseded_by
        v7_db.execute("UPDATE memories SET superseded_by = NULL")
        v7_db.commit()
        migrate_schema(v7_db)
        count = v7_db.execute("SELECT COUNT(*) FROM memory_relations").fetchone()[0]
        assert count == 0

    def test_idempotent_v8_migration(self, v7_db: sqlite3.Connection) -> None:
        migrate_schema(v7_db)
        assert get_schema_version(v7_db) == 10
        # Second run should be a no-op (edges not duplicated)
        migrate_schema(v7_db)
        assert get_schema_version(v7_db) == 10
        count = v7_db.execute("SELECT COUNT(*) FROM memory_relations").fetchone()[0]
        assert count == 1  # Still just the one backfilled edge

    def test_idempotent_edge_creation(self, v7_db: sqlite3.Connection) -> None:
        """INSERT OR IGNORE should prevent duplicate edges."""
        migrate_schema(v7_db)
        # Try to insert a duplicate edge manually
        v7_db.execute(
            "INSERT OR IGNORE INTO memory_relations "
            "(source_id, target_id, relation_type, created_at) "
            "VALUES (1, 2, 'supersedes', '2025-01-10T00:00:00')"
        )
        v7_db.commit()
        count = v7_db.execute("SELECT COUNT(*) FROM memory_relations").fetchone()[0]
        assert count == 1  # No duplicate
