# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# Licensed under the MIT License

"""Tests for the lightweight SQLite migration system."""

import sqlite3

import pytest

from familiar.core.migrations import (
    _registry,
    get_schema_version,
    register_migration,
    run_migrations,
)


@pytest.fixture(autouse=True)
def clean_registry():
    """Reset the migration registry between tests."""
    saved = dict(_registry)
    _registry.clear()
    yield
    _registry.clear()
    _registry.update(saved)


@pytest.fixture
def db():
    """In-memory SQLite database."""
    conn = sqlite3.connect(":memory:")
    yield conn
    conn.close()


class TestRegistration:
    def test_register_single(self):
        register_migration("test_db", 1, lambda c: None)
        assert len(_registry["test_db"]) == 1
        assert _registry["test_db"][0][0] == 1

    def test_register_multiple_sorted(self):
        register_migration("test_db", 3, lambda c: None)
        register_migration("test_db", 1, lambda c: None)
        register_migration("test_db", 2, lambda c: None)
        versions = [v for v, _ in _registry["test_db"]]
        assert versions == [1, 2, 3]

    def test_register_invalid_version(self):
        with pytest.raises(ValueError, match="must be >= 1"):
            register_migration("test_db", 0, lambda c: None)

    def test_separate_databases(self):
        register_migration("db_a", 1, lambda c: None)
        register_migration("db_b", 1, lambda c: None)
        assert "db_a" in _registry
        assert "db_b" in _registry


class TestSchemaVersion:
    def test_no_table_returns_zero(self, db):
        assert get_schema_version(db) == 0

    def test_empty_table_returns_zero(self, db):
        db.execute("CREATE TABLE _schema_version (version INTEGER PRIMARY KEY, applied_at TEXT)")
        assert get_schema_version(db) == 0

    def test_returns_max_version(self, db):
        db.execute("CREATE TABLE _schema_version (version INTEGER PRIMARY KEY, applied_at TEXT)")
        db.execute("INSERT INTO _schema_version VALUES (1, '2026-01-01')")
        db.execute("INSERT INTO _schema_version VALUES (3, '2026-01-03')")
        assert get_schema_version(db) == 3


class TestRunMigrations:
    def test_no_migrations_registered(self, db):
        assert run_migrations(db, "unregistered") == 0

    def test_applies_single_migration(self, db):
        def add_table(conn):
            conn.execute("CREATE TABLE items (id INTEGER PRIMARY KEY)")

        register_migration("test_db", 1, add_table)
        applied = run_migrations(db, "test_db")

        assert applied == 1
        assert get_schema_version(db) == 1
        # Verify table was created
        row = db.execute(
            "SELECT name FROM sqlite_master WHERE type='table' AND name='items'"
        ).fetchone()
        assert row is not None

    def test_applies_multiple_in_order(self, db):
        order = []

        def m1(conn):
            conn.execute("CREATE TABLE t1 (id INTEGER)")
            order.append(1)

        def m2(conn):
            conn.execute("CREATE TABLE t2 (id INTEGER)")
            order.append(2)

        def m3(conn):
            conn.execute("CREATE TABLE t3 (id INTEGER)")
            order.append(3)

        register_migration("test_db", 1, m1)
        register_migration("test_db", 2, m2)
        register_migration("test_db", 3, m3)

        applied = run_migrations(db, "test_db")
        assert applied == 3
        assert order == [1, 2, 3]
        assert get_schema_version(db) == 3

    def test_skips_already_applied(self, db):
        call_count = [0]

        def m1(conn):
            conn.execute("CREATE TABLE IF NOT EXISTS t1 (id INTEGER)")
            call_count[0] += 1

        register_migration("test_db", 1, m1)

        run_migrations(db, "test_db")
        assert call_count[0] == 1

        # Second run should skip
        applied = run_migrations(db, "test_db")
        assert applied == 0
        assert call_count[0] == 1

    def test_incremental_migration(self, db):
        register_migration("test_db", 1, lambda c: c.execute("CREATE TABLE t1 (id INTEGER)"))
        run_migrations(db, "test_db")
        assert get_schema_version(db) == 1

        # Add new migration later
        register_migration("test_db", 2, lambda c: c.execute("ALTER TABLE t1 ADD COLUMN name TEXT"))
        applied = run_migrations(db, "test_db")
        assert applied == 1
        assert get_schema_version(db) == 2

    def test_migration_gap_raises(self):
        db2 = sqlite3.connect(":memory:")
        # Manually set up as if v1 was already applied
        db2.execute("CREATE TABLE _schema_version (version INTEGER PRIMARY KEY, applied_at TEXT)")
        db2.execute("INSERT INTO _schema_version VALUES (1, '2026-01-01')")
        db2.commit()

        # Register v1 (already applied) and v3 (gap — no v2)
        _registry["gap_db"] = [
            (1, lambda c: None),
            (3, lambda c: None),
        ]

        with pytest.raises(RuntimeError, match="Migration gap"):
            run_migrations(db2, "gap_db")
        db2.close()

    def test_failed_migration_rolls_back(self, db):
        register_migration(
            "test_db", 1, lambda c: c.execute("CREATE TABLE t1 (id INTEGER, name TEXT)")
        )

        def bad_migration(conn):
            # DML can be rolled back (unlike DDL in some SQLite builds)
            conn.execute("INSERT INTO t1 (id, name) VALUES (99, 'bad')")
            raise RuntimeError("Intentional failure")

        register_migration("test_db", 2, bad_migration)

        with pytest.raises(RuntimeError, match="Intentional failure"):
            run_migrations(db, "test_db")

        # v1 should have been applied, v2 rolled back
        assert get_schema_version(db) == 1
        # Row from v2 should NOT exist (rolled back)
        row = db.execute("SELECT * FROM t1 WHERE id = 99").fetchone()
        assert row is None

    def test_creates_version_table_automatically(self, db):
        register_migration("test_db", 1, lambda c: None)
        run_migrations(db, "test_db")

        # _schema_version table should exist
        row = db.execute(
            "SELECT name FROM sqlite_master WHERE type='table' AND name='_schema_version'"
        ).fetchone()
        assert row is not None
