// Pinned acceptance for P5.1 — the VENDORED SQLite is linked and usable
// through src/db/sqlite.sx.
//
//   * version: sqlite3_libversion() equals the vendored amalgamation's
//     version (vendor/sqlite/README.md) — a silent fallback to the OS
//     libsqlite3 fails here, in both `sx run` (dlopen via -L
//     build/vendor/jit) and `sx build` (static link via -L build/vendor).
//   * round trip: create table, INSERT via bound parameters (text +
//     int64 + null), SELECT back with WHERE binding, typed columns.
//   * persistence: rows survive close + reopen of the same file.
//   * transactions: BEGIN/ROLLBACK leaves no row behind.
//   * errors: bad SQL raises, and errmsg names the problem.
#import "modules/std.sx";
#import "modules/std/fs.sx";
process :: #import "modules/std/process.sx";
sq :: #import "../src/db/sqlite.sx";

VENDORED_VERSION :: "3.53.2";
DBDIR  :: ".sx-tmp/sqlite_smoke";
DBPATH :: ".sx-tmp/sqlite_smoke/t.db";

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

check_version :: () -> bool {
    v := sq.sqlite_version();
    if v != VENDORED_VERSION {
        print("  loaded sqlite {} but vendor/sqlite is {}\n", v, VENDORED_VERSION);
        return false;
    }
    return true;
}

check_roundtrip :: () -> bool {
    db, oe := sq.Sqlite.open(DBPATH);
    if oe { return false; }

    ee := false;
    db.exec("CREATE TABLE apps (id INTEGER PRIMARY KEY, slug TEXT NOT NULL, owner TEXT, created_at INTEGER NOT NULL)") catch { ee = true; };
    if ee { db.close(); return false; }

    ins, pe := db.prepare("INSERT INTO apps (slug, owner, created_at) VALUES (?1, ?2, ?3)");
    if pe { db.close(); return false; }
    berr := false;
    ins.bind_text(1, "acme-app") catch { berr = true; };
    ins.bind_text(2, "ci") catch { berr = true; };
    ins.bind_int64(3, 1781250000) catch { berr = true; };
    se := false;
    rc := ins.step() catch { se = true; 0 };
    ins.finalize();
    if berr or se or rc != sq.SQLITE_DONE { db.close(); return false; }
    rowid := db.last_insert_rowid();
    if rowid != 1 { db.close(); return false; }

    // second row with a NULL owner
    ins2, pe2 := db.prepare("INSERT INTO apps (slug, owner, created_at) VALUES (?1, ?2, ?3)");
    if pe2 { db.close(); return false; }
    ins2.bind_text(1, "other-app") catch { berr = true; };
    ins2.bind_null(2) catch { berr = true; };
    ins2.bind_int64(3, 1781250001) catch { berr = true; };
    ins2.step() catch { se = true; 0 };
    ins2.finalize();
    if berr or se { db.close(); return false; }

    // select the first row back through a WHERE binding
    sel, pe3 := db.prepare("SELECT id, slug, owner, created_at FROM apps WHERE slug = ?1");
    if pe3 { db.close(); return false; }
    sel.bind_text(1, "acme-app") catch { berr = true; };
    src := sel.step() catch { se = true; 0 };
    ok := !berr and !se and src == sq.SQLITE_ROW;
    if ok {
        ok = sel.column_count() == 4
         and sel.column_int64(0) == 1
         and sel.column_text(1) == "acme-app"
         and sel.column_text(2) == "ci"
         and sel.column_int64(3) == 1781250000
         and sel.column_type(1) == sq.SQLITE_TEXT;
    }
    if ok {
        drc := sel.step() catch { se = true; 0 };
        ok = !se and drc == sq.SQLITE_DONE;   // exactly one matching row
    }
    sel.finalize();

    // NULL column reads as "" with column_type NULL
    if ok {
        sel2, pe4 := db.prepare("SELECT owner FROM apps WHERE slug = 'other-app'");
        if pe4 { db.close(); return false; }
        nrc := sel2.step() catch { se = true; 0 };
        ok = !se and nrc == sq.SQLITE_ROW
         and sel2.column_type(0) == sq.SQLITE_NULL
         and sel2.column_text(0) == "";
        sel2.finalize();
    }

    db.close();
    return ok;
}

check_persistence :: () -> bool {
    db, oe := sq.Sqlite.open(DBPATH);
    if oe { return false; }
    sel, pe := db.prepare("SELECT COUNT(*) FROM apps");
    if pe { db.close(); return false; }
    se := false;
    rc := sel.step() catch { se = true; 0 };
    ok := !se and rc == sq.SQLITE_ROW and sel.column_int64(0) == 2;
    sel.finalize();
    db.close();
    return ok;
}

check_transaction_rollback :: () -> bool {
    db, oe := sq.Sqlite.open(DBPATH);
    if oe { return false; }
    ee := false;
    db.exec("BEGIN") catch { ee = true; };
    db.exec("INSERT INTO apps (slug, owner, created_at) VALUES ('doomed', 'x', 0)") catch { ee = true; };
    db.exec("ROLLBACK") catch { ee = true; };
    if ee { db.close(); return false; }

    sel, pe := db.prepare("SELECT COUNT(*) FROM apps WHERE slug = 'doomed'");
    if pe { db.close(); return false; }
    se := false;
    rc := sel.step() catch { se = true; 0 };
    ok := !se and rc == sq.SQLITE_ROW and sel.column_int64(0) == 0;
    sel.finalize();
    db.close();
    return ok;
}

check_error_path :: () -> bool {
    db, oe := sq.Sqlite.open(DBPATH);
    if oe { return false; }
    failed := false;
    db.exec("FROBNICATE THE DATABASE") catch { failed = true; };
    msg := db.errmsg();
    db.close();
    if !failed { return false; }
    // errmsg names the offending token
    i := 0;
    found := false;
    while i + 10 <= msg.len {
        v := string.{ ptr = @msg[i], len = 10 };
        if v == "FROBNICATE" { found = true; break; }
        i += 1;
    }
    return found;
}

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

    failures : i32 = 0;
    failures += run_case("vendored version is loaded",        check_version());
    failures += run_case("create/insert/select round trip",   check_roundtrip());
    failures += run_case("rows persist across reopen",        check_persistence());
    failures += run_case("BEGIN/ROLLBACK leaves no row",      check_transaction_rollback());
    failures += run_case("bad SQL raises with errmsg detail", check_error_path());

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