//! SQLite schema migrations for the MNote control-plane database.
//!
//! Each migration is a named step that runs inside a transaction.
//! The `migrations` table tracks which steps have been applied.

use rusqlite::{Connection, Result as SqlResult};

const MIGRATIONS: &[(&str, &str)] = &[
    (
        "v1-initial-schema",
        include_str!("../migrations/001-initial-schema.sql"),
    ),
    (
        "v2-ai-runtime-store",
        include_str!("../migrations/002-ai-runtime-store.sql"),
    ),
];

/// Create the `_migrations` meta-table if it does not exist.
fn ensure_meta_table(conn: &Connection) -> SqlResult<()> {
    conn.execute_batch(
        "CREATE TABLE IF NOT EXISTS _migrations (
            name TEXT PRIMARY KEY,
            applied_at TEXT NOT NULL DEFAULT (datetime('now'))
        );",
    )
}

/// Run all pending migrations inside a single transaction.
pub fn run_migrations(conn: &Connection) -> SqlResult<()> {
    ensure_meta_table(conn)?;

    let already_applied: Vec<String> = conn
        .prepare("SELECT name FROM _migrations")?
        .query_map([], |row| row.get(0))?
        .filter_map(|r| r.ok())
        .collect();

    for (name, sql) in MIGRATIONS {
        if already_applied.contains(&name.to_string()) {
            continue;
        }
        tracing::info!("Running migration: {name}");
        conn.execute_batch(sql)?;
        conn.execute("INSERT INTO _migrations (name) VALUES (?1)", [name])?;
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Create a fresh in-memory SQLite database and run migrations.
    fn fresh_db() -> Connection {
        let conn = Connection::open_in_memory().expect("open in-memory db");
        conn.execute_batch(
            "PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL; PRAGMA foreign_keys=ON;",
        )
        .expect("pragmas");
        conn
    }

    fn table_exists(conn: &Connection, name: &str) -> bool {
        conn.query_row(
            "SELECT count(*) FROM sqlite_master WHERE type='table' AND name=?1",
            [name],
            |row| row.get::<_, i64>(0),
        )
        .unwrap_or(0)
            > 0
    }

    #[test]
    fn migration_is_idempotent() {
        let conn = fresh_db();
        run_migrations(&conn).expect("first run");
        run_migrations(&conn).expect("second run (idempotent)");

        // All expected tables exist
        for table in &[
            "users",
            "auth_identities",
            "auth_sessions",
            "workspaces",
            "workspace_members",
            "directory_grants",
            "share_links",
            "sync_state",
            "ai_policies",
            "audit_log",
            "outbox_events",
            "legacy_id_map",
            "ai_runtime_runs",
            "ai_runtime_events",
        ] {
            assert!(table_exists(&conn, table), "table {table} should exist");
        }
    }

    #[test]
    fn migration_runs_fresh_db_ok() {
        let conn = fresh_db();
        run_migrations(&conn).expect("fresh migration ok");
        assert!(table_exists(&conn, "users"));
        assert!(table_exists(&conn, "_migrations"));
    }
}
