/**
 * Tests for rate limiting module — store (sliding window) + types (Zod schemas).
 */

import { describe, it, expect, beforeEach } from 'vitest';
import { checkAndRecord, reset, peek, clearAll } from './store.js';
import { CheckRateLimitSchema, RateLimitConfigSchema } from './types.js';
import type { RateLimitRule } from './types.js';

// ── Store: sliding window counter ──────────────────────────────

describe('ratelimit store', () => {
  beforeEach(async () => {
    await clearAll();
  });

  describe('checkAndRecord', () => {
    const rule: RateLimitRule = { maxRequests: 3, windowSeconds: 60 };

    it('allows first request and returns full remaining', async () => {
      const result = await checkAndRecord('user:123', rule);
      expect(result.allowed).toBe(true);
      expect(result.remaining).toBe(2);
      expect(result.limit).toBe(3);
      expect(result.resetAt).toBeDefined();
    });

    it('decrements remaining on each call', async () => {
      const r1 = await checkAndRecord('user:a', rule);
      expect(r1.remaining).toBe(2);

      const r2 = await checkAndRecord('user:a', rule);
      expect(r2.remaining).toBe(1);

      const r3 = await checkAndRecord('user:a', rule);
      expect(r3.remaining).toBe(0);
    });

    it('blocks after exceeding maxRequests', async () => {
      await checkAndRecord('user:b', rule);
      await checkAndRecord('user:b', rule);
      await checkAndRecord('user:b', rule);

      const result = await checkAndRecord('user:b', rule);
      expect(result.allowed).toBe(false);
      expect(result.remaining).toBe(0);
      expect(result.retryAfterMs).toBeGreaterThan(0);
    });

    it('uses separate counters for different keys', async () => {
      await checkAndRecord('user:x', rule);
      await checkAndRecord('user:x', rule);
      await checkAndRecord('user:x', rule);

      const resultX = await checkAndRecord('user:x', rule);
      expect(resultX.allowed).toBe(false);

      const resultY = await checkAndRecord('user:y', rule);
      expect(resultY.allowed).toBe(true);
      expect(resultY.remaining).toBe(2);
    });

    it('returns resetAt as ISO string', async () => {
      const result = await checkAndRecord('user:time', rule);
      expect(() => new Date(result.resetAt)).not.toThrow();
      expect(new Date(result.resetAt).getTime()).toBeGreaterThan(Date.now() - 1000);
    });

    it('handles single-request limit', async () => {
      const strictRule: RateLimitRule = { maxRequests: 1, windowSeconds: 10 };
      const r1 = await checkAndRecord('user:strict', strictRule);
      expect(r1.allowed).toBe(true);
      expect(r1.remaining).toBe(0);

      const r2 = await checkAndRecord('user:strict', strictRule);
      expect(r2.allowed).toBe(false);
    });

    it('handles large maxRequests without issues', async () => {
      const bigRule: RateLimitRule = { maxRequests: 10000, windowSeconds: 60 };
      const result = await checkAndRecord('user:big', bigRule);
      expect(result.allowed).toBe(true);
      expect(result.remaining).toBe(9999);
    });
  });

  describe('reset', () => {
    it('clears rate limit for a specific key', async () => {
      const rule: RateLimitRule = { maxRequests: 2, windowSeconds: 60 };
      await checkAndRecord('user:reset', rule);
      await checkAndRecord('user:reset', rule);

      const blocked = await checkAndRecord('user:reset', rule);
      expect(blocked.allowed).toBe(false);

      await reset('user:reset');

      const afterReset = await checkAndRecord('user:reset', rule);
      expect(afterReset.allowed).toBe(true);
      expect(afterReset.remaining).toBe(1);
    });

    it('does not affect other keys', async () => {
      const rule: RateLimitRule = { maxRequests: 1, windowSeconds: 60 };
      await checkAndRecord('user:keep', rule);
      await checkAndRecord('user:clear', rule);

      await reset('user:clear');

      const keep = await checkAndRecord('user:keep', rule);
      expect(keep.allowed).toBe(false);

      const cleared = await checkAndRecord('user:clear', rule);
      expect(cleared.allowed).toBe(true);
    });

    it('is safe to reset non-existent key', async () => {
      await expect(reset('nonexistent')).resolves.toBeUndefined();
    });
  });

  describe('peek', () => {
    const rule: RateLimitRule = { maxRequests: 3, windowSeconds: 60 };

    it('returns full remaining for unknown key', async () => {
      const result = await peek('new-key', rule);
      expect(result.allowed).toBe(true);
      expect(result.remaining).toBe(3);
      expect(result.limit).toBe(3);
    });

    it('does not consume a request', async () => {
      await peek('user:peek', rule);
      await peek('user:peek', rule);
      await peek('user:peek', rule);

      const result = await checkAndRecord('user:peek', rule);
      expect(result.allowed).toBe(true);
      expect(result.remaining).toBe(2);
    });

    it('reflects current usage', async () => {
      await checkAndRecord('user:usage', rule);
      await checkAndRecord('user:usage', rule);

      const result = await peek('user:usage', rule);
      expect(result.remaining).toBe(1);
      expect(result.allowed).toBe(true);
    });

    it('shows blocked status when limit exceeded', async () => {
      await checkAndRecord('user:full', rule);
      await checkAndRecord('user:full', rule);
      await checkAndRecord('user:full', rule);

      const result = await peek('user:full', rule);
      expect(result.allowed).toBe(false);
      expect(result.remaining).toBe(0);
    });
  });

  describe('clearAll', () => {
    it('resets all keys', async () => {
      const rule: RateLimitRule = { maxRequests: 1, windowSeconds: 60 };
      await checkAndRecord('a', rule);
      await checkAndRecord('b', rule);

      await clearAll();

      expect((await checkAndRecord('a', rule)).allowed).toBe(true);
      expect((await checkAndRecord('b', rule)).allowed).toBe(true);
    });
  });
});

// ── Zod schemas ────────────────────────────────────────────────

describe('CheckRateLimitSchema', () => {
  it('accepts valid input with defaults', () => {
    const result = CheckRateLimitSchema.safeParse({ key: 'user:123' });
    expect(result.success).toBe(true);
    if (result.success) {
      expect(result.data.key).toBe('user:123');
      expect(result.data.productId).toBe('lysnrai');
      expect(result.data.routePrefix).toBeUndefined();
    }
  });

  it('accepts full input', () => {
    const result = CheckRateLimitSchema.safeParse({
      key: 'user:456',
      productId: 'mindlyst',
      routePrefix: '/api/auth',
    });
    expect(result.success).toBe(true);
    if (result.success) {
      expect(result.data.productId).toBe('mindlyst');
      expect(result.data.routePrefix).toBe('/api/auth');
    }
  });

  it('rejects empty key', () => {
    const result = CheckRateLimitSchema.safeParse({ key: '' });
    expect(result.success).toBe(false);
  });

  it('rejects missing key', () => {
    const result = CheckRateLimitSchema.safeParse({});
    expect(result.success).toBe(false);
  });
});

describe('RateLimitConfigSchema', () => {
  it('accepts valid config', () => {
    const result = RateLimitConfigSchema.safeParse({
      productId: 'lysnrai',
      rules: [
        { maxRequests: 60, windowSeconds: 60 },
        { maxRequests: 5, windowSeconds: 60, routePrefix: '/api/auth' },
      ],
    });
    expect(result.success).toBe(true);
  });

  it('rejects zero maxRequests', () => {
    const result = RateLimitConfigSchema.safeParse({
      productId: 'test',
      rules: [{ maxRequests: 0, windowSeconds: 60 }],
    });
    expect(result.success).toBe(false);
  });

  it('rejects zero windowSeconds', () => {
    const result = RateLimitConfigSchema.safeParse({
      productId: 'test',
      rules: [{ maxRequests: 10, windowSeconds: 0 }],
    });
    expect(result.success).toBe(false);
  });

  it('rejects negative values', () => {
    const result = RateLimitConfigSchema.safeParse({
      productId: 'test',
      rules: [{ maxRequests: -1, windowSeconds: 60 }],
    });
    expect(result.success).toBe(false);
  });

  it('rejects missing productId', () => {
    const result = RateLimitConfigSchema.safeParse({
      rules: [{ maxRequests: 10, windowSeconds: 60 }],
    });
    expect(result.success).toBe(false);
  });

  it('accepts empty rules array', () => {
    const result = RateLimitConfigSchema.safeParse({
      productId: 'test',
      rules: [],
    });
    expect(result.success).toBe(true);
  });

  it('rejects non-integer maxRequests', () => {
    const result = RateLimitConfigSchema.safeParse({
      productId: 'test',
      rules: [{ maxRequests: 10.5, windowSeconds: 60 }],
    });
    expect(result.success).toBe(false);
  });
});
