//! Matrix coverage for `insert into T (cols) values (vals)`
//! (Form A — explicit column list).
//!
//! Form A is the most complex insert form: the column list
//! `(cols)` constrains which values are required and in what
//! order, and the dispatch must mirror `do_insert`'s `user_cols`
//! contract (ADR-0018 §3, handoff-12 §B). Bugs unique to Form A
//! from handoff-12:
//!
//! - **E2**: `insert into T (` showed no column candidates
//!   because the value-literal suppression masked the
//!   `Ident{Columns}` expectation. Fixed in commit 619a8bd.
//! - **D2**: `insert into T (a, b, c) values (1, 2, 3` (no
//!   closing paren) classified as `DefiniteErrorAt(<values>)`
//!   because `walk_optional` was rolling back the partial value
//!   list. Fixed in commit 5815918.
//!
//! Every meaningful cursor position is exercised against the
//! relevant schema shapes; per-cell snapshots pin behaviour
//! and explicit property assertions document the invariants
//! that matter most.

use crate::typing_surface::*;
use rdbms_playground::input_render::InputState;

// =========================================================
// Cursor at the entry-word boundary.
// =========================================================

#[test]
fn after_insert_keyword_expects_into() {
    let schema = schema_serial_pk();
    let a = assess_at_end("insert", &schema);
    assert!(matches!(a.state, InputState::IncompleteAtEof));
    crate::snap!("after_insert_keyword", a);
}

#[test]
fn after_insert_space_expects_into() {
    let schema = schema_serial_pk();
    let a = assess_at_end("insert ", &schema);
    assert!(matches!(a.state, InputState::IncompleteAtEof));
    assert_candidate_present(&a, &["into"]);
    crate::snap!("after_insert_space", a);
}

#[test]
fn after_into_offers_table_candidates() {
    let schema = schema_serial_pk();
    let a = assess_at_end("insert into ", &schema);
    assert!(matches!(a.state, InputState::IncompleteAtEof));
    assert_candidate_present(&a, &["Customers"]);
    crate::snap!("after_into_serial_pk", a);
}

#[test]
fn after_into_with_multi_table_offers_both() {
    let schema = schema_multi_table();
    let a = assess_at_end("insert into ", &schema);
    assert_candidate_present(&a, &["Customers", "Orders"]);
    crate::snap!("after_into_multi_table", a);
}

// =========================================================
// Cursor immediately after the table-name boundary.
//
// At `insert into Customers ` the walker must offer two
// branches: `values` (Form B/A pivot) and `(` (Form A/C pivot).
// Handoff §D1 fixed this: the `(` punctuation now surfaces as
// a `CandidateKind::Punct` candidate.
// =========================================================

#[test]
fn after_table_name_offers_values_and_open_paren() {
    let schema = schema_serial_pk();
    let a = assess_at_end("insert into Customers ", &schema);
    assert!(matches!(a.state, InputState::IncompleteAtEof));
    assert_candidate_present(&a, &["values", "("]);
    crate::snap!("after_table_name", a);
}

// =========================================================
// Form A path enters at the `(`.
//
// The §1 bug E2 was that `insert into T (` showed nothing —
// no column candidates surfaced. Reproduces here as a property
// assertion (must include at least one column from the table)
// and the inverse leakage check (must NOT include columns
// from sibling tables).
// =========================================================

#[test]
fn after_open_paren_offers_active_table_columns_only() {
    let schema = schema_multi_table();
    let a = assess_at_end("insert into Customers (", &schema);
    assert!(matches!(a.state, InputState::IncompleteAtEof));
    assert_candidate_present(&a, &["id", "Name"]);
    assert_no_candidate_named(&a, &["OrderId", "CustId", "Total"]);
    crate::snap!("after_open_paren_multi_table", a);
}

#[test]
fn after_open_paren_serial_pk_offers_all_columns_including_serial() {
    let schema = schema_serial_pk();
    let a = assess_at_end("insert into Customers (", &schema);
    assert_candidate_present(&a, &["id", "Name", "Email"]);
    crate::snap!("after_open_paren_serial_pk", a);
}

#[test]
fn after_open_paren_text_pk_offers_text_pk_columns() {
    let schema = schema_text_pk();
    let a = assess_at_end("insert into Items (", &schema);
    assert_candidate_present(&a, &["Code", "Title"]);
    crate::snap!("after_open_paren_text_pk", a);
}

// =========================================================
// Mid-column-list positions.
// =========================================================

#[test]
fn mid_column_list_after_comma_offers_remaining_columns() {
    let schema = schema_serial_pk();
    let a = assess_at_end("insert into Customers (id, ", &schema);
    assert!(matches!(a.state, InputState::IncompleteAtEof));
    assert_candidate_present(&a, &["Name", "Email"]);
    crate::snap!("after_comma_in_column_list", a);
}

#[test]
fn after_close_paren_expects_values_keyword() {
    let schema = schema_serial_pk();
    let a = assess_at_end("insert into Customers (Name)", &schema);
    assert!(matches!(a.state, InputState::IncompleteAtEof));
    assert_candidate_present(&a, &["values"]);
    crate::snap!("after_close_paren", a);
}

#[test]
fn after_values_keyword_expects_open_paren() {
    let schema = schema_serial_pk();
    // Trailing space so we're past the `values` word boundary
    // — without it the partial-prefix logic re-offers `values`
    // itself as the candidate that matches the typed prefix.
    let a = assess_at_end("insert into Customers (Name) values ", &schema);
    assert!(matches!(a.state, InputState::IncompleteAtEof));
    assert_candidate_present(&a, &["("]);
    crate::snap!("after_values_keyword", a);
}

// =========================================================
// Value-list positions — typed-slot prose.
// =========================================================

#[test]
fn after_values_open_paren_form_a_text_column_prose_names_column() {
    let schema = schema_serial_pk();
    let a = assess_at_end("insert into Customers (Name) values (", &schema);
    assert!(
        hint_prose_contains(&a, "Name"),
        "expected column name in prose, got {:?}",
        a.hint,
    );
    assert!(
        hint_prose_contains(&a, "quoted string"),
        "expected text-slot prose, got {:?}",
        a.hint,
    );
    crate::snap!("after_values_open_paren_text_col", a);
}

#[test]
fn after_values_open_paren_form_a_serial_column_offers_null_to_auto_generate() {
    let schema = schema_serial_pk();
    let a = assess_at_end("insert into Customers (id, Name) values (", &schema);
    let prose = hint_prose(&a).unwrap_or_else(|| panic!("expected Prose hint, got {:?}", a.hint));
    assert!(
        prose.contains("id"),
        "prose should name `id`, got {prose:?}"
    );
    assert!(
        prose.contains("null") && prose.contains("auto-generate"),
        "prose should mention `null` to auto-generate, got {prose:?}",
    );
    crate::snap!("after_values_open_paren_serial_col", a);
}

#[test]
fn mid_value_list_after_comma_advances_to_next_column_prose() {
    let schema = schema_serial_pk();
    let a = assess_at_end(
        "insert into Customers (Name, Email) values ('Alice', ",
        &schema,
    );
    let prose = hint_prose(&a).unwrap_or_else(|| panic!("expected Prose hint, got {:?}", a.hint));
    assert!(
        prose.contains("Email"),
        "prose should name `Email`, got {prose:?}",
    );
    crate::snap!("after_first_value_advance_to_email", a);
}

// =========================================================
// In-progress Form A — regression for handoff §1 bug D2.
//
// `insert into Orders (...) values (..., ...` (no closing
// paren) must classify as IncompleteAtEof, not
// DefiniteErrorAt.
// =========================================================

#[test]
fn form_a_in_progress_values_list_is_incomplete_not_definite_error() {
    let schema = schema_multi_table();
    let a = assess_at_end(
        "insert into Orders (OrderId, CustId, Total) values (42, 89, 17.59",
        &schema,
    );
    assert!(
        matches!(a.state, InputState::IncompleteAtEof),
        "expected IncompleteAtEof (bug §1-D2 regression), got {:?}",
        a.state,
    );
    crate::snap!("form_a_in_progress_no_red", a);
}

#[test]
fn form_a_in_progress_with_one_value_is_incomplete() {
    let schema = schema_multi_table();
    let a = assess_at_end(
        "insert into Orders (OrderId, CustId, Total) values (42",
        &schema,
    );
    assert!(matches!(a.state, InputState::IncompleteAtEof));
    crate::snap!("form_a_in_progress_one_value", a);
}

// =========================================================
// Form A: complete inputs parse to Insert.
// =========================================================

#[test]
fn form_a_complete_parses_to_insert() {
    let schema = schema_serial_pk();
    let a = assess_at_end(
        "insert into Customers (Name, Email) values ('Alice', 'a@b.c')",
        &schema,
    );
    assert!(matches!(a.state, InputState::Valid));
    assert_eq!(a.parse_result.as_deref(), Ok("Insert"));
    crate::snap!("form_a_complete_two_values", a);
}

#[test]
fn form_a_complete_with_serial_in_list_parses() {
    let schema = schema_serial_pk();
    let a = assess_at_end(
        "insert into Customers (id, Name, Email) values (1, 'Alice', 'a@b.c')",
        &schema,
    );
    assert!(matches!(a.state, InputState::Valid));
    assert_eq!(a.parse_result.as_deref(), Ok("Insert"));
    crate::snap!("form_a_complete_with_serial", a);
}

// =========================================================
// Per-column-type prose across the Type axis (every_type
// schema). Each call to `Things.<col>` value position
// exercises one Type variant's catalog prose.
// =========================================================

#[test]
fn form_a_int_slot_prose_says_integer() {
    let schema = schema_every_type();
    let a = assess_at_end("insert into Things (k) values (", &schema);
    let prose =
        hint_prose(&a).unwrap_or_else(|| panic!("expected Prose for int slot, got {:?}", a.hint));
    assert!(
        prose.contains("integer"),
        "int-slot prose should say `integer`, got {prose:?}",
    );
    crate::snap!("form_a_int_slot", a);
}

#[test]
fn form_a_date_slot_prose_says_yyyy_mm_dd() {
    let schema = schema_every_type();
    let a = assess_at_end("insert into Things (dt) values (", &schema);
    let prose =
        hint_prose(&a).unwrap_or_else(|| panic!("expected Prose for date slot, got {:?}", a.hint));
    assert!(
        prose.contains("YYYY-MM-DD"),
        "date-slot prose should reference YYYY-MM-DD format, got {prose:?}",
    );
    crate::snap!("form_a_date_slot", a);
}

#[test]
fn form_a_bool_slot_prose_mentions_true_false() {
    let schema = schema_every_type();
    let a = assess_at_end("insert into Things (b) values (", &schema);
    let prose =
        hint_prose(&a).unwrap_or_else(|| panic!("expected Prose for bool slot, got {:?}", a.hint));
    assert!(
        prose.contains("true") && prose.contains("false"),
        "bool-slot prose should mention `true`/`false`, got {prose:?}",
    );
    crate::snap!("form_a_bool_slot", a);
}

#[test]
fn form_a_shortid_slot_prose_mentions_null_to_auto_generate() {
    let schema = schema_every_type();
    let a = assess_at_end("insert into Things (sid) values (", &schema);
    let prose = hint_prose(&a)
        .unwrap_or_else(|| panic!("expected Prose for shortid slot, got {:?}", a.hint));
    assert!(
        prose.contains("null") && prose.contains("auto-generate"),
        "shortid-slot prose should mention `null` to auto-generate, got {prose:?}",
    );
    crate::snap!("form_a_shortid_slot", a);
}

// =========================================================
// Column-name leakage: Form A's column list must not offer
// columns from sibling tables in a multi-table schema.
// =========================================================

#[test]
fn form_a_open_paren_no_leakage_from_other_tables() {
    let schema = schema_multi_table();
    let a = assess_at_end("insert into Orders (", &schema);
    assert_no_candidate_named(&a, &["Name"]);
    assert_candidate_present(&a, &["OrderId", "CustId", "Total"]);
    crate::snap!("form_a_open_paren_orders", a);
}
