import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { OpenRouterClient } from './client.js';
import type { ChatMessage } from './client.js';
import type { OpenRouterResponse } from '../../types/api.js';

// Mock undici fetch
const mockFetch = vi.fn();
vi.mock('undici', () => ({ fetch: (...args: unknown[]) => mockFetch(...args) }));

// Mock logger
vi.mock('../../utils/logger.js', () => ({
  createModuleLogger: () => ({
    debug: vi.fn(),
    info: vi.fn(),
    warn: vi.fn(),
    error: vi.fn(),
  }),
}));

// Mock config
vi.mock('../../utils/config.js', () => ({
  config: { openrouterApiKey: 'test-api-key' },
}));

function makeOkResponse(data: OpenRouterResponse) {
  return {
    ok: true,
    status: 200,
    statusText: 'OK',
    json: () => Promise.resolve(data),
    text: () => Promise.resolve(JSON.stringify(data)),
  };
}

function makeErrorResponse(status: number, body = 'error') {
  return {
    ok: false,
    status,
    statusText: 'Error',
    json: () => Promise.resolve({ error: body }),
    text: () => Promise.resolve(body),
  };
}

const SAMPLE_RESPONSE: OpenRouterResponse = {
  id: 'gen-123',
  choices: [{ message: { role: 'assistant', content: 'Hello!' }, finish_reason: 'stop' }],
  usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
};

const SAMPLE_MESSAGES: ChatMessage[] = [
  { role: 'system', content: 'You are helpful.' },
  { role: 'user', content: 'Hi' },
];

describe('OpenRouterClient', () => {
  let client: OpenRouterClient;

  beforeEach(() => {
    client = new OpenRouterClient('test-key-123', { initialBackoffMs: 1 });
    mockFetch.mockReset();
  });

  afterEach(() => {
  });

  describe('constructor', () => {
    it('allows empty API key for deferred configuration', () => {
      expect(new OpenRouterClient('')).toBeInstanceOf(OpenRouterClient);
    });

    it('creates instance with valid key', () => {
      expect(new OpenRouterClient('valid-key')).toBeInstanceOf(OpenRouterClient);
    });
  });

  describe('chatCompletion', () => {
    it('sends correct request and returns response', async () => {
      mockFetch.mockResolvedValueOnce(makeOkResponse(SAMPLE_RESPONSE));

      const result = await client.chatCompletion(SAMPLE_MESSAGES);

      expect(result).toEqual(SAMPLE_RESPONSE);
      expect(mockFetch).toHaveBeenCalledOnce();

      const [url, options] = mockFetch.mock.calls[0] as [string, RequestInit];
      expect(url).toBe('https://openrouter.ai/api/v1/chat/completions');
      expect(options.method).toBe('POST');

      const headers = options.headers as Record<string, string>;
      expect(headers['Authorization']).toBe('Bearer test-key-123');
      expect(headers['Content-Type']).toBe('application/json');

      const body = JSON.parse(options.body as string);
      expect(body.model).toBe('google/gemini-3.1-flash-lite-preview');
      expect(body.messages).toEqual(SAMPLE_MESSAGES);
      expect(body.max_tokens).toBe(500);
      expect(body.temperature).toBeGreaterThanOrEqual(0.7);
      expect(body.temperature).toBeLessThanOrEqual(0.9);
    });

    it('uses custom options when provided', async () => {
      mockFetch.mockResolvedValueOnce(makeOkResponse(SAMPLE_RESPONSE));

      await client.chatCompletion(SAMPLE_MESSAGES, {
        temperature: 0.75,
        max_tokens: 1000,
        top_p: 0.95,
      });

      const body = JSON.parse((mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string);
      expect(body.temperature).toBe(0.75);
      expect(body.max_tokens).toBe(1000);
      expect(body.top_p).toBe(0.95);
    });

    it('clamps temperature to 0.7-0.9 range', async () => {
      mockFetch.mockResolvedValue(makeOkResponse(SAMPLE_RESPONSE));

      await client.chatCompletion(SAMPLE_MESSAGES, { temperature: 0.3 });
      let body = JSON.parse((mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string);
      expect(body.temperature).toBe(0.7);

      await client.chatCompletion(SAMPLE_MESSAGES, { temperature: 1.5 });
      body = JSON.parse((mockFetch.mock.calls[1] as [string, RequestInit])[1].body as string);
      expect(body.temperature).toBe(0.9);
    });

    it('does not include top_p when not provided', async () => {
      mockFetch.mockResolvedValueOnce(makeOkResponse(SAMPLE_RESPONSE));

      await client.chatCompletion(SAMPLE_MESSAGES);

      const body = JSON.parse((mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string);
      expect(body).not.toHaveProperty('top_p');
    });
  });

  describe('chatCompletionWithVision', () => {
    it('sends multimodal messages correctly', async () => {
      mockFetch.mockResolvedValueOnce(makeOkResponse(SAMPLE_RESPONSE));

      const visionMessages: ChatMessage[] = [
        {
          role: 'user',
          content: [
            { type: 'text', text: 'What is in this image?' },
            { type: 'image_url', image_url: { url: 'https://example.com/img.jpg' } },
          ],
        },
      ];

      const result = await client.chatCompletionWithVision(visionMessages);

      expect(result).toEqual(SAMPLE_RESPONSE);
      const body = JSON.parse((mockFetch.mock.calls[0] as [string, RequestInit])[1].body as string);
      expect(body.messages[0].content).toEqual(visionMessages[0].content);
    });
  });

  describe('retry logic', () => {
    it('retries on 500 errors with exponential backoff', async () => {
      mockFetch
        .mockResolvedValueOnce(makeErrorResponse(500))
        .mockResolvedValueOnce(makeErrorResponse(502))
        .mockResolvedValueOnce(makeOkResponse(SAMPLE_RESPONSE));

      const result = await client.chatCompletion(SAMPLE_MESSAGES);
      expect(result).toEqual(SAMPLE_RESPONSE);
      expect(mockFetch).toHaveBeenCalledTimes(3);
    });

    it('retries on 429 rate limit errors', async () => {
      mockFetch
        .mockResolvedValueOnce(makeErrorResponse(429, 'rate limited'))
        .mockResolvedValueOnce(makeOkResponse(SAMPLE_RESPONSE));

      const result = await client.chatCompletion(SAMPLE_MESSAGES);
      expect(result).toEqual(SAMPLE_RESPONSE);
      expect(mockFetch).toHaveBeenCalledTimes(2);
    });

    it('throws immediately on 4xx client errors (not 429)', async () => {
      mockFetch.mockResolvedValueOnce(makeErrorResponse(400, 'bad request'));

      await expect(client.chatCompletion(SAMPLE_MESSAGES)).rejects.toThrow('OpenRouter API error: 400');
      expect(mockFetch).toHaveBeenCalledOnce();
    });

    it('throws after exhausting all retries', async () => {
      mockFetch
        .mockResolvedValueOnce(makeErrorResponse(500))
        .mockResolvedValueOnce(makeErrorResponse(500))
        .mockResolvedValueOnce(makeErrorResponse(500));

      await expect(client.chatCompletion(SAMPLE_MESSAGES)).rejects.toThrow('OpenRouter API error: 500');
      expect(mockFetch).toHaveBeenCalledTimes(3);
    });

    it('retries on network errors', async () => {
      mockFetch
        .mockRejectedValueOnce(new Error('fetch failed'))
        .mockResolvedValueOnce(makeOkResponse(SAMPLE_RESPONSE));

      const result = await client.chatCompletion(SAMPLE_MESSAGES);
      expect(result).toEqual(SAMPLE_RESPONSE);
      expect(mockFetch).toHaveBeenCalledTimes(2);
    });
  });
});

describe('createOpenRouterClient', () => {
  it('creates client from config', async () => {
    const { createOpenRouterClient } = await import('./client.js');
    const client = createOpenRouterClient();
    expect(client).toBeInstanceOf(OpenRouterClient);
  });
});
