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 setup_schema(conn: &Connection<'_>) {
    conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER)")
        .unwrap();
    conn.execute("CREATE INDEX idx_name ON users (name)")
        .unwrap();
}

fn insert_users(conn: &Connection<'_>) {
    conn.execute("INSERT INTO users (id, name, age) VALUES (1, 'Alice', 30)")
        .unwrap();
    conn.execute("INSERT INTO users (id, name, age) VALUES (2, 'Bob', 25)")
        .unwrap();
    conn.execute("INSERT INTO users (id, name, age) VALUES (3, 'Carol', 35)")
        .unwrap();
}

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

    let qr = query_result(
        conn.execute_params("SELECT name FROM users WHERE id = $1", &[Value::Integer(2)])
            .unwrap(),
    );
    assert_eq!(qr.rows.len(), 1);
    assert_eq!(qr.rows[0][0], Value::Text("Bob".into()));
}

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

    let qr = query_result(
        conn.execute_params(
            "SELECT * FROM users WHERE age > $1 AND age < $2",
            &[Value::Integer(26), Value::Integer(34)],
        )
        .unwrap(),
    );
    assert_eq!(qr.rows.len(), 1);
    assert_eq!(qr.rows[0][1], Value::Text("Alice".into()));
}

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

    conn.execute_params(
        "INSERT INTO users (id, name, age) VALUES ($1, $2, $3)",
        &[
            Value::Integer(10),
            Value::Text("Dave".into()),
            Value::Integer(40),
        ],
    )
    .unwrap();

    let qr = query_result(
        conn.execute("SELECT name FROM users WHERE id = 10")
            .unwrap(),
    );
    assert_eq!(qr.rows[0][0], Value::Text("Dave".into()));
}

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

    conn.execute_params(
        "UPDATE users SET name = $2 WHERE id = $1",
        &[Value::Integer(1), Value::Text("Alicia".into())],
    )
    .unwrap();

    let qr = query_result(conn.execute("SELECT name FROM users WHERE id = 1").unwrap());
    assert_eq!(qr.rows[0][0], Value::Text("Alicia".into()));
}

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

    conn.execute_params("DELETE FROM users WHERE id = $1", &[Value::Integer(2)])
        .unwrap();

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

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

    let result = conn.execute_params(
        "SELECT * FROM users WHERE id = $1 AND name = $2",
        &[Value::Integer(1)],
    );
    assert!(result.is_err());
    let err = format!("{}", result.unwrap_err());
    assert!(err.contains("mismatch"));
}

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

    let result = conn.execute_params(
        "SELECT * FROM users WHERE id = $1",
        &[Value::Integer(1), Value::Integer(2)],
    );
    assert!(result.is_err());
}

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

    let qr = query_result(conn.execute_params("SELECT * FROM users", &[]).unwrap());
    assert_eq!(qr.rows.len(), 3);
}

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

    let sql = "SELECT * FROM users WHERE id = $1";

    let qr1 = query_result(conn.execute_params(sql, &[Value::Integer(1)]).unwrap());
    assert_eq!(qr1.rows[0][1], Value::Text("Alice".into()));

    let qr2 = query_result(conn.execute_params(sql, &[Value::Integer(3)]).unwrap());
    assert_eq!(qr2.rows[0][1], Value::Text("Carol".into()));
}

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

    let sql = "SELECT * FROM users WHERE age = $1";
    conn.execute_params(sql, &[Value::Integer(30)]).unwrap();

    conn.execute("CREATE INDEX idx_age ON users (age)").unwrap();

    let qr = query_result(conn.execute_params(sql, &[Value::Integer(25)]).unwrap());
    assert_eq!(qr.rows.len(), 1);
    assert_eq!(qr.rows[0][1], Value::Text("Bob".into()));
}

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

    conn.execute_params("SELECT * FROM users WHERE id = $1", &[Value::Integer(1)])
        .unwrap();

    conn.execute("DROP TABLE users").unwrap();
    conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER)")
        .unwrap();
    conn.execute("INSERT INTO users (id, name, age) VALUES (1, 'NewAlice', 99)")
        .unwrap();

    let qr = query_result(
        conn.execute_params("SELECT name FROM users WHERE id = $1", &[Value::Integer(1)])
            .unwrap(),
    );
    assert_eq!(qr.rows[0][0], Value::Text("NewAlice".into()));
}

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

    conn.execute_params(
        "INSERT INTO users (id, name, age) VALUES ($1, $2, $3)",
        &[Value::Integer(1), Value::Text("Eve".into()), Value::Null],
    )
    .unwrap();

    let qr = query_result(conn.execute("SELECT age FROM users WHERE id = 1").unwrap());
    assert_eq!(qr.rows[0][0], Value::Null);
}

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

    let qr = query_result(
        conn.execute_params(
            "SELECT name FROM users WHERE age BETWEEN $1 AND $2",
            &[Value::Integer(26), Value::Integer(34)],
        )
        .unwrap(),
    );
    assert_eq!(qr.rows.len(), 1);
    assert_eq!(qr.rows[0][0], Value::Text("Alice".into()));
}

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

    let qr = query_result(
        conn.execute_params(
            "SELECT name FROM users WHERE name LIKE $1",
            &[Value::Text("A%".into())],
        )
        .unwrap(),
    );
    assert_eq!(qr.rows.len(), 1);
    assert_eq!(qr.rows[0][0], Value::Text("Alice".into()));
}

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

    let qr = query_result(
        conn.execute_params(
            "SELECT name FROM users WHERE id IN ($1, $2) ORDER BY id",
            &[Value::Integer(1), Value::Integer(3)],
        )
        .unwrap(),
    );
    assert_eq!(qr.rows.len(), 2);
    assert_eq!(qr.rows[0][0], Value::Text("Alice".into()));
    assert_eq!(qr.rows[1][0], Value::Text("Carol".into()));
}

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

    conn.execute("BEGIN").unwrap();
    conn.execute_params(
        "INSERT INTO users (id, name, age) VALUES ($1, $2, $3)",
        &[
            Value::Integer(1),
            Value::Text("TxnUser".into()),
            Value::Integer(20),
        ],
    )
    .unwrap();
    conn.execute("COMMIT").unwrap();

    let qr = query_result(conn.execute("SELECT name FROM users WHERE id = 1").unwrap());
    assert_eq!(qr.rows[0][0], Value::Text("TxnUser".into()));
}

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

    let qr = query_result(
        conn.execute_params(
            "EXPLAIN SELECT * FROM users WHERE id = $1",
            &[Value::Integer(5)],
        )
        .unwrap(),
    );
    let plan: Vec<String> = qr
        .rows
        .iter()
        .map(|r| match &r[0] {
            Value::Text(s) => s.to_string(),
            _ => panic!(),
        })
        .collect();
    assert!(plan[0].contains("SEARCH TABLE users"));
    assert!(plan[0].contains("USING PRIMARY KEY"));
}

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

    let qr = conn
        .query_params("SELECT name FROM users WHERE id = $1", &[Value::Integer(2)])
        .unwrap();
    assert_eq!(qr.rows[0][0], Value::Text("Bob".into()));
}

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

    let qr = query_result(
        conn.execute_params(
            "SELECT name FROM users ORDER BY name LIMIT $1",
            &[Value::Integer(2)],
        )
        .unwrap(),
    );
    assert_eq!(qr.rows.len(), 2);
}

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

    let qr = query_result(conn.execute("SELECT * FROM users").unwrap());
    assert_eq!(qr.rows.len(), 3);
}

#[test]
fn param_in_join_on_clause() {
    let dir = tempfile::tempdir().unwrap();
    let db = create_db(dir.path());
    let conn = Connection::open(&db).unwrap();
    setup_schema(&conn);
    insert_users(&conn);
    conn.execute("CREATE TABLE orders (id INTEGER PRIMARY KEY, user_id INTEGER, amount REAL)")
        .unwrap();
    conn.execute("INSERT INTO orders (id, user_id, amount) VALUES (1, 1, 99.0)")
        .unwrap();

    let qr = query_result(conn.execute_params(
        "SELECT u.name, o.amount FROM users u JOIN orders o ON u.id = o.user_id WHERE o.amount > $1",
        &[Value::Real(50.0)],
    ).unwrap());
    assert_eq!(qr.rows.len(), 1);
    assert_eq!(qr.rows[0][0], Value::Text("Alice".into()));
}
