//! zcatsql - SQLite wrapper for Zig
//!
//! A lightweight, idiomatic Zig wrapper around SQLite that compiles the
//! SQLite amalgamation directly into your binary for zero runtime dependencies.
//!
//! ## Quick Start
//!
//! ```zig
//! const sqlite = @import("zcatsql");
//!
//! pub fn main() !void {
//!     var db = try sqlite.open(":memory:");
//!     defer db.close();
//!
//!     try db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)");
//!
//!     var stmt = try db.prepare("INSERT INTO users (name) VALUES (?)");
//!     defer stmt.finalize();
//!
//!     try stmt.bindText(1, "Alice");
//!     _ = try stmt.step();
//! }
//! ```

const std = @import("std");

// Internal modules
const c_mod = @import("c.zig");
const errors_mod = @import("errors.zig");
const types_mod = @import("types.zig");
const database_mod = @import("database.zig");
const statement_mod = @import("statement.zig");
const functions_mod = @import("functions.zig");
const backup_mod = @import("backup.zig");
const pool_mod = @import("pool.zig");

// Extension helpers
pub const fts5 = @import("fts5.zig");
pub const json = @import("json.zig");
pub const rtree = @import("rtree.zig");
pub const vtable = @import("vtable.zig");

// Advanced features
pub const serialize = @import("serialize.zig");
pub const session = @import("session.zig");
pub const audit = @import("audit/mod.zig");

// Re-export C bindings (for advanced users)
pub const c = c_mod.c;

// Re-export error types
pub const Error = errors_mod.Error;
pub const resultToError = errors_mod.resultToError;
pub const errorDescription = errors_mod.errorDescription;

// Re-export common types
pub const OpenFlags = types_mod.OpenFlags;
pub const ColumnType = types_mod.ColumnType;
pub const Limit = types_mod.Limit;
pub const UpdateOperation = types_mod.UpdateOperation;
pub const AuthAction = types_mod.AuthAction;
pub const AuthResult = types_mod.AuthResult;

// Re-export main structs
pub const Database = database_mod.Database;
pub const Statement = statement_mod.Statement;
pub const Backup = backup_mod.Backup;
pub const Blob = backup_mod.Blob;
pub const ConnectionPool = pool_mod.ConnectionPool;

// Re-export iterator types
pub const RowIterator = statement_mod.RowIterator;
pub const Row = statement_mod.Row;

// Re-export extension helpers (convenience aliases)
pub const Fts5 = fts5.Fts5;
pub const Json = json.Json;
pub const RTree = rtree.RTree;
pub const BoundingBox2D = rtree.BoundingBox2D;
pub const BoundingBox3D = rtree.BoundingBox3D;
pub const GeoCoord = rtree.GeoCoord;

// Re-export function types
pub const FunctionContext = functions_mod.FunctionContext;
pub const FunctionValue = functions_mod.FunctionValue;
pub const AggregateContext = functions_mod.AggregateContext;
pub const PreUpdateContext = functions_mod.PreUpdateContext;
pub const ScalarFn = functions_mod.ScalarFn;
pub const AggregateStepFn = functions_mod.AggregateStepFn;
pub const AggregateFinalFn = functions_mod.AggregateFinalFn;
pub const WindowValueFn = functions_mod.WindowValueFn;
pub const WindowInverseFn = functions_mod.WindowInverseFn;
pub const CollationFn = functions_mod.CollationFn;
pub const ZigCommitHookFn = functions_mod.ZigCommitHookFn;
pub const ZigRollbackHookFn = functions_mod.ZigRollbackHookFn;
pub const ZigUpdateHookFn = functions_mod.ZigUpdateHookFn;
pub const ZigPreUpdateHookFn = functions_mod.ZigPreUpdateHookFn;
pub const ZigAuthorizerFn = functions_mod.ZigAuthorizerFn;
pub const ZigProgressFn = functions_mod.ZigProgressFn;
pub const ZigBusyHandlerFn = functions_mod.ZigBusyHandlerFn;

// Re-export backup convenience functions
pub const backupDatabase = backup_mod.backupDatabase;
pub const backupToFile = backup_mod.backupToFile;
pub const loadFromFile = backup_mod.loadFromFile;

// Re-export serialize types and functions
pub const SerializeFlags = serialize.SerializeFlags;
pub const DeserializeFlags = serialize.DeserializeFlags;

// Re-export session types
pub const Session = session.Session;
pub const ConflictAction = session.ConflictAction;
pub const ConflictType = session.ConflictType;
pub const ChangeOp = session.ChangeOp;
pub const ChangesetIterator = session.ChangesetIterator;
pub const Rebaser = session.Rebaser;
pub const applyChangeset = session.applyChangeset;
pub const invertChangeset = session.invertChangeset;
pub const concatChangesets = session.concatChangesets;

// Re-export audit types
pub const AuditLog = audit.AuditLog;
pub const AuditConfig = audit.Config;
pub const AuditEntry = audit.Entry;
pub const verifyAuditChain = audit.verifyChain;

// Re-export snapshot type from Database
pub const Snapshot = Database.Snapshot;

// ============================================================================
// Convenience functions
// ============================================================================

/// Opens a database connection.
pub fn open(path: [:0]const u8) Error!Database {
    return Database.open(path);
}

/// Opens an in-memory database.
pub fn openMemory() Error!Database {
    return Database.open(":memory:");
}

/// Opens a database using a URI connection string.
pub fn openUri(uri: [:0]const u8) Error!Database {
    return Database.openUri(uri);
}

/// Returns the SQLite version string.
pub fn version() []const u8 {
    return std.mem.span(c.sqlite3_libversion());
}

/// Returns the SQLite version number.
pub fn versionNumber() i32 {
    return c.sqlite3_libversion_number();
}

// ============================================================================
// Tests
// ============================================================================

test "version" {
    const ver = version();
    try std.testing.expect(ver.len > 0);
    try std.testing.expectEqualStrings("3.", ver[0..2]);
}

test "open and close memory database" {
    var db = try openMemory();
    defer db.close();
    try std.testing.expect(db.handle != null);
}

test "create table and insert" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)");
    try db.exec("INSERT INTO test (name) VALUES ('hello')");

    try std.testing.expectEqual(@as(i64, 1), db.lastInsertRowId());
    try std.testing.expectEqual(@as(i32, 1), db.changes());
}

test "prepared statement with binding" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)");

    var stmt = try db.prepare("INSERT INTO users (name, age) VALUES (?, ?)");
    defer stmt.finalize();

    try stmt.bindText(1, "Alice");
    try stmt.bindInt(2, 30);
    _ = try stmt.step();

    try stmt.reset();
    try stmt.clearBindings();

    try stmt.bindText(1, "Bob");
    try stmt.bindInt(2, 25);
    _ = try stmt.step();

    try std.testing.expectEqual(@as(i64, 2), db.lastInsertRowId());
}

test "select query" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)");
    try db.exec("INSERT INTO users (name) VALUES ('Alice'), ('Bob')");

    var stmt = try db.prepare("SELECT id, name FROM users ORDER BY id");
    defer stmt.finalize();

    try std.testing.expect(try stmt.step());
    try std.testing.expectEqual(@as(i64, 1), stmt.columnInt(0));
    try std.testing.expectEqualStrings("Alice", stmt.columnText(1).?);

    try std.testing.expect(try stmt.step());
    try std.testing.expectEqual(@as(i64, 2), stmt.columnInt(0));
    try std.testing.expectEqualStrings("Bob", stmt.columnText(1).?);

    try std.testing.expect(!try stmt.step());
}

test "transaction commit" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE test (x INTEGER)");

    try db.begin();
    try db.exec("INSERT INTO test VALUES (1)");
    try db.exec("INSERT INTO test VALUES (2)");
    try db.commit();

    var stmt = try db.prepare("SELECT COUNT(*) FROM test");
    defer stmt.finalize();
    _ = try stmt.step();
    try std.testing.expectEqual(@as(i64, 2), stmt.columnInt(0));
}

test "transaction rollback" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE test (x INTEGER)");
    try db.exec("INSERT INTO test VALUES (1)");

    try db.begin();
    try db.exec("INSERT INTO test VALUES (2)");
    try db.rollback();

    var stmt = try db.prepare("SELECT COUNT(*) FROM test");
    defer stmt.finalize();
    _ = try stmt.step();
    try std.testing.expectEqual(@as(i64, 1), stmt.columnInt(0));
}

test "foreign keys" {
    var db = try openMemory();
    defer db.close();

    try db.setForeignKeys(true);

    try db.exec(
        \\CREATE TABLE parent (id INTEGER PRIMARY KEY);
        \\CREATE TABLE child (id INTEGER PRIMARY KEY, parent_id INTEGER REFERENCES parent(id));
    );

    try db.exec("INSERT INTO parent VALUES (1)");
    try db.exec("INSERT INTO child VALUES (1, 1)");

    const result = db.exec("INSERT INTO child VALUES (2, 999)");
    try std.testing.expectError(Error.Constraint, result);
}

test "null values" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE test (x INTEGER, y TEXT)");

    var stmt = try db.prepare("INSERT INTO test VALUES (?, ?)");
    defer stmt.finalize();

    try stmt.bindInt(1, 42);
    try stmt.bindNull(2);
    _ = try stmt.step();

    var query = try db.prepare("SELECT x, y FROM test");
    defer query.finalize();

    _ = try query.step();
    try std.testing.expectEqual(@as(i64, 42), query.columnInt(0));
    try std.testing.expect(query.columnIsNull(1));
}

test "blob data" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE test (data BLOB)");

    const blob_data = [_]u8{ 0x00, 0x01, 0x02, 0xFF, 0xFE };

    var stmt = try db.prepare("INSERT INTO test VALUES (?)");
    defer stmt.finalize();
    try stmt.bindBlob(1, &blob_data);
    _ = try stmt.step();

    var query = try db.prepare("SELECT data FROM test");
    defer query.finalize();
    _ = try query.step();

    const result = query.columnBlob(0).?;
    try std.testing.expectEqualSlices(u8, &blob_data, result);
}

test "named parameters" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)");

    var stmt = try db.prepare("INSERT INTO users (name, age) VALUES (:name, :age)");
    defer stmt.finalize();

    try std.testing.expectEqual(@as(u32, 2), stmt.parameterCount());
    try std.testing.expectEqual(@as(?u32, 1), stmt.parameterIndex(":name"));
    try std.testing.expectEqual(@as(?u32, 2), stmt.parameterIndex(":age"));

    try stmt.bindTextNamed(":name", "Alice");
    try stmt.bindIntNamed(":age", 30);
    _ = try stmt.step();

    try std.testing.expectEqual(@as(i64, 1), db.lastInsertRowId());
}

test "savepoint" {
    const allocator = std.testing.allocator;
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE test (x INTEGER)");
    try db.exec("INSERT INTO test VALUES (1)");

    try db.begin();

    try db.savepoint(allocator, "sp1");
    try db.exec("INSERT INTO test VALUES (2)");
    try db.rollbackTo(allocator, "sp1");

    try db.exec("INSERT INTO test VALUES (3)");
    try db.release(allocator, "sp1");

    try db.commit();

    var stmt = try db.prepare("SELECT x FROM test ORDER BY x");
    defer stmt.finalize();

    _ = try stmt.step();
    try std.testing.expectEqual(@as(i64, 1), stmt.columnInt(0));
    _ = try stmt.step();
    try std.testing.expectEqual(@as(i64, 3), stmt.columnInt(0));
    try std.testing.expect(!try stmt.step());
}

test "busy timeout" {
    var db = try openMemory();
    defer db.close();
    try db.setBusyTimeout(5000);
}

test "statement metadata" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)");

    var select = try db.prepare("SELECT id, name FROM test");
    defer select.finalize();

    try std.testing.expect(select.isReadOnly());
    try std.testing.expectEqualStrings("SELECT id, name FROM test", select.sql().?);

    var insert = try db.prepare("INSERT INTO test (name) VALUES (?)");
    defer insert.finalize();

    try std.testing.expect(!insert.isReadOnly());
}

test "boolean bind and column" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE test (flag INTEGER)");

    var stmt = try db.prepare("INSERT INTO test VALUES (?)");
    defer stmt.finalize();

    try stmt.bindBool(1, true);
    _ = try stmt.step();

    try stmt.reset();
    try stmt.bindBool(1, false);
    _ = try stmt.step();

    var query = try db.prepare("SELECT flag FROM test ORDER BY rowid");
    defer query.finalize();

    _ = try query.step();
    try std.testing.expect(query.columnBool(0));

    _ = try query.step();
    try std.testing.expect(!query.columnBool(0));
}

test "backup memory to memory" {
    var source = try openMemory();
    defer source.close();

    try source.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)");
    try source.exec("INSERT INTO test (value) VALUES ('hello'), ('world')");

    var dest = try openMemory();
    defer dest.close();

    var backup = try Backup.initMain(&dest, &source);
    defer backup.deinit();

    try backup.stepAll();
    try backup.finish();

    var stmt = try dest.prepare("SELECT COUNT(*) FROM test");
    defer stmt.finalize();
    _ = try stmt.step();
    try std.testing.expectEqual(@as(i64, 2), stmt.columnInt(0));
}

test "backup convenience function" {
    var source = try openMemory();
    defer source.close();

    try source.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)");
    try source.exec("INSERT INTO users (name) VALUES ('Alice'), ('Bob'), ('Charlie')");

    var dest = try openMemory();
    defer dest.close();

    try backupDatabase(&dest, &source);

    var stmt = try dest.prepare("SELECT name FROM users ORDER BY id");
    defer stmt.finalize();

    _ = try stmt.step();
    try std.testing.expectEqualStrings("Alice", stmt.columnText(0).?);
    _ = try stmt.step();
    try std.testing.expectEqualStrings("Bob", stmt.columnText(0).?);
    _ = try stmt.step();
    try std.testing.expectEqualStrings("Charlie", stmt.columnText(0).?);
}

test "attach and detach memory database" {
    const allocator = std.testing.allocator;

    var db = try openMemory();
    defer db.close();

    try db.attachMemory(allocator, "db2");
    try db.exec("CREATE TABLE db2.items (id INTEGER PRIMARY KEY, name TEXT)");
    try db.exec("INSERT INTO db2.items (name) VALUES ('item1'), ('item2')");

    {
        var stmt = try db.prepare("SELECT COUNT(*) FROM db2.items");
        defer stmt.finalize();
        _ = try stmt.step();
        try std.testing.expectEqual(@as(i64, 2), stmt.columnInt(0));
    }

    const dbs = try db.listDatabases(allocator);
    defer Database.freeDatabaseList(allocator, dbs);

    try std.testing.expect(dbs.len >= 2);

    try db.detach(allocator, "db2");

    const result = db.prepare("SELECT * FROM db2.items");
    try std.testing.expectError(Error.SqliteError, result);
}

fn doubleInt(ctx: FunctionContext, args: []const FunctionValue) void {
    if (args.len != 1) {
        ctx.setError("double() requires 1 argument");
        return;
    }
    if (args[0].isNull()) {
        ctx.setNull();
        return;
    }
    const value = args[0].asInt();
    ctx.setInt(value * 2);
}

test "user-defined scalar function" {
    var db = try openMemory();
    defer db.close();

    try db.createScalarFunction("double", 1, doubleInt);

    try db.exec("CREATE TABLE test (value INTEGER)");
    try db.exec("INSERT INTO test VALUES (5), (10), (15)");

    var stmt = try db.prepare("SELECT double(value) FROM test ORDER BY value");
    defer stmt.finalize();

    _ = try stmt.step();
    try std.testing.expectEqual(@as(i64, 10), stmt.columnInt(0));

    _ = try stmt.step();
    try std.testing.expectEqual(@as(i64, 20), stmt.columnInt(0));

    _ = try stmt.step();
    try std.testing.expectEqual(@as(i64, 30), stmt.columnInt(0));
}

fn reverseCollation(a: []const u8, b: []const u8) i32 {
    const result = std.mem.order(u8, a, b);
    return switch (result) {
        .lt => 1,
        .gt => -1,
        .eq => 0,
    };
}

test "custom collation" {
    var db = try openMemory();
    defer db.close();

    try db.createCollation("REVERSE", reverseCollation);

    try db.exec("CREATE TABLE names (name TEXT)");
    try db.exec("INSERT INTO names VALUES ('Alice'), ('Bob'), ('Charlie')");

    {
        var stmt = try db.prepare("SELECT name FROM names ORDER BY name COLLATE REVERSE");
        defer stmt.finalize();

        _ = try stmt.step();
        try std.testing.expectEqualStrings("Charlie", stmt.columnText(0).?);
        _ = try stmt.step();
        try std.testing.expectEqualStrings("Bob", stmt.columnText(0).?);
        _ = try stmt.step();
        try std.testing.expectEqualStrings("Alice", stmt.columnText(0).?);
    }
}

test "blob incremental I/O" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE blobs (id INTEGER PRIMARY KEY, data BLOB)");

    var insert = try db.prepare("INSERT INTO blobs (data) VALUES (zeroblob(100))");
    defer insert.finalize();
    _ = try insert.step();

    const rowid = db.lastInsertRowId();

    var blob = try Blob.open(&db, "main", "blobs", "data", rowid, true);
    defer blob.deinit();

    try std.testing.expectEqual(@as(i32, 100), blob.bytes());

    const write_data = "Hello, Blob World!";
    try blob.write(write_data, 0);

    try blob.close();
    blob = try Blob.open(&db, "main", "blobs", "data", rowid, false);

    var read_buffer: [100]u8 = undefined;
    try blob.read(&read_buffer, 0);

    try std.testing.expectEqualStrings(write_data, read_buffer[0..write_data.len]);
}

test "blob read all" {
    const allocator = std.testing.allocator;

    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE files (id INTEGER PRIMARY KEY, content BLOB)");

    var insert = try db.prepare("INSERT INTO files (content) VALUES (?)");
    defer insert.finalize();
    const test_data = "This is test blob content for readAll test";
    try insert.bindBlob(1, test_data);
    _ = try insert.step();

    const rowid = db.lastInsertRowId();

    var blob = try Blob.open(&db, "main", "files", "content", rowid, false);
    defer blob.deinit();

    const data = try blob.readAll(allocator);
    defer allocator.free(data);

    try std.testing.expectEqualStrings(test_data, data);
}

var commit_count: u32 = 0;
var rollback_count: u32 = 0;
var update_count: u32 = 0;
var last_update_op: ?UpdateOperation = null;

fn testCommitHook() bool {
    commit_count += 1;
    return true;
}

fn testRollbackHook() void {
    rollback_count += 1;
}

fn testUpdateHook(op: UpdateOperation, _: []const u8, _: []const u8, _: i64) void {
    update_count += 1;
    last_update_op = op;
}

test "commit hook" {
    commit_count = 0;

    var db = try openMemory();
    defer db.close();

    try db.setCommitHook(testCommitHook);

    try db.exec("CREATE TABLE test (x INTEGER)");
    try std.testing.expect(commit_count >= 1);

    const before = commit_count;
    try db.begin();
    try db.exec("INSERT INTO test VALUES (1)");
    try db.commit();
    try std.testing.expect(commit_count > before);

    try db.setCommitHook(null);
}

test "rollback hook" {
    rollback_count = 0;

    var db = try openMemory();
    defer db.close();

    try db.setRollbackHook(testRollbackHook);

    try db.exec("CREATE TABLE test (x INTEGER)");

    try db.begin();
    try db.exec("INSERT INTO test VALUES (1)");
    try db.rollback();

    try std.testing.expect(rollback_count >= 1);

    try db.setRollbackHook(null);
}

test "update hook" {
    update_count = 0;
    last_update_op = null;

    var db = try openMemory();
    defer db.close();

    try db.setUpdateHook(testUpdateHook);

    try db.exec("CREATE TABLE test (x INTEGER)");

    try db.exec("INSERT INTO test VALUES (1)");
    try std.testing.expect(last_update_op == .insert);

    try db.exec("UPDATE test SET x = 2 WHERE x = 1");
    try std.testing.expect(last_update_op == .update);

    try db.exec("DELETE FROM test WHERE x = 2");
    try std.testing.expect(last_update_op == .delete);

    try std.testing.expect(update_count >= 3);

    db.clearHooks();
}

const SumSquaresState = struct {
    total: i64 = 0,
};

fn sumSquaresStep(ctx: AggregateContext, args: []const FunctionValue) void {
    const state = ctx.getAggregateContext(SumSquaresState) orelse return;
    if (args.len > 0 and !args[0].isNull()) {
        const val = args[0].asInt();
        state.total += val * val;
    }
}

fn sumSquaresFinal(ctx: AggregateContext) void {
    const state = ctx.getAggregateContext(SumSquaresState) orelse {
        ctx.setNull();
        return;
    };
    ctx.setInt(state.total);
}

test "aggregate function - sum of squares" {
    var db = try openMemory();
    defer db.close();

    try db.createAggregateFunction("sum_squares", 1, sumSquaresStep, sumSquaresFinal);

    try db.exec("CREATE TABLE numbers (value INTEGER)");
    try db.exec("INSERT INTO numbers VALUES (1), (2), (3), (4), (5)");

    var stmt = try db.prepare("SELECT sum_squares(value) FROM numbers");
    defer stmt.finalize();

    _ = try stmt.step();
    try std.testing.expectEqual(@as(i64, 55), stmt.columnInt(0));
}

var auth_deny_drop_table = false;

fn testAuthorizer(
    action: AuthAction,
    _: ?[]const u8,
    _: ?[]const u8,
    _: ?[]const u8,
    _: ?[]const u8,
) AuthResult {
    if (action == .drop_table and auth_deny_drop_table) {
        return .deny;
    }
    return .ok;
}

test "authorizer callback" {
    var db = try openMemory();
    defer db.close();

    try db.setAuthorizer(testAuthorizer);

    try db.exec("CREATE TABLE test (x INTEGER)");

    auth_deny_drop_table = false;
    try db.exec("DROP TABLE test");

    try db.exec("CREATE TABLE test (x INTEGER)");

    auth_deny_drop_table = true;
    const result = db.exec("DROP TABLE test");
    try std.testing.expectError(Error.Auth, result);

    try db.setAuthorizer(null);
    auth_deny_drop_table = false;
}

var progress_call_count: u32 = 0;

fn testProgressHandler() bool {
    progress_call_count += 1;
    return true;
}

test "progress handler" {
    progress_call_count = 0;

    var db = try openMemory();
    defer db.close();

    try db.setProgressHandler(1, testProgressHandler);

    try db.exec("CREATE TABLE test (x INTEGER)");
    try db.exec("INSERT INTO test VALUES (1), (2), (3), (4), (5)");

    try std.testing.expect(progress_call_count > 0);

    try db.setProgressHandler(0, null);
}

test "limits API" {
    var db = try openMemory();
    defer db.close();

    const old_limit = db.getLimit(.sql_length);
    try std.testing.expect(old_limit > 0);

    const prev = db.setLimit(.sql_length, 10000);
    try std.testing.expectEqual(old_limit, prev);

    const new_limit = db.getLimit(.sql_length);
    try std.testing.expectEqual(@as(i32, 10000), new_limit);

    _ = db.setLimit(.sql_length, old_limit);
}

test "expanded SQL" {
    const allocator = std.testing.allocator;

    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE users (id INTEGER, name TEXT)");

    var stmt = try db.prepare("SELECT * FROM users WHERE id = ? AND name = ?");
    defer stmt.finalize();

    try stmt.bindInt(1, 42);
    try stmt.bindText(2, "Alice");

    if (stmt.expandedSql(allocator)) |expanded| {
        defer allocator.free(expanded);
        try std.testing.expect(std.mem.indexOf(u8, expanded, "42") != null);
        try std.testing.expect(std.mem.indexOf(u8, expanded, "'Alice'") != null);
    }
}

test "column metadata" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)");
    try db.exec("INSERT INTO users VALUES (1, 'Alice', 'alice@example.com')");

    var stmt = try db.prepare("SELECT id, name, email FROM users");
    defer stmt.finalize();

    _ = try stmt.step();

    if (stmt.columnDatabaseName(0)) |db_name| {
        try std.testing.expectEqualStrings("main", db_name);
    }

    if (stmt.columnTableName(0)) |table_name| {
        try std.testing.expectEqualStrings("users", table_name);
    }

    if (stmt.columnOriginName(0)) |origin_name| {
        try std.testing.expectEqualStrings("id", origin_name);
    }
}

var preupdate_call_count: u32 = 0;
var preupdate_old_value: ?i64 = null;
var preupdate_new_value: ?i64 = null;
var preupdate_op: ?UpdateOperation = null;

fn testPreUpdateHook(
    ctx: PreUpdateContext,
    op: UpdateOperation,
    _: []const u8,
    _: []const u8,
    _: i64,
    _: i64,
) void {
    preupdate_call_count += 1;
    preupdate_op = op;

    if (op == .update or op == .delete) {
        if (ctx.oldValue(0)) |old_val| {
            preupdate_old_value = old_val.asInt();
        }
    }
    if (op == .update or op == .insert) {
        if (ctx.newValue(0)) |new_val| {
            preupdate_new_value = new_val.asInt();
        }
    }
}

test "pre-update hook" {
    preupdate_call_count = 0;
    preupdate_old_value = null;
    preupdate_new_value = null;
    preupdate_op = null;

    var db = try openMemory();
    defer db.close();

    try db.setPreUpdateHook(testPreUpdateHook);

    try db.exec("CREATE TABLE test (x INTEGER)");

    preupdate_call_count = 0;
    try db.exec("INSERT INTO test VALUES (10)");
    try std.testing.expect(preupdate_call_count > 0);
    try std.testing.expectEqual(UpdateOperation.insert, preupdate_op.?);
    try std.testing.expectEqual(@as(i64, 10), preupdate_new_value.?);

    preupdate_call_count = 0;
    preupdate_old_value = null;
    preupdate_new_value = null;
    try db.exec("UPDATE test SET x = 20 WHERE x = 10");
    try std.testing.expect(preupdate_call_count > 0);
    try std.testing.expectEqual(UpdateOperation.update, preupdate_op.?);
    try std.testing.expectEqual(@as(i64, 10), preupdate_old_value.?);
    try std.testing.expectEqual(@as(i64, 20), preupdate_new_value.?);

    preupdate_call_count = 0;
    preupdate_old_value = null;
    try db.exec("DELETE FROM test WHERE x = 20");
    try std.testing.expect(preupdate_call_count > 0);
    try std.testing.expectEqual(UpdateOperation.delete, preupdate_op.?);
    try std.testing.expectEqual(@as(i64, 20), preupdate_old_value.?);

    try db.setPreUpdateHook(null);
}

test "timestamp binding" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE events (id INTEGER PRIMARY KEY, name TEXT, created_at TEXT)");

    var stmt = try db.prepare("INSERT INTO events (name, created_at) VALUES (?, ?)");
    defer stmt.finalize();

    try stmt.bindText(1, "test event");
    try stmt.bindTimestamp(2, 1705314645);
    _ = try stmt.step();

    var query = try db.prepare("SELECT created_at FROM events WHERE name = ?");
    defer query.finalize();

    try query.bindText(1, "test event");
    const has_row = try query.step();
    try std.testing.expect(has_row);

    const created_at = query.columnText(0) orelse "";
    try std.testing.expectEqualStrings("2024-01-15 10:30:45", created_at);
}

const WindowSumState = struct {
    total: i64 = 0,
};

fn windowSumStep(ctx: AggregateContext, args: []const FunctionValue) void {
    const state = ctx.getAggregateContext(WindowSumState) orelse return;
    if (args.len > 0 and !args[0].isNull()) {
        state.total += args[0].asInt();
    }
}

fn windowSumInverse(ctx: AggregateContext, args: []const FunctionValue) void {
    const state = ctx.getAggregateContext(WindowSumState) orelse return;
    if (args.len > 0 and !args[0].isNull()) {
        state.total -= args[0].asInt();
    }
}

fn windowSumValue(ctx: AggregateContext) void {
    const state = ctx.getAggregateContext(WindowSumState) orelse {
        ctx.setNull();
        return;
    };
    ctx.setInt(state.total);
}

fn windowSumFinal(ctx: AggregateContext) void {
    windowSumValue(ctx);
}

test "window function" {
    var db = try openMemory();
    defer db.close();

    try db.createWindowFunction("mysum", 1, windowSumStep, windowSumFinal, windowSumValue, windowSumInverse);

    try db.exec("CREATE TABLE nums (id INTEGER PRIMARY KEY, value INTEGER)");
    try db.exec("INSERT INTO nums (value) VALUES (1), (2), (3), (4), (5)");

    var stmt = try db.prepare("SELECT mysum(value) FROM nums");
    defer stmt.finalize();

    _ = try stmt.step();
    try std.testing.expectEqual(@as(i64, 15), stmt.columnInt(0));
}

test "window function with OVER clause" {
    var db = try openMemory();
    defer db.close();

    try db.createWindowFunction("mysum", 1, windowSumStep, windowSumFinal, windowSumValue, windowSumInverse);

    try db.exec("CREATE TABLE nums (id INTEGER PRIMARY KEY, value INTEGER)");
    try db.exec("INSERT INTO nums (value) VALUES (1), (2), (3), (4), (5)");

    var stmt = try db.prepare(
        \\SELECT value, mysum(value) OVER (ORDER BY id ROWS UNBOUNDED PRECEDING) as running_sum
        \\FROM nums ORDER BY id
    );
    defer stmt.finalize();

    const expected_sums = [_]i64{ 1, 3, 6, 10, 15 };
    var i: usize = 0;
    while (try stmt.step()) {
        const running_sum = stmt.columnInt(1);
        try std.testing.expectEqual(expected_sums[i], running_sum);
        i += 1;
    }
    try std.testing.expectEqual(@as(usize, 5), i);
}

test "URI connection string" {
    var db = try openUri("file::memory:");
    defer db.close();

    try db.exec("CREATE TABLE test (x INTEGER)");
    try db.exec("INSERT INTO test VALUES (42)");

    var stmt = try db.prepare("SELECT x FROM test");
    defer stmt.finalize();

    _ = try stmt.step();
    try std.testing.expectEqual(@as(i64, 42), stmt.columnInt(0));
}

test "pragma cache size" {
    const allocator = std.testing.allocator;
    var db = try openMemory();
    defer db.close();

    try db.setCacheSize(allocator, -2000);

    try db.exec("CREATE TABLE test (x INTEGER)");
    try db.exec("INSERT INTO test VALUES (1)");
}

test "pragma query only" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE test (x INTEGER)");
    try db.exec("INSERT INTO test VALUES (1)");

    try db.setQueryOnly(true);

    var stmt = try db.prepare("SELECT x FROM test");
    defer stmt.finalize();
    _ = try stmt.step();
    try std.testing.expectEqual(@as(i64, 1), stmt.columnInt(0));

    const result = db.exec("INSERT INTO test VALUES (2)");
    try std.testing.expectError(Error.ReadOnly, result);

    try db.setQueryOnly(false);
    try db.exec("INSERT INTO test VALUES (2)");
}

test "integrity check" {
    const allocator = std.testing.allocator;
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE test (x INTEGER)");
    try db.exec("INSERT INTO test VALUES (1), (2), (3)");

    const result = try db.integrityCheck(allocator);
    defer allocator.free(result);

    try std.testing.expectEqualStrings("ok", result);
}

test "vacuum" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE test (x INTEGER)");
    try db.exec("INSERT INTO test VALUES (1), (2), (3)");
    try db.exec("DELETE FROM test");

    try db.vacuum();
}

test "optimize" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE test (x INTEGER)");
    try db.exec("INSERT INTO test VALUES (1), (2), (3)");

    try db.optimize();
}

test "connection pool basic" {
    const allocator = std.testing.allocator;

    // Use a temp file instead of shared memory cache
    // (shared cache is disabled with -DSQLITE_OMIT_SHARED_CACHE)
    const tmp_path = "/tmp/zcatsql_pool_test.db";
    std.fs.cwd().deleteFile(tmp_path) catch {};
    defer std.fs.cwd().deleteFile(tmp_path) catch {};

    var pool = try ConnectionPool.init(allocator, tmp_path, 3);
    defer pool.deinit();

    try std.testing.expectEqual(@as(usize, 3), pool.capacity());
    try std.testing.expectEqual(@as(usize, 0), pool.openCount());

    const conn1 = try pool.acquire();
    try std.testing.expectEqual(@as(usize, 1), pool.openCount());
    try std.testing.expectEqual(@as(usize, 1), pool.inUseCount());

    try conn1.exec("CREATE TABLE test (x INTEGER)");
    try conn1.exec("INSERT INTO test VALUES (1)");

    const conn2 = try pool.acquire();
    try std.testing.expectEqual(@as(usize, 2), pool.openCount());
    try std.testing.expectEqual(@as(usize, 2), pool.inUseCount());

    pool.release(conn1);
    try std.testing.expectEqual(@as(usize, 2), pool.openCount());
    try std.testing.expectEqual(@as(usize, 1), pool.inUseCount());

    pool.release(conn2);
    try std.testing.expectEqual(@as(usize, 0), pool.inUseCount());
}

test "connection pool reuse" {
    const allocator = std.testing.allocator;

    var pool = try ConnectionPool.init(allocator, "file::memory:?cache=shared", 2);
    defer pool.deinit();

    const conn1 = try pool.acquire();
    pool.release(conn1);

    const conn2 = try pool.acquire();
    try std.testing.expectEqual(@as(usize, 1), pool.openCount());

    pool.release(conn2);
}

// ============================================================================
// New feature tests: Batch bind, Row iterator, FTS5, JSON, R-Tree
// ============================================================================

test "batch bind with tuple" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE users (name TEXT, age INTEGER, score REAL)");

    var stmt = try db.prepare("INSERT INTO users VALUES (?, ?, ?)");
    defer stmt.finalize();

    // Bind all values from a tuple
    try stmt.bindAll(.{ "Alice", @as(i64, 30), @as(f64, 95.5) });
    _ = try stmt.step();

    // Rebind for second insert
    try stmt.rebind(.{ "Bob", @as(i64, 25), @as(f64, 88.0) });
    _ = try stmt.step();

    // Verify
    var query = try db.prepare("SELECT name, age, score FROM users ORDER BY name");
    defer query.finalize();

    _ = try query.step();
    try std.testing.expectEqualStrings("Alice", query.columnText(0).?);
    try std.testing.expectEqual(@as(i64, 30), query.columnInt(1));
    try std.testing.expectApproxEqAbs(@as(f64, 95.5), query.columnFloat(2), 0.01);

    _ = try query.step();
    try std.testing.expectEqualStrings("Bob", query.columnText(0).?);
}

test "batch bind with optional null" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE test (a TEXT, b INTEGER)");

    var stmt = try db.prepare("INSERT INTO test VALUES (?, ?)");
    defer stmt.finalize();

    // Bind with optional null
    const optional_text: ?[]const u8 = null;
    try stmt.bindAll(.{ optional_text, @as(i64, 42) });
    _ = try stmt.step();

    var query = try db.prepare("SELECT a, b FROM test");
    defer query.finalize();

    _ = try query.step();
    try std.testing.expect(query.columnIsNull(0));
    try std.testing.expectEqual(@as(i64, 42), query.columnInt(1));
}

test "row iterator" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE items (id INTEGER, name TEXT)");
    try db.exec("INSERT INTO items VALUES (1, 'Apple'), (2, 'Banana'), (3, 'Cherry')");

    var stmt = try db.prepare("SELECT id, name FROM items ORDER BY id");
    defer stmt.finalize();

    var iter = stmt.iterator();
    var count: i32 = 0;

    while (try iter.next()) |row| {
        count += 1;
        const id = row.int(0);
        const name = row.text(1).?;

        switch (count) {
            1 => {
                try std.testing.expectEqual(@as(i64, 1), id);
                try std.testing.expectEqualStrings("Apple", name);
            },
            2 => {
                try std.testing.expectEqual(@as(i64, 2), id);
                try std.testing.expectEqualStrings("Banana", name);
            },
            3 => {
                try std.testing.expectEqual(@as(i64, 3), id);
                try std.testing.expectEqualStrings("Cherry", name);
            },
            else => {},
        }
    }

    try std.testing.expectEqual(@as(i32, 3), count);
}

test "FTS5 basic search" {
    const allocator = std.testing.allocator;
    var db = try openMemory();
    defer db.close();

    var fts = Fts5.init(&db, allocator);

    // Create FTS5 table
    try fts.createSimpleTable("documents", &.{ "title", "body" });

    // Insert documents
    try db.exec("INSERT INTO documents VALUES ('Hello World', 'This is a test document')");
    try db.exec("INSERT INTO documents VALUES ('Zig Programming', 'Zig is a systems programming language')");
    try db.exec("INSERT INTO documents VALUES ('SQLite Guide', 'Learn about SQLite database')");

    // Search
    var stmt = try fts.search("documents", "programming", &.{ "title", "body" }, 10);
    defer stmt.finalize();

    var found = false;
    while (try stmt.step()) {
        const title = stmt.columnText(0).?;
        if (std.mem.eql(u8, title, "Zig Programming")) {
            found = true;
        }
    }
    try std.testing.expect(found);
}

test "FTS5 highlight" {
    const allocator = std.testing.allocator;
    var db = try openMemory();
    defer db.close();

    var fts = Fts5.init(&db, allocator);
    try fts.createSimpleTable("docs", &.{"content"});

    try db.exec("INSERT INTO docs VALUES ('The quick brown fox jumps over the lazy dog')");

    var stmt = try fts.searchWithHighlight("docs", "quick", 0, "<b>", "</b>", 10);
    defer stmt.finalize();

    if (try stmt.step()) {
        const highlighted = stmt.columnText(0).?;
        try std.testing.expect(std.mem.indexOf(u8, highlighted, "<b>quick</b>") != null);
    }
}

test "JSON validate and extract" {
    const allocator = std.testing.allocator;
    var db = try openMemory();
    defer db.close();

    var js = Json.init(&db, allocator);

    // Validate JSON
    const valid = try js.isValid("{\"name\": \"Alice\", \"age\": 30}");
    try std.testing.expect(valid);

    const invalid = try js.isValid("{invalid json}");
    try std.testing.expect(!invalid);

    // Extract values
    const json_data = "{\"name\": \"Bob\", \"age\": 25, \"active\": true}";

    if (try js.extract(json_data, "$.name")) |name| {
        defer allocator.free(name);
        try std.testing.expectEqualStrings("Bob", name);
    }

    const age = try js.extractInt(json_data, "$.age");
    try std.testing.expectEqual(@as(?i64, 25), age);
}

test "JSON modify" {
    const allocator = std.testing.allocator;
    var db = try openMemory();
    defer db.close();

    var js = Json.init(&db, allocator);

    const original = "{\"name\": \"Alice\"}";

    // Set a new field
    const modified = try js.setInt(original, "$.age", 30);
    defer allocator.free(modified);

    // Verify the modification
    const age = try js.extractInt(modified, "$.age");
    try std.testing.expectEqual(@as(?i64, 30), age);
}

test "JSON array operations" {
    const allocator = std.testing.allocator;
    var db = try openMemory();
    defer db.close();

    var js = Json.init(&db, allocator);

    // Create array
    const arr = try js.createArray(&.{ "1", "2", "3" });
    defer allocator.free(arr);

    // Check length
    const len = try js.arrayLength(arr, null);
    try std.testing.expectEqual(@as(?i64, 3), len);
}

test "R-Tree basic spatial query" {
    const allocator = std.testing.allocator;
    var db = try openMemory();
    defer db.close();

    var rt = RTree.init(&db, allocator);

    // Create R-Tree table
    try rt.createSimpleTable2D("locations");

    // Insert some points
    try rt.insertPoint2D("locations", 1, 10.0, 20.0);
    try rt.insertPoint2D("locations", 2, 15.0, 25.0);
    try rt.insertPoint2D("locations", 3, 100.0, 200.0); // Far away

    // Query points near (12, 22) with radius 10
    const ids = try rt.getIntersectingIds2D("locations", BoundingBox2D{
        .min_x = 5.0,
        .max_x = 25.0,
        .min_y = 15.0,
        .max_y = 35.0,
    });
    defer rt.freeIds(ids);

    // Should find points 1 and 2, but not 3
    try std.testing.expectEqual(@as(usize, 2), ids.len);
}

test "R-Tree bounding box operations" {
    const box1 = BoundingBox2D{
        .min_x = 0.0,
        .max_x = 10.0,
        .min_y = 0.0,
        .max_y = 10.0,
    };

    const box2 = BoundingBox2D{
        .min_x = 5.0,
        .max_x = 15.0,
        .min_y = 5.0,
        .max_y = 15.0,
    };

    const box3 = BoundingBox2D{
        .min_x = 20.0,
        .max_x = 30.0,
        .min_y = 20.0,
        .max_y = 30.0,
    };

    // box1 and box2 intersect
    try std.testing.expect(box1.intersects(box2));

    // box1 and box3 do not intersect
    try std.testing.expect(!box1.intersects(box3));

    // Point containment
    try std.testing.expect(box1.containsPoint(5.0, 5.0));
    try std.testing.expect(!box1.containsPoint(15.0, 15.0));

    // Area
    try std.testing.expectApproxEqAbs(@as(f64, 100.0), box1.area(), 0.01);
}

test "R-Tree geographic distance" {
    // Test Haversine distance calculation
    const london = GeoCoord{ .latitude = 51.5074, .longitude = -0.1278 };
    const paris = GeoCoord{ .latitude = 48.8566, .longitude = 2.3522 };

    const distance = london.distanceKm(paris);

    // London to Paris is approximately 343 km
    try std.testing.expect(distance > 340.0 and distance < 350.0);
}

// ============================================================================
// Row-to-Struct Mapping Tests
// ============================================================================

test "row mapping to struct" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, score REAL, active INTEGER)");
    try db.exec("INSERT INTO users VALUES (1, 'Alice', 95.5, 1)");
    try db.exec("INSERT INTO users VALUES (2, 'Bob', 87.0, 0)");

    const User = struct {
        id: i64,
        name: []const u8,
        score: f64,
        active: bool,
    };

    var stmt = try db.prepare("SELECT id, name, score, active FROM users ORDER BY id");
    defer stmt.finalize();

    var iter = stmt.iterator();

    // First row
    const row1 = (try iter.next()) orelse return error.ExpectedRow;
    const user1 = row1.to(User);
    try std.testing.expectEqual(@as(i64, 1), user1.id);
    try std.testing.expectEqualStrings("Alice", user1.name);
    try std.testing.expectApproxEqAbs(@as(f64, 95.5), user1.score, 0.01);
    try std.testing.expect(user1.active);

    // Second row
    const row2 = (try iter.next()) orelse return error.ExpectedRow;
    const user2 = row2.to(User);
    try std.testing.expectEqual(@as(i64, 2), user2.id);
    try std.testing.expectEqualStrings("Bob", user2.name);
    try std.testing.expect(!user2.active);

    // No more rows
    try std.testing.expect((try iter.next()) == null);
}

test "row mapping with optional fields" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE items (id INTEGER, description TEXT)");
    try db.exec("INSERT INTO items VALUES (1, 'Has description')");
    try db.exec("INSERT INTO items VALUES (2, NULL)");

    const Item = struct {
        id: i64,
        description: ?[]const u8,
    };

    var stmt = try db.prepare("SELECT id, description FROM items ORDER BY id");
    defer stmt.finalize();

    var iter = stmt.iterator();

    // First row has description
    const row1 = (try iter.next()) orelse return error.ExpectedRow;
    const item1 = row1.to(Item);
    try std.testing.expectEqual(@as(i64, 1), item1.id);
    try std.testing.expect(item1.description != null);
    try std.testing.expectEqualStrings("Has description", item1.description.?);

    // Second row has NULL description
    const row2 = (try iter.next()) orelse return error.ExpectedRow;
    const item2 = row2.to(Item);
    try std.testing.expectEqual(@as(i64, 2), item2.id);
    try std.testing.expect(item2.description == null);
}

test "row mapping with allocation (toAlloc)" {
    const allocator = std.testing.allocator;
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE products (id INTEGER, name TEXT)");
    try db.exec("INSERT INTO products VALUES (1, 'Widget')");
    try db.exec("INSERT INTO products VALUES (2, 'Gadget')");

    const Product = struct {
        id: i64,
        name: []const u8,
    };

    var products: std.ArrayListUnmanaged(Product) = .empty;
    defer {
        for (products.items) |p| Row.freeStruct(Product, p, allocator);
        products.deinit(allocator);
    }

    var stmt = try db.prepare("SELECT id, name FROM products ORDER BY id");
    defer stmt.finalize();

    var iter = stmt.iterator();
    while (try iter.next()) |row| {
        const product = try row.toAlloc(Product, allocator);
        try products.append(allocator, product);
    }

    try std.testing.expectEqual(@as(usize, 2), products.items.len);
    try std.testing.expectEqualStrings("Widget", products.items[0].name);
    try std.testing.expectEqualStrings("Gadget", products.items[1].name);
}

// ============================================================================
// Serialize/Deserialize Tests
// ============================================================================

test "serialize and deserialize database" {
    const allocator = std.testing.allocator;

    // Create and populate a database
    var db1 = try openMemory();
    defer db1.close();

    try db1.exec("CREATE TABLE data (id INTEGER PRIMARY KEY, value TEXT)");
    try db1.exec("INSERT INTO data VALUES (1, 'hello')");
    try db1.exec("INSERT INTO data VALUES (2, 'world')");

    // Serialize to bytes
    const bytes = try serialize.toBytes(&db1, allocator, "main");
    defer allocator.free(bytes);

    try std.testing.expect(bytes.len > 0);

    // Deserialize into new database
    var db2 = try serialize.fromBytes(bytes, ":memory:");
    defer db2.close();

    // Verify data was preserved
    var stmt = try db2.prepare("SELECT value FROM data WHERE id = 2");
    defer stmt.finalize();

    if (try stmt.step()) {
        const value = stmt.columnText(0) orelse "";
        try std.testing.expectEqualStrings("world", value);
    } else {
        return error.ExpectedRow;
    }
}

test "serialize clone to memory" {
    const allocator = std.testing.allocator;

    var db1 = try openMemory();
    defer db1.close();

    try db1.exec("CREATE TABLE test (x INTEGER)");
    try db1.exec("INSERT INTO test VALUES (42)");

    // Clone the database
    var db2 = try serialize.cloneToMemory(&db1, allocator);
    defer db2.close();

    // Modify original - clone should be unaffected
    try db1.exec("UPDATE test SET x = 100");

    // Check clone still has original value
    var stmt = try db2.prepare("SELECT x FROM test");
    defer stmt.finalize();

    if (try stmt.step()) {
        try std.testing.expectEqual(@as(i64, 42), stmt.columnInt(0));
    } else {
        return error.ExpectedRow;
    }
}

// ============================================================================
// VACUUM INTO Tests
// ============================================================================

test "vacuum into file" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, data TEXT)");
    try db.exec("INSERT INTO test VALUES (1, 'test data')");

    // Create a temp file path
    const tmp_path = "/tmp/zcatsql_vacuum_test.db";

    // Clean up any existing file
    std.fs.cwd().deleteFile(tmp_path) catch {};

    // VACUUM INTO the file
    try db.vacuumInto(tmp_path);

    // Verify the file was created and contains data
    var db2 = try Database.open(tmp_path);
    defer db2.close();

    var stmt = try db2.prepare("SELECT data FROM test WHERE id = 1");
    defer stmt.finalize();

    if (try stmt.step()) {
        const data = stmt.columnText(0) orelse "";
        try std.testing.expectEqualStrings("test data", data);
    } else {
        return error.ExpectedRow;
    }

    // Clean up
    std.fs.cwd().deleteFile(tmp_path) catch {};
}

test "vacuum in place" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE test (x INTEGER)");
    try db.exec("INSERT INTO test VALUES (1)");
    try db.exec("INSERT INTO test VALUES (2)");
    try db.exec("DELETE FROM test WHERE x = 1");

    // Vacuum should succeed
    try db.vacuum();

    // Data should still be accessible
    var stmt = try db.prepare("SELECT COUNT(*) FROM test");
    defer stmt.finalize();

    if (try stmt.step()) {
        try std.testing.expectEqual(@as(i64, 1), stmt.columnInt(0));
    }
}

// ============================================================================
// Session Extension Tests (basic functionality)
// ============================================================================

test "session basic changeset" {
    const allocator = std.testing.allocator;

    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)");

    // Create session and attach table
    var sess = try Session.init(&db, "main");
    defer sess.deinit();

    try sess.attach("users");

    // Session should be enabled by default
    try std.testing.expect(sess.isEnabled());

    // Session should start empty
    try std.testing.expect(sess.isEmpty());

    // Make some changes
    try db.exec("INSERT INTO users VALUES (1, 'Alice')");
    try db.exec("INSERT INTO users VALUES (2, 'Bob')");

    // Session should no longer be empty
    try std.testing.expect(!sess.isEmpty());

    // Generate changeset
    const changeset = try sess.changeset(allocator);
    defer allocator.free(changeset);

    // Changeset should have content
    try std.testing.expect(changeset.len > 0);
}

test "session enable/disable" {
    var db = try openMemory();
    defer db.close();

    try db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY)");

    var sess = try Session.init(&db, "main");
    defer sess.deinit();

    try sess.attach("test");

    // Session should be enabled by default
    try std.testing.expect(sess.isEnabled());

    // Disable session - setEnabled returns the FINAL state (not previous)
    const is_now_disabled = sess.setEnabled(false);
    try std.testing.expect(!is_now_disabled); // Now disabled
    try std.testing.expect(!sess.isEnabled());

    // Changes while disabled won't be tracked
    try db.exec("INSERT INTO test VALUES (1)");
    try std.testing.expect(sess.isEmpty());

    // Re-enable
    const is_now_enabled = sess.setEnabled(true);
    try std.testing.expect(is_now_enabled); // Now enabled
    try std.testing.expect(sess.isEnabled());

    // Now changes are tracked
    try db.exec("INSERT INTO test VALUES (2)");
    try std.testing.expect(!sess.isEmpty());
}

// Include audit module tests
test {
    _ = audit;
}
