import { describe, expect, test } from "bun:test";
import { Database } from "bun:sqlite";
import {
  RedisCaptchaStore,
  SqliteCaptchaStore,
  managedCaptchaProvider,
  type CaptchaChallengeRecord,
  type CaptchaResponseTokenRecord,
  type RedisCaptchaClient,
  type SqliteDatabaseLike,
} from "../src/index.ts";

const challenge = (id: string, expiresAt = Date.now() + 60_000): CaptchaChallengeRecord => ({
  id,
  provider: "self-hosted",
  type: "text",
  presentation: "visual",
  action: "signup",
  publicChallenge: {
    id,
    provider: "self-hosted",
    type: "text",
    presentation: "visual",
    action: "signup",
    prompt: "Type the text",
    createdAt: Date.now(),
    expiresAt,
    responseField: "wrn-captcha-response",
  },
  answerDigest: "digest",
  answerSalt: "salt",
  answerKind: "text",
  caseSensitive: false,
  attempts: 0,
  maxAttempts: 3,
  metadata: {},
  createdAt: Date.now(),
  expiresAt,
});

const token = (tokenHash: string): CaptchaResponseTokenRecord => ({
  tokenHash,
  provider: "self-hosted",
  action: "signup",
  createdAt: Date.now(),
  expiresAt: Date.now() + 60_000,
});

describe("CAPTCHA durable stores", () => {
  test("SQLite atomically mutates, consumes, deletes, and collects records", async () => {
    const db = new Database(":memory:");
    const store = new SqliteCaptchaStore(db as unknown as SqliteDatabaseLike);
    await store.createChallenge(challenge("c1"));
    expect((await store.incrementAttempts("c1", Date.now()))?.attempts).toBe(1);
    expect(await store.consumeChallenge("c1", Date.now())).toMatchObject({
      consumedAt: expect.any(Number),
    });
    expect(await store.consumeChallenge("c1", Date.now())).toBeUndefined();

    await store.createToken(token("t1"));
    expect(await store.consumeToken("t1", Date.now())).toMatchObject({
      consumedAt: expect.any(Number),
    });
    expect(await store.consumeToken("t1", Date.now())).toBeUndefined();
    await store.deleteChallenge("c1");
    await store.deleteToken("t1");
    await store.createChallenge(challenge("expired", Date.now() - 1));
    await store.gc(Date.now());
    expect(await store.getChallenge("expired")).toBeUndefined();
    db.close();
  });

  test("Redis fallback locking preserves one-time consumption without Lua", async () => {
    const values = new Map<string, string>();
    const redis: RedisCaptchaClient = {
      get: (key) => values.get(key) ?? null,
      set: (key, value) => void values.set(key, value),
      del: (key) => (values.delete(key) ? 1 : 0),
    };
    const store = new RedisCaptchaStore(redis, { prefix: "test:" });
    await store.createChallenge(challenge("c1"));
    expect((await store.incrementAttempts("c1", Date.now()))?.attempts).toBe(1);
    const consumed = await Promise.all([
      store.consumeChallenge("c1", Date.now()),
      store.consumeChallenge("c1", Date.now()),
    ]);
    expect(consumed.filter(Boolean)).toHaveLength(1);

    await store.createToken(token("t1"));
    const tokens = await Promise.all([
      store.consumeToken("t1", Date.now()),
      store.consumeToken("t1", Date.now()),
    ]);
    expect(tokens.filter(Boolean)).toHaveLength(1);
    await store.deleteChallenge("c1");
    await store.deleteToken("t1");
    expect(await store.getChallenge("c1")).toBeUndefined();
  });

  test("managed provider separates public creation from secret verification", async () => {
    const requests: RequestInit[] = [];
    const provider = managedCaptchaProvider({
      baseUrl: "https://captcha.test/",
      siteKey: "site",
      secretKey: "secret",
      fetch: (async (_url, init) => {
        requests.push(init ?? {});
        return Response.json(
          requests.length === 1
            ? {
                id: "c1",
                provider: "managed",
                type: "text",
                presentation: "visual",
                action: "signup",
                prompt: "Type",
                createdAt: 1,
                expiresAt: 2,
                responseField: "token",
              }
            : { success: true, provider: "managed", action: "signup" },
        );
      }) as typeof fetch,
    });
    await provider.createChallenge({ action: "signup" });
    await provider.verify({ action: "signup", providerToken: "answer" });
    expect(new Headers(requests[0]?.headers).has("authorization")).toBe(false);
    expect(new Headers(requests[1]?.headers).get("authorization")).toBe("Bearer secret");
  });
});
