use citadel::{Argon2Profile, DatabaseBuilder};
use citadel_sql::{Connection, ExecutionResult, QueryResult, Value};

fn create_db(dir: &std::path::Path) -> citadel::Database {
    let db_path = dir.join("test.db");
    DatabaseBuilder::new(db_path)
        .passphrase(b"test-passphrase")
        .argon2_profile(Argon2Profile::Iot)
        .create()
        .unwrap()
}

fn query_result(result: ExecutionResult) -> QueryResult {
    match result {
        ExecutionResult::Query(qr) => qr,
        other => panic!("expected Query, got {other:?}"),
    }
}

fn explain_lines(conn: &Connection<'_>, sql: &str) -> Vec<String> {
    let qr = query_result(conn.execute(sql).unwrap());
    assert_eq!(qr.columns, vec!["plan"]);
    qr.rows
        .into_iter()
        .map(|row| match &row[0] {
            Value::Text(s) => s.to_string(),
            other => panic!("expected Text, got {other:?}"),
        })
        .collect()
}

fn setup_full_schema(conn: &Connection<'_>) {
    conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER, email TEXT, dept TEXT)").unwrap();
    conn.execute("CREATE INDEX idx_name ON users (name)")
        .unwrap();
    conn.execute("CREATE INDEX idx_name_age ON users (name, age)")
        .unwrap();
    conn.execute("CREATE UNIQUE INDEX idx_email ON users (email)")
        .unwrap();
    conn.execute("CREATE INDEX idx_dept ON users (dept)")
        .unwrap();
    conn.execute("CREATE TABLE orders (id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL, amount REAL, status TEXT)").unwrap();
    conn.execute("CREATE INDEX idx_user_id ON orders (user_id)")
        .unwrap();
    conn.execute("CREATE INDEX idx_status ON orders (status)")
        .unwrap();
    conn.execute("CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, price REAL)")
        .unwrap();
    conn.execute("CREATE TABLE order_items (id INTEGER PRIMARY KEY, order_id INTEGER NOT NULL, product_id INTEGER NOT NULL)").unwrap();
    conn.execute("CREATE INDEX idx_order_id ON order_items (order_id)")
        .unwrap();
    conn.execute("CREATE INDEX idx_product_id ON order_items (product_id)")
        .unwrap();
}

#[test]
fn pk_lookup_shows_actual_value() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    setup_full_schema(&conn);

    let lines = explain_lines(&conn, "EXPLAIN SELECT * FROM users WHERE id = 42");
    assert!(lines[0].contains("id = 42"));
}

#[test]
fn pk_lookup_text_value() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    conn.execute("CREATE TABLE kv (k TEXT PRIMARY KEY, v TEXT)")
        .unwrap();

    let lines = explain_lines(&conn, "EXPLAIN SELECT * FROM kv WHERE k = 'hello'");
    assert!(lines[0].contains("SEARCH TABLE kv"));
    assert!(lines[0].contains("USING PRIMARY KEY"));
    assert!(lines[0].contains("k = 'hello'"));
}

#[test]
fn composite_pk_lookup() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    conn.execute("CREATE TABLE assoc (a INTEGER, b INTEGER, val TEXT, PRIMARY KEY (a, b))")
        .unwrap();

    let lines = explain_lines(&conn, "EXPLAIN SELECT * FROM assoc WHERE a = 1 AND b = 2");
    assert!(lines[0].contains("SEARCH TABLE assoc"));
    assert!(lines[0].contains("USING PRIMARY KEY"));
    assert!(lines[0].contains("a = 1"));
    assert!(lines[0].contains("b = 2"));
}

#[test]
fn partial_composite_pk_is_seq_scan() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    conn.execute("CREATE TABLE assoc (a INTEGER, b INTEGER, val TEXT, PRIMARY KEY (a, b))")
        .unwrap();

    let lines = explain_lines(&conn, "EXPLAIN SELECT * FROM assoc WHERE a = 1");
    assert!(lines[0].contains("SCAN TABLE assoc"));
}

#[test]
fn index_equality_plus_range() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    setup_full_schema(&conn);

    let lines = explain_lines(
        &conn,
        "EXPLAIN SELECT * FROM users WHERE name = 'Alice' AND age > 20",
    );
    assert!(lines[0].contains("USING INDEX idx_name_age"));
    assert!(lines[0].contains("name = ?"));
    assert!(lines[0].contains("age > ?"));
}

#[test]
fn unindexed_column_forces_seq_scan() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    setup_full_schema(&conn);

    let lines = explain_lines(&conn, "EXPLAIN SELECT * FROM users WHERE age = 30");
    assert!(lines[0].contains("SCAN TABLE users"));
    assert!(lines.contains(&"FILTER".to_string()));
}

#[test]
fn or_forces_seq_scan() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    setup_full_schema(&conn);

    let lines = explain_lines(
        &conn,
        "EXPLAIN SELECT * FROM users WHERE name = 'A' OR name = 'B'",
    );
    assert!(lines[0].contains("SCAN TABLE users"));
}

#[test]
fn join_shows_all_tables() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    setup_full_schema(&conn);

    let lines = explain_lines(&conn,
        "EXPLAIN SELECT * FROM users u JOIN orders o ON u.id = o.user_id JOIN order_items oi ON o.id = oi.order_id");
    assert!(lines.iter().any(|l| l.contains("users")));
    assert!(lines.iter().any(|l| l.contains("orders")));
    assert!(lines.iter().any(|l| l.contains("order_items")));
}

#[test]
fn self_join_different_aliases() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    setup_full_schema(&conn);

    let lines = explain_lines(
        &conn,
        "EXPLAIN SELECT * FROM users a JOIN users b ON a.id = b.id",
    );
    assert!(lines.iter().any(|l| l.contains("users AS a")));
    assert!(lines.iter().any(|l| l.contains("users AS b")));
}

#[test]
fn join_with_where_and_order() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    setup_full_schema(&conn);

    let lines = explain_lines(&conn,
        "EXPLAIN SELECT u.name, o.amount FROM users u JOIN orders o ON u.id = o.user_id ORDER BY o.amount LIMIT 5");
    assert!(lines.contains(&"SORT".to_string()));
    assert!(lines.contains(&"LIMIT 5".to_string()));
}

#[test]
fn distinct_sort_limit_offset_all_present() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    setup_full_schema(&conn);

    let lines = explain_lines(
        &conn,
        "EXPLAIN SELECT DISTINCT name FROM users ORDER BY name LIMIT 5 OFFSET 2",
    );
    let scan_pos = lines.iter().position(|l| l.contains("SCAN TABLE")).unwrap();
    let distinct_pos = lines.iter().position(|l| l == "DISTINCT").unwrap();
    let sort_pos = lines.iter().position(|l| l == "SORT").unwrap();
    let offset_pos = lines.iter().position(|l| l == "OFFSET 2").unwrap();
    let limit_pos = lines.iter().position(|l| l == "LIMIT 5").unwrap();

    assert!(scan_pos < distinct_pos);
    assert!(distinct_pos < sort_pos);
    assert!(sort_pos < offset_pos);
    assert!(offset_pos < limit_pos);
}

#[test]
fn group_by_with_having() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    setup_full_schema(&conn);

    let lines = explain_lines(
        &conn,
        "EXPLAIN SELECT dept, COUNT(*) FROM users GROUP BY dept HAVING COUNT(*) > 3",
    );
    assert!(lines.contains(&"GROUP BY".to_string()));
}

#[test]
fn group_by_with_order_and_limit() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    setup_full_schema(&conn);

    let lines = explain_lines(
        &conn,
        "EXPLAIN SELECT dept, COUNT(*) FROM users GROUP BY dept ORDER BY dept LIMIT 3",
    );
    assert!(lines.contains(&"GROUP BY".to_string()));
    assert!(lines.contains(&"SORT".to_string()));
    assert!(lines.contains(&"LIMIT 3".to_string()));
}

#[test]
fn in_subquery_shows_subquery_line() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    setup_full_schema(&conn);

    let lines = explain_lines(
        &conn,
        "EXPLAIN SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE amount > 100)",
    );
    assert!(lines.contains(&"SUBQUERY".to_string()));
}

#[test]
fn exists_subquery() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    setup_full_schema(&conn);

    let lines = explain_lines(
        &conn,
        "EXPLAIN SELECT * FROM users WHERE EXISTS (SELECT 1 FROM orders WHERE orders.user_id = 1)",
    );
    assert!(lines.contains(&"SUBQUERY".to_string()));
}

#[test]
fn scalar_subquery_in_select() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    setup_full_schema(&conn);

    let lines = explain_lines(
        &conn,
        "EXPLAIN SELECT name, (SELECT COUNT(*) FROM orders) FROM users",
    );
    assert!(lines.contains(&"SUBQUERY".to_string()));
}

#[test]
fn multiple_subqueries() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    setup_full_schema(&conn);

    let lines = explain_lines(&conn,
        "EXPLAIN SELECT * FROM users WHERE id IN (SELECT user_id FROM orders) AND name IN (SELECT name FROM products)");
    let subquery_count = lines.iter().filter(|l| l.as_str() == "SUBQUERY").count();
    assert_eq!(subquery_count, 2);
}

#[test]
fn update_with_index() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    setup_full_schema(&conn);

    let lines = explain_lines(
        &conn,
        "EXPLAIN UPDATE users SET age = 30 WHERE email = 'a@b.com'",
    );
    assert!(lines[0].contains("UPDATE"));
    assert!(lines[0].contains("SEARCH TABLE users"));
    assert!(lines[0].contains("USING INDEX idx_email"));
}

#[test]
fn delete_full_table() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    setup_full_schema(&conn);

    let lines = explain_lines(&conn, "EXPLAIN DELETE FROM users");
    assert_eq!(lines.len(), 1);
    assert!(lines[0].contains("DELETE FROM"));
    assert!(lines[0].contains("SCAN TABLE users"));
}

#[test]
fn delete_pk_lookup() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    setup_full_schema(&conn);

    let lines = explain_lines(&conn, "EXPLAIN DELETE FROM users WHERE id = 99");
    assert!(lines[0].contains("DELETE FROM"));
    assert!(lines[0].contains("SEARCH TABLE users"));
    assert!(lines[0].contains("USING PRIMARY KEY"));
}

#[test]
fn explain_nonexistent_table() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();

    let result = conn.execute("EXPLAIN SELECT * FROM nonexistent");
    assert!(result.is_err());
    let err = format!("{}", result.unwrap_err());
    assert!(err.contains("not found"));
}

#[test]
fn explain_drop_table_error() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();

    let result = conn.execute("EXPLAIN DROP TABLE users");
    assert!(result.is_err());
}

#[test]
fn explain_create_index_error() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();

    let result = conn.execute("EXPLAIN CREATE INDEX idx ON t (a)");
    assert!(result.is_err());
}

#[test]
fn explain_begin_error() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();

    let result = conn.execute("EXPLAIN BEGIN");
    assert!(result.is_err());
}

#[test]
fn explain_in_txn_does_not_modify() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    setup_full_schema(&conn);
    conn.execute("INSERT INTO users (id, name) VALUES (1, 'Alice')")
        .unwrap();

    conn.execute("BEGIN").unwrap();
    conn.execute("EXPLAIN INSERT INTO users (id, name) VALUES (2, 'Bob')")
        .unwrap();
    conn.execute("EXPLAIN DELETE FROM users WHERE id = 1")
        .unwrap();
    conn.execute("EXPLAIN UPDATE users SET name = 'X' WHERE id = 1")
        .unwrap();
    conn.execute("COMMIT").unwrap();

    let qr = query_result(conn.execute("SELECT COUNT(*) FROM users").unwrap());
    assert_eq!(qr.rows[0][0], Value::Integer(1));
}

#[test]
fn explain_sees_uncommitted_schema() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();

    conn.execute("BEGIN").unwrap();
    conn.execute("CREATE TABLE temp (id INTEGER PRIMARY KEY, val TEXT)")
        .unwrap();
    let lines = explain_lines(&conn, "EXPLAIN SELECT * FROM temp WHERE id = 1");
    assert!(lines[0].contains("SEARCH TABLE temp"));
    conn.execute("ROLLBACK").unwrap();
}

#[test]
fn no_alias_shown_when_same_as_name() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    setup_full_schema(&conn);

    let lines = explain_lines(&conn, "EXPLAIN SELECT * FROM users users");
    assert!(lines[0].contains("SCAN TABLE users"));
    assert!(!lines[0].contains("AS"));
}

#[test]
fn plan_same_before_and_after_data() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    setup_full_schema(&conn);

    let before = explain_lines(&conn, "EXPLAIN SELECT * FROM users WHERE id = 1");

    for i in 1..=100 {
        conn.execute(&format!(
            "INSERT INTO users (id, name, age, email) VALUES ({i}, 'user{i}', {}, 'user{i}@test.com')",
            20 + i % 50
        )).unwrap();
    }

    let after = explain_lines(&conn, "EXPLAIN SELECT * FROM users WHERE id = 1");
    assert_eq!(before, after);
}

#[test]
fn explain_with_between() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    setup_full_schema(&conn);

    let lines = explain_lines(
        &conn,
        "EXPLAIN SELECT * FROM users WHERE age BETWEEN 20 AND 30",
    );
    assert!(lines[0].contains("SCAN TABLE users"));
    assert!(lines.contains(&"FILTER".to_string()));
}

#[test]
fn explain_with_like() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    setup_full_schema(&conn);

    let lines = explain_lines(&conn, "EXPLAIN SELECT * FROM users WHERE name LIKE 'A%'");
    assert!(lines[0].contains("SCAN TABLE users"));
    assert!(lines.contains(&"FILTER".to_string()));
}

#[test]
fn explain_via_query_method() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    setup_full_schema(&conn);

    let qr = conn
        .query("EXPLAIN SELECT * FROM users WHERE id = 1")
        .unwrap();
    assert_eq!(qr.columns, vec!["plan"]);
    assert!(!qr.rows.is_empty());
    match &qr.rows[0][0] {
        Value::Text(s) => assert!(s.contains("SEARCH TABLE users")),
        other => panic!("expected Text, got {other:?}"),
    }
}
