//! Iteration-6 integration tests: `--resume` + persistent
//! input history + migration framework scaffold (ADR-0015 §7,
//! §9, §12).
//!
//! Boots no Tokio runtime and no terminal — these tests
//! exercise the persistent state behind `--resume` (the
//! `last_project` file under the data root) and the input
//! history hydration off `history.log`.

use std::fs;

use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};

use rdbms_playground::app::App;
use rdbms_playground::cli::{Args, ArgsError};
use rdbms_playground::event::AppEvent;
use rdbms_playground::persistence::Persistence;
use rdbms_playground::project::{
    self, LAST_PROJECT_FILE, Project, read_last_project, write_last_project,
};

fn tempdir() -> tempfile::TempDir {
    tempfile::tempdir().expect("create tempdir")
}

// --- Args parsing for --resume ---------------------------------

#[test]
fn args_parses_resume_flag() {
    let a = Args::parse(["--resume"]).unwrap();
    assert!(a.resume);
    assert!(a.project_path.is_none());
}

#[test]
fn args_resume_with_positional_path_is_an_error() {
    let err = Args::parse(["--resume", "/tmp/foo"]).unwrap_err();
    assert!(matches!(err, ArgsError::ResumeWithPath), "got: {err:?}");
}

#[test]
fn args_resume_after_positional_path_also_errors() {
    let err = Args::parse(["/tmp/foo", "--resume"]).unwrap_err();
    assert!(matches!(err, ArgsError::ResumeWithPath), "got: {err:?}");
}

#[test]
fn args_help_listing_mentions_resume() {
    assert!(rdbms_playground::cli::help_text().contains("--resume"));
}

// --- last_project read/write ----------------------------------

#[test]
fn last_project_round_trips_through_disk() {
    let tmp = tempdir();
    let target = tmp.path().join("MyProject");
    fs::create_dir(&target).unwrap();
    write_last_project(tmp.path(), &target).unwrap();

    let on_disk = fs::read_to_string(tmp.path().join(LAST_PROJECT_FILE)).unwrap();
    assert!(on_disk.contains("MyProject"));

    assert_eq!(read_last_project(tmp.path()).unwrap(), Some(target));
}

#[test]
fn last_project_is_overwritten_each_call() {
    let tmp = tempdir();
    let a = tmp.path().join("A");
    let b = tmp.path().join("B");
    fs::create_dir(&a).unwrap();
    fs::create_dir(&b).unwrap();
    write_last_project(tmp.path(), &a).unwrap();
    write_last_project(tmp.path(), &b).unwrap();
    assert_eq!(read_last_project(tmp.path()).unwrap(), Some(b));
}

#[test]
fn last_project_create_temp_path_resolves_to_existing_dir() {
    // Sanity: the path we record is in fact something that
    // exists when --resume tries to reopen it. This protects
    // against future refactors that might write a placeholder.
    let tmp = tempdir();
    let project = Project::create_temp(tmp.path()).unwrap();
    write_last_project(tmp.path(), project.path()).unwrap();
    let read_back = read_last_project(tmp.path()).unwrap();
    assert_eq!(read_back.as_deref(), Some(project.path()));
    assert!(read_back.unwrap().exists());
}

#[test]
fn read_last_project_handles_missing_data_root_directory() {
    let tmp = tempdir();
    let nested = tmp.path().join("does/not/exist/yet");
    // Reading from a directory that hasn't been created at
    // all should be Ok(None), not an error — the runtime's
    // first launch lands here.
    assert!(read_last_project(&nested).unwrap().is_none());
}

// --- Stale path on resume: read returns Some(path) but the
//     path does not exist. The runtime is responsible for
//     surfacing this; we verify the building block here.

#[test]
fn last_project_returns_stale_path_verbatim_for_runtime_to_detect() {
    let tmp = tempdir();
    let stale = tmp.path().join("Vanished");
    write_last_project(tmp.path(), &stale).unwrap();
    let read_back = read_last_project(tmp.path()).unwrap();
    assert_eq!(read_back.as_deref(), Some(stale.as_path()));
    assert!(!stale.exists());
}

// --- Project lifecycle writes last_project ---------------------
// (Smoke test: launching open_or_create then opening again
// should be the same as write_last_project + reopen.)

// --- History hydration on project open ----------------------

const fn key(code: KeyCode) -> AppEvent {
    AppEvent::Key(KeyEvent {
        code,
        modifiers: KeyModifiers::NONE,
        kind: KeyEventKind::Press,
        state: crossterm::event::KeyEventState::NONE,
    })
}

#[test]
fn read_recent_history_returns_empty_when_log_missing() {
    let tmp = tempdir();
    let p = Persistence::new(tmp.path().to_path_buf());
    let entries = p.read_recent_history(10).unwrap();
    assert!(entries.is_empty());
}

#[test]
fn read_recent_history_returns_appended_entries_in_order() {
    let tmp = tempdir();
    let project = Project::create_temp(tmp.path()).unwrap();
    let p = Persistence::new(project.path().to_path_buf());
    p.append_history("create table A with pk", false).unwrap();
    p.append_history("create table B with pk", false).unwrap();
    p.append_history("create table C with pk", false).unwrap();
    let entries = p.read_recent_history(10).unwrap();
    assert_eq!(
        entries,
        vec![
            "create table A with pk".to_string(),
            "create table B with pk".to_string(),
            "create table C with pk".to_string(),
        ]
    );
}

#[test]
fn hydration_reads_both_ok_and_err_records() {
    // ADR-0034 §1/§2: failed commands are journalled `err`, and
    // input-history hydration reads ALL records (ok + err) so a
    // typo'd / rejected command from a previous session is
    // recallable after restart — matching the in-session ring's
    // "record everything" behaviour.
    let tmp = tempdir();
    let project = Project::create_temp(tmp.path()).unwrap();
    let p = Persistence::new(project.path().to_path_buf());
    p.append_history("create table A with pk", false).unwrap();
    p.append_history_failure("insert into A (1, 2, 3)", false)
        .unwrap();
    p.append_history("show data A", false).unwrap();
    let entries = p.read_recent_history(10).unwrap();
    assert_eq!(
        entries,
        vec![
            "create table A with pk".to_string(),
            "insert into A (1, 2, 3)".to_string(), // the err record is recalled
            "show data A".to_string(),
        ],
        "hydration includes the err record",
    );
}

#[test]
fn seed_history_replaces_in_memory_history() {
    let mut app = App::new();
    // Pre-existing in-session entries — should be replaced.
    for c in "abc".chars() {
        app.update(key(KeyCode::Char(c)));
    }
    app.update(key(KeyCode::Enter));
    assert_eq!(app.history, vec!["abc".to_string()]);

    app.seed_history(vec!["x".to_string(), "y".to_string()]);
    assert_eq!(app.history, vec!["x".to_string(), "y".to_string()]);
}

#[test]
fn advanced_command_journalled_then_hydrated_recalls_with_colon_in_simple() {
    // ADR-0052 (issue #30) — the headline cross-session regression: an
    // advanced command journalled `ok:adv`, then hydrated on a fresh
    // session, recalls WITH its `:` so it re-runs in simple mode. (Before
    // the fix, the `:` was lost on disk and the command came back bare.)
    let tmp = tempdir();
    let project = Project::create_temp(tmp.path()).unwrap();
    let p = Persistence::new(project.path().to_path_buf());
    // The dispatch layer journals the canonical source + advanced flag.
    p.append_history("select * from T", true).unwrap();
    p.append_history("create table T with pk", false).unwrap();

    // Fresh session: hydrate the ring from disk.
    let entries = p.read_recent_history(10).unwrap();
    let mut app = App::new();
    app.seed_history(entries);

    // In simple mode the simple command recalls bare, the advanced one
    // recalls `:`-prefixed (runnable via the one-shot escape).
    app.update(key(KeyCode::Up));
    assert_eq!(app.input, "create table T with pk");
    app.update(key(KeyCode::Up));
    assert_eq!(app.input, ": select * from T");
}

#[test]
fn seed_history_preserves_chronological_order_for_navigation() {
    let mut app = App::new();
    app.seed_history(vec![
        "old".to_string(),
        "middle".to_string(),
        "newest".to_string(),
    ]);
    // Up should recall "newest" first (the most recent
    // entry, which is at the back of the vec by convention).
    app.update(key(KeyCode::Up));
    assert_eq!(app.input, "newest");
    app.update(key(KeyCode::Up));
    assert_eq!(app.input, "middle");
    app.update(key(KeyCode::Up));
    assert_eq!(app.input, "old");
}

#[test]
fn project_switched_event_seeds_history_from_payload() {
    let mut app = App::new();
    app.update(AppEvent::ProjectSwitched {
        display_name: "Foo".to_string(),
        is_temp: false,
        history_entries: vec!["aa".to_string(), "bb".to_string()],
        mode: rdbms_playground::mode::Mode::Simple,
    });
    assert_eq!(app.history, vec!["aa".to_string(), "bb".to_string()]);
    // Up navigates within the seeded entries.
    app.update(key(KeyCode::Up));
    assert_eq!(app.input, "bb");
}

#[test]
fn data_root_with_no_last_project_is_resume_safe() {
    let tmp = tempdir();
    // Fresh data root with no projects, no last_project.
    let _project = project::open_or_create(None, Some(tmp.path())).unwrap();
    // open_or_create itself doesn't write last_project (the
    // runtime does, after a successful open). That's fine —
    // the runtime test would write it. Verify that
    // read_last_project here returns None as expected.
    assert!(read_last_project(tmp.path()).unwrap().is_none());
}
