/**
 * Rate Limiter Tests
 * Tests for request throttling and rate limiting functionality
 */

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { MemoryRateLimitStore, RateLimiter, SlidingWindowRateLimiter } from './rate-limiter';
import { RateLimitOptions } from './types';

describe('RateLimiter', () => {
  let rateLimiter: RateLimiter;
  let store: MemoryRateLimitStore;

  beforeEach(() => {
    store = new MemoryRateLimitStore();
    const options: RateLimitOptions = {
      windowMs: 60000, // 1 minute
      maxRequests: 5
    };
    rateLimiter = new RateLimiter(options, store);
  });

  afterEach(() => {
    store.destroy();
  });

  describe('Basic Rate Limiting', () => {
    it('should allow requests within limit', async () => {
      const mockRequest = { ip: '127.0.0.1' };

      for (let i = 0; i < 5; i++) {
        const result = await rateLimiter.checkLimit(mockRequest);
        expect(result.allowed).toBe(true);
        expect(result.remaining).toBe(4 - i);
      }
    });

    it('should block requests exceeding limit', async () => {
      const mockRequest = { ip: '127.0.0.1' };

      // Make 5 allowed requests
      for (let i = 0; i < 5; i++) {
        await rateLimiter.checkLimit(mockRequest);
      }

      // 6th request should be blocked
      const result = await rateLimiter.checkLimit(mockRequest);
      expect(result.allowed).toBe(false);
      expect(result.remaining).toBe(0);
      expect(result.retryAfter).toBeGreaterThan(0);
    });

    it('should track different IPs separately', async () => {
      const request1 = { ip: '127.0.0.1' };
      const request2 = { ip: '192.168.1.1' };

      // Make 5 requests from first IP
      for (let i = 0; i < 5; i++) {
        const result = await rateLimiter.checkLimit(request1);
        expect(result.allowed).toBe(true);
      }

      // First IP should be blocked
      const blockedResult = await rateLimiter.checkLimit(request1);
      expect(blockedResult.allowed).toBe(false);

      // Second IP should still be allowed
      const allowedResult = await rateLimiter.checkLimit(request2);
      expect(allowedResult.allowed).toBe(true);
    });

    it('should reset limits after window expires', async () => {
      const mockRequest = { ip: '127.0.0.1' };

      // Use a short window for testing
      const shortWindowLimiter = new RateLimiter({
        windowMs: 100, // 100ms
        maxRequests: 2
      }, store);

      // Make 2 requests (at limit)
      await shortWindowLimiter.checkLimit(mockRequest);
      await shortWindowLimiter.checkLimit(mockRequest);

      // 3rd request should be blocked
      const blockedResult = await shortWindowLimiter.checkLimit(mockRequest);
      expect(blockedResult.allowed).toBe(false);

      // Wait for window to expire
      await new Promise(resolve => setTimeout(resolve, 150));

      // Should be allowed again
      const allowedResult = await shortWindowLimiter.checkLimit(mockRequest);
      expect(allowedResult.allowed).toBe(true);
    });
  });

  describe('Custom Key Generation', () => {
    it('should use custom key generator', async () => {
      const customKeyGenerator = (req: any) => `custom:${req.userId}`;
      const customLimiter = new RateLimiter({
        windowMs: 60000,
        maxRequests: 3,
        keyGenerator: customKeyGenerator
      }, store);

      const request1 = { userId: 'user1' };
      const request2 = { userId: 'user2' };

      // Make 3 requests for user1
      for (let i = 0; i < 3; i++) {
        const result = await customLimiter.checkLimit(request1);
        expect(result.allowed).toBe(true);
      }

      // 4th request for user1 should be blocked
      const blockedResult = await customLimiter.checkLimit(request1);
      expect(blockedResult.allowed).toBe(false);

      // user2 should still be allowed
      const allowedResult = await customLimiter.checkLimit(request2);
      expect(allowedResult.allowed).toBe(true);
    });
  });

  describe('Callbacks and Options', () => {
    it('should call onLimitReached callback', async () => {
      const onLimitReached = vi.fn();
      const callbackLimiter = new RateLimiter({
        windowMs: 60000,
        maxRequests: 1,
        onLimitReached
      }, store);

      const mockRequest = { ip: '127.0.0.1' };

      // First request should be allowed
      await callbackLimiter.checkLimit(mockRequest);

      // Second request should trigger callback
      await callbackLimiter.checkLimit(mockRequest);
      expect(onLimitReached).toHaveBeenCalledWith(mockRequest);
    });
  });

  describe('Middleware Integration', () => {
    it('should create Express-compatible middleware', async () => {
      const middleware = rateLimiter.middleware();

      const mockReq = { ip: '127.0.0.1' };
      const mockRes = {
        setHeader: vi.fn(),
        status: vi.fn().mockReturnThis(),
        json: vi.fn()
      };
      const mockNext = vi.fn();

      // Should call next() for allowed requests
      await middleware(mockReq, mockRes, mockNext);
      expect(mockNext).toHaveBeenCalled();
      expect(mockRes.setHeader).toHaveBeenCalledWith('X-RateLimit-Limit', 5);
    });

    it('should return 429 for blocked requests', async () => {
      const middleware = rateLimiter.middleware();

      const mockReq = { ip: '127.0.0.1' };
      const mockRes = {
        setHeader: vi.fn(),
        status: vi.fn().mockReturnThis(),
        json: vi.fn()
      };
      const mockNext = vi.fn();

      // Make requests to exceed limit
      for (let i = 0; i < 6; i++) {
        await middleware(mockReq, mockRes, mockNext);
      }

      // Last call should have returned 429
      expect(mockRes.status).toHaveBeenCalledWith(429);
      expect(mockRes.json).toHaveBeenCalledWith({
        error: 'Too Many Requests',
        message: 'Rate limit exceeded',
        retryAfter: expect.any(Number)
      });
    });
  });

  describe('Reset Functionality', () => {
    it('should reset limits for specific requests', async () => {
      const mockRequest = { ip: '127.0.0.1' };

      // Make requests to reach limit
      for (let i = 0; i < 5; i++) {
        await rateLimiter.checkLimit(mockRequest);
      }

      // Should be blocked
      const blockedResult = await rateLimiter.checkLimit(mockRequest);
      expect(blockedResult.allowed).toBe(false);

      // Reset the limit
      await rateLimiter.resetLimit(mockRequest);

      // Should be allowed again
      const allowedResult = await rateLimiter.checkLimit(mockRequest);
      expect(allowedResult.allowed).toBe(true);
    });
  });
});

describe('MemoryRateLimitStore', () => {
  let store: MemoryRateLimitStore;

  beforeEach(() => {
    store = new MemoryRateLimitStore();
  });

  afterEach(() => {
    store.destroy();
  });

  describe('Basic Operations', () => {
    it('should store and retrieve values', async () => {
      await store.set('test-key', 5, 1000);
      const value = await store.get('test-key');
      expect(value).toBe(5);
    });

    it('should return null for non-existent keys', async () => {
      const value = await store.get('non-existent');
      expect(value).toBeNull();
    });

    it('should increment values', async () => {
      const count1 = await store.increment('counter', 1000);
      const count2 = await store.increment('counter', 1000);

      expect(count1).toBe(1);
      expect(count2).toBe(2);
    });

    it('should reset values', async () => {
      await store.set('test-key', 10, 1000);
      await store.reset('test-key');

      const value = await store.get('test-key');
      expect(value).toBeNull();
    });

    it('should expire values after TTL', async () => {
      await store.set('expire-key', 1, 50); // 50ms TTL

      // Should exist immediately
      let value = await store.get('expire-key');
      expect(value).toBe(1);

      // Wait for expiration
      await new Promise(resolve => setTimeout(resolve, 100));

      // Should be expired
      value = await store.get('expire-key');
      expect(value).toBeNull();
    });
  });
});

describe('SlidingWindowRateLimiter', () => {
  let slidingLimiter: SlidingWindowRateLimiter;
  let store: MemoryRateLimitStore;

  beforeEach(() => {
    store = new MemoryRateLimitStore();
    slidingLimiter = new SlidingWindowRateLimiter({
      windowMs: 60000,
      maxRequests: 5
    }, store);
  });

  afterEach(() => {
    store.destroy();
  });

  it('should implement sliding window logic', async () => {
    const mockRequest = { ip: '127.0.0.1' };

    // Make requests within limit
    for (let i = 0; i < 5; i++) {
      const result = await slidingLimiter.checkLimit(mockRequest);
      expect(result.allowed).toBe(true);
    }

    // Should be blocked after limit
    const blockedResult = await slidingLimiter.checkLimit(mockRequest);
    expect(blockedResult.allowed).toBe(false);
  });

  it('should create middleware', async () => {
    const middleware = slidingLimiter.middleware();
    expect(typeof middleware).toBe('function');
  });
});
