import { describe, it, expect } from 'vitest';
import type { ModelId, Backend } from '../types';
import {
  MODELS,
  DEFAULT_MODEL,
  recommendModel,
  modelsByTier,
  listModels,
  type ModelInfo,
  type ModelTier,
  type DeviceCaps,
} from './catalog';

const ALL_IDS: ModelId[] = [
  'tiny.en',
  'tiny',
  'base.en',
  'base',
  'small.en',
  'small',
];

describe('MODELS catalog', () => {
  it('contains exactly the six expected ids', () => {
    const keys = Object.keys(MODELS).sort();
    expect(keys).toEqual([...ALL_IDS].sort());
  });

  it('each entry id field matches its key', () => {
    for (const id of ALL_IDS) {
      const info = MODELS[id];
      expect(info.id).toBe(id);
    }
  });

  it('populates required descriptive fields with sane values', () => {
    for (const id of ALL_IDS) {
      const info = MODELS[id];
      expect(typeof info.label).toBe('string');
      expect(info.label.length).toBeGreaterThan(0);
      expect(info.approxMB).toBeGreaterThan(0);
      expect(info.webRepo.length).toBeGreaterThan(0);
      expect(info.ggml.length).toBeGreaterThan(0);
    }
  });

  it('webRepo and ggml follow the documented naming conventions', () => {
    for (const id of ALL_IDS) {
      const info = MODELS[id];
      expect(info.webRepo).toBe(`Xenova/whisper-${id}`);
      expect(info.ggml).toBe(`ggml-${id}.bin`);
    }
  });

  it('marks .en ids as English-only and the rest as multilingual', () => {
    for (const id of ALL_IDS) {
      const info = MODELS[id];
      if (id.endsWith('.en')) {
        expect(info.multilingual).toBe(false);
      } else {
        expect(info.multilingual).toBe(true);
      }
    }
  });

  it('assigns tiers by family: tiny=fast, base=balanced, small=accurate', () => {
    const expected: Record<ModelId, ModelTier> = {
      'tiny.en': 'fast',
      tiny: 'fast',
      'base.en': 'balanced',
      base: 'balanced',
      'small.en': 'accurate',
      small: 'accurate',
    };
    for (const id of ALL_IDS) {
      expect(MODELS[id].tier).toBe(expected[id]);
    }
  });

  it('uses the documented rough size buckets', () => {
    expect(MODELS['tiny.en'].approxMB).toBe(75);
    expect(MODELS.tiny.approxMB).toBe(75);
    expect(MODELS['base.en'].approxMB).toBe(145);
    expect(MODELS.base.approxMB).toBe(145);
    expect(MODELS['small.en'].approxMB).toBe(480);
    expect(MODELS.small.approxMB).toBe(480);
  });
});

describe('DEFAULT_MODEL', () => {
  it('is tiny.en', () => {
    expect(DEFAULT_MODEL).toBe('tiny.en');
  });

  it('is a real catalog entry', () => {
    expect(MODELS[DEFAULT_MODEL]).toBeDefined();
  });
});

describe('recommendModel', () => {
  const gpuBackends: Backend[] = ['webgpu', 'coreml', 'metal'];
  const cpuBackends: Backend[] = ['cpu', 'wasm'];

  it('recommends base.en for every GPU-class backend', () => {
    for (const backend of gpuBackends) {
      expect(recommendModel({ backend })).toBe('base.en');
    }
  });

  it('recommends tiny.en for every CPU-class backend', () => {
    for (const backend of cpuBackends) {
      expect(recommendModel({ backend })).toBe('tiny.en');
    }
  });

  it('recommends tiny.en when lowMemory, even on a GPU backend', () => {
    for (const backend of gpuBackends) {
      expect(recommendModel({ backend, lowMemory: true })).toBe('tiny.en');
    }
  });

  it('recommends tiny.en when lowMemory on a CPU backend', () => {
    for (const backend of cpuBackends) {
      expect(recommendModel({ backend, lowMemory: true })).toBe('tiny.en');
    }
  });

  it('treats lowMemory:false the same as omitting it', () => {
    expect(recommendModel({ backend: 'webgpu', lowMemory: false })).toBe(
      'base.en',
    );
    expect(recommendModel({ backend: 'cpu', lowMemory: false })).toBe(
      'tiny.en',
    );
  });

  it('never recommends a small.* model for any input', () => {
    const allBackends: Backend[] = [...gpuBackends, ...cpuBackends];
    for (const backend of allBackends) {
      for (const lowMemory of [undefined, false, true]) {
        const caps: DeviceCaps = { backend, lowMemory };
        const picked = recommendModel(caps);
        expect(picked.startsWith('small')).toBe(false);
      }
    }
  });

  it('always returns an id present in the catalog', () => {
    const allBackends: Backend[] = [...gpuBackends, ...cpuBackends];
    for (const backend of allBackends) {
      const picked = recommendModel({ backend });
      expect(MODELS[picked]).toBeDefined();
    }
  });
});

describe('modelsByTier', () => {
  it('returns the tiny models for the fast tier', () => {
    const fast = modelsByTier('fast');
    const ids = fast.map((m) => m.id).sort();
    expect(ids).toEqual(['tiny', 'tiny.en']);
  });

  it('returns the base models for the balanced tier', () => {
    const balanced = modelsByTier('balanced');
    const ids = balanced.map((m) => m.id).sort();
    expect(ids).toEqual(['base', 'base.en']);
  });

  it('returns the small models for the accurate tier', () => {
    const accurate = modelsByTier('accurate');
    const ids = accurate.map((m) => m.id).sort();
    expect(ids).toEqual(['small', 'small.en']);
  });

  it('returns entries whose tier matches the query', () => {
    const tiers: ModelTier[] = ['fast', 'balanced', 'accurate'];
    for (const tier of tiers) {
      for (const info of modelsByTier(tier)) {
        expect(info.tier).toBe(tier);
      }
    }
  });
});

describe('listModels', () => {
  it('lists all six models', () => {
    const list = listModels();
    expect(list).toHaveLength(6);
  });

  it('returns full ModelInfo objects with matching ids', () => {
    for (const info of listModels()) {
      const typed: ModelInfo = info;
      expect(MODELS[typed.id]).toEqual(typed);
    }
  });

  it('covers exactly the catalog ids with no duplicates', () => {
    const ids = listModels().map((m) => m.id);
    expect(new Set(ids).size).toBe(ids.length);
    expect([...ids].sort()).toEqual([...ALL_IDS].sort());
  });
});
