//! Iteration-4a integration tests: the explicit `rebuild`
//! app-level command (ADR-0015 §7, §11).
//!
//! Covers the App-level dispatch (typing `rebuild` opens the
//! confirmation modal) and the worker-level wipe-and-rebuild
//! against a populated database. The runtime's spawn glue
//! is exercised manually here since we don't boot a Tokio
//! event loop in tests; we drive `Database::rebuild_from_text`
//! directly to verify it works on a populated db.

use std::fs;

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

use rdbms_playground::action::Action;
use rdbms_playground::app::{App, Modal, RebuildConfirmModal};
use rdbms_playground::db::Database;
use rdbms_playground::dsl::{ColumnSpec, Type, Value};
use rdbms_playground::event::AppEvent;
use rdbms_playground::persistence::Persistence;
use rdbms_playground::project;

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

fn type_str(app: &mut App, s: &str) {
    for c in s.chars() {
        app.update(key(KeyCode::Char(c)));
    }
}

fn submit(app: &mut App) -> Vec<Action> {
    app.update(key(KeyCode::Enter))
}

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

fn rt() -> tokio::runtime::Runtime {
    tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .expect("tokio rt")
}

#[test]
fn typing_rebuild_emits_prepare_action() {
    let mut app = App::new();
    type_str(&mut app, "rebuild");
    let actions = submit(&mut app);
    assert_eq!(actions, vec![Action::PrepareRebuild]);
    // No modal yet — the runtime still has to compute the
    // summary and post `RebuildPrepared` back.
    assert!(app.modal.is_none());
}

#[test]
fn rebuild_prepared_event_opens_modal_with_summary() {
    let mut app = App::new();
    app.update(AppEvent::RebuildPrepared {
        summary: "3 tables and 47 rows will be reconstructed".to_string(),
    });
    match app.modal.as_ref() {
        Some(Modal::RebuildConfirm(RebuildConfirmModal { summary })) => {
            assert!(summary.contains("3 tables"));
        }
        other => panic!("expected RebuildConfirm modal, got {other:?}"),
    }
}

#[test]
fn modal_y_emits_rebuild_action_and_closes() {
    let mut app = App::new();
    app.update(AppEvent::RebuildPrepared {
        summary: "summary".to_string(),
    });
    let actions = app.update(key(KeyCode::Char('Y')));
    assert_eq!(actions.len(), 1);
    let Action::Rebuild { source } = &actions[0] else {
        panic!("expected Rebuild action, got {:?}", actions[0]);
    };
    assert_eq!(source, "rebuild");
    assert!(app.modal.is_none(), "modal should close on confirm");
}

#[test]
fn modal_n_or_esc_dismisses_without_action() {
    for code in [KeyCode::Char('N'), KeyCode::Esc] {
        let mut app = App::new();
        app.update(AppEvent::RebuildPrepared {
            summary: "summary".to_string(),
        });
        let actions = app.update(key(code));
        assert!(actions.is_empty(), "no actions emitted on dismiss");
        assert!(app.modal.is_none(), "modal should close on dismiss");
    }
}

#[test]
fn modal_swallows_unrelated_keys() {
    let mut app = App::new();
    app.update(AppEvent::RebuildPrepared {
        summary: "summary".to_string(),
    });
    // A regular character key should not type into the input
    // field while the modal is up.
    app.update(key(KeyCode::Char('x')));
    assert!(app.input.is_empty(), "modal should swallow key input");
    assert!(
        app.modal.is_some(),
        "modal still active after unrelated key"
    );
}

#[test]
fn rebuild_against_populated_db_wipes_and_reloads() {
    let data = tempdir();
    let project_path = {
        let project = project::open_or_create(None, Some(data.path())).unwrap();
        let path = project.path().to_path_buf();
        let db = Database::open_with_persistence(project.db_path(), Persistence::new(path.clone()))
            .unwrap();
        rt().block_on(async {
            db.create_table(
                "Customers".to_string(),
                vec![
                    ColumnSpec::new("id".to_string(), Type::Serial),
                    ColumnSpec::new("Name".to_string(), Type::Text),
                ],
                vec!["id".to_string()],
                Some("create".to_string()),
            )
            .await
            .unwrap();
            db.insert(
                "Customers".to_string(),
                None,
                vec![Value::Text("Alice".to_string())],
                Some("insert".to_string()),
            )
            .await
            .unwrap();
        });
        drop(db);
        drop(project);
        path
    };

    // Hand-edit the CSV to introduce a different row content.
    // Rebuild should pick up the edited content.
    let csv_path = project_path.join("data").join("Customers.csv");
    let edited = fs::read_to_string(&csv_path)
        .unwrap()
        .replace("Alice", "Edna");
    fs::write(&csv_path, edited).unwrap();

    // Reopen with persistence (the .db still exists but has
    // "Alice"). Run rebuild — it should wipe and reload.
    let project = project::Project::open(&project_path).unwrap();
    let db = Database::open_with_persistence(
        project.db_path(),
        Persistence::new(project.path().to_path_buf()),
    )
    .unwrap();
    rt().block_on(async {
        db.rebuild_from_text(project.path().to_path_buf(), Some("rebuild".to_string()))
            .await
            .expect("rebuild");
    });
    let rows = rt()
        .block_on(async { db.query_data("Customers".to_string(), None, None).await })
        .unwrap();
    assert_eq!(rows.rows.len(), 1);
    assert_eq!(rows.rows[0][1].as_deref(), Some("Edna"));

    // ADR-0052: `rebuild` journaling moved to the dispatch layer
    // (`spawn_rebuild`), so the direct worker call here no longer writes
    // history.log; this test verifies the wipe/reload behaviour only.
}
