#include "auth/auth_store.h"
#include "auth/auth_crypto.h"
#include "deita/deita.h"

#include <assert.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

/* ------------------------------------------------------------------ */
/* Helpers                                                              */
/* ------------------------------------------------------------------ */

static char g_hash_buf[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE];

static const char *get_test_hash(void)
{
  if (g_hash_buf[0] == '\0')
  {
    Auth_Crypto_Result r = Auth_Crypto_Password_Hash(
        "hunter2", g_hash_buf, sizeof(g_hash_buf));
    assert(r == AUTH_CRYPTO_OK);
  }
  return g_hash_buf;
}

static void make_test_hash(
    const char *password,
    char         output[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE])
{
  assert(Auth_Crypto_Password_Hash(
      password, output, AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE) ==
      AUTH_CRYPTO_OK);
}

/* Simple UUID generator for the test, matching the pattern in
   conversation_store.c (no dependency on auth_store internals). */
static boolean test__make_uuid(char output[37])
{
  uint8   bytes[16];
  int     fd     = open("/dev/urandom", O_RDONLY);
  size_t  offset = 0;
  if (fd < 0)
    return FALSE;
  while (offset < sizeof(bytes))
  {
    ssize_t n = read(fd, bytes + offset, sizeof(bytes) - offset);
    if (n <= 0) { close(fd); return FALSE; }
    offset += (size_t)n;
  }
  close(fd);
  bytes[6] = (uint8)((bytes[6] & 0x0f) | 0x40);
  bytes[8] = (uint8)((bytes[8] & 0x3f) | 0x80);
  snprintf(output, 37,
      "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
      bytes[0],bytes[1],bytes[2],bytes[3],bytes[4],bytes[5],
      bytes[6],bytes[7],bytes[8],bytes[9],bytes[10],bytes[11],
      bytes[12],bytes[13],bytes[14],bytes[15]);
  return TRUE;
}

/* ------------------------------------------------------------------ */
/* 1. Username normalization and rejection                              */
/* ------------------------------------------------------------------ */

static void test_username_normalization(void)
{
  char out[AUTH_STORE_USERNAME_MAX + 1];

  assert(Auth_Store_Normalize_Username("  JohnDoe  ", out, sizeof(out)));
  assert(strcmp(out, "johndoe") == 0);

  assert(Auth_Store_Normalize_Username("Alice", out, sizeof(out)));
  assert(strcmp(out, "alice") == 0);

  assert(Auth_Store_Normalize_Username("june_bot-2.0", out, sizeof(out)));
  assert(strcmp(out, "june_bot-2.0") == 0);

  assert(Auth_Store_Normalize_Username("abc", out, sizeof(out)));

  /* Exactly 32 chars */
  assert(Auth_Store_Normalize_Username(
      "abcdefghijklmnopqrstuvwxyz123456", out, sizeof(out)));

  /* Too short after trimming */
  assert(!Auth_Store_Normalize_Username("ab", out, sizeof(out)));
  assert(!Auth_Store_Normalize_Username("  z  ", out, sizeof(out)));

  /* Too long (33 chars) */
  assert(!Auth_Store_Normalize_Username(
      "abcdefghijklmnopqrstuvwxyz1234567", out, sizeof(out)));

  /* Invalid characters */
  assert(!Auth_Store_Normalize_Username("hello world", out, sizeof(out)));
  assert(!Auth_Store_Normalize_Username("invalid!", out, sizeof(out)));
  assert(!Auth_Store_Normalize_Username("utf8\xc3\xa9", out, sizeof(out)));

  /* Buffer too small */
  char tiny[3];
  assert(!Auth_Store_Normalize_Username("abc", tiny, sizeof(tiny)));

  /* Validate pre-normalized */
  assert( Auth_Store_Validate_Username("johndoe"));
  assert( Auth_Store_Validate_Username("abc"));
  assert( Auth_Store_Validate_Username("june_bot-2.0"));
  assert(!Auth_Store_Validate_Username("ab"));
  assert(!Auth_Store_Validate_Username("Hello"));   /* uppercase */
  assert(!Auth_Store_Validate_Username("bad char!"));
  assert(!Auth_Store_Validate_Username(NULL));

  puts("test_username_normalization: PASS");
}

/* ------------------------------------------------------------------ */
/* 2. Migration idempotency / reopen                                    */
/* ------------------------------------------------------------------ */

static void test_migrations_idempotent(const char *db_path)
{
  Auth_Store *p = Auth_Store_Create(db_path);
  assert(p);
  Auth_Store_Destroy(p);

  /* Reopen: migrations must be no-ops. */
  p = Auth_Store_Create(db_path);
  assert(p);
  Auth_Store_Destroy(p);

  /* Verify all expected tables exist via a second connection. */
  Dowa_Arena       *p_arena = Dowa_Arena_Create(4096);
  assert(p_arena);
  Deita_Connection *p_conn = Deita_Connection_Create(
      DEITA_DATABASE_TYPE_SQLITE3, db_path);
  assert(p_conn);

  static const char *expected[] = {
    "admin_audit_log", "auth_schema_migrations", "auth_sessions",
    "guest_identities", "guest_usage", "guest_usage_reservations", "users",
  };
  size_t found = 0;

  Deita_Result_Set *p_result = Deita_Query_Execute(
      p_conn,
      "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name",
      p_arena);
  assert(p_result);
  while (Deita_Result_Set_Next(p_result))
  {
    const char *name = Deita_Result_Set_Get_Text(p_result, 0);
    for (size_t i = 0; i < sizeof(expected)/sizeof(expected[0]); i++)
      if (name && strcmp(name, expected[i]) == 0) { found++; break; }
  }
  Deita_Result_Set_Free(p_result);
  Deita_Connection_Close(p_conn);
  Dowa_Arena_Free(p_arena);

  assert(found == sizeof(expected)/sizeof(expected[0]));
  puts("test_migrations_idempotent: PASS");
}

/* ------------------------------------------------------------------ */
/* 3. Bootstrap admin idempotency                                       */
/* ------------------------------------------------------------------ */

static void test_bootstrap_admin(Auth_Store *p_store)
{
  char id1[37], id2[37];
  Auth_Store_Bootstrap_Result br;

  assert(Auth_Store_Bootstrap_Admin(
      p_store, "Admin", get_test_hash(), &br, id1) == AUTH_STORE_OK);
  assert(br == AUTH_STORE_BOOTSTRAP_CREATED);
  assert(strlen(id1) == 36);

  /* Second call with a different username: must not overwrite existing admin. */
  assert(Auth_Store_Bootstrap_Admin(
      p_store, "OtherAdmin", get_test_hash(), &br, id2) == AUTH_STORE_OK);
  assert(br == AUTH_STORE_BOOTSTRAP_ALREADY_PRESENT);
  assert(strcmp(id1, id2) == 0);

  /* Exactly one admin must exist. */
  Auth_User_Record *records = NULL;
  Dowa_Arena *p_arena = Dowa_Arena_Create(64 * 1024);
  assert(p_arena);
  assert(Auth_Store_List_Users(p_store, &records, p_arena) == AUTH_STORE_OK);
  size_t admin_count = 0;
  for (size_t i = 0; i < Dowa_Array_Length(records); i++)
    if (strcmp(records[i].role, "admin") == 0)
      admin_count++;
  assert(admin_count == 1);
  Dowa_Arena_Free(p_arena);

  puts("test_bootstrap_admin: PASS");
}

/* ------------------------------------------------------------------ */
/* 4. Last-admin protection (run while only one admin exists)          */
/* ------------------------------------------------------------------ */

static void test_last_admin_protection(Auth_Store *p_store)
{
  /* At this point only the bootstrap admin ("Admin"/"admin") exists. */
  Auth_User_Auth_Record ar;
  assert(Auth_Store_Find_User_By_Username(
      p_store, "admin", &ar) == AUTH_STORE_OK);
  const char *admin_id = ar.user.id;

  /* Cannot disable the last active admin. */
  assert(Auth_Store_Update_User_Status(
      p_store, admin_id, "disabled", NULL) == AUTH_STORE_LAST_ADMIN);

  /* Cannot demote the last active admin. */
  assert(Auth_Store_Update_User_Role(
      p_store, admin_id, "member", NULL) == AUTH_STORE_LAST_ADMIN);
  assert(Auth_Store_Disable_User_And_Revoke_Sessions(
      p_store, admin_id, NULL) == AUTH_STORE_LAST_ADMIN);
  assert(Auth_Store_Update_Role_And_Revoke_Sessions(
      p_store, admin_id, "member", NULL) == AUTH_STORE_LAST_ADMIN);

  /* Add a second active admin — operations on the first should now succeed. */
  char id2[37];
  assert(Auth_Store_Create_User(
      p_store, "SecondAdmin", get_test_hash(), "admin", FALSE, id2) ==
      AUTH_STORE_OK);

  /* Now demotion of the first admin is allowed (two active admins). */
  assert(Auth_Store_Update_User_Role(
      p_store, admin_id, "member", id2) == AUTH_STORE_OK);

  /* Re-promote so remaining tests can rely on admin being an admin. */
  assert(Auth_Store_Update_User_Role(
      p_store, admin_id, "admin", id2) == AUTH_STORE_OK);

  puts("test_last_admin_protection: PASS");
}

/* ------------------------------------------------------------------ */
/* 5. Username uniqueness                                               */
/* ------------------------------------------------------------------ */

static void test_username_uniqueness(Auth_Store *p_store)
{
  char id[37];
  assert(Auth_Store_Create_User(
      p_store, "UniqueUser", get_test_hash(), "member", FALSE, id) ==
      AUTH_STORE_OK);

  char id2[37];
  assert(Auth_Store_Create_User(
      p_store, "uniqueuser", get_test_hash(), "member", FALSE, id2) ==
      AUTH_STORE_CONFLICT);

  puts("test_username_uniqueness: PASS");
}

/* ------------------------------------------------------------------ */
/* 6. User lookup                                                       */
/* ------------------------------------------------------------------ */

static void test_user_lookup(Auth_Store *p_store)
{
  char id[37];
  assert(Auth_Store_Create_User(
      p_store, "LookupUser", get_test_hash(), "member", FALSE, id) ==
      AUTH_STORE_OK);

  Auth_User_Record r;
  assert(Auth_Store_Get_User(p_store, id, &r) == AUTH_STORE_OK);
  assert(strcmp(r.id, id) == 0);
  assert(strcmp(r.username, "LookupUser") == 0);
  assert(strcmp(r.normalized_username, "lookupuser") == 0);
  assert(strcmp(r.role, "member") == 0);
  assert(strcmp(r.status, "active") == 0);
  assert(r.must_change_password == FALSE);

  /* Not found */
  assert(Auth_Store_Get_User(
      p_store, "00000000-0000-0000-0000-000000000000", &r) ==
      AUTH_STORE_NOT_FOUND);

  /* Case-insensitive find by username */
  Auth_User_Auth_Record auth_r;
  assert(Auth_Store_Find_User_By_Username(
      p_store, "  LOOKUPUSER  ", &auth_r) == AUTH_STORE_OK);
  assert(strcmp(auth_r.user.id, id) == 0);
  assert(strncmp(auth_r.password_hash, "zenbu-scrypt$", 13) == 0);

  assert(Auth_Store_Find_User_By_Username(
      p_store, "nobody", &auth_r) == AUTH_STORE_NOT_FOUND);

  puts("test_user_lookup: PASS");
}

/* ------------------------------------------------------------------ */
/* 7. Forced password flag                                              */
/* ------------------------------------------------------------------ */

static void test_forced_password_flag(Auth_Store *p_store)
{
  char id[37];
  assert(Auth_Store_Create_User(
      p_store, "ForcedPwUser", get_test_hash(), "member", TRUE, id) ==
      AUTH_STORE_OK);

  Auth_User_Record r;
  assert(Auth_Store_Get_User(p_store, id, &r) == AUTH_STORE_OK);
  assert(r.must_change_password == TRUE);

  assert(Auth_Store_Set_Must_Change_Password(
      p_store, id, FALSE, NULL) == AUTH_STORE_OK);
  assert(Auth_Store_Get_User(p_store, id, &r) == AUTH_STORE_OK);
  assert(r.must_change_password == FALSE);

  assert(Auth_Store_Set_Must_Change_Password(
      p_store, id, TRUE, NULL) == AUTH_STORE_OK);
  assert(Auth_Store_Get_User(p_store, id, &r) == AUTH_STORE_OK);
  assert(r.must_change_password == TRUE);

  /* Updating password clears the flag. */
  assert(Auth_Store_Update_Password(
      p_store, id, get_test_hash(), FALSE, NULL) == AUTH_STORE_OK);
  assert(Auth_Store_Get_User(p_store, id, &r) == AUTH_STORE_OK);
  assert(r.must_change_password == FALSE);

  puts("test_forced_password_flag: PASS");
}

/* ------------------------------------------------------------------ */
/* 8. Session create / resolve / touch / revoke                        */
/* ------------------------------------------------------------------ */

static void test_session_lifecycle(Auth_Store *p_store)
{
  char user_id[37];
  assert(Auth_Store_Create_User(
      p_store, "SessUser", get_test_hash(), "member", FALSE, user_id) ==
      AUTH_STORE_OK);

  const char *tok  = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
  const char *csrf = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
  int64 now      = 1700000000LL;
  int64 idle_ttl = 3600;
  int64 abs_ttl  = 86400;

  Auth_Session_Record sess;
  assert(Auth_Store_Create_Session(
      p_store, user_id, tok, csrf, idle_ttl, abs_ttl, now, &sess) ==
      AUTH_STORE_OK);
  assert(strcmp(sess.user_id, user_id) == 0);
  assert(sess.created_at          == now);
  assert(sess.idle_expires_at     == now + idle_ttl);
  assert(sess.absolute_expires_at == now + abs_ttl);
  assert(sess.password_changed_at_snapshot == 0);

  Auth_Session_Record fs;
  Auth_User_Record    fu;
  int64 check = now + 60;

  assert(Auth_Store_Find_Session(
      p_store, tok, check, &fs, &fu) == AUTH_STORE_OK);
  assert(strcmp(fu.id, user_id) == 0);

  /* Touch extends idle expiry. */
  assert(Auth_Store_Touch_Session(
      p_store, tok, check, idle_ttl) == AUTH_STORE_OK);
  assert(Auth_Store_Find_Session(
      p_store, tok, check, &fs, &fu) == AUTH_STORE_OK);
  assert(fs.idle_expires_at == check + idle_ttl);

  /* Revoke. */
  assert(Auth_Store_Revoke_Session(p_store, tok) == AUTH_STORE_OK);
  assert(Auth_Store_Find_Session(
      p_store, tok, check, &fs, &fu) == AUTH_STORE_REVOKED);

  /* Unknown digest. */
  const char *unk = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";
  assert(Auth_Store_Find_Session(
      p_store, unk, check, &fs, &fu) == AUTH_STORE_NOT_FOUND);

  puts("test_session_lifecycle: PASS");
}

/* ------------------------------------------------------------------ */
/* 9. Stale password snapshot                                           */
/* ------------------------------------------------------------------ */

static void test_stale_password_snapshot(Auth_Store *p_store)
{
  char user_id[37];
  assert(Auth_Store_Create_User(
      p_store, "StaleUser", get_test_hash(), "member", FALSE, user_id) ==
      AUTH_STORE_OK);

  const char *tok  = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd";
  const char *csrf = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
  int64 now = 1700100000LL;

  Auth_Session_Record sess;
  assert(Auth_Store_Create_Session(
      p_store, user_id, tok, csrf, 3600, 86400, now, &sess) == AUTH_STORE_OK);
  assert(sess.password_changed_at_snapshot == 0);

  Auth_Session_Record fs;
  Auth_User_Record    fu;
  assert(Auth_Store_Find_Session(
      p_store, tok, now + 10, &fs, &fu) == AUTH_STORE_OK);

  assert(Auth_Store_Update_Password(
      p_store, user_id, get_test_hash(), FALSE, NULL) == AUTH_STORE_OK);

  assert(Auth_Store_Find_Session(
      p_store, tok, now + 20, &fs, &fu) == AUTH_STORE_STALE_PASSWORD);

  puts("test_stale_password_snapshot: PASS");
}

/* ------------------------------------------------------------------ */
/* 10. Disabled user blocks session resolution and creation             */
/* ------------------------------------------------------------------ */

static void test_disabled_user(Auth_Store *p_store)
{
  char user_id[37];
  assert(Auth_Store_Create_User(
      p_store, "DisabledUser", get_test_hash(), "member", FALSE, user_id) ==
      AUTH_STORE_OK);

  const char *tok  = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
  const char *csrf = "1111111111111111111111111111111111111111111111111111111111111111";
  int64 now = 1700200000LL;

  Auth_Session_Record sess;
  assert(Auth_Store_Create_Session(
      p_store, user_id, tok, csrf, 3600, 86400, now, &sess) == AUTH_STORE_OK);

  Auth_Session_Record fs;
  Auth_User_Record    fu;
  assert(Auth_Store_Find_Session(
      p_store, tok, now + 10, &fs, &fu) == AUTH_STORE_OK);

  /* Disable the member user (no last-admin protection applies). */
  assert(Auth_Store_Update_User_Status(
      p_store, user_id, "disabled", NULL) == AUTH_STORE_OK);

  assert(Auth_Store_Find_Session(
      p_store, tok, now + 20, &fs, &fu) == AUTH_STORE_USER_DISABLED);

  /* New session for a disabled user must fail. */
  const char *tok2  = "2222222222222222222222222222222222222222222222222222222222222222";
  const char *csrf2 = "3333333333333333333333333333333333333333333333333333333333333333";
  assert(Auth_Store_Create_Session(
      p_store, user_id, tok2, csrf2, 3600, 86400, now + 30, &sess) ==
      AUTH_STORE_USER_DISABLED);

  /* Re-enable. */
  assert(Auth_Store_Update_User_Status(
      p_store, user_id, "active", NULL) == AUTH_STORE_OK);
  assert(Auth_Store_Find_Session(
      p_store, tok, now + 30, &fs, &fu) == AUTH_STORE_OK);

  puts("test_disabled_user: PASS");
}

/* ------------------------------------------------------------------ */
/* 11. Password update revokes other sessions atomically               */
/* ------------------------------------------------------------------ */

static void test_password_update_revokes_others(Auth_Store *p_store)
{
  char user_id[37];
  assert(Auth_Store_Create_User(
      p_store, "RevokeOthers", get_test_hash(), "member", FALSE, user_id) ==
      AUTH_STORE_OK);

  int64 now = 1700300000LL;
  const char *tok_keep = "4444444444444444444444444444444444444444444444444444444444444444";
  const char *tok_rev1 = "5555555555555555555555555555555555555555555555555555555555555555";
  const char *tok_rev2 = "6666666666666666666666666666666666666666666666666666666666666666";
  const char *csrf_k   = "aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111";
  const char *csrf_1   = "bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222";
  const char *csrf_2   = "cccc3333cccc3333cccc3333cccc3333cccc3333cccc3333cccc3333cccc3333";

  Auth_Session_Record sess;
  assert(Auth_Store_Create_Session(
      p_store, user_id, tok_keep, csrf_k, 3600, 86400, now, &sess) ==
      AUTH_STORE_OK);
  assert(Auth_Store_Create_Session(
      p_store, user_id, tok_rev1, csrf_1, 3600, 86400, now, &sess) ==
      AUTH_STORE_OK);
  assert(Auth_Store_Create_Session(
      p_store, user_id, tok_rev2, csrf_2, 3600, 86400, now, &sess) ==
      AUTH_STORE_OK);

  /* Update password: keep tok_keep, revoke all others. */
  assert(Auth_Store_Update_Password(
      p_store, user_id, get_test_hash(), TRUE, tok_keep) == AUTH_STORE_OK);

  int64 check = now + 60;
  Auth_Session_Record fs;
  Auth_User_Record    fu;

  /* tok_keep is not revoked but snapshot is stale because password changed. */
  assert(Auth_Store_Find_Session(
      p_store, tok_keep, check, &fs, &fu) == AUTH_STORE_STALE_PASSWORD);

  assert(Auth_Store_Find_Session(
      p_store, tok_rev1, check, &fs, &fu) == AUTH_STORE_REVOKED);
  assert(Auth_Store_Find_Session(
      p_store, tok_rev2, check, &fs, &fu) == AUTH_STORE_REVOKED);

  puts("test_password_update_revokes_others: PASS");
}

/* ------------------------------------------------------------------ */
/* 12. Revoke_All_Sessions                                              */
/* ------------------------------------------------------------------ */

static void test_revoke_all_sessions(Auth_Store *p_store)
{
  char user_id[37];
  assert(Auth_Store_Create_User(
      p_store, "RevokeAll", get_test_hash(), "member", FALSE, user_id) ==
      AUTH_STORE_OK);

  int64 now = 1700400000LL;
  const char *t1 = "7777777777777777777777777777777777777777777777777777777777777777";
  const char *t2 = "8888888888888888888888888888888888888888888888888888888888888888";
  const char *t3 = "9999999999999999999999999999999999999999999999999999999999999999";
  const char *c1 = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2";
  const char *c2 = "b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3";
  const char *c3 = "c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4";

  Auth_Session_Record sess;
  assert(Auth_Store_Create_Session(
      p_store, user_id, t1, c1, 3600, 86400, now, &sess) == AUTH_STORE_OK);
  assert(Auth_Store_Create_Session(
      p_store, user_id, t2, c2, 3600, 86400, now, &sess) == AUTH_STORE_OK);
  assert(Auth_Store_Create_Session(
      p_store, user_id, t3, c3, 3600, 86400, now, &sess) == AUTH_STORE_OK);

  /* Revoke all except t2. */
  assert(Auth_Store_Revoke_All_Sessions(p_store, user_id, t2) == AUTH_STORE_OK);

  int64 check = now + 60;
  Auth_Session_Record fs;
  Auth_User_Record    fu;
  assert(Auth_Store_Find_Session(p_store, t1, check, &fs, &fu) == AUTH_STORE_REVOKED);
  assert(Auth_Store_Find_Session(p_store, t2, check, &fs, &fu) == AUTH_STORE_OK);
  assert(Auth_Store_Find_Session(p_store, t3, check, &fs, &fu) == AUTH_STORE_REVOKED);

  puts("test_revoke_all_sessions: PASS");
}

/* ------------------------------------------------------------------ */
/* 13. Expired session                                                  */
/* ------------------------------------------------------------------ */

static void test_expired_session(Auth_Store *p_store)
{
  char user_id[37];
  assert(Auth_Store_Create_User(
      p_store, "ExpiredUser", get_test_hash(), "member", FALSE, user_id) ==
      AUTH_STORE_OK);

  const char *tok  = "abababababababababababababababababababababababababababababababab";
  const char *csrf = "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd";
  int64 now = 1700500000LL;

  Auth_Session_Record sess;
  assert(Auth_Store_Create_Session(
      p_store, user_id, tok, csrf, 100, 200, now, &sess) == AUTH_STORE_OK);

  Auth_Session_Record fs;
  Auth_User_Record    fu;

  assert(Auth_Store_Find_Session(
      p_store, tok, now + 50, &fs, &fu) == AUTH_STORE_OK);

  assert(Auth_Store_Find_Session(
      p_store, tok, now + 110, &fs, &fu) == AUTH_STORE_EXPIRED);

  assert(Auth_Store_Find_Session(
      p_store, tok, now + 201, &fs, &fu) == AUTH_STORE_EXPIRED);

  puts("test_expired_session: PASS");
}

/* ------------------------------------------------------------------ */
/* 14. Guest identity persistence and no raw IP storage                */
/* ------------------------------------------------------------------ */

static void test_guest_identity(Auth_Store *p_store, const char *db_path)
{
  uint8 secret[AUTH_CRYPTO_COOKIE_SECRET_MIN_BYTES];
  memset(secret, 0xab, sizeof(secret));
  const char *raw_ip = "203.0.113.42"; /* TEST-NET-3, never stored */

  char ip_digest[AUTH_CRYPTO_IP_BINDING_DIGEST_SIZE];
  assert(Auth_Crypto_IP_Binding_Digest(
      secret, sizeof(secret), raw_ip,
      ip_digest, sizeof(ip_digest)) == AUTH_CRYPTO_OK);

  char guest_id[37];
  assert(test__make_uuid(guest_id));
  int64 now = 1700600000LL;
  int64 exp = now + 86400;

  Auth_Guest_Identity_Record g;
  assert(Auth_Store_Upsert_Guest_Identity(
      p_store, guest_id, ip_digest, exp, &g) == AUTH_STORE_OK);
  assert(strcmp(g.id, guest_id) == 0);
  assert(g.expires_at == exp);

  /* Find within expiry. */
  Auth_Guest_Identity_Record g2;
  assert(Auth_Store_Find_Guest_Identity(
      p_store, guest_id, now + 60, &g2) == AUTH_STORE_OK);
  assert(strcmp(g2.id, guest_id) == 0);

  /* Find after expiry. */
  assert(Auth_Store_Find_Guest_Identity(
      p_store, guest_id, exp + 1, &g2) == AUTH_STORE_EXPIRED);

  /* Upsert refreshes expiry without losing the identity row. */
  int64 new_exp = exp + 86400;
  assert(Auth_Store_Upsert_Guest_Identity(
      p_store, guest_id, ip_digest, new_exp, &g) == AUTH_STORE_OK);
  assert(g.expires_at == new_exp);
  assert(Auth_Store_Find_Guest_Identity(
      p_store, guest_id, exp + 1, &g2) == AUTH_STORE_OK);

  /* Unknown guest. */
  assert(Auth_Store_Find_Guest_Identity(
      p_store, "00000000-0000-4000-8000-000000000000",
      now, &g2) == AUTH_STORE_NOT_FOUND);

  /*
   * Verify raw IP is NOT stored in the database: open a second connection
   * and inspect the ip_binding_digest column directly.
   */
  Dowa_Arena       *p_arena = Dowa_Arena_Create(4096);
  assert(p_arena);
  Deita_Connection *p_conn = Deita_Connection_Create(
      DEITA_DATABASE_TYPE_SQLITE3, db_path);
  assert(p_conn);

  const char       *sel_params[] = {guest_id};
  Deita_Result_Set *p_result = Deita_Query_Execute_Prepared(
      p_conn,
      "SELECT ip_binding_digest FROM guest_identities WHERE id = ?",
      1, sel_params, p_arena);
  assert(p_result);
  assert(Deita_Result_Set_Next(p_result));
  const char *stored = Deita_Result_Set_Get_Text(p_result, 0);
  assert(stored != NULL);
  assert(strcmp(stored, ip_digest) == 0);   /* HMAC digest is stored */
  assert(strstr(stored, raw_ip) == NULL);   /* raw IP is NOT stored  */
  Deita_Result_Set_Free(p_result);
  Deita_Connection_Close(p_conn);
  Dowa_Arena_Free(p_arena);

  puts("test_guest_identity: PASS");
}

/* ------------------------------------------------------------------ */
/* 16. Rotate_Session                                                   */
/* ------------------------------------------------------------------ */

static void test_rotate_session(Auth_Store *p_store)
{
  char user_id[37];
  assert(Auth_Store_Create_User(
      p_store, "RotateUser", get_test_hash(), "member", FALSE, user_id) ==
      AUTH_STORE_OK);

  int64 now = 1700400000LL;
  const char *old_tok = "e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1e1";
  const char *old_csrf = "f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2";
  const char *new_tok = "a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3a3";
  const char *new_csrf = "b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4";

  Auth_Session_Record sess;
  assert(Auth_Store_Create_Session(
      p_store, user_id, old_tok, old_csrf, 3600, 86400, now, &sess) ==
      AUTH_STORE_OK);

  /* Rotate: new session created, old revoked atomically */
  Auth_Session_Record new_sess;
  assert(Auth_Store_Rotate_Session(
      p_store, user_id, old_tok, new_tok, new_csrf,
      3600, 86400, now + 10, &new_sess) == AUTH_STORE_OK);

  int64 check = now + 60;
  Auth_Session_Record fs;
  Auth_User_Record    fu;

  /* Old session must be revoked */
  assert(Auth_Store_Find_Session(p_store, old_tok, check, &fs, &fu) ==
         AUTH_STORE_REVOKED);

  /* New session must be valid */
  assert(Auth_Store_Find_Session(p_store, new_tok, check, &fs, &fu) ==
         AUTH_STORE_OK);
  assert(strcmp(fs.user_id, user_id) == 0);

  /* Rotate again with already-revoked old token must still succeed
   * (idempotent revocation) */
  const char *new_tok2 = "c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5c5";
  const char *new_csrf2 = "d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6d6";
  Auth_Session_Record sess2;
  assert(Auth_Store_Rotate_Session(
      p_store, user_id, old_tok, new_tok2, new_csrf2,
      3600, 86400, now + 20, &sess2) == AUTH_STORE_OK);
  assert(Auth_Store_Find_Session(p_store, new_tok2, check, &fs, &fu) ==
         AUTH_STORE_OK);

  puts("test_rotate_session: PASS");
}

/* ------------------------------------------------------------------ */
/* 17. Compare-and-swap session creation                                */
/* ------------------------------------------------------------------ */

static void test_create_session_cas(Auth_Store *p_store)
{
  char first_hash[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE];
  char second_hash[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE];
  char reset_hash[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE];
  make_test_hash("cas-first-password", first_hash);
  make_test_hash("cas-second-password", second_hash);
  make_test_hash("cas-reset-password", reset_hash);

  char user_id[37];
  assert(Auth_Store_Create_User(
      p_store, "CasSessionUser", first_hash, "member", FALSE, user_id) ==
      AUTH_STORE_OK);

  const char *token1 =
      "1010101010101010101010101010101010101010101010101010101010101010";
  const char *csrf1 =
      "2020202020202020202020202020202020202020202020202020202020202020";
  const char *token2 =
      "3030303030303030303030303030303030303030303030303030303030303030";
  const char *csrf2 =
      "4040404040404040404040404040404040404040404040404040404040404040";
  const char *token3 =
      "5050505050505050505050505050505050505050505050505050505050505050";
  const char *csrf3 =
      "6060606060606060606060606060606060606060606060606060606060606060";
  const char *token4 =
      "7070707070707070707070707070707070707070707070707070707070707070";
  const char *csrf4 =
      "8080808080808080808080808080808080808080808080808080808080808080";
  const char *disabled_token =
      "9090909090909090909090909090909090909090909090909090909090909090";
  const char *disabled_csrf =
      "a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0";
  int64 now = 1800000000LL;
  Auth_Session_Record session;

  assert(Auth_Store_Create_Session_CAS(
      p_store, user_id, first_hash, token1, csrf1,
      3600, 86400, now, &session) == AUTH_STORE_OK);

  assert(Auth_Store_Update_User_Status(
      p_store, user_id, "disabled", NULL) == AUTH_STORE_OK);
  assert(Auth_Store_Create_Session_CAS(
      p_store, user_id, first_hash, disabled_token, disabled_csrf,
      3600, 86400, now + 1, &session) == AUTH_STORE_USER_DISABLED);
  assert(Auth_Store_Update_User_Status(
      p_store, user_id, "active", NULL) == AUTH_STORE_OK);

  Auth_Session_Record found_session;
  Auth_User_Record found_user;
  assert(Auth_Store_Find_Session(
      p_store, disabled_token, now + 2, &found_session, &found_user) ==
      AUTH_STORE_NOT_FOUND);

  assert(Auth_Store_Update_Password(
      p_store, user_id, second_hash, FALSE, NULL) == AUTH_STORE_OK);
  assert(Auth_Store_Create_Session_CAS(
      p_store, user_id, first_hash, token2, csrf2,
      3600, 86400, now + 10, &session) == AUTH_STORE_STALE_PASSWORD);

  assert(Auth_Store_Find_Session(
      p_store, token2, now + 20, &found_session, &found_user) ==
      AUTH_STORE_NOT_FOUND);

  assert(Auth_Store_Admin_Reset_Password(
      p_store, user_id, reset_hash, NULL) == AUTH_STORE_OK);
  assert(Auth_Store_Create_Session_CAS(
      p_store, user_id, second_hash, token3, csrf3,
      3600, 86400, now + 20, &session) == AUTH_STORE_STALE_PASSWORD);
  assert(Auth_Store_Find_Session(
      p_store, token3, now + 30, &found_session, &found_user) ==
      AUTH_STORE_NOT_FOUND);

  assert(Auth_Store_Create_Session_CAS(
      p_store, user_id, reset_hash, token4, csrf4,
      3600, 86400, now + 30, &session) == AUTH_STORE_OK);
  assert(Auth_Store_Find_Session(
      p_store, token4, now + 40, &found_session, &found_user) ==
      AUTH_STORE_OK);

  memset(first_hash, 0, sizeof(first_hash));
  memset(second_hash, 0, sizeof(second_hash));
  memset(reset_hash, 0, sizeof(reset_hash));
  puts("test_create_session_cas: PASS");
}

/* ------------------------------------------------------------------ */
/* 18. Atomic self-service password change                              */
/* ------------------------------------------------------------------ */

static void test_self_change_password(Auth_Store *p_store)
{
  char old_hash[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE];
  char new_hash[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE];
  char unused_hash[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE];
  make_test_hash("self-change-old", old_hash);
  make_test_hash("self-change-new", new_hash);
  make_test_hash("self-change-unused", unused_hash);

  char user_id[37];
  assert(Auth_Store_Create_User(
      p_store, "SelfChangeUser", old_hash, "member", TRUE, user_id) ==
      AUTH_STORE_OK);

  const char *old_token1 =
      "1111222211112222111122221111222211112222111122221111222211112222";
  const char *old_csrf1 =
      "2222333322223333222233332222333322223333222233332222333322223333";
  const char *old_token2 =
      "3333444433334444333344443333444433334444333344443333444433334444";
  const char *old_csrf2 =
      "4444555544445555444455554444555544445555444455554444555544445555";
  const char *new_token =
      "5555666655556666555566665555666655556666555566665555666655556666";
  const char *new_csrf =
      "6666777766667777666677776666777766667777666677776666777766667777";
  const char *failed_token =
      "7777888877778888777788887777888877778888777788887777888877778888";
  const char *failed_csrf =
      "8888999988889999888899998888999988889999888899998888999988889999";
  int64 now = 1810000000LL;
  Auth_Session_Record session;

  assert(Auth_Store_Create_Session_CAS(
      p_store, user_id, old_hash, old_token1, old_csrf1,
      3600, 86400, now, &session) == AUTH_STORE_OK);
  assert(Auth_Store_Create_Session_CAS(
      p_store, user_id, old_hash, old_token2, old_csrf2,
      3600, 86400, now, &session) == AUTH_STORE_OK);

  assert(Auth_Store_Self_Change_Password(
      p_store, user_id, old_hash, new_hash, new_token, new_csrf,
      3600, 86400, now + 100, &session) == AUTH_STORE_OK);
  assert(session.password_changed_at_snapshot == now + 100);

  Auth_Session_Record found_session;
  Auth_User_Record found_user;
  assert(Auth_Store_Find_Session(
      p_store, old_token1, now + 101, &found_session, &found_user) ==
      AUTH_STORE_REVOKED);
  assert(Auth_Store_Find_Session(
      p_store, old_token2, now + 101, &found_session, &found_user) ==
      AUTH_STORE_REVOKED);
  assert(Auth_Store_Find_Session(
      p_store, new_token, now + 101, &found_session, &found_user) ==
      AUTH_STORE_OK);
  assert(found_user.must_change_password == FALSE);
  assert(found_user.password_changed_at == now + 100);

  Auth_User_Auth_Record auth_record;
  assert(Auth_Store_Find_User_By_Username(
      p_store, "SelfChangeUser", &auth_record) == AUTH_STORE_OK);
  assert(strcmp(auth_record.password_hash, new_hash) == 0);
  memset(&auth_record, 0, sizeof(auth_record));

  assert(Auth_Store_Self_Change_Password(
      p_store, user_id, old_hash, unused_hash, failed_token, failed_csrf,
      3600, 86400, now + 200, &session) == AUTH_STORE_STALE_PASSWORD);
  assert(Auth_Store_Find_Session(
      p_store, failed_token, now + 201, &found_session, &found_user) ==
      AUTH_STORE_NOT_FOUND);
  assert(Auth_Store_Find_Session(
      p_store, new_token, now + 201, &found_session, &found_user) ==
      AUTH_STORE_OK);
  assert(Auth_Store_Find_User_By_Username(
      p_store, "SelfChangeUser", &auth_record) == AUTH_STORE_OK);
  assert(strcmp(auth_record.password_hash, new_hash) == 0);

  memset(&auth_record, 0, sizeof(auth_record));
  memset(old_hash, 0, sizeof(old_hash));
  memset(new_hash, 0, sizeof(new_hash));
  memset(unused_hash, 0, sizeof(unused_hash));
  puts("test_self_change_password: PASS");
}

/* ------------------------------------------------------------------ */
/* 19. Audited and session-revoking admin operations                    */
/* ------------------------------------------------------------------ */

static void test_audited_admin_operations(
    Auth_Store *p_store,
    const char *db_path)
{
  char user_id[37];
  const char *actor_id = "00000000-0000-4000-8000-000000000019";
  assert(Auth_Store_Create_User_Audited(
      p_store, "AuditedUser", get_test_hash(), "member", FALSE,
      actor_id, user_id) == AUTH_STORE_OK);

  Dowa_Arena *p_arena = Dowa_Arena_Create(2048);
  assert(p_arena);
  Deita_Connection *p_conn = Deita_Connection_Create(
      DEITA_DATABASE_TYPE_SQLITE3, db_path);
  assert(p_conn);
  const char *audit_params[] = {user_id};
  Deita_Result_Set *p_result = Deita_Query_Execute_Prepared(
      p_conn,
      "SELECT actor_user_id, action, detail FROM admin_audit_log"
      "  WHERE target_user_id = ? ORDER BY id DESC LIMIT 1",
      1, audit_params, p_arena);
  assert(p_result);
  assert(Deita_Result_Set_Next(p_result));
  assert(strcmp(Deita_Result_Set_Get_Text(p_result, 0), actor_id) == 0);
  assert(strcmp(Deita_Result_Set_Get_Text(p_result, 1),
                "admin_user_created") == 0);
  assert(strcmp(Deita_Result_Set_Get_Text(p_result, 2), "member") == 0);
  assert(strstr(Deita_Result_Set_Get_Text(p_result, 2), "zenbu-scrypt") == NULL);
  Deita_Result_Set_Free(p_result);
  Deita_Connection_Close(p_conn);
  Dowa_Arena_Free(p_arena);

  const char *token1 =
      "9191919191919191919191919191919191919191919191919191919191919191";
  const char *csrf1 =
      "9292929292929292929292929292929292929292929292929292929292929292";
  const char *token2 =
      "9393939393939393939393939393939393939393939393939393939393939393";
  const char *csrf2 =
      "9494949494949494949494949494949494949494949494949494949494949494";
  const char *token3 =
      "9595959595959595959595959595959595959595959595959595959595959595";
  const char *csrf3 =
      "9696969696969696969696969696969696969696969696969696969696969696";
  int64 now = 1820000000LL;
  Auth_Session_Record session;
  Auth_Session_Record found_session;
  Auth_User_Record found_user;

  assert(Auth_Store_Create_Session(
      p_store, user_id, token1, csrf1, 3600, 86400, now, &session) ==
      AUTH_STORE_OK);
  assert(Auth_Store_Enable_User(p_store, user_id, actor_id) == AUTH_STORE_OK);
  assert(Auth_Store_Find_Session(
      p_store, token1, now + 1, &found_session, &found_user) == AUTH_STORE_OK);

  assert(Auth_Store_Disable_User_And_Revoke_Sessions(
      p_store, user_id, actor_id) == AUTH_STORE_OK);
  assert(Auth_Store_Find_Session(
      p_store, token1, now + 2, &found_session, &found_user) ==
      AUTH_STORE_REVOKED);
  assert(Auth_Store_Enable_User(p_store, user_id, actor_id) == AUTH_STORE_OK);
  assert(Auth_Store_Find_Session(
      p_store, token1, now + 3, &found_session, &found_user) ==
      AUTH_STORE_REVOKED);

  assert(Auth_Store_Create_Session(
      p_store, user_id, token2, csrf2, 3600, 86400, now + 4, &session) ==
      AUTH_STORE_OK);
  assert(Auth_Store_Update_Role_And_Revoke_Sessions(
      p_store, user_id, "admin", actor_id) == AUTH_STORE_OK);
  assert(Auth_Store_Find_Session(
      p_store, token2, now + 5, &found_session, &found_user) ==
      AUTH_STORE_REVOKED);
  assert(Auth_Store_Get_User(p_store, user_id, &found_user) == AUTH_STORE_OK);
  assert(strcmp(found_user.role, "admin") == 0);

  assert(Auth_Store_Create_Session(
      p_store, user_id, token3, csrf3, 3600, 86400, now + 6, &session) ==
      AUTH_STORE_OK);
  assert(Auth_Store_Revoke_All_Sessions_Audited(
      p_store, user_id, NULL, actor_id) == AUTH_STORE_OK);
  assert(Auth_Store_Find_Session(
      p_store, token3, now + 7, &found_session, &found_user) ==
      AUTH_STORE_REVOKED);
  assert(Auth_Store_Enable_User(
      p_store, "00000000-0000-4000-8000-000000000000", actor_id) ==
      AUTH_STORE_NOT_FOUND);

  puts("test_audited_admin_operations: PASS");
}

/* ------------------------------------------------------------------ */
/* 20. Audit failures roll back transactional mutations                 */
/* ------------------------------------------------------------------ */

static void test_audit_failure_rollback(
    Auth_Store *p_store,
    const char *db_path)
{
  char reset_hash[AUTH_CRYPTO_PASSWORD_HASH_ENCODED_SIZE];
  make_test_hash("audit-reset-password", reset_hash);

  char user_id[37];
  assert(Auth_Store_Create_User(
      p_store, "AuditRollbackUser", get_test_hash(), "member", FALSE,
      user_id) == AUTH_STORE_OK);

  const char *token =
      "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1";
  const char *csrf =
      "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2";
  int64 now = 1830000000LL;
  Auth_Session_Record session;
  assert(Auth_Store_Create_Session(
      p_store, user_id, token, csrf, 3600, 86400, now, &session) ==
      AUTH_STORE_OK);

  Deita_Connection *p_conn = Deita_Connection_Create(
      DEITA_DATABASE_TYPE_SQLITE3, db_path);
  assert(p_conn);
  assert(Deita_Query_Execute_Update(
      p_conn, "UPDATE users SET role = 'member' WHERE role = 'admin'") >= 0);
  assert(Deita_Query_Execute_Update(
      p_conn,
      "CREATE TRIGGER fail_auth_audit BEFORE INSERT ON admin_audit_log"
      " BEGIN SELECT RAISE(ABORT, 'forced audit failure'); END") >= 0);
  Deita_Connection_Close(p_conn);

  assert(Auth_Store_Set_Must_Change_Password(
      p_store, user_id, TRUE, NULL) == AUTH_STORE_ERROR);
  Auth_User_Record user;
  assert(Auth_Store_Get_User(p_store, user_id, &user) == AUTH_STORE_OK);
  assert(user.must_change_password == FALSE);

  Auth_Session_Record found_session;
  Auth_User_Record found_user;
  assert(Auth_Store_Update_User_Status(
      p_store, user_id, "disabled", NULL) == AUTH_STORE_ERROR);
  assert(Auth_Store_Get_User(p_store, user_id, &user) == AUTH_STORE_OK);
  assert(strcmp(user.status, "active") == 0);
  assert(Auth_Store_Find_Session(
      p_store, token, now + 1, &found_session, &found_user) == AUTH_STORE_OK);

  assert(Auth_Store_Update_User_Role(
      p_store, user_id, "admin", NULL) == AUTH_STORE_ERROR);
  assert(Auth_Store_Get_User(p_store, user_id, &user) == AUTH_STORE_OK);
  assert(strcmp(user.role, "member") == 0);

  assert(Auth_Store_Disable_User_And_Revoke_Sessions(
      p_store, user_id, NULL) == AUTH_STORE_ERROR);
  assert(Auth_Store_Update_Role_And_Revoke_Sessions(
      p_store, user_id, "admin", NULL) == AUTH_STORE_ERROR);
  assert(Auth_Store_Revoke_All_Sessions_Audited(
      p_store, user_id, NULL, NULL) == AUTH_STORE_ERROR);
  assert(Auth_Store_Find_Session(
      p_store, token, now + 2, &found_session, &found_user) == AUTH_STORE_OK);

  assert(Auth_Store_Admin_Reset_Password(
      p_store, user_id, reset_hash, NULL) == AUTH_STORE_ERROR);
  Auth_User_Auth_Record auth_record;
  assert(Auth_Store_Find_User_By_Username(
      p_store, "AuditRollbackUser", &auth_record) == AUTH_STORE_OK);
  assert(strcmp(auth_record.password_hash, get_test_hash()) == 0);
  assert(auth_record.user.must_change_password == FALSE);
  memset(&auth_record, 0, sizeof(auth_record));
  assert(Auth_Store_Find_Session(
      p_store, token, now + 3, &found_session, &found_user) == AUTH_STORE_OK);

  Auth_Store_Bootstrap_Result bootstrap_result;
  char bootstrap_id[37];
  assert(Auth_Store_Bootstrap_Admin(
      p_store, "AuditBootstrapRollback", get_test_hash(),
      &bootstrap_result, bootstrap_id) == AUTH_STORE_ERROR);
  assert(Auth_Store_Find_User_By_Username(
      p_store, "AuditBootstrapRollback", &auth_record) == AUTH_STORE_NOT_FOUND);

  assert(Auth_Store_Insert_Audit_Log(
      p_store, NULL, "forced_failure", user_id, NULL) == AUTH_STORE_ERROR);

  char failed_id[37];
  assert(Auth_Store_Create_User_Audited(
      p_store, "AuditCreateRollback", get_test_hash(), "member", FALSE,
      NULL, failed_id) == AUTH_STORE_ERROR);
  assert(Auth_Store_Find_User_By_Username(
      p_store, "AuditCreateRollback", &auth_record) == AUTH_STORE_NOT_FOUND);

  p_conn = Deita_Connection_Create(DEITA_DATABASE_TYPE_SQLITE3, db_path);
  assert(p_conn);
  assert(Deita_Query_Execute_Update(
      p_conn, "DROP TRIGGER fail_auth_audit") >= 0);
  Deita_Connection_Close(p_conn);

  memset(reset_hash, 0, sizeof(reset_hash));
  puts("test_audit_failure_rollback: PASS");
}


/* ------------------------------------------------------------------ */
/* Guest quota tests                                                     */
/* ------------------------------------------------------------------ */

/* Create a guest identity row (required as FK parent). */
static void make_guest(Auth_Store *p_store, const char *guest_id)
{
  uint8 secret[AUTH_CRYPTO_COOKIE_SECRET_MIN_BYTES];
  memset(secret, 0xab, sizeof(secret));
  char ip_digest[AUTH_CRYPTO_IP_BINDING_DIGEST_SIZE];
  assert(Auth_Crypto_IP_Binding_Digest(
      secret, sizeof(secret), "10.0.0.1",
      ip_digest, sizeof(ip_digest)) == AUTH_CRYPTO_OK);
  Auth_Guest_Identity_Record g;
  assert(Auth_Store_Upsert_Guest_Identity(
      p_store, guest_id, ip_digest,
      2000000000LL, &g) == AUTH_STORE_OK);
}

static void test_guest_quota(Auth_Store *p_store)
{
  char guest_id[37];
  assert(test__make_uuid(guest_id));
  make_guest(p_store, guest_id);

  int64 window_start = 1700524800LL; /* UTC midnight 2023-11-21, used as window ID */
  int64 expires      = (int64)time(NULL) + 7200LL; /* 2 h from now — always future */
  const int64 turns_limit  = 3;
  const int64 tokens_limit = 1000;
  const int64 req_tokens   = 200;

  /* 1. Initial usage is zero. */
  {
    Auth_Store_Guest_Usage u;
    assert(Auth_Store_Guest_Get_Usage(
        p_store, guest_id, window_start, &u) == AUTH_STORE_OK);
    assert(u.turns_used == 0);
    assert(u.output_tokens_used == 0);
    assert(u.output_tokens_reserved == 0);
  }
  puts("  guest_quota/initial_usage: PASS");

  /* 2. Reserve 3 turns; fourth must be TURNS_EXHAUSTED. */
  {
    char req1[37], req2[37], req3[37];
    assert(test__make_uuid(req1));
    assert(test__make_uuid(req2));
    assert(test__make_uuid(req3));
    assert(Auth_Store_Guest_Reserve(
        p_store, guest_id, req1, window_start,
        req_tokens, turns_limit, tokens_limit,
        expires) == AUTH_STORE_GUEST_QUOTA_OK);
    assert(Auth_Store_Guest_Reserve(
        p_store, guest_id, req2, window_start,
        req_tokens, turns_limit, tokens_limit,
        expires) == AUTH_STORE_GUEST_QUOTA_OK);
    assert(Auth_Store_Guest_Reserve(
        p_store, guest_id, req3, window_start,
        req_tokens, turns_limit, tokens_limit,
        expires) == AUTH_STORE_GUEST_QUOTA_OK);

    /* Check state: 3 turns used, 600 tokens reserved. */
    Auth_Store_Guest_Usage u;
    assert(Auth_Store_Guest_Get_Usage(
        p_store, guest_id, window_start, &u) == AUTH_STORE_OK);
    assert(u.turns_used == 3);
    assert(u.output_tokens_used == 0);
    assert(u.output_tokens_reserved == req_tokens * 3);

    /* Fourth reservation: turns exhausted. */
    char req4[37];
    assert(test__make_uuid(req4));
    assert(Auth_Store_Guest_Reserve(
        p_store, guest_id, req4, window_start,
        req_tokens, turns_limit, tokens_limit,
        expires) == AUTH_STORE_GUEST_QUOTA_TURNS_EXHAUSTED);
    puts("  guest_quota/turn_exhaustion: PASS");

    /* Release all 3 reservations (keeps turn charge). */
    assert(Auth_Store_Guest_Release(p_store, req1) == AUTH_STORE_OK);
    assert(Auth_Store_Guest_Release(p_store, req2) == AUTH_STORE_OK);
    assert(Auth_Store_Guest_Release(p_store, req3) == AUTH_STORE_OK);

    /* After release: turns still charged, tokens freed. */
    assert(Auth_Store_Guest_Get_Usage(
        p_store, guest_id, window_start, &u) == AUTH_STORE_OK);
    assert(u.turns_used == 3);
    assert(u.output_tokens_reserved == 0);
    puts("  guest_quota/release_retains_turns: PASS");
  }

  /* 3. Token exhaustion on a fresh window. */
  {
    char guest2[37];
    assert(test__make_uuid(guest2));
    make_guest(p_store, guest2);
    int64 win2 = window_start + 86400LL;
    const int64 small_limit = 250;

    char r1[37], r2[37];
    assert(test__make_uuid(r1));
    assert(test__make_uuid(r2));

    /* Reserve 200; only 250 total → second 200 would overflow. */
    assert(Auth_Store_Guest_Reserve(
        p_store, guest2, r1, win2,
        200, 10, small_limit, expires) == AUTH_STORE_GUEST_QUOTA_OK);
    assert(Auth_Store_Guest_Reserve(
        p_store, guest2, r2, win2,
        200, 10, small_limit, expires) ==
        AUTH_STORE_GUEST_QUOTA_TOKENS_EXHAUSTED);
    puts("  guest_quota/token_exhaustion: PASS");

    /* Reconcile r1 with 150 actual (< 200 reserved). */
    assert(Auth_Store_Guest_Reconcile(p_store, r1, 150) == AUTH_STORE_OK);
    Auth_Store_Guest_Usage u;
    assert(Auth_Store_Guest_Get_Usage(
        p_store, guest2, win2, &u) == AUTH_STORE_OK);
    assert(u.output_tokens_used == 150);
    assert(u.output_tokens_reserved == 0);
    puts("  guest_quota/reconcile_actual_lt_reserved: PASS");

    /* Reconcile with actual > reserved and charge the provider's actual use. */
    char r3[37];
    assert(test__make_uuid(r3));
    assert(Auth_Store_Guest_Reserve(
        p_store, guest2, r3, win2,
        50, 10, small_limit, expires) == AUTH_STORE_GUEST_QUOTA_OK);
    assert(Auth_Store_Guest_Reconcile(p_store, r3, 9999) == AUTH_STORE_OK);
    assert(Auth_Store_Guest_Get_Usage(
        p_store, guest2, win2, &u) == AUTH_STORE_OK);
    assert(u.output_tokens_used == 150 + 9999);
    puts("  guest_quota/reconcile_oversize_actual: PASS");
  }

  /* 4. Idempotent reconcile/release. */
  {
    char guest3[37];
    assert(test__make_uuid(guest3));
    make_guest(p_store, guest3);
    int64 win3 = window_start + 2 * 86400LL;
    char rid[37];
    assert(test__make_uuid(rid));
    assert(Auth_Store_Guest_Reserve(
        p_store, guest3, rid, win3,
        100, 5, 500, expires) == AUTH_STORE_GUEST_QUOTA_OK);
    assert(Auth_Store_Guest_Reconcile(p_store, rid, 80) == AUTH_STORE_OK);
    /* Second reconcile on same request_id: idempotent OK. */
    assert(Auth_Store_Guest_Reconcile(p_store, rid, 80) == AUTH_STORE_OK);
    /* Release after reconcile: idempotent OK. */
    assert(Auth_Store_Guest_Release(p_store, rid) == AUTH_STORE_OK);
    puts("  guest_quota/idempotent_reconcile_release: PASS");
  }

  /* 5. Concurrent reservation invariant: two reservations, check totals. */
  {
    char guest4[37];
    assert(test__make_uuid(guest4));
    make_guest(p_store, guest4);
    int64 win4 = window_start + 3 * 86400LL;
    char ra[37], rb[37];
    assert(test__make_uuid(ra));
    assert(test__make_uuid(rb));
    assert(Auth_Store_Guest_Reserve(
        p_store, guest4, ra, win4, 300, 5, 500, expires) ==
        AUTH_STORE_GUEST_QUOTA_OK);
    assert(Auth_Store_Guest_Reserve(
        p_store, guest4, rb, win4, 300, 5, 500, expires) ==
        AUTH_STORE_GUEST_QUOTA_TOKENS_EXHAUSTED);
    /* 300 used, 300 reserved → 600 total, limit 500 → second fails. */
    Auth_Store_Guest_Usage u;
    assert(Auth_Store_Guest_Get_Usage(
        p_store, guest4, win4, &u) == AUTH_STORE_OK);
    /* invariant: used + reserved <= limit */
    assert(u.output_tokens_used + u.output_tokens_reserved <= 500);
    puts("  guest_quota/concurrent_invariant: PASS");
    assert(Auth_Store_Guest_Release(p_store, ra) == AUTH_STORE_OK);
  }

  /* 6. Clear all reservations (login transfer). */
  {
    char guest5[37];
    assert(test__make_uuid(guest5));
    make_guest(p_store, guest5);
    int64 win5 = window_start + 4 * 86400LL;
    char rc[37], rd[37];
    assert(test__make_uuid(rc));
    assert(test__make_uuid(rd));
    assert(Auth_Store_Guest_Reserve(
        p_store, guest5, rc, win5, 100, 5, 500, expires) ==
        AUTH_STORE_GUEST_QUOTA_OK);
    assert(Auth_Store_Guest_Reserve(
        p_store, guest5, rd, win5, 100, 5, 500, expires) ==
        AUTH_STORE_GUEST_QUOTA_OK);
    assert(Auth_Store_Guest_Clear_Reservations(
        p_store, guest5) == AUTH_STORE_OK);
    Auth_Store_Guest_Usage u;
    assert(Auth_Store_Guest_Get_Usage(
        p_store, guest5, win5, &u) == AUTH_STORE_OK);
    assert(u.output_tokens_reserved == 0);
    assert(u.turns_used == 2); /* turns kept */
    puts("  guest_quota/clear_reservations: PASS");
  }

  /* 7. UTC rollover: windows are independent. */
  {
    char guest6[37];
    assert(test__make_uuid(guest6));
    make_guest(p_store, guest6);
    int64 winA = window_start + 5 * 86400LL;
    int64 winB = winA + 86400LL;
    char re[37], rf[37];
    assert(test__make_uuid(re));
    assert(test__make_uuid(rf));
    assert(Auth_Store_Guest_Reserve(
        p_store, guest6, re, winA, 100, 2, 200, expires) ==
        AUTH_STORE_GUEST_QUOTA_OK);
    assert(Auth_Store_Guest_Reserve(
        p_store, guest6, rf, winA, 100, 2, 200, expires) ==
        AUTH_STORE_GUEST_QUOTA_OK);
    /* winA full; winB is a fresh window. */
    char rg[37];
    assert(test__make_uuid(rg));
    assert(Auth_Store_Guest_Reserve(
        p_store, guest6, rg, winB, 100, 2, 200, expires) ==
        AUTH_STORE_GUEST_QUOTA_OK);
    Auth_Store_Guest_Usage uB;
    assert(Auth_Store_Guest_Get_Usage(
        p_store, guest6, winB, &uB) == AUTH_STORE_OK);
    assert(uB.turns_used == 1);
    puts("  guest_quota/utc_rollover: PASS");
    assert(Auth_Store_Guest_Release(p_store, re) == AUTH_STORE_OK);
    assert(Auth_Store_Guest_Release(p_store, rf) == AUTH_STORE_OK);
    assert(Auth_Store_Guest_Release(p_store, rg) == AUTH_STORE_OK);
  }

  puts("test_guest_quota: PASS");
}/* ------------------------------------------------------------------ */
/* 19. Migration v2 preserves legacy guest_usage.count in turns_used    */
/* ------------------------------------------------------------------ */

/*
 * Build a genuine v1-only database using a raw Deita connection (no
 * Auth_Store_Create) so that the v2 migration has not yet run.  Insert
 * a legacy guest_usage row with count=7 and no turns_used column, then
 * open the database through Auth_Store_Create — which applies v2 —
 * and verify that turns_used is backfilled from count.
 */
static void test_migration_v2_legacy_backfill(const char *db_path)
{
  (void)db_path; /* we use our own temp file */

  char legacy_db[] = "/tmp/zenbu-auth-legacy-XXXXXX";
  int  fd          = mkstemp(legacy_db);
  assert(fd >= 0);
  close(fd);

  /* Build the v1-only schema directly. */
  Deita_Connection *p_conn = Deita_Connection_Create(
      DEITA_DATABASE_TYPE_SQLITE3, legacy_db);
  assert(p_conn);
  Deita_Query_Execute_Update(p_conn,
      "PRAGMA foreign_keys = OFF;"
      "PRAGMA journal_mode = WAL;");

  /* auth_schema_migrations ledger */
  Deita_Query_Execute_Update(p_conn,
      "CREATE TABLE IF NOT EXISTS auth_schema_migrations ("
      "  version INTEGER PRIMARY KEY,"
      "  applied_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))"
      ");");

  /* v1 tables (condensed: only what the migration test needs) */
  Deita_Query_Execute_Update(p_conn,
      "CREATE TABLE IF NOT EXISTS guest_identities ("
      "  id TEXT PRIMARY KEY,"
      "  ip_binding_digest TEXT NOT NULL,"
      "  created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),"
      "  last_seen_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),"
      "  expires_at INTEGER NOT NULL"
      ");");
  Deita_Query_Execute_Update(p_conn,
      /* v1 guest_usage: count only, no turns_used/token columns */
      "CREATE TABLE IF NOT EXISTS guest_usage ("
      "  guest_id TEXT NOT NULL"
      "    REFERENCES guest_identities(id) ON DELETE CASCADE,"
      "  window_start INTEGER NOT NULL,"
      "  count INTEGER NOT NULL DEFAULT 0,"
      "  PRIMARY KEY (guest_id, window_start)"
      ");");
  Deita_Query_Execute_Update(p_conn,
      /* v1 reservations: no request_id, no token count */
      "CREATE TABLE IF NOT EXISTS guest_usage_reservations ("
      "  id INTEGER PRIMARY KEY AUTOINCREMENT,"
      "  guest_id TEXT NOT NULL"
      "    REFERENCES guest_identities(id) ON DELETE CASCADE,"
      "  reserved_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),"
      "  expires_at INTEGER NOT NULL"
      ");");
  /* Stub other tables so FK checks don't break */
  Deita_Query_Execute_Update(p_conn,
      "CREATE TABLE IF NOT EXISTS users ("
      "  id TEXT PRIMARY KEY,"
      "  username TEXT NOT NULL,"
      "  normalized_username TEXT NOT NULL UNIQUE,"
      "  password_hash TEXT NOT NULL,"
      "  role TEXT NOT NULL,"
      "  status TEXT NOT NULL DEFAULT 'active',"
      "  must_change_password INTEGER NOT NULL DEFAULT 0,"
      "  password_changed_at INTEGER NOT NULL DEFAULT 0,"
      "  created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),"
      "  updated_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))"
      ");");
  Deita_Query_Execute_Update(p_conn,
      "CREATE TABLE IF NOT EXISTS auth_sessions ("
      "  token_digest TEXT PRIMARY KEY,"
      "  csrf_digest TEXT NOT NULL,"
      "  user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,"
      "  created_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),"
      "  last_seen_at INTEGER NOT NULL DEFAULT (strftime('%s','now')),"
      "  idle_expires_at INTEGER NOT NULL,"
      "  absolute_expires_at INTEGER NOT NULL,"
      "  password_changed_at_snapshot INTEGER NOT NULL DEFAULT 0,"
      "  revoked_at INTEGER"
      ");");
  Deita_Query_Execute_Update(p_conn,
      "CREATE TABLE IF NOT EXISTS admin_audit_log ("
      "  id INTEGER PRIMARY KEY AUTOINCREMENT,"
      "  actor_user_id TEXT,"
      "  action TEXT NOT NULL,"
      "  target_user_id TEXT,"
      "  detail TEXT,"
      "  created_at INTEGER NOT NULL DEFAULT (strftime('%s','now'))"
      ");");

  /* Mark v1 as applied; v2 is intentionally absent. */
  Deita_Query_Execute_Update(p_conn,
      "INSERT OR IGNORE INTO auth_schema_migrations (version) VALUES (1);");

  /* Insert a guest identity and a legacy usage row with count=7. */
  char g_id[37];
  assert(test__make_uuid(g_id));
  const char *gid_p[] = {g_id};
  Deita_Query_Execute_Update_Prepared(p_conn,
      "INSERT INTO guest_identities (id, ip_binding_digest, expires_at)"
      "  VALUES (?, 'test-digest', 9999999999)",
      1, gid_p);
  int64 win = 1700524800LL;
  char win_str[32];
  snprintf(win_str, sizeof(win_str), "%lld", (long long)win);
  const char *gu_p[] = {g_id, win_str};
  /* count = 7 in the legacy column */
  Deita_Query_Execute_Update_Prepared(p_conn,
      "INSERT INTO guest_usage (guest_id, window_start, count) VALUES (?, ?, 7)",
      2, gu_p);
  Deita_Connection_Close(p_conn);

  /* Open through Auth_Store_Create: should apply v2 (backfill turns_used). */
  Auth_Store *p_store = Auth_Store_Create(legacy_db);
  assert(p_store);

  Auth_Store_Guest_Usage u;
  assert(Auth_Store_Guest_Get_Usage(
      p_store, g_id, win, &u) == AUTH_STORE_OK);
  assert(u.turns_used == 7); /* backfilled from count */
  puts("test_migration_v2_legacy_backfill: PASS");

  Auth_Store_Destroy(p_store);
  unlink(legacy_db);
}

/* ------------------------------------------------------------------ */
/* 20. Expired reservation reaping                                       */
/* ------------------------------------------------------------------ */

static void test_reap_expired_reservations(Auth_Store *p_store)
{
  char gid[37];
  assert(test__make_uuid(gid));
  make_guest(p_store, gid);

  int64 now     = (int64)time(NULL);
  /* Use a window far in the future so it can never collide with clock-based
   * reaping inside Auth_Store_Guest_Reserve. */
  int64 win     = now + 86400LL;       /* tomorrow's window */
  /* "past" and "future" are relative to now_sim (= now + 3600), but both
   * must be > now so the internal Reserve reap does not touch them. */
  int64 past    = now + 1800LL;        /* expires in 30 min: past from now_sim */
  int64 future  = now + 7200LL;        /* expires in 2 h: future from now_sim */
  int64 now_sim = now + 3600LL;        /* simulated "now": 1 h from now */

  char ra[37], rb[37];
  assert(test__make_uuid(ra));
  assert(test__make_uuid(rb));

  /* Reserve two turns: ra expires before now_sim, rb expires after. */
  assert(Auth_Store_Guest_Reserve(
      p_store, gid, ra, win, 100, 10, 1000, past) ==
      AUTH_STORE_GUEST_QUOTA_OK);
  assert(Auth_Store_Guest_Reserve(
      p_store, gid, rb, win, 200, 10, 1000, future) ==
      AUTH_STORE_GUEST_QUOTA_OK);

  Auth_Store_Guest_Usage u;
  assert(Auth_Store_Guest_Get_Usage(p_store, gid, win, &u) == AUTH_STORE_OK);
  assert(u.turns_used == 2);
  assert(u.output_tokens_reserved == 300); /* 100 + 200 */

  /* Reap with now_sim > past but < future: only ra should be reaped. */
  assert(Auth_Store_Guest_Reap_Expired(p_store, now_sim) == AUTH_STORE_OK);

  assert(Auth_Store_Guest_Get_Usage(p_store, gid, win, &u) == AUTH_STORE_OK);
  assert(u.turns_used == 2);        /* turns retained */
  assert(u.output_tokens_reserved == 200); /* only ra's 100 removed */
  puts("  reap/partial: PASS");

  /* Idempotent: reaping again changes nothing. */
  assert(Auth_Store_Guest_Reap_Expired(p_store, now_sim) == AUTH_STORE_OK);
  assert(Auth_Store_Guest_Get_Usage(p_store, gid, win, &u) == AUTH_STORE_OK);
  assert(u.output_tokens_reserved == 200);
  puts("  reap/idempotent: PASS");

  /* Reap with future time: rb is now expired too. */
  assert(Auth_Store_Guest_Reap_Expired(p_store, future + 1) == AUTH_STORE_OK);
  assert(Auth_Store_Guest_Get_Usage(p_store, gid, win, &u) == AUTH_STORE_OK);
  assert(u.output_tokens_reserved == 0);
  assert(u.turns_used == 2);
  puts("  reap/all_expired: PASS");

  /* Release on an already-reaped reservation: idempotent OK. */
  assert(Auth_Store_Guest_Release(p_store, ra) == AUTH_STORE_OK);
  assert(Auth_Store_Guest_Release(p_store, rb) == AUTH_STORE_OK);
  puts("  reap/release_after_reap: PASS");

  puts("test_reap_expired_reservations: PASS");
}

/* ------------------------------------------------------------------ */
/* Main                                                                 */
/* ------------------------------------------------------------------ */

int main(void)
{
  char db_path[] = "/tmp/zenbu-auth-XXXXXX";
  int  fd        = mkstemp(db_path);
  assert(fd >= 0);
  close(fd);

  test_migrations_idempotent(db_path);
  test_migration_v2_legacy_backfill(db_path);

  Auth_Store *p_store = Auth_Store_Create(db_path);
  assert(p_store);

  test_username_normalization();

  /* Bootstrap and last-admin tests must run first (only one admin). */
  test_bootstrap_admin(p_store);
  test_last_admin_protection(p_store);

  test_username_uniqueness(p_store);
  test_user_lookup(p_store);
  test_forced_password_flag(p_store);
  test_session_lifecycle(p_store);
  test_stale_password_snapshot(p_store);
  test_disabled_user(p_store);
  test_password_update_revokes_others(p_store);
  test_revoke_all_sessions(p_store);
  test_expired_session(p_store);
  test_guest_identity(p_store, db_path);
  test_rotate_session(p_store);
  test_create_session_cas(p_store);
  test_self_change_password(p_store);
  test_audited_admin_operations(p_store, db_path);
  test_audit_failure_rollback(p_store, db_path);
  test_guest_quota(p_store);
  test_reap_expired_reservations(p_store);

  Auth_Store_Destroy(p_store);
  unlink(db_path);

  puts("auth_store_test: ALL PASS");
  return 0;
}
