//! Integration tests for `runtime::enrich_dsl_failure`
//! (ADR-0019 §6).
//!
//! Each test:
//!   1. Bootstraps a real `Database` (in-memory).
//!   2. Constructs the schema/data needed to trigger one
//!      class of engine error.
//!   3. Provokes the failure through the public Database API,
//!      capturing the resulting `DbError`.
//!   4. Calls `enrich_dsl_failure` and asserts the
//!      `FailureContext` carries the schema-resolved facts a
//!      learner would expect to see in the rendered error.
//!
//! Pinpoint diagnostic-table presence is verified for the
//! UNIQUE INSERT case (the most pedagogically valuable
//! pinpoint today).

use tokio::runtime::Runtime;

use rdbms_playground::db::{Database, DbError, SqliteErrorKind};
use rdbms_playground::dsl::parser::parse_command;
use rdbms_playground::dsl::{
    ColumnSpec, Command, RowFilter, Type, Value, action::ReferentialAction,
};
use rdbms_playground::runtime::enrich_dsl_failure;

fn rt() -> Runtime {
    tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .expect("tokio rt")
}

fn db() -> Database {
    Database::open(":memory:").expect("open in-memory db")
}

// ---- UNIQUE -----------------------------------------------------

#[test]
fn enrich_unique_insert_resolves_table_column_value_and_pinpoint() {
    let db = db();
    rt().block_on(async {
        // Create a table with a serial PK; insert a row; insert
        // again with the same PK value to trigger UNIQUE.
        db.create_table(
            "Customers".to_string(),
            vec![
                ColumnSpec::new("id".to_string(), Type::Int),
                ColumnSpec::new("name".to_string(), Type::Text),
            ],
            vec!["id".to_string()],
            None,
        )
        .await
        .unwrap();
        db.insert(
            "Customers".to_string(),
            None,
            vec![
                Value::Number("5".to_string()),
                Value::Text("Alice".to_string()),
            ],
            None,
        )
        .await
        .unwrap();

        // Second insert with the same PK — UNIQUE violation.
        let cmd = Command::Insert {
            table: "Customers".to_string(),
            columns: Some(vec!["id".to_string(), "name".to_string()]),
            values: vec![
                Value::Number("5".to_string()),
                Value::Text("Bob".to_string()),
            ],
        };
        let err = db
            .insert(
                "Customers".to_string(),
                Some(vec!["id".to_string(), "name".to_string()]),
                vec![
                    Value::Number("5".to_string()),
                    Value::Text("Bob".to_string()),
                ],
                None,
            )
            .await
            .unwrap_err();
        assert!(matches!(
            err,
            DbError::Sqlite {
                kind: SqliteErrorKind::UniqueViolation,
                ..
            }
        ));

        let facts = enrich_dsl_failure(&db, &cmd, &err).await;
        assert_eq!(facts.table.as_deref(), Some("Customers"));
        assert_eq!(facts.column.as_deref(), Some("id"));
        assert_eq!(facts.value.as_deref(), Some("5"));
        // Pinpoint: existing row with id=5 should be present.
        let table = facts.diagnostic_table.expect("UNIQUE pinpoint expected");
        assert_eq!(table.headers, vec!["id".to_string(), "name".to_string()]);
        assert_eq!(table.rows.len(), 1);
        assert_eq!(table.rows[0][0], "5");
        assert_eq!(table.rows[0][1], "Alice");
    });
}

#[test]
fn enrich_unique_insert_natural_order_short_form_resolves_value_via_schema() {
    // `insert into T (1)` — natural-order short form, the
    // helper falls back to schema-driven lookup.
    let db = db();
    rt().block_on(async {
        db.create_table(
            "thing".to_string(),
            vec![ColumnSpec::new("id".to_string(), Type::Int)],
            vec!["id".to_string()],
            None,
        )
        .await
        .unwrap();
        db.insert(
            "thing".to_string(),
            None,
            vec![Value::Number("1".to_string())],
            None,
        )
        .await
        .unwrap();

        let cmd = Command::Insert {
            table: "thing".to_string(),
            columns: None,
            values: vec![Value::Number("1".to_string())],
        };
        let err = db
            .insert(
                "thing".to_string(),
                None,
                vec![Value::Number("1".to_string())],
                None,
            )
            .await
            .unwrap_err();

        let facts = enrich_dsl_failure(&db, &cmd, &err).await;
        assert_eq!(facts.value.as_deref(), Some("1"));
        assert!(facts.diagnostic_table.is_some());
    });
}

#[test]
fn enrich_unique_sql_insert_natural_order_resolves_value_via_schema() {
    // ADR-0036 Phase 1 follow-up: a no-column-list (natural-order) SQL
    // INSERT also names the offending value in a constraint error. The
    // schema maps each VALUES position to its column, in declaration
    // order — ALL columns (advanced-mode Form B auto-fills nothing, so
    // the user supplies a value for every column).
    let db = db();
    rt().block_on(async {
        db.create_table(
            "Customers".to_string(),
            vec![
                ColumnSpec::new("id".to_string(), Type::Int),
                ColumnSpec::new("name".to_string(), Type::Text),
            ],
            vec!["id".to_string()],
            None,
        )
        .await
        .unwrap();
        db.insert(
            "Customers".to_string(),
            None,
            vec![
                Value::Number("5".to_string()),
                Value::Text("Alice".to_string()),
            ],
            None,
        )
        .await
        .unwrap();

        // Natural-order SQL insert (no column list) collides on id=5.
        let input = "insert into Customers values (5, 'Bob')";
        let cmd = parse_command(input).expect("parses as advanced-mode SQL insert");
        let Command::SqlInsert {
            sql,
            target_table,
            listed_columns,
            row_source,
            returning,
            literal_rows,
        } = cmd.clone()
        else {
            panic!("expected Command::SqlInsert, got {cmd:?}");
        };
        assert!(
            listed_columns.is_empty(),
            "natural-order form has no column list"
        );
        let err = db
            .run_sql_insert_with_literals(
                sql,
                None,
                target_table,
                listed_columns,
                row_source,
                returning,
                literal_rows,
            )
            .await
            .unwrap_err();
        assert!(matches!(
            err,
            DbError::Sqlite {
                kind: SqliteErrorKind::UniqueViolation,
                ..
            }
        ));

        let facts = enrich_dsl_failure(&db, &cmd, &err).await;
        assert_eq!(facts.column.as_deref(), Some("id"));
        assert_eq!(
            facts.value.as_deref(),
            Some("5"),
            "the offending value is named even without an explicit column list",
        );
    });
}

#[test]
fn enrich_unique_update_resolves_value_from_assignments() {
    let db = db();
    rt().block_on(async {
        db.create_table(
            "Customers".to_string(),
            vec![
                ColumnSpec::new("id".to_string(), Type::Int),
                ColumnSpec::new("name".to_string(), Type::Text),
            ],
            vec!["id".to_string()],
            None,
        )
        .await
        .unwrap();
        db.insert(
            "Customers".to_string(),
            None,
            vec![
                Value::Number("1".to_string()),
                Value::Text("Alice".to_string()),
            ],
            None,
        )
        .await
        .unwrap();
        db.insert(
            "Customers".to_string(),
            None,
            vec![
                Value::Number("2".to_string()),
                Value::Text("Bob".to_string()),
            ],
            None,
        )
        .await
        .unwrap();

        // Try to update Bob's id to 1 — collides with Alice.
        let cmd = Command::Update {
            table: "Customers".to_string(),
            assignments: vec![("id".to_string(), Value::Number("1".to_string()))],
            filter: RowFilter::eq("name", Value::Text("Bob".to_string())),
        };
        let err = db
            .update(
                "Customers".to_string(),
                vec![("id".to_string(), Value::Number("1".to_string()))],
                RowFilter::eq("name", Value::Text("Bob".to_string())),
                None,
            )
            .await
            .unwrap_err();

        let facts = enrich_dsl_failure(&db, &cmd, &err).await;
        assert_eq!(facts.column.as_deref(), Some("id"));
        assert_eq!(facts.value.as_deref(), Some("1"));
    });
}

#[test]
fn enrich_unique_sql_update_resolves_value_from_set_literals() {
    // ADR-0036 Phase 2: an advanced-mode SQL `UPDATE` now retains its
    // `SET` literals, so a UNIQUE violation names the offending value —
    // closing the error-value gap for advanced mode, mirroring the DSL
    // `Update` case above. The value flows from the parse-captured
    // `set_literals` through `user_value_for_column`.
    let db = db();
    rt().block_on(async {
        db.create_table(
            "Customers".to_string(),
            vec![
                ColumnSpec::new("id".to_string(), Type::Int),
                ColumnSpec::new("name".to_string(), Type::Text),
            ],
            vec!["id".to_string()],
            None,
        )
        .await
        .unwrap();
        db.insert(
            "Customers".to_string(),
            None,
            vec![
                Value::Number("1".to_string()),
                Value::Text("Alice".to_string()),
            ],
            None,
        )
        .await
        .unwrap();
        db.insert(
            "Customers".to_string(),
            None,
            vec![
                Value::Number("2".to_string()),
                Value::Text("Bob".to_string()),
            ],
            None,
        )
        .await
        .unwrap();

        // Advanced-mode SQL: set Bob's id to 1 — collides with Alice.
        let input = "update Customers set id = 1 where name = 'Bob'";
        let cmd = parse_command(input).expect("parses as advanced-mode SQL update");
        let Command::SqlUpdate {
            sql,
            target_table,
            returning,
            set_literals,
        } = cmd.clone()
        else {
            panic!("expected Command::SqlUpdate, got {cmd:?}");
        };
        // The literal `1` is a valid int, so Phase-2 validation passes and
        // the engine-level UNIQUE violation is what surfaces.
        let err = db
            .run_sql_update_with_literals(sql, None, target_table, returning, set_literals)
            .await
            .unwrap_err();
        assert!(matches!(
            err,
            DbError::Sqlite {
                kind: SqliteErrorKind::UniqueViolation,
                ..
            }
        ));

        let facts = enrich_dsl_failure(&db, &cmd, &err).await;
        assert_eq!(facts.column.as_deref(), Some("id"));
        assert_eq!(
            facts.value.as_deref(),
            Some("1"),
            "the offending SET value is named (from set_literals)"
        );
    });
}

// ---- NOT NULL ---------------------------------------------------

#[test]
fn enrich_not_null_resolves_table_and_column() {
    let db = db();
    rt().block_on(async {
        // Create a table with a NOT NULL column. The current
        // schema_to_ddl emits NOT NULL on PK columns; make
        // a non-PK column NOT NULL via a multi-column PK
        // setup, then the second column is NOT NULL because
        // it's part of the PK.
        // (We're testing the enrichment, not the constraint
        // emission — even a PK NOT NULL works.)
        db.create_table(
            "T".to_string(),
            vec![
                ColumnSpec::new("a".to_string(), Type::Int),
                ColumnSpec::new("b".to_string(), Type::Text),
            ],
            vec!["a".to_string(), "b".to_string()],
            None,
        )
        .await
        .unwrap();

        // Try to insert with NULL for the second PK column.
        let cmd = Command::Insert {
            table: "T".to_string(),
            columns: Some(vec!["a".to_string(), "b".to_string()]),
            values: vec![Value::Number("1".to_string()), Value::Null],
        };
        let err = db
            .insert(
                "T".to_string(),
                Some(vec!["a".to_string(), "b".to_string()]),
                vec![Value::Number("1".to_string()), Value::Null],
                None,
            )
            .await
            .unwrap_err();

        let facts = enrich_dsl_failure(&db, &cmd, &err).await;
        assert_eq!(facts.table.as_deref(), Some("T"));
        assert_eq!(facts.column.as_deref(), Some("b"));
        // Per design: no value field for NOT NULL (the value is null).
        assert!(facts.value.is_none());
        // No pinpoint for NOT NULL.
        assert!(facts.diagnostic_table.is_none());
    });
}

// ---- FOREIGN KEY (child-side, INSERT) ---------------------------

#[test]
fn enrich_fk_insert_resolves_parent_table_column_and_value() {
    let db = db();
    rt().block_on(async {
        db.create_table(
            "Customers".to_string(),
            vec![ColumnSpec::new("id".to_string(), Type::Int)],
            vec!["id".to_string()],
            None,
        )
        .await
        .unwrap();
        db.create_table(
            "Orders".to_string(),
            vec![
                ColumnSpec::new("id".to_string(), Type::Int),
                ColumnSpec::new("CustId".to_string(), Type::Int),
            ],
            vec!["id".to_string()],
            None,
        )
        .await
        .unwrap();
        db.add_relationship(
            None,
            "Customers".to_string(),
            vec!["id".to_string()],
            "Orders".to_string(),
            vec!["CustId".to_string()],
            ReferentialAction::NoAction,
            ReferentialAction::NoAction,
            false,
            None,
        )
        .await
        .unwrap();

        // Insert into Orders with a CustId that has no parent.
        let cmd = Command::Insert {
            table: "Orders".to_string(),
            columns: Some(vec!["id".to_string(), "CustId".to_string()]),
            values: vec![
                Value::Number("1".to_string()),
                Value::Number("999".to_string()),
            ],
        };
        let err = db
            .insert(
                "Orders".to_string(),
                Some(vec!["id".to_string(), "CustId".to_string()]),
                vec![
                    Value::Number("1".to_string()),
                    Value::Number("999".to_string()),
                ],
                None,
            )
            .await
            .unwrap_err();

        let facts = enrich_dsl_failure(&db, &cmd, &err).await;
        assert_eq!(facts.table.as_deref(), Some("Orders"));
        assert_eq!(facts.column.as_deref(), Some("CustId"));
        assert_eq!(facts.parent_table.as_deref(), Some("Customers"));
        assert_eq!(facts.parent_column.as_deref(), Some("id"));
        assert_eq!(facts.value.as_deref(), Some("999"));
        // FK pinpoint not implemented in v1.
        assert!(facts.diagnostic_table.is_none());
    });
}

#[test]
fn enrich_fk_insert_compound_names_every_column_pair() {
    // ADR-0043 residual: a compound-FK violation must name *every*
    // child->parent column pair, not just the first. The single-column
    // facts slots carry the comma-joined lists.
    let db = db();
    rt().block_on(async {
        db.create_table(
            "Region".to_string(),
            vec![
                ColumnSpec::new("country".to_string(), Type::Int),
                ColumnSpec::new("code".to_string(), Type::Int),
            ],
            vec!["country".to_string(), "code".to_string()],
            None,
        )
        .await
        .unwrap();
        db.create_table(
            "City".to_string(),
            vec![
                ColumnSpec::new("country".to_string(), Type::Int),
                ColumnSpec::new("region_code".to_string(), Type::Int),
            ],
            vec![],
            None,
        )
        .await
        .unwrap();
        db.add_relationship(
            None,
            "Region".to_string(),
            vec!["country".to_string(), "code".to_string()],
            "City".to_string(),
            vec!["country".to_string(), "region_code".to_string()],
            ReferentialAction::NoAction,
            ReferentialAction::NoAction,
            false,
            None,
        )
        .await
        .unwrap();

        // Insert a City whose (country, region_code) has no parent Region.
        let cmd = Command::Insert {
            table: "City".to_string(),
            columns: Some(vec!["country".to_string(), "region_code".to_string()]),
            values: vec![
                Value::Number("7".to_string()),
                Value::Number("8".to_string()),
            ],
        };
        let err = db
            .insert(
                "City".to_string(),
                Some(vec!["country".to_string(), "region_code".to_string()]),
                vec![
                    Value::Number("7".to_string()),
                    Value::Number("8".to_string()),
                ],
                None,
            )
            .await
            .unwrap_err();

        let facts = enrich_dsl_failure(&db, &cmd, &err).await;
        assert_eq!(facts.table.as_deref(), Some("City"));
        assert_eq!(facts.parent_table.as_deref(), Some("Region"));
        // Both pairs named, not just the first.
        assert_eq!(facts.column.as_deref(), Some("country, region_code"));
        assert_eq!(facts.parent_column.as_deref(), Some("country, code"));
        assert_eq!(facts.value.as_deref(), Some("7, 8"));
    });
}

#[test]
fn enrich_fk_insert_natural_order_multi_value_resolves_via_schema() {
    // Regression: `insert into Orders values (4, 11.99)` —
    // natural-order multi-value INSERT, no explicit columns,
    // and the schema has a serial PK that gets auto-skipped.
    // Enrichment must still resolve parent_table /
    // parent_column / value via the schema-aware lookup.
    let db = db();
    rt().block_on(async {
        db.create_table(
            "Customers".to_string(),
            vec![ColumnSpec::new("id".to_string(), Type::Int)],
            vec!["id".to_string()],
            None,
        )
        .await
        .unwrap();
        db.create_table(
            "Orders".to_string(),
            vec![
                ColumnSpec::new("id".to_string(), Type::Serial),
                ColumnSpec::new("CustId".to_string(), Type::Int),
                ColumnSpec::new("Total".to_string(), Type::Real),
            ],
            vec!["id".to_string()],
            None,
        )
        .await
        .unwrap();
        db.add_relationship(
            None,
            "Customers".to_string(),
            vec!["id".to_string()],
            "Orders".to_string(),
            vec!["CustId".to_string()],
            ReferentialAction::NoAction,
            ReferentialAction::NoAction,
            false,
            None,
        )
        .await
        .unwrap();

        // Natural-order: serial PK auto-fills, so positional
        // values map to (CustId, Total). CustId=4 has no
        // matching parent → FK violation.
        let cmd = Command::Insert {
            table: "Orders".to_string(),
            columns: None,
            values: vec![
                Value::Number("4".to_string()),
                Value::Number("11.99".to_string()),
            ],
        };
        let err = db
            .insert(
                "Orders".to_string(),
                None,
                vec![
                    Value::Number("4".to_string()),
                    Value::Number("11.99".to_string()),
                ],
                None,
            )
            .await
            .unwrap_err();

        let facts = enrich_dsl_failure(&db, &cmd, &err).await;
        assert_eq!(facts.parent_table.as_deref(), Some("Customers"));
        assert_eq!(facts.parent_column.as_deref(), Some("id"));
        assert_eq!(
            facts.value.as_deref(),
            Some("4"),
            "natural-order with serial PK skip should map values[0] to CustId"
        );
    });
}

// ---- FOREIGN KEY (parent-side, DELETE) --------------------------

#[test]
fn enrich_fk_delete_resolves_child_table() {
    let db = db();
    rt().block_on(async {
        db.create_table(
            "Customers".to_string(),
            vec![ColumnSpec::new("id".to_string(), Type::Int)],
            vec!["id".to_string()],
            None,
        )
        .await
        .unwrap();
        db.create_table(
            "Orders".to_string(),
            vec![
                ColumnSpec::new("id".to_string(), Type::Int),
                ColumnSpec::new("CustId".to_string(), Type::Int),
            ],
            vec!["id".to_string()],
            None,
        )
        .await
        .unwrap();
        db.add_relationship(
            None,
            "Customers".to_string(),
            vec!["id".to_string()],
            "Orders".to_string(),
            vec!["CustId".to_string()],
            ReferentialAction::NoAction,
            ReferentialAction::NoAction,
            false,
            None,
        )
        .await
        .unwrap();
        db.insert(
            "Customers".to_string(),
            None,
            vec![Value::Number("1".to_string())],
            None,
        )
        .await
        .unwrap();
        db.insert(
            "Orders".to_string(),
            None,
            vec![
                Value::Number("1".to_string()),
                Value::Number("1".to_string()),
            ],
            None,
        )
        .await
        .unwrap();

        // Delete the parent that has children — engine refuses.
        let cmd = Command::Delete {
            table: "Customers".to_string(),
            filter: RowFilter::eq("id", Value::Number("1".to_string())),
        };
        let err = db
            .delete(
                "Customers".to_string(),
                RowFilter::eq("id", Value::Number("1".to_string())),
                None,
            )
            .await
            .unwrap_err();

        let facts = enrich_dsl_failure(&db, &cmd, &err).await;
        assert_eq!(facts.table.as_deref(), Some("Customers"));
        assert_eq!(facts.child_table.as_deref(), Some("Orders"));
    });
}

// ---- CHECK (ADR-0029 §10) ---------------------------------------

#[test]
fn enrich_check_insert_resolves_table_column_value_and_rule() {
    let db = db();
    rt().block_on(async {
        // `Scores(id serial pk)` plus a non-PK `score` column
        // carrying `CHECK (score >= 0)`.
        db.create_table(
            "Scores".to_string(),
            vec![ColumnSpec::new("id".to_string(), Type::Serial)],
            vec!["id".to_string()],
            None,
        )
        .await
        .unwrap();
        let score_spec =
            match parse_command("create table __probe with pk score(int) check (score >= 0)")
                .expect("probe create parses")
            {
                Command::CreateTable { columns, .. } => {
                    columns.into_iter().next().expect("one column")
                }
                other => panic!("expected CreateTable, got {other:?}"),
            };
        db.add_column("Scores".to_string(), score_spec, None)
            .await
            .unwrap();

        // An insert that violates the CHECK.
        let cmd = Command::Insert {
            table: "Scores".to_string(),
            columns: Some(vec!["score".to_string()]),
            values: vec![Value::Number("-5".to_string())],
        };
        let err = db
            .insert(
                "Scores".to_string(),
                Some(vec!["score".to_string()]),
                vec![Value::Number("-5".to_string())],
                None,
            )
            .await
            .unwrap_err();

        let facts = enrich_dsl_failure(&db, &cmd, &err).await;
        assert_eq!(facts.table.as_deref(), Some("Scores"));
        assert_eq!(facts.column.as_deref(), Some("score"));
        assert_eq!(facts.value.as_deref(), Some("-5"));
        let rule = facts.check_rule.expect("the CHECK rule is resolved");
        assert!(
            rule.contains("score"),
            "the resolved rule names the column: {rule}",
        );
    });
}

// ---- non-engine error → empty enrichment ------------------------

#[test]
fn enrich_unsupported_returns_default_facts() {
    let db = db();
    rt().block_on(async {
        let err = DbError::Unsupported("nope".to_string());
        let cmd = Command::DropTable {
            name: "X".to_string(),
        };
        let facts = enrich_dsl_failure(&db, &cmd, &err).await;
        assert!(facts.table.is_none());
        assert!(facts.column.is_none());
        assert!(facts.value.is_none());
        assert!(facts.parent_table.is_none());
        assert!(facts.child_table.is_none());
        assert!(facts.diagnostic_table.is_none());
    });
}
