//! End-to-end migration test for the Magnotia -> Lumotia rebrand.
//!
//! The in-crate unit tests in `crates/core/src/paths.rs` prove that the
//! migration copies bytes and renames files correctly, but they don't
//! verify that the resulting `lumotia.db` is *openable* by
//! `lumotia-storage`. A byte-perfect copy with a torn SQLite header,
//! a stale WAL pointer, or a row count off by one would still pass
//! those tests. This integration test closes that gap by:
//!
//!   1. Seeding a real on-disk `magnotia/magnotia.db` via the public
//!      `lumotia_storage::init` API (which runs every schema migration
//!      head-to-tail). A real transcript row is inserted via
//!      `insert_transcript`, then the pool is dropped to flush + close.
//!   2. Running the rebrand migration via
//!      `lumotia_core::paths::migrate_legacy_data_dir_with_pairs` with
//!      the synthesised (legacy, target) pair.
//!   3. Re-opening the migrated `lumotia/lumotia.db` via `init`
//!      (which re-runs migrations — they must be no-ops against the
//!      already-migrated schema) and querying for the inserted
//!      transcript by id.
//!
//! If any of the three steps fails — rename, reopen, or query — the
//! migration is unsafe to ship even though the unit tests pass.

use std::path::Path;

use lumotia_core::paths::{migrate_legacy_data_dir_with_pairs, MigrationStatus};
use lumotia_storage::{
    get_transcript, init, insert_transcript, list_transcripts, InsertTranscriptParams,
    DEFAULT_PROFILE_ID,
};
use tempfile::TempDir;

const SEEDED_TRANSCRIPT_ID: &str = "t-rebrand-survivor";
const SEEDED_TRANSCRIPT_TEXT: &str =
    "Migration sentinel row. If this read fails, the rebrand orphaned user data.";
const SEEDED_TRANSCRIPT_TITLE: &str = "rebrand-survivor";

/// Drop the pool and yield long enough for sqlx's background reaper to
/// close its connections. The integration test re-opens the migrated DB
/// immediately afterwards, so any sqlx-held file descriptor on Windows
/// would block the rename. tokio's `yield_now` is a single scheduler
/// hop, not a sleep — enough to flush the pool's tokio task queue.
async fn drop_and_yield(pool: sqlx::SqlitePool) {
    pool.close().await;
    tokio::task::yield_now().await;
}

/// Build the canonical seed-row params. Centralised so the assertions
/// in each test can reference the same expected values without drift.
fn seed_params() -> InsertTranscriptParams<'static> {
    InsertTranscriptParams {
        id: SEEDED_TRANSCRIPT_ID,
        text: SEEDED_TRANSCRIPT_TEXT,
        source: "microphone",
        profile_id: DEFAULT_PROFILE_ID,
        title: Some(SEEDED_TRANSCRIPT_TITLE),
        audio_path: None,
        duration: 2.5,
        engine: Some("whisper"),
        model_id: Some("whisper-tiny-en"),
        inference_ms: Some(420),
        sample_rate: Some(16_000),
        audio_channels: Some(1),
        format_mode: Some("plain"),
        remove_fillers: false,
        british_english: true,
        anti_hallucination: false,
    }
}

/// Seed `<legacy>/magnotia.db` with the live schema head and one
/// transcript row. Returns nothing; the asserts live in the test body.
async fn seed_legacy_db(legacy_dir: &Path) {
    std::fs::create_dir_all(legacy_dir).expect("create legacy dir");
    let legacy_db = legacy_dir.join("magnotia.db");

    let pool = init(&legacy_db).await.expect("init legacy magnotia.db");
    insert_transcript(&pool, &seed_params())
        .await
        .expect("insert seed transcript into legacy db");
    drop_and_yield(pool).await;

    assert!(
        legacy_db.exists(),
        "legacy magnotia.db should be on disk after pool drop"
    );
}

#[tokio::test(flavor = "multi_thread")]
async fn legacy_db_survives_rebrand_and_remains_queryable() {
    let tmp = TempDir::new().expect("tempdir");
    let legacy = tmp.path().join("magnotia");
    let target = tmp.path().join("lumotia");

    seed_legacy_db(&legacy).await;

    // Bonus: drop a non-DB file inside the legacy tree to confirm the
    // migration sweeps the whole directory, not just the .db file.
    let companion = legacy
        .join("recordings")
        .join("2026-05-13")
        .join("clip.wav");
    std::fs::create_dir_all(companion.parent().unwrap()).expect("nested dir");
    std::fs::write(&companion, b"fake-wav-bytes").expect("write companion file");

    // Migrate. Blocking I/O is fine inside the multi-thread flavour.
    let statuses = migrate_legacy_data_dir_with_pairs(vec![(legacy.clone(), target.clone())])
        .expect("migration must not error");

    assert_eq!(statuses.len(), 1, "single pair must yield single status");
    match &statuses[0] {
        MigrationStatus::Migrated {
            from,
            to,
            renamed_db,
        } => {
            assert_eq!(from, &legacy);
            assert_eq!(to, &target);
            assert!(renamed_db, "magnotia.db must have been renamed");
        }
        other => panic!("expected Migrated, got {other:?}"),
    }

    assert!(!legacy.exists(), "legacy dir should be gone after rename");
    assert!(target.exists(), "target dir should exist after rename");
    let migrated_db = target.join("lumotia.db");
    assert!(migrated_db.exists(), "lumotia.db must be at new path");
    assert!(
        !target.join("magnotia.db").exists(),
        "old db filename must not survive at new path"
    );
    let migrated_companion = target
        .join("recordings")
        .join("2026-05-13")
        .join("clip.wav");
    assert!(
        migrated_companion.exists(),
        "non-DB files inside legacy must be carried along"
    );
    assert_eq!(
        std::fs::read(&migrated_companion).expect("read migrated companion"),
        b"fake-wav-bytes",
        "non-DB file contents must survive verbatim"
    );

    // Re-open via the same public API a fresh app boot would use. This
    // also re-runs `run_migrations`, which must be idempotent against
    // the already-migrated schema.
    let pool = init(&migrated_db)
        .await
        .expect("init migrated lumotia.db must succeed");

    let found = get_transcript(&pool, SEEDED_TRANSCRIPT_ID)
        .await
        .expect("get_transcript must not error against migrated db")
        .expect("seeded transcript must survive the migration");

    assert_eq!(found.id, SEEDED_TRANSCRIPT_ID);
    assert_eq!(found.text, SEEDED_TRANSCRIPT_TEXT);
    assert_eq!(found.title.as_deref(), Some(SEEDED_TRANSCRIPT_TITLE));

    // And the list view sees exactly one row, ruling out duplicates or
    // schema-rebuild-from-empty (which would yield zero).
    let listed = list_transcripts(&pool, 100)
        .await
        .expect("list_transcripts must not error against migrated db");
    assert_eq!(
        listed.len(),
        1,
        "exactly one transcript should be visible post-migration"
    );
    assert_eq!(listed[0].id, SEEDED_TRANSCRIPT_ID);

    drop_and_yield(pool).await;
}

#[tokio::test(flavor = "multi_thread")]
async fn rerun_after_migration_is_idempotent_and_preserves_user_state() {
    // First boot: migrate. Second boot: legacy preserved? Per the design
    // the legacy DOES NOT preserve (rename moves it; see paths.rs
    // rename_or_copy_tree). So the second run sees no legacy and yields
    // NoLegacyFound. This test exists to nail down that contract — if a
    // future change starts copying-rather-than-renaming, the test will
    // catch the subsequent loss of idempotency.
    let tmp = TempDir::new().expect("tempdir");
    let legacy = tmp.path().join("magnotia");
    let target = tmp.path().join("lumotia");

    seed_legacy_db(&legacy).await;

    let first = migrate_legacy_data_dir_with_pairs(vec![(legacy.clone(), target.clone())])
        .expect("first migration");
    assert!(matches!(first[0], MigrationStatus::Migrated { .. }));

    // User immediately starts using the app: writes new data to the
    // migrated DB. The follow-up reboot must NOT clobber this.
    let pool = init(&target.join("lumotia.db"))
        .await
        .expect("post-migrate init");
    insert_transcript(
        &pool,
        &InsertTranscriptParams {
            id: "t-post-migration",
            text: "Written after the rebrand boot.",
            ..seed_params()
        },
    )
    .await
    .expect("insert new row post-migration");
    drop_and_yield(pool).await;

    // Second boot: no legacy on disk, nothing to do. Note empty input
    // yields a single NoLegacyFound entry by contract.
    let second = migrate_legacy_data_dir_with_pairs(Vec::new()).expect("second migration");
    assert_eq!(second, vec![MigrationStatus::NoLegacyFound]);

    // Open and confirm both rows present + user's post-migration write
    // intact.
    let pool = init(&target.join("lumotia.db"))
        .await
        .expect("reopen after second boot");
    let listed = list_transcripts(&pool, 100).await.expect("list");
    let ids: Vec<_> = listed.iter().map(|r| r.id.as_str()).collect();
    assert!(
        ids.contains(&SEEDED_TRANSCRIPT_ID),
        "pre-migration row should survive second boot: {ids:?}"
    );
    assert!(
        ids.contains(&"t-post-migration"),
        "post-migration row should survive second boot: {ids:?}"
    );
    drop_and_yield(pool).await;
}

#[tokio::test(flavor = "multi_thread")]
async fn both_paths_present_refuses_to_overwrite_user_data() {
    // Reproduces the "stale legacy + freshly-installed lumotia" scenario:
    // user reinstalls after deleting their data dir manually, or runs an
    // older + newer build side-by-side. The migration must NOT clobber
    // the lumotia-side DB even if the legacy contains a richer one —
    // user authority on what's authoritative.
    let tmp = TempDir::new().expect("tempdir");
    let legacy = tmp.path().join("magnotia");
    let target = tmp.path().join("lumotia");

    // Both DBs exist; the legacy has the seeded row, the target is
    // freshly initialised with no rows.
    seed_legacy_db(&legacy).await;
    let pool = init(&target.join("lumotia.db")).await.expect("seed target");
    drop_and_yield(pool).await;

    let statuses = migrate_legacy_data_dir_with_pairs(vec![(legacy.clone(), target.clone())])
        .expect("migration must not error in both-exist case");

    assert_eq!(statuses.len(), 1);
    assert_eq!(
        statuses[0],
        MigrationStatus::TargetAlreadyExists {
            target: target.clone()
        }
    );

    // Critical: target's empty DB must be untouched.
    let pool = init(&target.join("lumotia.db"))
        .await
        .expect("reopen target post-decision");
    let listed = list_transcripts(&pool, 100).await.expect("list");
    assert_eq!(
        listed.len(),
        0,
        "target's user-installed empty DB must not have been merged with legacy"
    );
    drop_and_yield(pool).await;

    // And the legacy DB must still be on disk for manual recovery —
    // we don't quietly delete user data the migration refused to copy.
    assert!(
        legacy.join("magnotia.db").exists(),
        "legacy DB must be preserved as a backup when target exists"
    );
}
