/**
 * Tests for LLM providers, factory, types, and helpers.
 */

import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { MockLLMProvider } from '../providers/mock.js';
import { createLLMProvider, _resetLLM } from '../factory.js';
import { isVisionMessage, hasVisionContent, buildVisionMessage, getMessageText } from '../types.js';
import type { ChatMessage, ChatCompletionRequest, EmbeddingResponse } from '../types.js';

// ── Helper function tests ─────────────────────────────────────────

describe('isVisionMessage', () => {
  it('returns false for string content', () => {
    const msg: ChatMessage = { role: 'user', content: 'hello' };
    expect(isVisionMessage(msg)).toBe(false);
  });

  it('returns false for text-only multipart', () => {
    const msg: ChatMessage = {
      role: 'user',
      content: [{ type: 'text', text: 'hello' }],
    };
    expect(isVisionMessage(msg)).toBe(false);
  });

  it('returns true when message contains image_url part', () => {
    const msg: ChatMessage = {
      role: 'user',
      content: [
        { type: 'text', text: 'describe this' },
        { type: 'image_url', image_url: { url: 'https://example.com/img.png' } },
      ],
    };
    expect(isVisionMessage(msg)).toBe(true);
  });
});

describe('hasVisionContent', () => {
  it('returns false for text-only request', () => {
    const req: ChatCompletionRequest = {
      messages: [
        { role: 'system', content: 'You are helpful' },
        { role: 'user', content: 'hello' },
      ],
    };
    expect(hasVisionContent(req)).toBe(false);
  });

  it('returns true when any message has image content', () => {
    const req: ChatCompletionRequest = {
      messages: [
        { role: 'system', content: 'You are helpful' },
        {
          role: 'user',
          content: [
            { type: 'text', text: 'what is this?' },
            { type: 'image_url', image_url: { url: 'data:image/png;base64,abc' } },
          ],
        },
      ],
    };
    expect(hasVisionContent(req)).toBe(true);
  });
});

describe('buildVisionMessage', () => {
  it('builds a multipart user message with text and image', () => {
    const msg = buildVisionMessage('Describe this', 'https://img.com/a.png');
    expect(msg.role).toBe('user');
    expect(Array.isArray(msg.content)).toBe(true);
    const parts = msg.content as Array<{ type: string }>;
    expect(parts).toHaveLength(2);
    expect(parts[0]).toEqual({ type: 'text', text: 'Describe this' });
    expect(parts[1]).toEqual({
      type: 'image_url',
      image_url: { url: 'https://img.com/a.png', detail: 'auto' },
    });
  });

  it('respects detail parameter', () => {
    const msg = buildVisionMessage('hi', 'https://img.com/b.png', 'high');
    const parts = msg.content as Array<{ type: string; image_url?: { detail: string } }>;
    expect(parts[1]?.image_url?.detail).toBe('high');
  });
});

describe('getMessageText', () => {
  it('returns string content directly', () => {
    expect(getMessageText({ role: 'user', content: 'hello' })).toBe('hello');
  });

  it('extracts text from multipart content', () => {
    const msg: ChatMessage = {
      role: 'user',
      content: [
        { type: 'text', text: 'line one' },
        { type: 'image_url', image_url: { url: 'https://img.com/x.png' } },
        { type: 'text', text: 'line two' },
      ],
    };
    expect(getMessageText(msg)).toBe('line one\nline two');
  });

  it('returns empty for image-only multipart', () => {
    const msg: ChatMessage = {
      role: 'user',
      content: [{ type: 'image_url', image_url: { url: 'https://img.com/x.png' } }],
    };
    expect(getMessageText(msg)).toBe('');
  });
});

// ── MockLLMProvider tests ─────────────────────────────────────────

describe('MockLLMProvider', () => {
  let provider: MockLLMProvider;

  beforeEach(() => {
    provider = new MockLLMProvider();
  });

  it('isConfigured returns true', () => {
    expect(provider.isConfigured()).toBe(true);
  });

  it('returns default echo response', async () => {
    const result = await provider.chatCompletion({
      messages: [{ role: 'user', content: 'Hello' }],
    });
    expect(result.content).toContain('Hello');
    expect(result.finishReason).toBe('stop');
  });

  it('returns queued responses', async () => {
    provider.addResponse({
      content: 'Custom response',
      model: 'test-model',
      finishReason: 'stop',
      usage: { promptTokens: 5, completionTokens: 5, totalTokens: 10 },
    });

    const result = await provider.chatCompletion({
      messages: [{ role: 'user', content: 'Hello' }],
    });
    expect(result.content).toBe('Custom response');
  });

  it('tracks calls', async () => {
    const req = { messages: [{ role: 'user' as const, content: 'Test' }] };
    await provider.chatCompletion(req);
    expect(provider.calls).toHaveLength(1);
    expect(provider.calls[0]).toEqual(req);
  });

  it('reset clears calls and responses', async () => {
    provider.addResponse({
      content: 'x',
      model: 'm',
      finishReason: 'stop',
      usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
    });
    await provider.chatCompletion({ messages: [{ role: 'user', content: 'Test' }] });
    provider.reset();
    expect(provider.calls).toHaveLength(0);
  });

  it('handles multipart (vision) content in echo response', async () => {
    const result = await provider.chatCompletion({
      messages: [
        {
          role: 'user',
          content: [
            { type: 'text', text: 'Describe this image' },
            { type: 'image_url', image_url: { url: 'https://example.com/img.png' } },
          ],
        },
      ],
    });
    expect(result.content).toContain('Describe this image');
  });

  // ── Streaming tests ──────────────────────────────────

  it('streams default echo response word by word', async () => {
    const stream = provider.chatCompletionStream!({
      messages: [{ role: 'user', content: 'Hi' }],
    });
    const chunks: string[] = [];
    for await (const chunk of stream) {
      chunks.push(chunk);
    }
    expect(chunks.length).toBeGreaterThan(0);
    const full = chunks.join('');
    expect(full).toContain('Hi');
  });

  it('streams queued response word by word', async () => {
    provider.addResponse({
      content: 'Hello World',
      model: 'test',
      finishReason: 'stop',
      usage: { promptTokens: 1, completionTokens: 2, totalTokens: 3 },
    });
    const stream = provider.chatCompletionStream!({
      messages: [{ role: 'user', content: 'x' }],
    });
    const chunks: string[] = [];
    for await (const chunk of stream) {
      chunks.push(chunk);
    }
    expect(chunks).toEqual(['Hello ', 'World ']);
  });

  it('streaming tracks calls', async () => {
    const req = { messages: [{ role: 'user' as const, content: 'stream test' }] };
    const stream = provider.chatCompletionStream!(req);
    const drained: string[] = [];
    for await (const chunk of stream) {
      drained.push(chunk);
    }
    expect(drained.length).toBeGreaterThan(0);
    expect(provider.calls).toHaveLength(1);
  });

  // ── Embedding tests ──────────────────────────────────

  it('returns deterministic embeddings for single input', async () => {
    const result = await provider.embed!({ input: 'hello world' });
    expect(result.embeddings).toHaveLength(1);
    expect(result.embeddings[0].length).toBe(8);
    expect(result.model).toBe('mock-embedding-model');
    // Verify normalized (magnitude ≈ 1)
    const mag = Math.sqrt(result.embeddings[0].reduce((s, v) => s + v * v, 0));
    expect(mag).toBeCloseTo(1.0, 3);
  });

  it('returns multiple embeddings for array input', async () => {
    const result = await provider.embed!({ input: ['hello', 'world'] });
    expect(result.embeddings).toHaveLength(2);
    expect(result.embeddings[0]).not.toEqual(result.embeddings[1]);
  });

  it('returns queued embedding response', async () => {
    const custom: EmbeddingResponse = {
      embeddings: [[0.1, 0.2, 0.3]],
      model: 'custom-embed',
      usage: { promptTokens: 1, completionTokens: 0, totalTokens: 1 },
    };
    provider.addEmbeddingResponse(custom);
    const result = await provider.embed!({ input: 'test' });
    expect(result).toEqual(custom);
  });

  it('tracks embed calls', async () => {
    await provider.embed!({ input: 'track me' });
    expect(provider.embedCalls).toHaveLength(1);
    expect(provider.embedCalls[0].input).toBe('track me');
  });

  it('deterministic: same input produces same embedding', async () => {
    const r1 = await provider.embed!({ input: 'identical text' });
    const r2 = await provider.embed!({ input: 'identical text' });
    expect(r1.embeddings[0]).toEqual(r2.embeddings[0]);
  });

  it('reset clears embed state', async () => {
    const custom: EmbeddingResponse = {
      embeddings: [[0.5]],
      model: 'm',
      usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
    };
    provider.addEmbeddingResponse(custom);
    await provider.embed!({ input: 'test' });
    provider.reset();
    expect(provider.embedCalls).toHaveLength(0);
    // After reset, embed should return default (not queued) response
    const result = await provider.embed!({ input: 'after reset' });
    expect(result.model).toBe('mock-embedding-model');
  });
});

// ── Factory tests ─────────────────────────────────────────────────

describe('createLLMProvider', () => {
  afterEach(() => {
    _resetLLM();
    vi.unstubAllEnvs();
  });

  it('creates mock provider', () => {
    const provider = createLLMProvider('mock');
    expect(provider).toBeInstanceOf(MockLLMProvider);
  });

  it('throws for unknown type', () => {
    expect(() => createLLMProvider('unknown' as 'mock')).toThrow('Unknown LLM_PROVIDER');
  });
});
