//! SQL `DELETE` grammar (ADR-0033 §1/§7, sub-phase 3f).
//!
//! Grammar-as-text (ADR-0030 §4): the walker validates that the
//! `DELETE` is in the supported subset; the worker executes the
//! validated SQL text, observes any FK cascade by row-count diffing
//! (ADR-0033 Amendment 2), and re-persists the target table's CSV
//! plus every cascade-affected child (ADR-0030 §11). The shape here
//! is the post-`DELETE` portion — the entry-word dispatch consumes
//! the leading `DELETE` keyword before this shape walks, so the
//! shape opens at `FROM`. `delete` is a shared entry word
//! (sub-phase 3j): this `Advanced` SQL shape and the `Simple` DSL
//! delete node both register under `delete`.
//!
//! Scope (3f): `FROM <table> [ WHERE … ] [ ';' ]`, the `__rdbms_*`
//! target rejection, and the shared `sql_expr` on the WHERE
//! predicate. There is no `--all-rows` rail — a SQL `DELETE` without
//! `WHERE` runs as written (ADR-0030 §12). `RETURNING` (3g) lands
//! later. The worker never inspects the WHERE clause (Amendment 2),
//! so no predicate-byte extraction is needed.

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

/// The `DELETE` target table. `__rdbms_*` rejected (ADR-0030 §6 /
/// ADR-0033 §1). `writes_table` populates `current_table` /
/// `current_table_columns` so the WHERE predicate gets 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 WHERE columns against it for
/// free (ADR-0033 §2's "cross-cut from Phase-2 machinery"; the
/// handoff-31 §3e finding that a bespoke role leaves the WHERE
/// unchecked).
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,
};

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

/// The post-`DELETE` portion of a SQL `DELETE` statement
/// (ADR-0033 §1): `FROM <table> [ WHERE … ] [ ';' ]`.
///
/// The entry-word dispatch consumes the leading `DELETE` keyword
/// before this shape walks, so a `CommandNode` references it as its
/// `shape` (sub-phase 3f registers a development entry word;
/// sub-phase 3j wires the shared `delete` entry word).
pub static SQL_DELETE_SHAPE: Node = Node::Seq(SQL_DELETE_TAIL_NODES);

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

#[cfg(test)]
mod tests {
    use super::SQL_DELETE_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 DELETE 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_DELETE_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 DELETE tail");
    }

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

    #[test]
    fn delete_with_where() {
        good("from orders where id = 1");
        good("from orders where id = 1;");
    }

    #[test]
    fn delete_without_where_runs_across_all_rows() {
        // ADR-0030 §12: no `--all-rows` rail — a SQL DELETE without
        // WHERE is structurally valid.
        good("from orders");
        good("from orders;");
    }

    #[test]
    fn where_admits_sql_expr() {
        good("from orders where total > 100 and note is null");
        good("from orders where created < '2025-01-01'");
    }

    #[test]
    fn where_admits_subquery_r2() {
        // R2 invariant (ADR-0033 §7 / Amendment 2): the WHERE may
        // itself contain a subquery. The shape admits it; the worker
        // executes the verbatim statement and never extracts the
        // predicate, so the nested subquery is just part of the SQL.
        good("from orders where customer_id in (select id from customers where country = 'DE')");
    }

    #[test]
    fn returning_tail_admitted() {
        // 3g: optional RETURNING projection_list tail.
        good("from orders where id = 1 returning *");
        good("from orders returning id, total");
        good("from orders where id = 1 returning id as gone;");
    }

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

    #[test]
    fn structurally_incomplete_or_wrong_rejected() {
        // Missing FROM.
        bad("orders where id = 1");
        bad("orders");
        // FROM with no table.
        bad("from");
        bad("from where id = 1");
        // Incomplete WHERE predicate.
        bad("from orders where");
        // DELETE has no SET clause — trailing SET must not consume.
        bad("from orders set v = 1");
    }
}
