import { describe, it, expect, beforeEach, afterAll } from 'vitest';

import { redis } from '@/lib/redis';
import { checkRateLimit, rateLimiters } from '@/lib/rate-limit';

const TEST_USER = `rl-test-${crypto.randomUUID()}`;

async function clearTestKeys() {
  // Wipe any keys created during this test run.
  for (const cfg of Object.values(rateLimiters)) {
    await redis.del(`rl:${cfg.keyPrefix}:${TEST_USER}`);
  }
}

beforeEach(async () => {
  await clearTestKeys();
});

afterAll(async () => {
  await clearTestKeys();
  await redis.quit();
});

describe('rate-limit catalog', () => {
  it('exposes the canonical names with the documented limits', () => {
    expect(rateLimiters.ocr).toMatchObject({ max: 10, windowMs: 60_000 });
    expect(rateLimiters.ai).toMatchObject({ max: 60, windowMs: 60_000 });
    expect(rateLimiters.exports).toMatchObject({ max: 30, windowMs: 60 * 60_000 });
  });
});

describe('checkRateLimit (sliding window)', () => {
  it('allows up to `max` calls in a window then refuses', async () => {
    const cfg = rateLimiters.ocr;
    let lastResult;
    for (let i = 0; i < cfg.max; i++) {
      lastResult = await checkRateLimit(TEST_USER, cfg);
      expect(lastResult.allowed).toBe(true);
    }
    expect(lastResult!.remaining).toBe(0);

    const overflow = await checkRateLimit(TEST_USER, cfg);
    expect(overflow.allowed).toBe(false);
    expect(overflow.remaining).toBe(0);
    expect(overflow.limit).toBe(cfg.max);
  });

  it('isolates limits across keyPrefixes (ocr vs ai vs exports)', async () => {
    // Burn through ocr limit.
    for (let i = 0; i < rateLimiters.ocr.max; i++) {
      await checkRateLimit(TEST_USER, rateLimiters.ocr);
    }
    const ocrOverflow = await checkRateLimit(TEST_USER, rateLimiters.ocr);
    expect(ocrOverflow.allowed).toBe(false);

    // ai limit is independent.
    const ai = await checkRateLimit(TEST_USER, rateLimiters.ai);
    expect(ai.allowed).toBe(true);

    // exports limit is independent.
    const exp = await checkRateLimit(TEST_USER, rateLimiters.exports);
    expect(exp.allowed).toBe(true);
  });

  it('isolates limits across users (one user hitting cap does not affect another)', async () => {
    const otherUser = `rl-test-${crypto.randomUUID()}`;
    try {
      for (let i = 0; i < rateLimiters.ocr.max; i++) {
        await checkRateLimit(TEST_USER, rateLimiters.ocr);
      }
      const overflowSelf = await checkRateLimit(TEST_USER, rateLimiters.ocr);
      expect(overflowSelf.allowed).toBe(false);

      const otherFirst = await checkRateLimit(otherUser, rateLimiters.ocr);
      expect(otherFirst.allowed).toBe(true);
      expect(otherFirst.remaining).toBe(rateLimiters.ocr.max - 1);
    } finally {
      await redis.del(`rl:${rateLimiters.ocr.keyPrefix}:${otherUser}`);
    }
  });

  it('reports a sensible resetAt in the future', async () => {
    const before = Date.now();
    const r = await checkRateLimit(TEST_USER, rateLimiters.ocr);
    expect(r.resetAt).toBeGreaterThanOrEqual(before + rateLimiters.ocr.windowMs - 1000);
    expect(r.resetAt).toBeLessThanOrEqual(Date.now() + rateLimiters.ocr.windowMs + 1000);
  });
});
