const { createLimiter, createCircuitBreaker, API_BUDGETS } = require('../../src/utils/rateLimiter');

describe('createLimiter', () => {
  test('first N tokens within capacity resolve immediately', async () => {
    const limiter = createLimiter({ tokensPerInterval: 3, interval: 60_000 });
    const start = Date.now();
    expect(await limiter.waitForToken()).toBe(true);
    expect(await limiter.waitForToken()).toBe(true);
    expect(await limiter.waitForToken()).toBe(true);
    expect(Date.now() - start).toBeLessThan(100);
  });

  test('fourth token in a small bucket waits for refill', async () => {
    const limiter = createLimiter({ tokensPerInterval: 2, interval: 200 });
    expect(await limiter.waitForToken()).toBe(true);
    expect(await limiter.waitForToken()).toBe(true);
    const start = Date.now();
    expect(await limiter.waitForToken(2_000)).toBe(true);
    const elapsed = Date.now() - start;
    // Should take roughly interval/tokens = 100ms before refill yields one.
    expect(elapsed).toBeGreaterThan(50);
    expect(elapsed).toBeLessThan(500);
  });

  test('timeout returns false (does not throw) so caller can proceed', async () => {
    const limiter = createLimiter({ tokensPerInterval: 1, interval: 60_000 });
    expect(await limiter.waitForToken()).toBe(true); // consume the one token
    const result = await limiter.waitForToken(120);  // wait briefly, then bail
    expect(result).toBe(false);
  });

  test('throws on invalid config', () => {
    expect(() => createLimiter({ tokensPerInterval: 0, interval: 1000 })).toThrow();
    expect(() => createLimiter({ tokensPerInterval: 1, interval: 0 })).toThrow();
  });
});

describe('createCircuitBreaker', () => {
  test('closed initially; success resets failure count', async () => {
    const cb = createCircuitBreaker({ failureThreshold: 2, resetTimeout: 60_000 });
    const result = await cb.call(async () => 'ok');
    expect(result).toBe('ok');
    expect(cb.snapshot().state).toBe('closed');
  });

  test('opens after threshold failures and rejects further calls', async () => {
    const cb = createCircuitBreaker({ failureThreshold: 2, resetTimeout: 60_000 });
    await expect(cb.call(async () => { throw new Error('boom'); })).rejects.toThrow('boom');
    await expect(cb.call(async () => { throw new Error('boom'); })).rejects.toThrow('boom');
    expect(cb.snapshot().state).toBe('open');
    await expect(cb.call(async () => 'should never run')).rejects.toMatchObject({ code: 'CIRCUIT_OPEN' });
  });

  test('transitions to half-open after resetTimeout, closes on success', async () => {
    const cb = createCircuitBreaker({ failureThreshold: 1, resetTimeout: 50 });
    await expect(cb.call(async () => { throw new Error('x'); })).rejects.toThrow();
    expect(cb.snapshot().state).toBe('open');
    await new Promise((r) => setTimeout(r, 80));
    // Reading the snapshot triggers the transition check.
    expect(cb.snapshot().state).toBe('half_open');
    const result = await cb.call(async () => 'recovered');
    expect(result).toBe('recovered');
    expect(cb.snapshot().state).toBe('closed');
  });

  test('half-open failure re-opens the circuit immediately', async () => {
    const cb = createCircuitBreaker({ failureThreshold: 1, resetTimeout: 30 });
    await expect(cb.call(async () => { throw new Error('first'); })).rejects.toThrow();
    await new Promise((r) => setTimeout(r, 60));
    await expect(cb.call(async () => { throw new Error('second'); })).rejects.toThrow('second');
    expect(cb.snapshot().state).toBe('open');
  });
});

describe('API_BUDGETS', () => {
  test('contains the five named upstreams', () => {
    expect(API_BUDGETS).toMatchObject({
      sharpApi: { tokensPerInterval: 10, interval: 60_000 },
      espn: { tokensPerInterval: 2, interval: 60_000 },
      mlbStats: { tokensPerInterval: 2, interval: 60_000 },
      oddsPapi: { tokensPerInterval: 5, interval: 60_000 },
      openRouter: { tokensPerInterval: 15, interval: 60_000 },
    });
  });

  test('is frozen — adapters cannot mutate it accidentally', () => {
    expect(Object.isFrozen(API_BUDGETS)).toBe(true);
  });
});
