import { describe, it, expect } from "vitest";
import { InMemoryRateLimit } from "@/rate-limit/in-memory-rate-limit";
import type { IRateLimit } from "@/rate-limit/rate-limit.interface";

const BUDGET = { name: "signin", window: "1m", budget: 3 };

function makeClock(start = 0) {
  let now = start;
  return {
    tick: (ms: number) => {
      now += ms;
    },
    fn: () => now,
  };
}

describe("InMemoryRateLimit", () => {
  it("satisfies IRateLimit", () => {
    const rl: IRateLimit = new InMemoryRateLimit([BUDGET]);
    expect(rl).toBeDefined();
  });

  it("allows consume up to budget", async () => {
    const rl = new InMemoryRateLimit([BUDGET]);
    for (let i = 0; i < 3; i++) {
      const result = await rl.consume("signin", "u1");
      expect(result.allowed).toBe(true);
    }
  });

  it("decrements remaining on each consume", async () => {
    const rl = new InMemoryRateLimit([BUDGET]);
    const r1 = await rl.consume("signin", "u1");
    expect(r1.remaining).toBe(2);
    const r2 = await rl.consume("signin", "u1");
    expect(r2.remaining).toBe(1);
    const r3 = await rl.consume("signin", "u1");
    expect(r3.remaining).toBe(0);
  });

  it("denies consume beyond budget", async () => {
    const rl = new InMemoryRateLimit([BUDGET]);
    for (let i = 0; i < 3; i++) await rl.consume("signin", "u1");
    const result = await rl.consume("signin", "u1");
    expect(result.allowed).toBe(false);
    expect(result.remaining).toBe(0);
  });

  it("tracks per-key buckets independently", async () => {
    const rl = new InMemoryRateLimit([BUDGET]);
    for (let i = 0; i < 3; i++) await rl.consume("signin", "u1");
    const result = await rl.consume("signin", "u2");
    expect(result.allowed).toBe(true);
  });

  it("resets bucket at window expiry (check-at-read)", async () => {
    const clock = makeClock(1000);
    const rl = new InMemoryRateLimit([BUDGET], clock.fn);
    for (let i = 0; i < 3; i++) await rl.consume("signin", "u1");
    // advance past the 1-minute window
    clock.tick(61_000);
    const result = await rl.consume("signin", "u1");
    expect(result.allowed).toBe(true);
    expect(result.remaining).toBe(2);
  });

  it("explicit reset restores budget to full", async () => {
    const rl = new InMemoryRateLimit([BUDGET]);
    for (let i = 0; i < 3; i++) await rl.consume("signin", "u1");
    await rl.reset("signin", "u1");
    const result = await rl.consume("signin", "u1");
    expect(result.allowed).toBe(true);
    expect(result.remaining).toBe(2);
  });

  it("reset for unknown key is a no-op", async () => {
    const rl = new InMemoryRateLimit([BUDGET]);
    await expect(rl.reset("signin", "never-seen")).resolves.toBeUndefined();
  });

  it("throws for unknown budget name", async () => {
    const rl = new InMemoryRateLimit([BUDGET]);
    await expect(rl.consume("unknown-budget", "u1")).rejects.toThrow(
      /Unknown rate-limit budget/,
    );
  });

  it("throws for malformed window string when consume is called", async () => {
    const rl = new InMemoryRateLimit([{ name: "x", window: "bad", budget: 5 }]);
    await expect(rl.consume("x", "k")).rejects.toThrow(
      /Invalid rate-limit window/,
    );
  });

  it("resetAt is set to window boundary", async () => {
    const clock = makeClock(10_000);
    const rl = new InMemoryRateLimit([BUDGET], clock.fn);
    const result = await rl.consume("signin", "u1");
    expect(result.resetAt.getTime()).toBe(10_000 + 60_000);
  });

  it("parses ms window unit", async () => {
    const clock = makeClock(0);
    const rl = new InMemoryRateLimit(
      [{ name: "fast", window: "500ms", budget: 1 }],
      clock.fn,
    );
    const r = await rl.consume("fast", "u1");
    expect(r.resetAt.getTime()).toBe(500);
  });

  it("parses s window unit", async () => {
    const clock = makeClock(0);
    const rl = new InMemoryRateLimit(
      [{ name: "second", window: "30s", budget: 1 }],
      clock.fn,
    );
    const r = await rl.consume("second", "u1");
    expect(r.resetAt.getTime()).toBe(30_000);
  });

  it("parses h window unit", async () => {
    const clock = makeClock(0);
    const rl = new InMemoryRateLimit(
      [{ name: "hourly", window: "2h", budget: 1 }],
      clock.fn,
    );
    const r = await rl.consume("hourly", "u1");
    expect(r.resetAt.getTime()).toBe(7_200_000);
  });

  it("parses d window unit", async () => {
    const clock = makeClock(0);
    const rl = new InMemoryRateLimit(
      [{ name: "daily", window: "1d", budget: 1 }],
      clock.fn,
    );
    const r = await rl.consume("daily", "u1");
    expect(r.resetAt.getTime()).toBe(86_400_000);
  });

  it("respects weight parameter", async () => {
    const rl = new InMemoryRateLimit([BUDGET]);
    const r1 = await rl.consume("signin", "u1", 2);
    expect(r1.allowed).toBe(true);
    expect(r1.remaining).toBe(1);
    const r2 = await rl.consume("signin", "u1", 2);
    expect(r2.allowed).toBe(false);
  });
});
