/**
 * Tests for PerplexityProvider and GeminiProvider.
 * Uses vi.stubGlobal to mock fetch — no real API calls.
 */

import { describe, it, expect, vi, afterEach } from 'vitest';
import { PerplexityProvider } from '../providers/perplexity.js';
import { GeminiProvider } from '../providers/gemini.js';

const makeOpenAIResponse = (content: string, model = 'test-model') => ({
  choices: [{ message: { content }, finish_reason: 'stop' }],
  model,
  usage: { prompt_tokens: 5, completion_tokens: 10, total_tokens: 15 },
});

const makeGeminiResponse = (text: string) => ({
  candidates: [{ content: { parts: [{ text }] }, finishReason: 'STOP' }],
  usageMetadata: { promptTokenCount: 5, candidatesTokenCount: 10, totalTokenCount: 15 },
});

afterEach(() => {
  vi.restoreAllMocks();
  vi.unstubAllEnvs();
});

// ── PerplexityProvider ──────────────────────────────────────────

describe('PerplexityProvider', () => {
  it('isConfigured false without API key', () => {
    const p = new PerplexityProvider({ apiKey: '' });
    expect(p.isConfigured()).toBe(false);
  });

  it('isConfigured true with API key', () => {
    const p = new PerplexityProvider({ apiKey: 'test-key' });
    expect(p.isConfigured()).toBe(true);
  });

  it('reads apiKey from env', () => {
    vi.stubEnv('PERPLEXITY_API_KEY', 'env-key');
    const p = new PerplexityProvider();
    expect(p.isConfigured()).toBe(true);
  });

  it('throws when not configured', async () => {
    const p = new PerplexityProvider({ apiKey: '' });
    await expect(p.chatCompletion({ messages: [{ role: 'user', content: 'hi' }] })).rejects.toThrow(
      'Perplexity is not configured'
    );
  });

  it('calls Perplexity API and maps response', async () => {
    const fetchMock = vi.fn().mockResolvedValue({
      ok: true,
      json: async () => makeOpenAIResponse('analysis result', 'sonar'),
    });
    vi.stubGlobal('fetch', fetchMock);

    const p = new PerplexityProvider({ apiKey: 'test-key', model: 'sonar' });
    const result = await p.chatCompletion({
      messages: [{ role: 'user', content: 'analyse BTC' }],
      temperature: 0.2,
    });

    expect(result.content).toBe('analysis result');
    expect(result.model).toBe('sonar');
    expect(result.finishReason).toBe('stop');
    expect(result.usage.totalTokens).toBe(15);

    const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
    expect(url).toBe('https://api.perplexity.ai/chat/completions');
    expect((init.headers as Record<string, string>)['Authorization']).toBe('Bearer test-key');
  });

  it('throws on non-ok response', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: false,
        status: 429,
        text: async () => 'rate limited',
      })
    );

    const p = new PerplexityProvider({ apiKey: 'test-key' });
    await expect(p.chatCompletion({ messages: [{ role: 'user', content: 'hi' }] })).rejects.toThrow(
      'Perplexity error 429'
    );
  });
});

// ── GeminiProvider ──────────────────────────────────────────────

describe('GeminiProvider', () => {
  it('isConfigured false without API key', () => {
    const p = new GeminiProvider({ apiKey: '' });
    expect(p.isConfigured()).toBe(false);
  });

  it('isConfigured true with API key', () => {
    const p = new GeminiProvider({ apiKey: 'test-key' });
    expect(p.isConfigured()).toBe(true);
  });

  it('reads apiKey from env', () => {
    vi.stubEnv('GEMINI_API_KEY', 'env-key');
    const p = new GeminiProvider();
    expect(p.isConfigured()).toBe(true);
  });

  it('throws when not configured', async () => {
    const p = new GeminiProvider({ apiKey: '' });
    await expect(p.chatCompletion({ messages: [{ role: 'user', content: 'hi' }] })).rejects.toThrow(
      'Gemini is not configured'
    );
  });

  it('calls Gemini API and maps response', async () => {
    const fetchMock = vi.fn().mockResolvedValue({
      ok: true,
      json: async () => makeGeminiResponse('gemini analysis'),
    });
    vi.stubGlobal('fetch', fetchMock);

    const p = new GeminiProvider({ apiKey: 'test-key', model: 'gemini-1.5-flash' });
    const result = await p.chatCompletion({
      messages: [
        { role: 'system', content: 'You are a trading assistant.' },
        { role: 'user', content: 'analyse BTC' },
      ],
      temperature: 0.2,
    });

    expect(result.content).toBe('gemini analysis');
    expect(result.model).toBe('gemini-1.5-flash');
    expect(result.finishReason).toBe('stop');
    expect(result.usage.totalTokens).toBe(15);

    const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
    expect(url).toContain('generativelanguage.googleapis.com');
    expect(url).toContain('gemini-1.5-flash');
    expect(url).toContain('test-key');

    const body = JSON.parse(init.body as string);
    expect(body.systemInstruction.parts[0].text).toBe('You are a trading assistant.');
    expect(body.contents[0].role).toBe('user');
  });

  it('maps MAX_TOKENS finish reason to length', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: true,
        json: async () => ({
          candidates: [{ content: { parts: [{ text: 'truncated' }] }, finishReason: 'MAX_TOKENS' }],
          usageMetadata: { promptTokenCount: 1, candidatesTokenCount: 1, totalTokenCount: 2 },
        }),
      })
    );

    const p = new GeminiProvider({ apiKey: 'test-key' });
    const result = await p.chatCompletion({ messages: [{ role: 'user', content: 'hi' }] });
    expect(result.finishReason).toBe('length');
  });

  it('throws on non-ok response', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: false,
        status: 400,
        text: async () => 'bad request',
      })
    );

    const p = new GeminiProvider({ apiKey: 'test-key' });
    await expect(p.chatCompletion({ messages: [{ role: 'user', content: 'hi' }] })).rejects.toThrow(
      'Gemini error 400'
    );
  });
});
