import { beforeEach, describe, expect, it, vi } from "vitest";
import { consumeTotpWindow } from "../totp-consume.js";

describe("consumeTotpWindow — atomic replay guard", () => {
  let updateMany: ReturnType<typeof vi.fn>;
  let db: { user: { updateMany: typeof updateMany } };

  beforeEach(() => {
    updateMany = vi.fn();
    db = { user: { updateMany } };
  });

  it("returns true when the update affected a row", async () => {
    updateMany.mockResolvedValue({ count: 1 });
    await expect(consumeTotpWindow(db, "user-1")).resolves.toBe(true);
  });

  it("returns false when another concurrent request already consumed the window", async () => {
    updateMany.mockResolvedValue({ count: 0 });
    await expect(consumeTotpWindow(db, "user-1")).resolves.toBe(false);
  });

  it("issues a WHERE clause that only updates null or older-than-30-s rows", async () => {
    updateMany.mockResolvedValue({ count: 1 });
    const now = new Date("2026-04-17T12:00:30.000Z");
    await consumeTotpWindow(db, "user-1", now);

    expect(updateMany).toHaveBeenCalledTimes(1);
    const call = updateMany.mock.calls[0]![0] as {
      where: { id: string; OR: Array<{ lastTotpAt: unknown }> };
      data: { lastTotpAt: Date };
    };
    expect(call.where.id).toBe("user-1");
    expect(call.where.OR).toEqual([
      { lastTotpAt: null },
      { lastTotpAt: { lt: new Date("2026-04-17T12:00:00.000Z") } },
    ]);
    expect(call.data.lastTotpAt).toEqual(now);
  });

  it("simulated race: two parallel calls — exactly one wins", async () => {
    // Model Postgres row-lock serialisation: the first updateMany to land
    // sees count=1, the second (in the same 30-s window) sees count=0.
    let served = 0;
    updateMany.mockImplementation(async () => {
      await new Promise((r) => setTimeout(r, 1));
      return { count: served++ === 0 ? 1 : 0 };
    });

    const [a, b] = await Promise.all([
      consumeTotpWindow(db, "user-1"),
      consumeTotpWindow(db, "user-1"),
    ]);

    expect([a, b].sort()).toEqual([false, true]);
    expect(updateMany).toHaveBeenCalledTimes(2);
  });
});
