use ic_sqlite_vfs::db::migrate::Migration;
use ic_sqlite_vfs::db::{DbError, Value};
use ic_sqlite_vfs::sqlite_vfs::{lock, stable_blob};
use ic_sqlite_vfs::stable::memory;
use ic_sqlite_vfs::{named_params, params, Db};
use serial_test::serial;

fn reset() {
    stable_blob::invalidate_read_cache();
    memory::reset_for_tests();
    lock::reset_for_tests();
    Db::init(memory::memory_for_tests()).unwrap();
}

#[test]
#[serial]
fn text_bind_preserves_embedded_nul_bytes() {
    reset();
    Db::migrate(&[Migration {
        version: 1,
        sql: "CREATE TABLE text_binds(id INTEGER PRIMARY KEY, body TEXT NOT NULL);",
    }])
    .unwrap();

    Db::update(|connection| {
        connection.execute(
            "INSERT INTO text_binds(id, body) VALUES (?1, ?2)",
            params![1_i64, Value::Text("A\0B")],
        )
    })
    .unwrap();

    let value = Db::query(|connection| {
        connection.query_scalar::<String>("SELECT body FROM text_binds WHERE id = 1", params![])
    })
    .unwrap();
    assert_eq!(value.as_bytes(), b"A\0B");
}

#[test]
#[serial]
fn empty_blob_bind_stays_blob_not_null() {
    reset();
    Db::migrate(&[Migration {
        version: 1,
        sql: "CREATE TABLE blob_binds(id INTEGER PRIMARY KEY, body BLOB);",
    }])
    .unwrap();

    Db::update(|connection| {
        let empty: &[u8] = &[];
        connection.execute(
            "INSERT INTO blob_binds(id, body) VALUES (?1, ?2)",
            params![1_i64, empty],
        )
    })
    .unwrap();

    let row = Db::query(|connection| {
        connection.query_one(
            "SELECT body IS NOT NULL, length(body), body FROM blob_binds WHERE id = 1",
            params![],
            |row| {
                Ok((
                    row.get::<i64>(0)?,
                    row.get::<i64>(1)?,
                    row.get::<Vec<u8>>(2)?,
                ))
            },
        )
    })
    .unwrap();
    assert_eq!(row, (1, 0, Vec::new()));
}

#[test]
#[serial]
fn parameter_bindings_must_match_sql_parameters() {
    reset();
    Db::migrate(&[Migration {
        version: 1,
        sql: "CREATE TABLE strict_params(id INTEGER PRIMARY KEY, value TEXT);",
    }])
    .unwrap();

    let missing_positional = Db::update(|connection| {
        connection.execute(
            "INSERT INTO strict_params(id, value) VALUES (?1, ?2)",
            params![1_i64],
        )
    });
    assert!(matches!(
        missing_positional,
        Err(DbError::ParameterCountMismatch { .. })
    ));

    let extra_positional = Db::update(|connection| {
        connection.execute(
            "INSERT INTO strict_params(id) VALUES (?1)",
            params![1_i64, "extra"],
        )
    });
    assert!(matches!(
        extra_positional,
        Err(DbError::ParameterCountMismatch { .. })
    ));

    let missing_named = Db::update(|connection| {
        connection.execute_named(
            "INSERT INTO strict_params(id, value) VALUES (:id, :value)",
            named_params![":id" => 1_i64],
        )
    });
    assert!(matches!(
        missing_named,
        Err(DbError::ParameterCountMismatch { .. })
    ));

    let anonymous_named = Db::update(|connection| {
        connection.execute_named(
            "INSERT INTO strict_params(id, value) VALUES (:id, ?)",
            named_params![":id" => 1_i64, ":value" => "value"],
        )
    });
    assert!(matches!(
        anonymous_named,
        Err(DbError::AnonymousParameterInNamedBind { .. })
    ));
}

#[test]
#[serial]
fn prepare_rejects_empty_sql_and_trailing_statement_text() {
    reset();
    Db::migrate(&[Migration {
        version: 1,
        sql: "CREATE TABLE strict_sql(id INTEGER PRIMARY KEY);",
    }])
    .unwrap();

    let empty = Db::update(|connection| connection.execute("", params![]));
    assert!(matches!(empty, Err(DbError::EmptySql)));

    let trailing = Db::update(|connection| {
        connection.execute(
            "INSERT INTO strict_sql(id) VALUES (1); INSERT INTO strict_sql(id) VALUES (2)",
            params![],
        )
    });
    assert!(matches!(trailing, Err(DbError::TrailingSql)));
}
