import { describe, expect, it, vi } from 'vitest';

import type { ModelClient } from './types.js';
import { RotatingModelClient } from './rotating.js';

function makeClient(chatImpl: ModelClient['chat']): ModelClient {
  return { chat: chatImpl };
}

describe('RotatingModelClient', () => {
  it('throws when created with no clients', () => {
    expect(() => new RotatingModelClient([])).toThrow(/at least one client/i);
  });

  it('falls through to the next profile when the first fails', async () => {
    const first = makeClient(vi.fn().mockRejectedValue(new Error('rate limited')));
    const second = makeClient(vi.fn().mockResolvedValue({ content: 'ok' }));
    const rotating = new RotatingModelClient([first, second]);

    const response = await rotating.chat({ messages: [{ role: 'user', content: 'hello' }] });
    expect(response.content).toBe('ok');
    expect(first.chat).toHaveBeenCalledTimes(1);
    expect(second.chat).toHaveBeenCalledTimes(1);
  });

  it('sticks to the last successful profile until it fails', async () => {
    const first = makeClient(vi.fn().mockRejectedValue(new Error('429')));
    const second = makeClient(vi.fn().mockResolvedValue({ content: 'ok' }));
    const rotating = new RotatingModelClient([first, second]);

    await rotating.chat({ messages: [{ role: 'user', content: 'a' }] });
    await rotating.chat({ messages: [{ role: 'user', content: 'b' }] });

    expect(first.chat).toHaveBeenCalledTimes(1);
    expect(second.chat).toHaveBeenCalledTimes(2);
  });

  it('applies cooldown to failed profiles before retrying them', async () => {
    let nowMs = 1_000;
    const now = () => nowMs;

    const first = makeClient(vi.fn()
      .mockRejectedValueOnce(new Error('429'))
      .mockResolvedValue({ content: 'first-ok' }));
    const second = makeClient(vi.fn()
      .mockResolvedValueOnce({ content: 'second-ok' })
      .mockResolvedValueOnce({ content: 'second-ok' })
      .mockRejectedValueOnce(new Error('temp-2'))
      .mockResolvedValue({ content: 'second-ok' }));
    const rotating = new RotatingModelClient([first, second], { cooldownMs: 10_000, now });

    const r1 = await rotating.chat({ messages: [{ role: 'user', content: 'a' }] });
    expect(r1.content).toBe('second-ok');

    nowMs += 1_000;
    const r2 = await rotating.chat({ messages: [{ role: 'user', content: 'b' }] });
    expect(r2.content).toBe('second-ok');

    nowMs += 11_000;
    const r3 = await rotating.chat({ messages: [{ role: 'user', content: 'c' }] });
    expect(r3.content).toBe('first-ok');

    expect(first.chat).toHaveBeenCalledTimes(2);
    expect(second.chat).toHaveBeenCalledTimes(3);
  });

  it('still attempts cooling profiles when all profiles are cooling down', async () => {
    let nowMs = 1_000;
    const now = () => nowMs;
    const first = makeClient(vi.fn().mockRejectedValue(new Error('down-1')));
    const second = makeClient(vi.fn().mockRejectedValue(new Error('down-2')));
    const rotating = new RotatingModelClient([first, second], { cooldownMs: 30_000, now });

    await expect(rotating.chat({ messages: [{ role: 'user', content: 'a' }] }))
      .rejects.toThrow(/all auth profiles failed/i);

    nowMs += 100;
    await expect(rotating.chat({ messages: [{ role: 'user', content: 'b' }] }))
      .rejects.toThrow(/all auth profiles failed/i);

    expect(first.chat).toHaveBeenCalledTimes(2);
    expect(second.chat).toHaveBeenCalledTimes(2);
  });
});
