//! ADR-0002 user-facing posture: regression audit.
//!
//! ADR-0002's "User-facing posture" section commits to never
//! exposing the underlying engine's name in user-visible
//! strings. The chosen product (and its idioms — STRICT,
//! PRAGMA, the rusqlite crate) is an implementation detail;
//! students should leave with knowledge of relational concepts,
//! not of one specific RDBMS.
//!
//! This test file exists so that a future change can't silently
//! regress that posture. The strings asserted here are a
//! representative cross-section of user-reachable surfaces:
//!
//! - CLI usage banner (`HELP_TEXT`).
//! - In-app `help` output (`note_help`).
//! - DSL parse-error wording.
//! - Realistic `DbError` payloads carried via
//!   `friendly_message()` (the surface the runtime forwards to
//!   `AppEvent::DslFailed`).
//!
//! See ADR-0002 §"User-facing posture" for the contract.
//! Code comments and ADR prose are explicitly allowed to name
//! the engine — only user-facing strings are policed.

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

use rdbms_playground::app::App;
use rdbms_playground::cli::help_text;
use rdbms_playground::db::{DbError, SqliteErrorKind};
use rdbms_playground::dsl::parse_command;
use rdbms_playground::event::AppEvent;

const FORBIDDEN: &[&str] = &[
    // Product names.
    "SQLite", "sqlite",   // Crate name.
    "rusqlite", // Engine-specific keywords / idioms.
    "STRICT", "PRAGMA",
];

/// Report the first forbidden token found in `s`, with byte
/// offset, so failure output points at exactly what leaked.
fn engine_vocab_leak(s: &str) -> Option<(&'static str, usize)> {
    for needle in FORBIDDEN {
        if let Some(pos) = s.find(needle) {
            return Some((needle, pos));
        }
    }
    None
}

fn assert_clean(label: &str, s: &str) {
    if let Some((needle, pos)) = engine_vocab_leak(s) {
        panic!("ADR-0002 leak in {label}: found `{needle}` at byte {pos} in:\n{s}");
    }
}

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) {
    app.update(key(KeyCode::Enter));
}

fn collect_output(app: &App) -> String {
    app.output
        .iter()
        .map(|l| l.text.as_str())
        .collect::<Vec<_>>()
        .join("\n")
}

#[test]
fn cli_help_text_uses_no_engine_vocabulary() {
    assert_clean("CLI help_text()", &help_text());
}

#[test]
fn in_app_help_uses_no_engine_vocabulary() {
    let mut app = App::new();
    type_str(&mut app, "help");
    submit(&mut app);
    assert_clean("in-app help", &collect_output(&app));
}

#[test]
fn parse_errors_use_no_engine_vocabulary() {
    // A representative set of failing inputs: structural
    // (missing colon, wrong keyword), unknown type, and the
    // change-column flag conflict. All must produce
    // engine-free messages.
    let inputs: &[&str] = &[
        // structural: column-name-first typo (the parser
        // tiny-win recipe from handoff-5).
        "change column Tag in Customers: Tag (text)",
        // unknown type token.
        "create table T with pk id(varchar)",
        // mutually exclusive flags on change column.
        "change column T: c (int) --force-conversion --dont-convert",
        // missing required clause.
        "create table T",
        // garbage.
        "this is not a command",
    ];
    for input in inputs {
        let err = parse_command(input).expect_err(&format!("expected parse failure for `{input}`"));
        let rendered = format!("{err:?}");
        assert_clean(&format!("parse error for `{input}`"), &rendered);
    }
}

#[test]
fn db_error_friendly_message_uses_no_engine_vocabulary() {
    // A representative set of `DbError` payloads, mirroring the
    // shapes the runtime actually surfaces via
    // `AppEvent::DslFailed { error: DbError::friendly_message }`.
    // These cover the three code-constructed variants: Sqlite
    // (engine-classified, message comes from rusqlite or our own
    // hand-rolled "no such ..."), Unsupported (refusals), and
    // InvalidValue (input validation).
    let cases: Vec<(&str, DbError)> = vec![
        (
            "no-such-table",
            DbError::Sqlite {
                message: "no such table: Customers".to_string(),
                kind: SqliteErrorKind::NoSuchTable,
            },
        ),
        (
            "no-such-column",
            DbError::Sqlite {
                message: "no such column: Customers.zip".to_string(),
                kind: SqliteErrorKind::NoSuchColumn,
            },
        ),
        (
            "unique-violation",
            DbError::Sqlite {
                message: "UNIQUE constraint failed: T.id".to_string(),
                kind: SqliteErrorKind::UniqueViolation,
            },
        ),
        (
            "fk-violation",
            DbError::Sqlite {
                message: "FOREIGN KEY constraint failed".to_string(),
                kind: SqliteErrorKind::Other,
            },
        ),
        (
            "unsupported-refusal",
            DbError::Unsupported(
                "cannot drop primary-key column `T.id`. \
                 Drop the table or change the primary key first."
                    .to_string(),
            ),
        ),
        (
            "invalid-value",
            DbError::InvalidValue("expected 3 value(s), got 2".to_string()),
        ),
    ];
    for (label, err) in cases {
        assert_clean(label, &err.friendly_message());
    }
}
