//! Tier-3 integration tests for ADR-0021 (per-command usage in
//! parse errors). Drives synthetic crossterm events through
//! `App::update` and asserts on the rendered output lines.
//!
//! Each test exercises the full input → parse → error-render
//! chain. The unit tests in `dsl::usage::tests` cover the
//! registry logic in isolation; these tests pin the user-visible
//! composition (caret + structural error + usage block, or the
//! available-commands fallback).

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

use rdbms_playground::action::Action;
use rdbms_playground::app::{App, OutputKind};
use rdbms_playground::event::AppEvent;

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))
}

/// Run `input` through the app and return every error-kind
/// output line. Asserts the submission parse-failed — which now
/// emits exactly a `JournalFailure` (ADR-0034: the failed line is
/// journalled `err`) and dispatches no command to the worker.
fn error_lines_for(input: &str) -> Vec<String> {
    let mut app = App::new();
    type_str(&mut app, input);
    let actions = submit(&mut app);
    assert!(
        matches!(actions.as_slice(), [Action::JournalFailure { .. }]),
        "expected parse failure (only a JournalFailure) for {input:?}, got {actions:?}",
    );
    app.output
        .iter()
        .filter(|l| l.kind == OutputKind::Error)
        .map(|l| l.text.clone())
        .collect()
}

fn dump(input: &str, lines: &[String]) -> String {
    format!(
        "INPUT: {input:?}\nERROR LINES:\n{}",
        lines.join("\n"),
    )
}

#[test]
fn create_alone_renders_create_table_usage() {
    let lines = error_lines_for("create");
    let dump_msg = dump("create", &lines);
    assert!(
        lines.iter().any(|l| l.starts_with("parse error")),
        "{dump_msg}",
    );
    assert!(
        lines.iter().any(|l| l == "usage:"),
        "missing usage: header\n{dump_msg}",
    );
    assert!(
        lines.iter().any(|l| l.contains("create table") && l.contains("with pk")),
        "missing create_table usage template\n{dump_msg}",
    );
}

#[test]
fn add_alone_renders_both_add_family_usages() {
    let lines = error_lines_for("add");
    let dump_msg = dump("add", &lines);
    // Aggregation across `choice` (ADR-0020): the structural
    // error line lists both add-family entries.
    assert!(
        lines.iter().any(|l| {
            l.starts_with("parse error")
                && l.contains("`1`")
                && l.contains("`column`")
        }),
        "expected aggregated `1` or `column` in structural error\n{dump_msg}",
    );
    // Usage block (ADR-0021): both add-* templates surface.
    assert!(
        lines.iter().any(|l| l.contains("add column")),
        "missing add_column usage\n{dump_msg}",
    );
    assert!(
        lines.iter().any(|l| l.contains("add 1:n relationship")),
        "missing add_relationship usage\n{dump_msg}",
    );
}

#[test]
fn drop_alone_renders_all_three_drop_family_usages() {
    let lines = error_lines_for("drop");
    let dump_msg = dump("drop", &lines);
    assert!(
        lines.iter().any(|l| l.contains("drop table")),
        "missing drop_table usage\n{dump_msg}",
    );
    assert!(
        lines.iter().any(|l| l.contains("drop column")),
        "missing drop_column usage\n{dump_msg}",
    );
    assert!(
        lines.iter().any(|l| l.contains("drop relationship")),
        "missing drop_relationship usage\n{dump_msg}",
    );
}

#[test]
fn show_alone_renders_both_show_family_usages() {
    let lines = error_lines_for("show");
    let dump_msg = dump("show", &lines);
    assert!(
        lines.iter().any(|l| l.contains("show data")),
        "missing show_data usage\n{dump_msg}",
    );
    assert!(
        lines.iter().any(|l| l.contains("show table")),
        "missing show_table usage\n{dump_msg}",
    );
}

#[test]
fn unknown_command_falls_back_to_available_commands_list() {
    let lines = error_lines_for("frobulate Customers");
    let dump_msg = dump("frobulate Customers", &lines);
    // No "usage:" header — the no-prefix fallback path renders
    // the available-commands list instead.
    assert!(
        lines.iter().all(|l| l != "usage:"),
        "should not render usage: header for unknown command\n{dump_msg}",
    );
    let available = lines
        .iter()
        .find(|l| l.starts_with("available commands:"))
        .unwrap_or_else(|| panic!("missing available commands line\n{dump_msg}"));
    // The list must include all ten command-entry keywords.
    for cmd in [
        "add", "change", "create", "delete", "drop", "insert",
        "rename", "replay", "show", "update",
    ] {
        assert!(
            available.contains(&format!("`{cmd}`")),
            "available commands missing `{cmd}`: {available}",
        );
    }
}

#[test]
fn update_partial_renders_update_usage_template() {
    // `update Customers set Active=false` parses through to
    // end-of-input; the missing `where` / `--all-rows` clause
    // triggers the structural error. The entry keyword is
    // `update`, so the update usage template is shown.
    let lines = error_lines_for("update Customers set Active=false");
    let dump_msg = dump("update Customers set Active=false", &lines);
    assert!(
        lines.iter().any(|l| l.contains("update <Table> set")),
        "missing update usage template\n{dump_msg}",
    );
}

#[test]
fn create_table_without_pk_renders_create_table_usage() {
    // The custom `try_map` error fires after `create table
    // Customers` is fully consumed; failure position points at
    // the start of the matched range, but matched_entry's `<=`
    // condition still resolves the entry keyword.
    let lines = error_lines_for("create table Customers");
    let dump_msg = dump("create table Customers", &lines);
    // Custom error wording (not just structural) is preserved.
    assert!(
        lines
            .iter()
            .any(|l| l.starts_with("parse error") && l.contains("with pk")),
        "missing custom-error wording about with pk\n{dump_msg}",
    );
    // And the usage template surfaces as well.
    assert!(
        lines
            .iter()
            .any(|l| l.contains("create table") && l.contains("with pk")),
        "missing create_table usage template\n{dump_msg}",
    );
}

#[test]
fn insert_partial_renders_insert_usage_template() {
    // `insert into T` needs either column-list or value-list to
    // follow. Parser reports a structural error; usage template
    // surfaces.
    let lines = error_lines_for("insert into T");
    let dump_msg = dump("insert into T", &lines);
    assert!(
        lines.iter().any(|l| l.contains("insert into <Table>")),
        "missing insert usage template\n{dump_msg}",
    );
}

#[test]
fn caret_aligns_under_offending_token() {
    // The caret line is whitespace + `^`. After the "running: "
    // prefix (9 chars) plus the byte offset of the failure
    // position, the `^` should sit directly under the
    // offending character. For `frobulate Customers`, the
    // failure is at position 0, so the caret is at column 9.
    let lines = error_lines_for("frobulate Customers");
    let caret = lines
        .iter()
        .find(|l| l.trim_start_matches(' ').starts_with('^'))
        .expect("missing caret line");
    let leading_spaces = caret.chars().take_while(|c| *c == ' ').count();
    assert_eq!(
        leading_spaces, 9,
        "caret should sit at column 9 (under `f` of `frobulate` after the `running: ` prefix); got {leading_spaces} spaces in {caret:?}",
    );
}
