import { describe, it, expect } from 'vitest';
import {
  isGatewayVirtualModel,
  parseVirtualModel,
  orderCandidates,
  splitComposite,
  fleetModelCandidates,
} from '../gateway.js';
import type { ModelScore } from '../routing-scores.js';
import { createFleetState, ensureHostState } from '../fleet-state.js';

function score(compositeId: string, partial: Partial<ModelScore> = {}): ModelScore {
  return {
    compositeId,
    providerId: compositeId.split('/')[0]!,
    model: compositeId.split('/').slice(1).join('/'),
    codeScore: null,
    chatScore: null,
    evalScore: null,
    avgGenTps: null,
    avgLatencyMs: null,
    sampleCount: 0,
    healthy: true,
    badges: [],
    ...partial,
  };
}

describe('isGatewayVirtualModel', () => {
  it('matches auto and auto:* tokens', () => {
    expect(isGatewayVirtualModel('auto')).toBe(true);
    expect(isGatewayVirtualModel('auto:code')).toBe(true);
    expect(isGatewayVirtualModel('auto:fast')).toBe(true);
  });
  it('does not match ordinary models', () => {
    expect(isGatewayVirtualModel('qwopus-35b')).toBe(false);
    expect(isGatewayVirtualModel('autobahn')).toBe(false);
  });
});

describe('parseVirtualModel', () => {
  it('strips a gateway provider prefix', () => {
    expect(parseVirtualModel('auto/auto:code')).toBe('auto:code');
  });
  it('passes a bare virtual model through', () => {
    expect(parseVirtualModel('auto:fast')).toBe('auto:fast');
  });
});

describe('splitComposite', () => {
  it('splits provider/model', () => {
    expect(splitComposite('sam-desktop/qwopus-35b')).toEqual({ providerId: 'sam-desktop', model: 'qwopus-35b' });
  });
  it('returns null for a bare id', () => {
    expect(splitComposite('qwopus-35b')).toBeNull();
  });
});

describe('orderCandidates', () => {
  it('orders auto:code by code score among healthy hosts', () => {
    const scores = [
      score('a/m1', { codeScore: 0.6 }),
      score('a/m2', { codeScore: 0.9 }),
      score('a/m3', { codeScore: 0.7, healthy: false }),
    ];
    expect(orderCandidates('auto:code', null, scores)).toEqual(['a/m2', 'a/m1']);
  });

  it('orders auto:fast by throughput', () => {
    const scores = [
      score('a/slow', { avgGenTps: 10 }),
      score('a/fast', { avgGenTps: 50 }),
    ];
    expect(orderCandidates('auto:fast', null, scores)).toEqual(['a/fast', 'a/slow']);
  });

  it('honors an explicit policy order and appends the fallback', () => {
    const scores = [score('a/m1'), score('a/m2'), score('a/fb')];
    const ordered = orderCandidates('auto:code', { candidates: ['a/m2', 'a/m1'], fallback: 'a/fb' }, scores);
    expect(ordered).toEqual(['a/m2', 'a/m1', 'a/fb']);
  });

  it('drops policy candidates whose host is unhealthy', () => {
    const scores = [score('a/m1', { healthy: false }), score('a/m2', { healthy: true })];
    const ordered = orderCandidates('auto:code', { candidates: ['a/m1', 'a/m2'], fallback: null }, scores);
    expect(ordered).toEqual(['a/m2']);
  });

  it('keeps a never-seen policy candidate (unknown health) for dispatch to try', () => {
    const scores = [score('a/known', { healthy: true })];
    const ordered = orderCandidates('auto:code', { candidates: ['a/never-seen', 'a/known'], fallback: null }, scores);
    expect(ordered).toEqual(['a/never-seen', 'a/known']);
  });
});

describe('fleetModelCandidates (cold-start fallback)', () => {
  it('lists connected hosts models, ready first, skips down hosts', () => {
    const fleet = createFleetState();
    const a = ensureHostState(fleet, 'sam-desktop');
    a.liveness = 'connected';
    a.models.set('m-ready', { model: 'm-ready', state: 'ready', ts: new Date(0), ttlDeadline: null, inflight: 0 });
    a.models.set('m-stop', { model: 'm-stop', state: 'stopped', ts: new Date(0), ttlDeadline: null, inflight: 0 });
    const b = ensureHostState(fleet, 'embedding');
    b.liveness = 'down';
    b.models.set('x', { model: 'x', state: 'ready', ts: new Date(0), ttlDeadline: null, inflight: 0 });

    const c = fleetModelCandidates(fleet);
    expect(c).toContain('sam-desktop/m-ready');
    expect(c).toContain('sam-desktop/m-stop');
    expect(c.indexOf('sam-desktop/m-ready')).toBeLessThan(c.indexOf('sam-desktop/m-stop')); // ready first
    expect(c).not.toContain('embedding/x'); // down host excluded
  });

  it('returns [] for an all-down fleet', () => {
    const fleet = createFleetState();
    const a = ensureHostState(fleet, 'h');
    a.liveness = 'down';
    expect(fleetModelCandidates(fleet)).toEqual([]);
  });
});
