// Pinned acceptance for the FULL SQLite mapping (src/db/sqlite.sx) —
// every wrapper family beyond the P5.1 smoke (which keeps owning the
// vendored-version pin):
//
//   * library: version_number, sourceid, threadsafe=false (built
//     THREADSAFE=0), compileoption_used, complete, errstr, strglob/
//     strlike/stricmp, randomness.
//   * connections: open_v2 flag behavior (READONLY on a missing file
//     refuses; CREATE works; db_readonly reflects the mode),
//     db_filename, busy_timeout, autocommit/txn_state through a real
//     transaction, changes/total_changes, set_last_insert_rowid,
//     limit (prior value answered), error_offset, extended errcodes
//     on a UNIQUE violation (19 / 2067).
//   * statements: double/blob round trips (blob with interior NULs),
//     named parameters (count/index/name), clear_bindings + reset
//     reuse, column_name/decltype/origin metadata, data_count,
//     sql/expanded_sql, stmt readonly/isexplain, table_column_metadata.
//   * blob I/O: bind_zeroblob reservation, open/write/read/bytes,
//     reopen onto another row.
//   * backup: full db -> db copy via sqlite_backup_run.
//   * serialization: serialize -> fresh connection -> deserialize ->
//     query answers match.
#import "modules/std.sx";
process :: #import "modules/std/process.sx";
sq :: #import "../src/db/sqlite.sx";

DBDIR :: ".sx-tmp/sqlite_api";

run_case :: (label: string, ok: bool) -> i32 {
    if ok { print("  PASS  {}\n", label); return 0; }
    print("  FAIL  {}\n", label);
    return 1;
}

// ── library utilities ─────────────────────────────────────────────────

check_library_info :: () -> bool {
    if sq.sqlite_version_number() < 3053000 { return false; }
    if sq.sqlite_sourceid().len < 10 { return false; }
    if sq.sqlite_threadsafe() { return false; }            // THREADSAFE=0 build
    if !sq.sqlite_compileoption_used("THREADSAFE=0") { return false; }
    if sq.sqlite_compileoption_get(0).len == 0 { return false; }
    return true;
}

check_text_utils :: () -> bool {
    if !sq.sqlite_complete("SELECT 1;") { return false; }
    if sq.sqlite_complete("SELECT 1") { return false; }     // no terminator
    if !sq.sqlite_strglob("acme-*", "acme-app") { return false; }
    if sq.sqlite_strglob("acme-*", "other") { return false; }
    if !sq.sqlite_strlike("acme-%", "ACME-APP", 0) { return false; }  // LIKE is case-folding
    if sq.sqlite_stricmp("AbC", "abc") != 0 { return false; }
    if sq.sqlite_errstr(sq.SQLITE_BUSY).len == 0 { return false; }
    return true;
}

check_randomness :: () -> bool {
    r := sq.sqlite_randomness(16);
    if r.len != 16 { return false; }
    all_zero := true;
    i := 0;
    while i < r.len {
        if r[i] != 0 { all_zero = false; break; }
        i += 1;
    }
    return !all_zero;
}

// ── connections ───────────────────────────────────────────────────────

check_open_v2_flags :: () -> bool {
    // READONLY on a file that doesn't exist must refuse.
    refused := false;
    nope, ne := sq.Sqlite.open_v2(".sx-tmp/sqlite_api/nope.db", sq.SQLITE_OPEN_READONLY);
    if ne { refused = true; }
    if !refused { nope.close(); return false; }

    // CREATE | READWRITE builds the file; db_readonly answers 0.
    db, oe := sq.Sqlite.open_v2(".sx-tmp/sqlite_api/v2.db", sq.SQLITE_OPEN_READWRITE | sq.SQLITE_OPEN_CREATE);
    if oe { return false; }
    ee := false;
    db.exec("CREATE TABLE t (x)") catch { ee = true; };
    rw := db.db_readonly();
    fname := db.db_filename();
    db.close();
    if ee or rw != 0 { return false; }

    // contains check: the filename carries the path we opened
    found := false;
    i := 0;
    while i + 5 <= fname.len {
        v := string.{ ptr = @fname[i], len = 5 };
        if v == "v2.db" { found = true; break; }
        i += 1;
    }
    if !found { return false; }

    // Reopened READONLY: db_readonly answers 1 and writes refuse.
    ro, roe := sq.Sqlite.open_v2(".sx-tmp/sqlite_api/v2.db", sq.SQLITE_OPEN_READONLY);
    if roe { return false; }
    wfail := false;
    ro.exec("INSERT INTO t VALUES (1)") catch { wfail = true; };
    is_ro := ro.db_readonly();
    ro.close();
    return wfail and is_ro == 1;
}

check_txn_state :: () -> bool {
    db, oe := sq.Sqlite.open(".sx-tmp/sqlite_api/txn.db");
    if oe { return false; }
    db.busy_timeout(2000);
    ee := false;
    db.exec("CREATE TABLE t (x)") catch { ee = true; };

    ok := db.get_autocommit() and db.txn_state() == sq.SQLITE_TXN_NONE;
    db.exec("BEGIN IMMEDIATE") catch { ee = true; };
    if ok { ok = !db.get_autocommit() and db.txn_state() == sq.SQLITE_TXN_WRITE; }
    db.exec("COMMIT") catch { ee = true; };
    if ok { ok = db.get_autocommit(); }
    db.close();
    return ok and !ee;
}

check_changes_and_rowid :: () -> bool {
    db, oe := sq.Sqlite.open(".sx-tmp/sqlite_api/chg.db");
    if oe { return false; }
    ee := false;
    db.exec("CREATE TABLE t (x); INSERT INTO t VALUES (1),(2),(3)") catch { ee = true; };
    if ee { db.close(); return false; }
    ok := db.changes() == 3;
    db.exec("UPDATE t SET x = x + 1") catch { ee = true; };
    if ok { ok = db.changes() == 3 and db.total_changes() >= 6; }
    db.set_last_insert_rowid(424242);
    if ok { ok = db.last_insert_rowid() == 424242; }
    db.close();
    return ok and !ee;
}

check_limit :: () -> bool {
    db, oe := sq.Sqlite.open(":memory:");
    if oe { return false; }
    prior := db.limit(sq.SQLITE_LIMIT_VARIABLE_NUMBER, 100);
    now := db.limit(sq.SQLITE_LIMIT_VARIABLE_NUMBER, -1);   // read back
    db.close();
    return prior > 0 and now == 100;
}

check_error_detail :: () -> bool {
    db, oe := sq.Sqlite.open(".sx-tmp/sqlite_api/err.db");
    if oe { return false; }
    ee := false;
    db.exec("CREATE TABLE u (name TEXT UNIQUE); INSERT INTO u VALUES ('a')") catch { ee = true; };
    if ee { db.close(); return false; }

    dup := false;
    db.exec("INSERT INTO u VALUES ('a')") catch { dup = true; };
    ok := dup and db.errcode() == sq.SQLITE_CONSTRAINT
              and db.extended_errcode() == sq.SQLITE_CONSTRAINT_UNIQUE;

    // error_offset points at the offending token of a syntax error
    perr := false;
    bad, bade := db.prepare("SELECT * FRM u");
    if bade { perr = true; }
    if !perr { bad.finalize(); }
    if ok { ok = perr and db.error_offset() >= 9; }
    db.close();
    return ok;
}

// ── statements ────────────────────────────────────────────────────────

// A 6-byte blob with interior NULs.
test_blob :: () -> string {
    raw : [*]u8 = xx context.allocator.alloc_bytes(7);
    raw[0] = 1; raw[1] = 0; raw[2] = 255; raw[3] = 0; raw[4] = 7; raw[5] = 42; raw[6] = 0;
    return string.{ ptr = raw, len = 6 };
}

check_typed_round_trip :: () -> bool {
    db, oe := sq.Sqlite.open(".sx-tmp/sqlite_api/types.db");
    if oe { return false; }
    ee := false;
    db.exec("CREATE TABLE v (d REAL, b BLOB)") catch { ee = true; };
    if ee { db.close(); return false; }

    ins, pe := db.prepare("INSERT INTO v VALUES (?1, ?2)");
    if pe { db.close(); return false; }
    berr := false;
    se := false;
    ins.bind_double(1, 3.5) catch { berr = true; };
    ins.bind_blob(2, test_blob()) catch { berr = true; };
    ins.step() catch { se = true; 0 };
    ins.finalize();
    if berr or se { db.close(); return false; }

    sel, pe2 := db.prepare("SELECT d, b FROM v");
    if pe2 { db.close(); return false; }
    rc := sel.step() catch { se = true; 0 };
    ok := !se and rc == sq.SQLITE_ROW
       and sel.column_type(0) == sq.SQLITE_FLOAT
       and sel.column_double(0) == 3.5
       and sel.column_type(1) == sq.SQLITE_BLOB
       and sel.column_bytes(1) == 6
       and sel.column_blob(1) == test_blob()
       and sel.data_count() == 2;
    sel.finalize();
    db.close();
    return ok;
}

check_named_parameters :: () -> bool {
    db, oe := sq.Sqlite.open(":memory:");
    if oe { return false; }
    st, pe := db.prepare("SELECT :alpha, ?, :beta");
    if pe { db.close(); return false; }
    ok := st.parameter_count() == 3
      and st.parameter_index(":alpha") == 1
      and st.parameter_index(":beta") == 3
      and st.parameter_index(":nope") == 0
      and st.parameter_name(1) == ":alpha"
      and st.parameter_name(2) == "";       // a bare ? is nameless (?N would be "?N")
    st.finalize();
    db.close();
    return ok;
}

check_rebind_reuse :: () -> bool {
    db, oe := sq.Sqlite.open(":memory:");
    if oe { return false; }
    ee := false;
    db.exec("CREATE TABLE t (x)") catch { ee = true; };
    if ee { db.close(); return false; }
    st, pe := db.prepare("INSERT INTO t VALUES (?1)");
    if pe { db.close(); return false; }
    berr := false;
    se := false;
    st.bind_int64(1, 10) catch { berr = true; };
    st.step() catch { se = true; 0 };
    st.reset();
    st.clear_bindings();                    // NULL now bound
    st.step() catch { se = true; 0 };
    st.reset();
    st.bind_int64(1, 20) catch { berr = true; };
    st.step() catch { se = true; 0 };
    st.finalize();
    if berr or se { db.close(); return false; }

    sel, pe2 := db.prepare("SELECT COUNT(*), COUNT(x), SUM(x) FROM t");
    if pe2 { db.close(); return false; }
    rc := sel.step() catch { se = true; 0 };
    ok := !se and rc == sq.SQLITE_ROW
       and sel.column_int64(0) == 3        // three rows
       and sel.column_int64(1) == 2        // one is NULL
       and sel.column_int64(2) == 30;
    sel.finalize();
    db.close();
    return ok;
}

check_statement_introspection :: () -> bool {
    db, oe := sq.Sqlite.open(".sx-tmp/sqlite_api/meta.db");
    if oe { return false; }
    ee := false;
    db.exec("CREATE TABLE apps (id INTEGER PRIMARY KEY AUTOINCREMENT, slug TEXT NOT NULL)") catch { ee = true; };
    if ee { db.close(); return false; }

    sel, pe := db.prepare("SELECT slug FROM apps WHERE id = ?1");
    if pe { db.close(); return false; }
    berr := false;
    sel.bind_int64(1, 7) catch { berr = true; };
    ok := !berr
      and sel.readonly()
      and sel.isexplain() == 0
      and sel.column_name(0) == "slug"
      and sel.column_decltype(0) == "TEXT"
      and sel.column_table_name(0) == "apps"
      and sel.column_origin_name(0) == "slug"
      and sel.sql() == "SELECT slug FROM apps WHERE id = ?1"
      and sel.expanded_sql() == "SELECT slug FROM apps WHERE id = 7";
    sel.finalize();

    if ok {
        ins, pe2 := db.prepare("INSERT INTO apps (slug) VALUES ('x')");
        if pe2 { db.close(); return false; }
        ok = !ins.readonly();
        ins.finalize();
    }

    if ok {
        meta, me := db.table_column_metadata("apps", "id");
        if me { db.close(); return false; }
        ok = meta.primary_key and meta.autoinc and meta.data_type == "INTEGER";
        if ok {
            meta2, me2 := db.table_column_metadata("apps", "slug");
            if me2 { db.close(); return false; }
            ok = meta2.not_null and !meta2.primary_key and meta2.data_type == "TEXT";
        }
    }
    db.close();
    return ok;
}

// ── blob I/O ──────────────────────────────────────────────────────────

check_blob_io :: () -> bool {
    db, oe := sq.Sqlite.open(".sx-tmp/sqlite_api/blob.db");
    if oe { return false; }
    ee := false;
    db.exec("CREATE TABLE files (id INTEGER PRIMARY KEY, data BLOB)") catch { ee = true; };
    if ee { db.close(); return false; }

    // reserve 8 zero bytes in row 1, 4 in row 2
    ins, pe := db.prepare("INSERT INTO files (id, data) VALUES (?1, ?2)");
    if pe { db.close(); return false; }
    berr := false;
    se := false;
    ins.bind_int64(1, 1) catch { berr = true; };
    ins.bind_zeroblob(2, 8) catch { berr = true; };
    ins.step() catch { se = true; 0 };
    ins.reset();
    ins.bind_int64(1, 2) catch { berr = true; };
    ins.bind_zeroblob(2, 4) catch { berr = true; };
    ins.step() catch { se = true; 0 };
    ins.finalize();
    if berr or se { db.close(); return false; }

    bl, be := sq.SqliteBlob.open(@db, "files", "data", 1, true);
    if be { db.close(); return false; }
    werr := false;
    bl.write(2, "HI") catch { werr = true; };
    rd, re := bl.read(0, 8);
    sz := bl.bytes();
    if re { bl.close(); db.close(); return false; }
    if werr { bl.close(); db.close(); return false; }
    ok := sz == 8 and rd.len == 8 and rd[0] == 0 and rd[2] == 72 and rd[3] == 73 and rd[4] == 0;

    // reopen onto row 2 — different size, fresh content
    roerr := false;
    bl.reopen(2) catch { roerr = true; };
    if ok { ok = !roerr and bl.bytes() == 4; }
    bl.close();
    db.close();
    return ok;
}

// ── backup + serialization ────────────────────────────────────────────

count_rows :: (db: *sq.Sqlite, table: string) -> i64 {
    sel, pe := db.prepare(concat("SELECT COUNT(*) FROM ", table));
    if pe { return -1; }
    se := false;
    rc := sel.step() catch { se = true; 0 };
    n : i64 = -1;
    if !se and rc == sq.SQLITE_ROW { n = sel.column_int64(0); }
    sel.finalize();
    return n;
}

check_backup :: () -> bool {
    src, oe := sq.Sqlite.open(".sx-tmp/sqlite_api/bk-src.db");
    if oe { return false; }
    ee := false;
    src.exec("CREATE TABLE t (x); INSERT INTO t VALUES (1),(2),(3),(4)") catch { ee = true; };
    if ee { src.close(); return false; }

    dst, oe2 := sq.Sqlite.open(".sx-tmp/sqlite_api/bk-dst.db");
    if oe2 { src.close(); return false; }
    bke := false;
    sq.sqlite_backup_run(@dst, @src) catch { bke = true; };
    ok := !bke and count_rows(@dst, "t") == 4;
    src.close();
    dst.close();
    return ok;
}

check_serialize_round_trip :: () -> bool {
    db, oe := sq.Sqlite.open(":memory:");
    if oe { return false; }
    ee := false;
    db.exec("CREATE TABLE t (x); INSERT INTO t VALUES (10),(20)") catch { ee = true; };
    if ee { db.close(); return false; }
    img, se := db.serialize();
    db.close();
    if se { return false; }
    if img.len < 512 { return false; }

    db2, oe2 := sq.Sqlite.open(":memory:");
    if oe2 { return false; }
    de := false;
    db2.deserialize(img) catch { de = true; };
    ok := !de and count_rows(@db2, "t") == 2;
    db2.close();
    return ok;
}

main :: () -> i32 {
    process.run(concat("rm -rf ", DBDIR));
    process.run(concat("mkdir -p ", DBDIR));

    failures : i32 = 0;
    failures += run_case("library: version/sourceid/threadsafe/options", check_library_info());
    failures += run_case("library: complete/glob/like/stricmp/errstr",   check_text_utils());
    failures += run_case("library: randomness yields bytes",             check_randomness());
    failures += run_case("conn: open_v2 flags + db_readonly/filename",   check_open_v2_flags());
    failures += run_case("conn: autocommit + txn_state through BEGIN",   check_txn_state());
    failures += run_case("conn: changes64 + set_last_insert_rowid",      check_changes_and_rowid());
    failures += run_case("conn: limit answers the prior value",          check_limit());
    failures += run_case("conn: constraint errcodes + error_offset",     check_error_detail());
    failures += run_case("stmt: double/blob round trip (interior NULs)", check_typed_round_trip());
    failures += run_case("stmt: named parameter introspection",          check_named_parameters());
    failures += run_case("stmt: clear_bindings + reset reuse",           check_rebind_reuse());
    failures += run_case("stmt: names/decltypes/metadata/expanded_sql",  check_statement_introspection());
    failures += run_case("blob: zeroblob + write/read/reopen",           check_blob_io());
    failures += run_case("backup: full db -> db copy",                   check_backup());
    failures += run_case("serialize: image round-trips into a fresh db", check_serialize_round_trip());

    process.run(concat("rm -rf ", DBDIR));
    print("------------------------------------------------\n");
    if failures == 0 {
        print("sqlite_api: ALL CASES PASS\n");
        return 0;
    }
    print("sqlite_api: {} CASE(S) FAILED\n", failures);
    return 1;
}
