import { describe, expect, it } from 'vitest';
import {
  buildRunCostSnapshot,
  comparativeCosts,
  estimateCost,
  matchPricing,
  normalizeModelId,
  pickFamilyRefs,
  type PricingTable,
} from '../src/tier1/pricing';

const FIXTURE: PricingTable = {
  'anthropic/claude-sonnet-4': {
    promptUsdPerTok: 3e-6,
    completionUsdPerTok: 15e-6,
    created: 2_000,
  },
  'anthropic/claude-opus-4': {
    promptUsdPerTok: 15e-6,
    completionUsdPerTok: 75e-6,
    created: 1_900,
  },
  'anthropic/claude-sonnet-4:free': {
    promptUsdPerTok: 0,
    completionUsdPerTok: 0,
    created: 2_100,
  },
  'openai/gpt-4o': {
    promptUsdPerTok: 2.5e-6,
    completionUsdPerTok: 10e-6,
    created: 1_800,
  },
  'openai/gpt-4o-mini': {
    promptUsdPerTok: 0.15e-6,
    completionUsdPerTok: 0.6e-6,
    created: 1_850,
  },
  'google/gemini-2.5-pro': {
    promptUsdPerTok: 1.25e-6,
    completionUsdPerTok: 10e-6,
    created: 1_700,
  },
  'x-ai/grok-3': {
    promptUsdPerTok: 3e-6,
    completionUsdPerTok: 15e-6,
    created: 1_600,
  },
  'moonshotai/kimi-k2': {
    promptUsdPerTok: 0.6e-6,
    completionUsdPerTok: 2.5e-6,
    created: 1_500,
  },
  'deepseek/deepseek-chat': {
    promptUsdPerTok: 0.14e-6,
    completionUsdPerTok: 0.28e-6,
    created: 1_400,
  },
  'qwen/qwen3-32b': {
    promptUsdPerTok: 0.1e-6,
    completionUsdPerTok: 0.3e-6,
    created: 1_300,
  },
  // Ambiguous normalized pair (same leaf after strip)
  'provider-a/dup-model': { promptUsdPerTok: 1e-6, completionUsdPerTok: 2e-6 },
  'provider-b/dup-model': { promptUsdPerTok: 3e-6, completionUsdPerTok: 4e-6 },
};

describe('matchPricing', () => {
  it('matches exact id', () => {
    const m = matchPricing('openai/gpt-4o', FIXTURE);
    expect(m.matched).toBe(true);
    if (m.matched) {
      expect(m.how).toBe('exact');
      expect(m.id).toBe('openai/gpt-4o');
    }
  });

  it('matches normalized unique (strip prefix / :free / -it)', () => {
    expect(normalizeModelId('OpenAI/GPT-4o:free')).toBe('gpt-4o');
    const m = matchPricing('gpt-4o', FIXTURE);
    expect(m.matched).toBe(true);
    if (m.matched) {
      expect(m.how).toBe('normalized');
      expect(m.id).toBe('openai/gpt-4o');
    }
  });

  it('refuses ambiguous normalized matches', () => {
    const m = matchPricing('dup-model', FIXTURE);
    expect(m.matched).toBe(false);
  });

  it('no match → unavailable reason', () => {
    const m = matchPricing('totally-unknown-model', FIXTURE);
    expect(m.matched).toBe(false);
    if (!m.matched) expect(m.reason).toContain('totally-unknown-model');
  });
});

describe('estimateCost arithmetic', () => {
  it('computes known dollars from fixture tokens', () => {
    const line = estimateCost({
      modelId: 'anthropic/claude-sonnet-4',
      tokensIn: 1_000_000,
      tokensOut: 500_000,
      table: FIXTURE,
    });
    expect(line.kind).toBe('estimated');
    // 1e6 * 3e-6 + 5e5 * 15e-6 = 3 + 7.5 = 10.5
    expect(line.usdTotal).toBeCloseTo(10.5, 6);
    expect(line.usdIn).toBeCloseTo(3, 6);
    expect(line.usdOut).toBeCloseTo(7.5, 6);
  });

  it('private host → $0 local compute', () => {
    const line = estimateCost({
      modelId: 'anything',
      tokensIn: 999,
      tokensOut: 999,
      table: FIXTURE,
      baseURL: 'http://192.168.1.10:1234/v1',
    });
    expect(line.kind).toBe('local');
    expect(line.usdTotal).toBe(0);
    expect(line.label).toContain('local compute');
  });

  it('explicit privateHost override', () => {
    const line = estimateCost({
      modelId: 'openai/gpt-4o',
      tokensIn: 100,
      tokensOut: 100,
      table: FIXTURE,
      privateHost: true,
    });
    expect(line.kind).toBe('local');
  });
});

describe('SET 12 family roster', () => {
  it('picks newest non-:free non-suffix member per family from feed', () => {
    const refs = pickFamilyRefs(FIXTURE);
    const byKey = Object.fromEntries(refs.map(r => [r.key, r.id]));
    expect(byKey.claude).toBe('anthropic/claude-sonnet-4'); // newer than opus; skips :free
    expect(byKey.chatgpt).toBe('openai/gpt-4o-mini'); // created 1850 > 1800
    expect(byKey.gemini).toBe('google/gemini-2.5-pro');
    expect(byKey.grok).toBe('x-ai/grok-3');
    expect(byKey.kimi).toBe('moonshotai/kimi-k2');
    expect(byKey.deepseek).toBe('deepseek/deepseek-chat');
    expect(byKey.qwen).toBe('qwen/qwen3-32b');
    expect(byKey.glm).toBeUndefined(); // absent from feed → dropped
  });

  it('comparativeCosts uses same arithmetic', () => {
    const rows = comparativeCosts(1_000_000, 0, FIXTURE);
    const claude = rows.find(r => r.id === 'anthropic/claude-sonnet-4');
    expect(claude?.usdTotal).toBeCloseTo(3, 6);
  });
});

describe('buildRunCostSnapshot', () => {
  it('returns null when feed unavailable', () => {
    expect(buildRunCostSnapshot({
      modelId: 'openai/gpt-4o',
      tokensIn: 10,
      tokensOut: 10,
      feed: { table: {}, fetchedAt: '', unavailable: true },
    })).toBeNull();
  });

  it('persists provenance + comparative snapshot', () => {
    const snap = buildRunCostSnapshot({
      modelId: 'openai/gpt-4o',
      tokensIn: 1000,
      tokensOut: 500,
      feed: { table: FIXTURE, fetchedAt: '2026-07-26T00:00:00.000Z', stale: true },
    });
    expect(snap).not.toBeNull();
    expect(snap!.provenance).toContain('openrouter.ai');
    expect(snap!.provenance).toContain('stale');
    expect(snap!.comparative.length).toBeGreaterThan(0);
    expect(snap!.usdTotal).toBeCloseTo(1000 * 2.5e-6 + 500 * 10e-6, 9);
  });
});
