/**
 * 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(() => {
    clearAll();
  });

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

    it('allows first request and returns full remaining', () => {
      const result = 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', () => {
      const r1 = checkAndRecord('user:a', rule);
      expect(r1.remaining).toBe(2);

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

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

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

      const result = 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', () => {
      checkAndRecord('user:x', rule);
      checkAndRecord('user:x', rule);
      checkAndRecord('user:x', rule);

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

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

    it('returns resetAt as ISO string', () => {
      const result = 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', () => {
      const strictRule: RateLimitRule = { maxRequests: 1, windowSeconds: 10 };
      const r1 = checkAndRecord('user:strict', strictRule);
      expect(r1.allowed).toBe(true);
      expect(r1.remaining).toBe(0);

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

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

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

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

      reset('user:reset');

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

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

      reset('user:clear');

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

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

    it('is safe to reset non-existent key', () => {
      expect(() => reset('nonexistent')).not.toThrow();
    });
  });

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

    it('returns full remaining for unknown key', () => {
      const result = 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', () => {
      peek('user:peek', rule);
      peek('user:peek', rule);
      peek('user:peek', rule);

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

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

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

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

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

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

      clearAll();

      expect(checkAndRecord('a', rule).allowed).toBe(true);
      expect(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);
  });
});
