import {
  OpenRouterAsrProvider,
  OpenRouterExtractionProvider,
} from './openrouter.provider';
import { VoiceProviderError } from './voice.providers';

const CONFIG = {
  apiKey: 'test-key',
  baseUrl: 'https://openrouter.test/api/v1',
  model: 'm',
};

const CATALOG = {
  treatmentTypes: [{ code: 'prosthesis', label: 'پروتز' }],
  prosthesisTypes: [{ code: 'pfm_crown', label: 'روکش پی‌اف‌ام' }],
  labs: [{ id: 'lab-1', name: 'لابراتوار سینا' }],
};

type ChatBody = {
  temperature: number;
  provider: { require_parameters: boolean };
  response_format: { type: string; json_schema: { strict: boolean } };
  messages: { role: string; content: string }[];
};

function parseBody(spy: jest.Mock): ChatBody {
  const init = (spy.mock.calls[0] as [string, RequestInit])[1];
  return JSON.parse(init.body as string) as ChatBody;
}

function mockFetch(response: {
  ok: boolean;
  status?: number;
  body?: unknown;
  text?: string;
}) {
  const spy = jest.fn().mockResolvedValue({
    ok: response.ok,
    status: response.status ?? (response.ok ? 200 : 500),
    json: () => Promise.resolve(response.body),
    text: () => Promise.resolve(response.text ?? ''),
  });
  global.fetch = spy;
  return spy;
}

afterEach(() => {
  jest.restoreAllMocks();
});

describe('OpenRouterAsrProvider', () => {
  it('posts base64 JSON with the language hint, per the documented STT contract', async () => {
    const spy = mockFetch({
      ok: true,
      body: { text: '  سلام  ', usage: { seconds: 12, cost: 0.002 } },
    });

    const result = await new OpenRouterAsrProvider(CONFIG).transcribe(
      { data: 'BASE64', format: 'webm' },
      'fa',
    );

    const [url, init] = spy.mock.calls[0] as [string, RequestInit];
    expect(url).toBe('https://openrouter.test/api/v1/audio/transcriptions');
    expect(init.headers).toMatchObject({ Authorization: 'Bearer test-key' });
    expect(JSON.parse(init.body as string)).toEqual({
      model: 'm',
      input_audio: { data: 'BASE64', format: 'webm' },
      language: 'fa',
    });
    expect(result.text).toBe('سلام');
    expect(result.usage).toEqual({ seconds: 12, costUsd: 0.002 });
  });

  it('reports missing usage as null rather than zero', async () => {
    mockFetch({ ok: true, body: { text: 'x' } });
    const result = await new OpenRouterAsrProvider(CONFIG).transcribe(
      { data: 'B', format: 'webm' },
      'en',
    );
    expect(result.usage).toEqual({ seconds: null, costUsd: null });
  });

  it('raises a staged error without leaking the vendor body', async () => {
    // A 4xx can echo the request back, transcript included.
    mockFetch({
      ok: false,
      status: 400,
      text: 'transcript: patient name here',
    });
    const provider = new OpenRouterAsrProvider(CONFIG);

    await expect(
      provider.transcribe({ data: 'B', format: 'webm' }, 'fa'),
    ).rejects.toMatchObject({
      name: 'VoiceProviderError',
      stage: 'asr',
      status: 400,
    });
    await expect(
      provider.transcribe({ data: 'B', format: 'webm' }, 'fa'),
    ).rejects.not.toThrow(/patient name/);
  });
});

describe('OpenRouterExtractionProvider', () => {
  const wireContent = JSON.stringify({
    treatmentType: 'prosthesis',
    teeth: [
      { spoken: 'یک چهار', fdi: '14', arch: null, side: null, position: null },
    ],
    connectedSpans: [],
    comment: null,
    prosthesisDefaultType: 'pfm_crown',
    prosthesisOverrides: [],
    labId: 'lab-1',
    labMatchExact: true,
    due: {
      kind: 'none',
      weekday: null,
      which: null,
      unit: null,
      amount: null,
      jy: null,
      jm: null,
      jd: null,
      y: null,
      m: null,
      d: null,
    },
  });

  it('constrains output with a strict JSON schema and a schema-honouring provider', async () => {
    const spy = mockFetch({
      ok: true,
      body: {
        choices: [{ message: { content: wireContent } }],
        usage: { cost: 0.0008 },
      },
    });

    const result = await new OpenRouterExtractionProvider(CONFIG).extract(
      'روی دندان ۱۴ روکش',
      CATALOG,
      'fa',
    );

    const body = parseBody(spy);
    expect(body.response_format.type).toBe('json_schema');
    expect(body.response_format.json_schema.strict).toBe(true);
    // Without require_parameters OpenRouter may route to a provider that treats the
    // schema as a hint and returns prose, failing parsing intermittently.
    expect(body.provider).toEqual({ require_parameters: true });
    expect(body.temperature).toBe(0);

    expect(result.intent.treatmentType).toBe('prosthesis');
    expect(result.intent.teeth[0]).toEqual({
      kind: 'explicit',
      fdi: '14',
      spoken: 'یک چهار',
    });
    expect(result.intent.due).toBeNull();
    expect(result.costUsd).toBe(0.0008);
  });

  it('sends the catalog codes and lab ids the model is allowed to choose from', async () => {
    const spy = mockFetch({
      ok: true,
      body: { choices: [{ message: { content: wireContent } }] },
    });
    await new OpenRouterExtractionProvider(CONFIG).extract('x', CATALOG, 'fa');

    const body = parseBody(spy);
    const system = body.messages[0].content;
    expect(system).toContain('prosthesis');
    expect(system).toContain('pfm_crown');
    expect(system).toContain('lab-1');
    expect(body.messages[1]).toEqual({ role: 'user', content: 'x' });
  });

  it('fails loudly on unparseable content rather than passing rubbish downstream', async () => {
    mockFetch({
      ok: true,
      body: { choices: [{ message: { content: 'I think tooth 14?' } }] },
    });
    await expect(
      new OpenRouterExtractionProvider(CONFIG).extract('x', CATALOG, 'fa'),
    ).rejects.toBeInstanceOf(VoiceProviderError);
  });

  it('fails when the model returns no content at all', async () => {
    mockFetch({ ok: true, body: { choices: [] } });
    await expect(
      new OpenRouterExtractionProvider(CONFIG).extract('x', CATALOG, 'fa'),
    ).rejects.toMatchObject({ stage: 'extraction' });
  });
});
