//! SQL `UPDATE` grammar (ADR-0033 §2, sub-phase 3e).
//!
//! Grammar-as-text (ADR-0030 §4): the walker validates that the
//! `UPDATE` is in the supported subset; the worker executes the
//! validated SQL text and re-persists the target table's CSV
//! (ADR-0030 §11). The shape here is the post-`UPDATE` portion —
//! the entry-word dispatch consumes the leading `UPDATE` keyword
//! before this shape walks. `update` is a shared entry word
//! (sub-phase 3j): this `Advanced` SQL shape and the `Simple` DSL
//! update node both register under `update`.
//!
//! Scope (3e): `<table> SET assignment_list [ WHERE … ]`, the
//! `__rdbms_*` target rejection, and the shared `sql_expr` on both
//! the assignment RHS and the WHERE predicate. There is no
//! `--all-rows` rail — a SQL `UPDATE` without `WHERE` runs as
//! written (ADR-0030 §12). `RETURNING` (3g) lands later.

use crate::dsl::grammar::shared::SET_VALUE;
use crate::dsl::grammar::sql_select::{RETURNING_CLAUSE, WHERE_CLAUSE, reject_internal_table};
use crate::dsl::grammar::{IdentSource, Node, Word};

static COMMA: Node = Node::Punct(',');

/// The `UPDATE` target table. `__rdbms_*` rejected (ADR-0030 §6 /
/// ADR-0033 §1). `writes_table` populates `current_table` /
/// `current_table_columns` so the `SET` columns and the WHERE
/// predicate get column completion against the target.
///
/// Uses the shared `table_name` role (not a bespoke one) so the
/// Phase-2 schema-existence + predicate-warning passes collect it
/// as a scope binding and check the SET / WHERE columns against it
/// for free (ADR-0033 §2's "cross-cut from Phase-2 machinery").
const TARGET_TABLE: Node = Node::Ident {
    source: IdentSource::Tables,
    role: "table_name",
    validator: Some(reject_internal_table),
    highlight_override: None,
    writes_table: true,
    writes_column: false,
    writes_user_listed_column: false,
    writes_table_alias: false,
    writes_cte_name: false,
    writes_projection_alias: false,
};

/// The column on the left of one `SET col = expr` assignment.
///
/// `writes_column: true` resolves the column's type into
/// `current_column` (and frames the value-slot hint via
/// `pending_value_column`), so the RHS `SET_VALUE` lookahead can
/// dispatch the column-typed slot for a lone literal (ADR-0036
/// Phase 3a).
const ASSIGN_COLUMN: Node = Node::Ident {
    source: IdentSource::Columns,
    role: "update_set_column",
    validator: None,
    highlight_override: None,
    writes_table: false,
    writes_column: true,
    writes_user_listed_column: false,
    writes_table_alias: false,
    writes_cte_name: false,
    writes_projection_alias: false,
};

/// `column_name '=' <value>` — the RHS is the boundary-aware
/// `SET_VALUE` slot (ADR-0036 Phase 3a): a lone literal routes to the
/// column-typed slot (live per-column hint + numeric-shape highlight,
/// shared with the DSL), while any expression — arithmetic, a
/// literal-prefixed form, `CASE`, function calls, scalar subqueries —
/// falls through to the full `sql_expr` grammar (ADR-0031), which the
/// engine evaluates at execution time.
static ASSIGNMENT_NODES: &[Node] = &[ASSIGN_COLUMN, Node::Punct('='), SET_VALUE];
static ASSIGNMENT: Node = Node::Seq(ASSIGNMENT_NODES);

/// `assignment ( ',' assignment )*`.
const ASSIGNMENT_LIST: Node = Node::Repeated {
    inner: &ASSIGNMENT,
    separator: Some(&COMMA),
    min: 1,
};

static SQL_UPDATE_TAIL_NODES: &[Node] = &[
    TARGET_TABLE,
    Node::Word(Word::keyword("set")),
    ASSIGNMENT_LIST,
    Node::Optional(&WHERE_CLAUSE),
    Node::Optional(&RETURNING_CLAUSE),
    Node::Optional(&Node::Punct(';')),
];

/// The post-`UPDATE` portion of a SQL `UPDATE` statement
/// (ADR-0033 §2): `<table> SET col = expr (',' col = expr)*
/// [ WHERE … ] [ ';' ]`.
///
/// The entry-word dispatch consumes the leading `UPDATE` keyword
/// before this shape walks, so a `CommandNode` references it as
/// its `shape` (sub-phase 3e registers a development entry word;
/// sub-phase 3j wires the shared `update` entry word).
pub static SQL_UPDATE_SHAPE: Node = Node::Seq(SQL_UPDATE_TAIL_NODES);

// =================================================================
// Tests — grammar accept/reject for the post-`UPDATE` tail.
// =================================================================

#[cfg(test)]
mod tests {
    use super::SQL_UPDATE_SHAPE;
    use crate::dsl::walker::context::WalkContext;
    use crate::dsl::walker::driver::{NodeWalkResult, walk_node};
    use crate::dsl::walker::outcome::MatchedPath;

    /// Walk `input` against the UPDATE tail. Returns `true` only
    /// when the walk matches *and* consumes all of `input`
    /// (trailing whitespace allowed). Schemaless: the shape is
    /// structural, so table/column idents match by shape and
    /// `reject_internal_table` still fires on `__rdbms_*`.
    fn walks(input: &str) -> bool {
        let mut ctx = WalkContext::new();
        let mut path = MatchedPath::new();
        let mut per_byte = Vec::new();
        match walk_node(
            input,
            0,
            &SQL_UPDATE_SHAPE,
            &mut ctx,
            &mut path,
            &mut per_byte,
        ) {
            NodeWalkResult::Matched { end, .. } => input[end..].trim().is_empty(),
            _ => false,
        }
    }

    fn good(input: &str) {
        assert!(walks(input), "{input:?} should be a valid UPDATE tail");
    }

    fn bad(input: &str) {
        assert!(
            !walks(input),
            "{input:?} should NOT walk as a complete UPDATE tail"
        );
    }

    #[test]
    fn single_assignment_with_where() {
        good("t set v = 'x' where id = 1");
        good("t set v = 1 where id = 1;");
    }

    #[test]
    fn multi_assignment() {
        good("t set a = 1, b = 2 where id = 1");
        good("orders set total = 0, note = 'void' where id = 7");
    }

    #[test]
    fn no_where_runs_across_all_rows() {
        // ADR-0030 §12: no `--all-rows` rail — a SQL UPDATE without
        // WHERE is structurally valid.
        good("t set active = false");
        good("t set a = 1, b = 2");
    }

    #[test]
    fn assignment_rhs_admits_sql_expr() {
        good("t set total = price * quantity where id = 1");
        good("t set v = case when x > 0 then x else 0 end");
        good("t set v = (select max(other) from other_table) where id = 1");
    }

    #[test]
    fn returning_tail_admitted() {
        // 3g: optional RETURNING projection_list tail.
        good("t set v = 1 where id = 1 returning *");
        good("t set v = 1 returning id, v");
        good("t set v = 1 where id = 1 returning v as new_v;");
    }

    #[test]
    fn internal_target_table_rejected() {
        bad("__rdbms_playground_columns set a = 1");
        bad("__rdbms_playground_relationships set a = 1 where id = 1");
    }

    #[test]
    fn structurally_incomplete_or_wrong_rejected() {
        // Missing SET.
        bad("t where id = 1");
        bad("t");
        // SET with no assignment.
        bad("t set");
        bad("t set where id = 1");
        // Assignment missing RHS.
        bad("t set v =");
        // Trailing comma with no following assignment.
        bad("t set a = 1,");
    }
}
