import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

describe("rate limiter", () => {
  beforeEach(() => {
    vi.useFakeTimers();
    vi.setSystemTime(new Date("2026-03-30T10:00:00.000Z"));
  });

  afterEach(() => {
    vi.useRealTimers();
    vi.resetModules();
    vi.unmock("ioredis");
    vi.unstubAllEnvs();
  });

  it("enforces limits and reset in memory mode", async () => {
    const { createRateLimiter } = await import("../middleware/rate-limit.js");
    const limiter = createRateLimiter(60_000, 2, { backend: "memory", name: "memory-test" });

    const first = await limiter("user-1");
    const second = await limiter("user-1");
    const third = await limiter("user-1");

    expect(first.allowed).toBe(true);
    expect(first.remaining).toBe(1);
    expect(second.allowed).toBe(true);
    expect(second.remaining).toBe(0);
    expect(third.allowed).toBe(false);
    expect(third.remaining).toBe(0);

    await limiter.reset();

    const afterReset = await limiter("user-1");
    expect(afterReset.allowed).toBe(true);
    expect(afterReset.remaining).toBe(1);
  });

  it("uses a shared Redis counter when Redis mode is enabled", async () => {
    const store = new Map<string, { count: number; resetAt: number }>();
    const delMock = vi.fn(async (...keys: string[]) => {
      for (const key of keys) {
        store.delete(key);
      }
      return keys.length;
    });

    vi.doMock("ioredis", () => ({
      Redis: vi.fn().mockImplementation(() => ({
        on: vi.fn(),
        eval: vi.fn(async (_script: string, _numKeys: number, key: string, windowMsValue: string) => {
          const now = Date.now();
          const windowMs = Number(windowMsValue);
          const existing = store.get(key);
          if (!existing || existing.resetAt <= now) {
            store.set(key, { count: 1, resetAt: now + windowMs });
          } else {
            existing.count += 1;
          }
          const current = store.get(key)!;
          return [current.count, current.resetAt - now];
        }),
        scan: vi.fn(async () => ["0", [...store.keys()]]),
        del: delMock,
      })),
    }));

    const { createRateLimiter } = await import("../middleware/rate-limit.js");
    const limiter = createRateLimiter(60_000, 2, {
      backend: "redis",
      redisUrl: "redis://test",
      name: "redis-test",
    });

    const first = await limiter("user-1");
    const second = await limiter("user-1");
    const third = await limiter("user-1");

    expect(first.allowed).toBe(true);
    expect(second.allowed).toBe(true);
    expect(third.allowed).toBe(false);
    expect(third.remaining).toBe(0);

    await limiter.reset();

    expect(delMock).toHaveBeenCalled();
    const afterReset = await limiter("user-1");
    expect(afterReset.allowed).toBe(true);
    expect(afterReset.remaining).toBe(1);
  });

  it("falls back to in-memory counters when Redis is unavailable", async () => {
    vi.doMock("ioredis", () => ({
      Redis: vi.fn().mockImplementation(() => ({
        on: vi.fn(),
        eval: vi.fn(async () => {
          throw new Error("redis down");
        }),
        scan: vi.fn(async () => ["0", []]),
        del: vi.fn(async () => 0),
      })),
    }));

    const { createRateLimiter } = await import("../middleware/rate-limit.js");
    const limiter = createRateLimiter(60_000, 2, {
      backend: "redis",
      redisUrl: "redis://test",
      name: "redis-fallback-test",
    });

    const first = await limiter("user-1");
    const second = await limiter("user-1");
    const third = await limiter("user-1");

    expect(first.allowed).toBe(true);
    expect(second.allowed).toBe(true);
    expect(third.allowed).toBe(false);
    expect(third.remaining).toBe(0);
  });
});
