//! Sub-phase 4c integration tests for advanced-mode SQL
//! `DROP TABLE [IF EXISTS]` (ADR-0035 §4).
//!
//! `SqlDropTable` executes through the same `do_drop_table` machinery
//! as the simple `drop table` (cascade / inbound-relationship refusal /
//! metadata cleanup); the only new behaviour is `IF EXISTS` as a
//! no-op-with-note (`DropOutcome::Skipped`). These drive the worker
//! directly; parsing (text → `Command::SqlDropTable`) is covered by the
//! `sql_drop_table_tests` in `src/dsl/grammar/ddl.rs`.

use rdbms_playground::db::{Database, DropOutcome};
use rdbms_playground::dsl::{ColumnSpec, SqlForeignKey, Type, Value};
use rdbms_playground::persistence::Persistence;
use rdbms_playground::project;

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

fn open(undo: bool) -> (project::Project, Database, tempfile::TempDir) {
    let dir = tempfile::tempdir().expect("create tempdir");
    let project = project::open_or_create(None, Some(dir.path())).expect("open or create project");
    let persistence = Persistence::new(project.path().to_path_buf());
    let db = Database::open_with_persistence_and_undo(project.db_path(), persistence, undo)
        .expect("open db with persistence");
    (project, db, dir)
}

/// Create a simple `T (id int primary key, body text)`.
fn make_t(db: &Database, r: &tokio::runtime::Runtime) {
    r.block_on(db.sql_create_table(
        "T".to_string(),
        vec![
            ColumnSpec::new("id", Type::Int),
            ColumnSpec::new("body", Type::Text),
        ],
        vec!["id".to_string()],
        vec![],
        vec![],
        vec![],
        false,
        Some("create table T (id int primary key, body text)".to_string()),
    ))
    .expect("create T");
}

#[test]
fn drop_table_removes_an_existing_table() {
    let (_p, db, _d) = open(false);
    let r = rt();
    make_t(&db, &r);
    let out = r
        .block_on(db.sql_drop_table("T".to_string(), false, Some("drop table T".to_string())))
        .expect("drop");
    assert!(matches!(out, DropOutcome::Dropped));
    assert!(
        !r.block_on(db.list_tables())
            .unwrap()
            .contains(&"T".to_string())
    );
}

#[test]
fn if_exists_on_an_absent_table_is_a_noop_and_journalled() {
    let (_p, db, _d) = open(false);
    let r = rt();
    let line = "drop table if exists Ghost";
    let out = r
        .block_on(db.sql_drop_table("Ghost".to_string(), true, Some(line.to_string())))
        .expect("IF EXISTS on an absent table succeeds as a no-op");
    assert!(matches!(out, DropOutcome::Skipped));
    // The no-op is still journalled (ADR-0034), like the create-skip.
    // ADR-0052: journaling moved to the dispatch layer; this test now
    // asserts only the no-op `Skipped` outcome.
}

#[test]
fn plain_drop_of_an_absent_table_errors() {
    let (_p, db, _d) = open(false);
    let r = rt();
    let res = r.block_on(db.sql_drop_table(
        "Ghost".to_string(),
        false,
        Some("drop table Ghost".to_string()),
    ));
    assert!(
        res.is_err(),
        "plain DROP TABLE on an absent table errors (no IF EXISTS)"
    );
}

#[test]
fn dropping_a_referenced_parent_is_refused() {
    // Parity with `do_drop_table`: a table with inbound relationships
    // can't be dropped (ADR-0013), via the SQL path too.
    let (_p, db, _d) = open(false);
    let r = rt();
    r.block_on(db.sql_create_table(
        "parent".to_string(),
        vec![
            ColumnSpec::new("id", Type::Serial),
            ColumnSpec::new("label", Type::Text),
        ],
        vec!["id".to_string()],
        vec![],
        vec![],
        vec![],
        false,
        Some("create table parent (id serial primary key, label text)".to_string()),
    ))
    .expect("create parent");
    r.block_on(db.sql_create_table(
        "child".to_string(),
        vec![
            ColumnSpec::new("id", Type::Serial),
            ColumnSpec::new("pid", Type::Int),
        ],
        vec!["id".to_string()],
        vec![],
        vec![],
        vec![SqlForeignKey {
            name: None,
            child_columns: vec!["pid".to_string()],
            parent_table: "parent".to_string(),
            parent_columns: Some(vec!["id".to_string()]),
            on_delete: rdbms_playground::dsl::ReferentialAction::NoAction,
            on_update: rdbms_playground::dsl::ReferentialAction::NoAction,
            inline: true,
        }],
        false,
        Some(
            "create table child (id serial primary key, pid int references parent(id))".to_string(),
        ),
    ))
    .expect("create child with FK");

    // The parent is referenced — refused (even with IF EXISTS, since the
    // table *does* exist; the refusal is about the relationship).
    assert!(
        r.block_on(db.sql_drop_table(
            "parent".to_string(),
            false,
            Some("drop table parent".to_string())
        ))
        .is_err(),
        "a referenced parent can't be dropped"
    );
    // Dropping the child first succeeds, then the parent.
    r.block_on(db.sql_drop_table(
        "child".to_string(),
        false,
        Some("drop table child".to_string()),
    ))
    .expect("drop child");
    r.block_on(db.sql_drop_table(
        "parent".to_string(),
        false,
        Some("drop table parent".to_string()),
    ))
    .expect("now the parent drops");
}

#[test]
fn drop_table_is_one_undo_step_and_restores_data() {
    let (_p, db, _d) = open(true); // undo enabled
    let r = rt();
    make_t(&db, &r);
    r.block_on(db.insert(
        "T".to_string(),
        Some(vec!["id".to_string(), "body".to_string()]),
        vec![
            Value::Number("1".to_string()),
            Value::Text("hi".to_string()),
        ],
        Some("insert".to_string()),
    ))
    .expect("row");
    r.block_on(db.sql_drop_table("T".to_string(), false, Some("drop table T".to_string())))
        .expect("drop");
    assert!(
        !r.block_on(db.list_tables())
            .unwrap()
            .contains(&"T".to_string())
    );

    // One undo brings the table — and its row — back.
    assert!(
        r.block_on(db.undo()).expect("undo").is_some(),
        "the drop was one undo step"
    );
    assert!(
        r.block_on(db.list_tables())
            .unwrap()
            .contains(&"T".to_string())
    );
    let data = r
        .block_on(db.query_data("T".to_string(), None, None))
        .expect("query");
    assert_eq!(data.rows.len(), 1, "the dropped row was restored by undo");
}
