import { describe, it, expect, beforeEach } from 'vitest';
import { RateLimiter } from './rate-limiter.js';
import type { ServiceName } from './rate-limiter.js';

describe('RateLimiter', () => {
  let limiter: RateLimiter;

  beforeEach(() => {
    limiter = new RateLimiter();
  });

  describe('acquire', () => {
    it('should allow requests within per-second limit', async () => {
      const result = await limiter.acquire('openrouter');
      expect(result.allowed).toBe(true);
      expect(result.waitMs).toBe(0);
    });

    it('should deny requests exceeding per-second limit', async () => {
      // openrouter default: 5 req/s
      for (let i = 0; i < 5; i++) {
        const r = await limiter.acquire('openrouter');
        expect(r.allowed).toBe(true);
      }
      const denied = await limiter.acquire('openrouter');
      expect(denied.allowed).toBe(false);
      expect(denied.waitMs).toBeGreaterThan(0);
    });

    it('should deny requests exceeding per-minute limit', async () => {
      // Use a custom config with low per-minute limit but high per-second
      const lim = new RateLimiter({
        kieApi: { requestsPerSecond: 100, requestsPerMinute: 3 },
      });
      for (let i = 0; i < 3; i++) {
        const r = await lim.acquire('kieApi');
        expect(r.allowed).toBe(true);
      }
      const denied = await lim.acquire('kieApi');
      expect(denied.allowed).toBe(false);
    });

    it('should use chat-specific key when chatId is provided', async () => {
      // telegramBotApi messagesPerChatPerSecond = 1
      const r1 = await limiter.acquire('telegramBotApi', 'chat123');
      expect(r1.allowed).toBe(true);

      const r2 = await limiter.acquire('telegramBotApi', 'chat123');
      expect(r2.allowed).toBe(false);

      // Different chat should still be allowed
      const r3 = await limiter.acquire('telegramBotApi', 'chat456');
      expect(r3.allowed).toBe(true);
    });
  });

  describe('exponential backoff', () => {
    it('should calculate exponential backoff: 1s, 2s, 4s, 8s, 16s', async () => {
      const lim = new RateLimiter({
        openrouter: { requestsPerSecond: 1, requestsPerMinute: 1000 },
      });

      // First request succeeds
      await lim.acquire('openrouter');

      // Subsequent requests hit per-second limit and get increasing backoff
      const waitTimes: number[] = [];
      for (let i = 0; i < 5; i++) {
        const r = await lim.acquire('openrouter');
        if (!r.allowed) waitTimes.push(r.waitMs);
      }

      expect(waitTimes[0]).toBe(1000);  // 2^0 * 1000
      expect(waitTimes[1]).toBe(2000);  // 2^1 * 1000
      expect(waitTimes[2]).toBe(4000);  // 2^2 * 1000
      expect(waitTimes[3]).toBe(8000);  // 2^3 * 1000
    });

    it('should cap backoff at 60 seconds', async () => {
      const lim = new RateLimiter({
        openrouter: { requestsPerSecond: 1, requestsPerMinute: 1000 },
      });

      await lim.acquire('openrouter');

      // Hit limit many times to exceed cap
      let lastWait = 0;
      for (let i = 0; i < 10; i++) {
        const r = await lim.acquire('openrouter');
        if (!r.allowed) lastWait = r.waitMs;
      }

      expect(lastWait).toBeLessThanOrEqual(60000);
    });

    it('should reset backoff on successful acquire', async () => {
      const lim = new RateLimiter({
        openrouter: { requestsPerSecond: 2, requestsPerMinute: 1000 },
      });

      // Use up limit
      await lim.acquire('openrouter');
      await lim.acquire('openrouter');

      // This should fail and increment backoff
      const denied = await lim.acquire('openrouter');
      expect(denied.allowed).toBe(false);
      expect(lim.getBackoffCount('openrouter')).toBe(1);

      // Create a fresh limiter to simulate time passing (backoff resets on success)
      const lim2 = new RateLimiter({
        openrouter: { requestsPerSecond: 2, requestsPerMinute: 1000 },
      });
      const ok = await lim2.acquire('openrouter');
      expect(ok.allowed).toBe(true);
      expect(lim2.getBackoffCount('openrouter')).toBe(0);
    });
  });

  describe('isMaxRetriesExceeded', () => {
    it('should return false when backoff count is below max', () => {
      expect(limiter.isMaxRetriesExceeded('openrouter')).toBe(false);
    });

    it('should return true after max retries exceeded', async () => {
      const lim = new RateLimiter({
        openrouter: { requestsPerSecond: 1, requestsPerMinute: 1000 },
      });

      await lim.acquire('openrouter');

      // Hit limit 5 times (default maxRetries)
      for (let i = 0; i < 5; i++) {
        await lim.acquire('openrouter');
      }

      expect(lim.isMaxRetriesExceeded('openrouter')).toBe(true);
    });

    it('should support custom maxRetries', async () => {
      const lim = new RateLimiter({
        openrouter: { requestsPerSecond: 1, requestsPerMinute: 1000 },
      });

      await lim.acquire('openrouter');

      for (let i = 0; i < 3; i++) {
        await lim.acquire('openrouter');
      }

      expect(lim.isMaxRetriesExceeded('openrouter', 3)).toBe(true);
      expect(lim.isMaxRetriesExceeded('openrouter', 5)).toBe(false);
    });
  });

  describe('resetBackoff', () => {
    it('should reset backoff count to zero', async () => {
      const lim = new RateLimiter({
        openrouter: { requestsPerSecond: 1, requestsPerMinute: 1000 },
      });

      await lim.acquire('openrouter');
      await lim.acquire('openrouter'); // triggers backoff

      expect(lim.getBackoffCount('openrouter')).toBeGreaterThan(0);

      lim.resetBackoff('openrouter');
      expect(lim.getBackoffCount('openrouter')).toBe(0);
    });
  });

  describe('recordRequest', () => {
    it('should record a request in the window', async () => {
      limiter.recordRequest('openrouter');
      // After recording, the next acquire should see it in the window
      // openrouter allows 5/s, so 4 more should be fine
      for (let i = 0; i < 4; i++) {
        const r = await limiter.acquire('openrouter');
        expect(r.allowed).toBe(true);
      }
      // 6th should be denied (1 recorded + 4 acquired = 5, next is over)
      const denied = await limiter.acquire('openrouter');
      expect(denied.allowed).toBe(false);
    });
  });

  describe('config management', () => {
    it('should use default config when none provided', () => {
      const config = limiter.getConfig();
      expect(config.openrouter.requestsPerSecond).toBe(5);
      expect(config.openrouter.requestsPerMinute).toBe(60);
      expect(config.kieApi.requestsPerSecond).toBe(2);
      expect(config.telegramBotApi.requestsPerSecond).toBe(30);
    });

    it('should merge partial config with defaults', () => {
      const lim = new RateLimiter({
        openrouter: { requestsPerSecond: 10, requestsPerMinute: 120 },
      });
      const config = lim.getConfig();
      expect(config.openrouter.requestsPerSecond).toBe(10);
      expect(config.kieApi.requestsPerSecond).toBe(2); // default preserved
    });

    it('should update config at runtime', () => {
      limiter.updateConfig({
        openrouter: { requestsPerSecond: 20, requestsPerMinute: 200 },
      });
      const config = limiter.getConfig();
      expect(config.openrouter.requestsPerSecond).toBe(20);
      expect(config.openrouter.requestsPerMinute).toBe(200);
    });

    it('should return a copy of config (not reference)', () => {
      const config = limiter.getConfig();
      config.openrouter.requestsPerSecond = 999;
      expect(limiter.getConfig().openrouter.requestsPerSecond).toBe(5);
    });
  });
});
