import { describe, it, expect, beforeEach } from 'vitest';
import {
  selectCandidates,
  pickNext,
  excludeCandidate,
  createRoundRobinState,
} from '../selector.js';
import { HealthTracker } from '../health.js';
import type { ProviderConfig } from '../types.js';

const MOCK_PROVIDERS: ProviderConfig[] = [
  {
    name: 'fast-provider',
    baseUrl: 'https://fast.example.com/v1',
    apiKeyEnv: 'FAST_KEY',
    rpmLimit: 30,
    tpmLimit: 10_000,
    models: [
      {
        id: 'small-8b',
        label: 'Small 8B',
        contextWindow: 8_192,
        strengths: ['general'],
        speedTier: 1,
      },
      {
        id: 'large-70b',
        label: 'Large 70B',
        contextWindow: 128_000,
        strengths: ['code', 'reasoning'],
        speedTier: 1,
      },
    ],
  },
  {
    name: 'quality-provider',
    baseUrl: 'https://quality.example.com/v1',
    apiKeyEnv: 'QUALITY_KEY',
    rpmLimit: 20,
    tpmLimit: 0,
    models: [
      {
        id: 'deepseek-r1',
        label: 'DeepSeek R1',
        contextWindow: 64_000,
        strengths: ['reasoning', 'code', 'math'],
        speedTier: 3,
      },
    ],
  },
];

describe('selectCandidates', () => {
  let health: HealthTracker;

  beforeEach(() => {
    health = new HealthTracker();
  });

  it('returns candidates sorted by score for code', () => {
    const candidates = selectCandidates(MOCK_PROVIDERS, 'code', health);
    expect(candidates.length).toBeGreaterThan(0);
    // large-70b and deepseek-r1 should score high for code
    const names = candidates.map(c => c.model.id);
    expect(names[0]).toBe('large-70b'); // speed 1 + code strength + 70b bonus
  });

  it('returns candidates sorted by score for general', () => {
    const candidates = selectCandidates(MOCK_PROVIDERS, 'general', health);
    // small-8b has 'general' strength + speed tier 1
    expect(candidates[0]!.model.id).toBe('small-8b');
  });

  it('filters out unhealthy providers', () => {
    // Make fast-provider/large-70b unhealthy
    for (let i = 0; i < 5; i++) {
      health.record('fast-provider', 'large-70b', {
        timestamp: Date.now(),
        latencyMs: 100,
        status: 'error',
      });
    }
    const candidates = selectCandidates(MOCK_PROVIDERS, 'code', health);
    const ids = candidates.map(c => `${c.provider.name}::${c.model.id}`);
    expect(ids).not.toContain('fast-provider::large-70b');
  });
});

describe('pickNext', () => {
  it('returns null for empty candidates', () => {
    const state = createRoundRobinState();
    expect(pickNext([], state)).toBeNull();
  });

  it('returns the only candidate when there is one', () => {
    const state = createRoundRobinState();
    const candidate = { provider: MOCK_PROVIDERS[0]!, model: MOCK_PROVIDERS[0]!.models[0]! };
    expect(pickNext([candidate], state)).toBe(candidate);
  });

  it('round-robins across providers', () => {
    const state = createRoundRobinState();
    const candidates = [
      { provider: MOCK_PROVIDERS[0]!, model: MOCK_PROVIDERS[0]!.models[0]! },
      { provider: MOCK_PROVIDERS[1]!, model: MOCK_PROVIDERS[1]!.models[0]! },
    ];

    const first = pickNext(candidates, state);
    const second = pickNext(candidates, state);
    expect(first!.provider.name).not.toBe(second!.provider.name);
  });

  it('uses independent state per instance', () => {
    const stateA = createRoundRobinState();
    const stateB = createRoundRobinState();
    const candidates = [
      { provider: MOCK_PROVIDERS[0]!, model: MOCK_PROVIDERS[0]!.models[0]! },
      { provider: MOCK_PROVIDERS[1]!, model: MOCK_PROVIDERS[1]!.models[0]! },
    ];

    const fromA = pickNext(candidates, stateA);
    const fromB = pickNext(candidates, stateB);
    // Both start at same position since states are independent
    expect(fromA!.provider.name).toBe(fromB!.provider.name);
  });
});

describe('excludeCandidate', () => {
  it('removes the specified candidate', () => {
    const candidates = [
      { provider: MOCK_PROVIDERS[0]!, model: MOCK_PROVIDERS[0]!.models[0]! },
      { provider: MOCK_PROVIDERS[1]!, model: MOCK_PROVIDERS[1]!.models[0]! },
    ];
    const remaining = excludeCandidate(candidates, 'fast-provider', 'small-8b');
    expect(remaining).toHaveLength(1);
    expect(remaining[0]!.provider.name).toBe('quality-provider');
  });
});
