import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { callOpenRouter, callOpenRouterStream } from '@/lib/token/openrouter';
import type { StreamCompletePayload } from '@/lib/token/openrouter';

// Mock logger
vi.mock('@/lib/logger', () => ({
  logger: { info: vi.fn(), error: vi.fn(), warn: vi.fn() },
}));

// ---------- helpers ----------

/** Build a ReadableStream that emits SSE-formatted events. */
function createMockSSEStream(events: string[]): ReadableStream<Uint8Array> {
  const encoder = new TextEncoder();
  const data = events.map((e) => `data: ${e}\n\n`).join('');
  return new ReadableStream({
    start(controller) {
      controller.enqueue(encoder.encode(data));
      controller.close();
    },
  });
}

/** Build a ReadableStream that emits SSE data split across multiple enqueues. */
function createMultiChunkSSEStream(chunks: string[]): ReadableStream<Uint8Array> {
  const encoder = new TextEncoder();
  return new ReadableStream({
    start(controller) {
      for (const c of chunks) {
        controller.enqueue(encoder.encode(c));
      }
      controller.close();
    },
  });
}

/** Build a ReadableStream that errors mid-read. */
function createErrorSSEStream(events: string[], errorAfter: number): ReadableStream<Uint8Array> {
  const encoder = new TextEncoder();
  let emitted = 0;
  return new ReadableStream({
    start(controller) {
      for (const e of events) {
        if (emitted >= errorAfter) {
          controller.error(new Error('upstream failure'));
          return;
        }
        controller.enqueue(encoder.encode(`data: ${e}\n\n`));
        emitted++;
      }
      controller.close();
    },
  });
}

/** Drain a ReadableStream and return decoded string. */
async function drainStream(stream: ReadableStream<Uint8Array>): Promise<string> {
  const reader = stream.getReader();
  const decoder = new TextDecoder();
  let result = '';
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    result += decoder.decode(value, { stream: true });
  }
  return result;
}

/** Parse SSE events from raw text. */
function parseSSEEvents(raw: string): unknown[] {
  return raw
    .split('\n\n')
    .map((block) => block.trim())
    .filter((block) => block.startsWith('data: '))
    .map((block) => JSON.parse(block.slice(6)));
}

describe('callOpenRouter', () => {
  const originalEnv = process.env.OPENROUTER_API_KEY;

  beforeEach(() => {
    vi.clearAllMocks();
    process.env.OPENROUTER_API_KEY = 'test-openrouter-key';
  });

  afterEach(() => {
    vi.restoreAllMocks();
    if (originalEnv !== undefined) {
      process.env.OPENROUTER_API_KEY = originalEnv;
    } else {
      delete process.env.OPENROUTER_API_KEY;
    }
  });

  it('should return content, model, tokens, cost, and latency on success', async () => {
    // Arrange
    const mockResponse = {
      ok: true,
      json: async () => ({
        choices: [{ message: { content: 'Hello from Token' } }],
        usage: { prompt_tokens: 100, completion_tokens: 50 },
        model: 'google/gemini-2.5-flash',
      }),
      headers: new Headers({ 'x-openrouter-cost': '0.00125' }),
      text: async () => '',
    };
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse));

    // Act
    const result = await callOpenRouter({
      messages: [{ role: 'user', content: 'Hi' }],
      model: 'google/gemini-2.5-flash',
    });

    // Assert
    expect(result.content).toBe('Hello from Token');
    expect(result.model).toBe('google/gemini-2.5-flash');
    expect(result.inputTokens).toBe(100);
    expect(result.outputTokens).toBe(50);
    expect(result.costUsd).toBe(0.00125);
    expect(result.latencyMs).toBeGreaterThanOrEqual(0);
  });

  it('should throw when OPENROUTER_API_KEY is not configured', async () => {
    // Arrange
    delete process.env.OPENROUTER_API_KEY;

    // Act & Assert
    await expect(
      callOpenRouter({
        messages: [{ role: 'user', content: 'Hi' }],
        model: 'google/gemini-2.5-flash',
      }),
    ).rejects.toThrow('OPENROUTER_API_KEY not configured');
  });

  it('should throw with status code on non-2xx response', async () => {
    // Arrange
    const mockResponse = {
      ok: false,
      status: 429,
      text: async () => 'Rate limit exceeded',
      headers: new Headers(),
    };
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse));

    // Act & Assert
    await expect(
      callOpenRouter({
        messages: [{ role: 'user', content: 'Hi' }],
        model: 'google/gemini-2.5-flash',
      }),
    ).rejects.toThrow('OpenRouter API error (429)');
  });

  it('should parse token counts from usage object', async () => {
    // Arrange
    const mockResponse = {
      ok: true,
      json: async () => ({
        choices: [{ message: { content: 'response' } }],
        usage: { prompt_tokens: 250, completion_tokens: 120 },
        model: 'anthropic/claude-sonnet-4.6',
      }),
      headers: new Headers({ 'x-openrouter-cost': '0.005' }),
      text: async () => '',
    };
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse));

    // Act
    const result = await callOpenRouter({
      messages: [{ role: 'user', content: 'test' }],
      model: 'anthropic/claude-sonnet-4.6',
    });

    // Assert
    expect(result.inputTokens).toBe(250);
    expect(result.outputTokens).toBe(120);
  });

  it('should parse cost from x-openrouter-cost header', async () => {
    // Arrange
    const mockResponse = {
      ok: true,
      json: async () => ({
        choices: [{ message: { content: 'ok' } }],
        usage: {},
        model: 'test-model',
      }),
      headers: new Headers({ 'x-openrouter-cost': '0.042' }),
      text: async () => '',
    };
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse));

    // Act
    const result = await callOpenRouter({
      messages: [{ role: 'user', content: 'test' }],
      model: 'test-model',
    });

    // Assert
    expect(result.costUsd).toBe(0.042);
  });

  it('should default to 0 tokens and 0 cost when usage/headers are missing', async () => {
    // Arrange
    const mockResponse = {
      ok: true,
      json: async () => ({
        choices: [{ message: { content: 'ok' } }],
        // No usage object
        model: 'test-model',
      }),
      headers: new Headers(), // No cost header
      text: async () => '',
    };
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse));

    // Act
    const result = await callOpenRouter({
      messages: [{ role: 'user', content: 'test' }],
      model: 'test-model',
    });

    // Assert
    expect(result.inputTokens).toBe(0);
    expect(result.outputTokens).toBe(0);
    expect(result.costUsd).toBe(0);
  });

  it('should send correct headers and payload to OpenRouter', async () => {
    // Arrange
    const mockFetch = vi.fn().mockResolvedValue({
      ok: true,
      json: async () => ({
        choices: [{ message: { content: 'ok' } }],
        usage: {},
        model: 'google/gemini-2.5-flash',
      }),
      headers: new Headers(),
      text: async () => '',
    });
    vi.stubGlobal('fetch', mockFetch);

    // Act
    await callOpenRouter({
      messages: [{ role: 'user', content: 'Hello' }],
      model: 'google/gemini-2.5-flash',
      temperature: 0.7,
      maxTokens: 2048,
    });

    // Assert
    expect(mockFetch).toHaveBeenCalledWith(
      'https://openrouter.ai/api/v1/chat/completions',
      expect.objectContaining({
        method: 'POST',
        headers: expect.objectContaining({
          Authorization: 'Bearer test-openrouter-key',
          'Content-Type': 'application/json',
        }),
      }),
    );

    const body = JSON.parse(mockFetch.mock.calls[0][1].body);
    expect(body.model).toBe('google/gemini-2.5-flash');
    expect(body.messages).toEqual([{ role: 'user', content: 'Hello' }]);
    expect(body.temperature).toBe(0.7);
    expect(body.max_tokens).toBe(2048);
    expect(body.stream).toBe(false);
  });

  it('should use default temperature 0.4 and maxTokens 4096 when not provided', async () => {
    // Arrange
    const mockFetch = vi.fn().mockResolvedValue({
      ok: true,
      json: async () => ({
        choices: [{ message: { content: 'ok' } }],
        usage: {},
        model: 'test',
      }),
      headers: new Headers(),
      text: async () => '',
    });
    vi.stubGlobal('fetch', mockFetch);

    // Act
    await callOpenRouter({
      messages: [{ role: 'user', content: 'test' }],
      model: 'test',
    });

    // Assert
    const body = JSON.parse(mockFetch.mock.calls[0][1].body);
    expect(body.temperature).toBe(0.4);
    expect(body.max_tokens).toBe(4096);
  });

  it('should return empty content when choices are missing', async () => {
    // Arrange
    const mockResponse = {
      ok: true,
      json: async () => ({
        choices: [],
        usage: { prompt_tokens: 10, completion_tokens: 0 },
        model: 'test',
      }),
      headers: new Headers(),
      text: async () => '',
    };
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse));

    // Act
    const result = await callOpenRouter({
      messages: [{ role: 'user', content: 'test' }],
      model: 'test',
    });

    // Assert
    expect(result.content).toBe('');
  });

  it('should fall back to request model when response model is missing', async () => {
    // Arrange
    const mockResponse = {
      ok: true,
      json: async () => ({
        choices: [{ message: { content: 'ok' } }],
        usage: {},
        // No model field
      }),
      headers: new Headers(),
      text: async () => '',
    };
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse));

    // Act
    const result = await callOpenRouter({
      messages: [{ role: 'user', content: 'test' }],
      model: 'my-fallback-model',
    });

    // Assert
    expect(result.model).toBe('my-fallback-model');
  });

  it('should include HTTP-Referer and X-Title headers', async () => {
    // Arrange
    const mockFetch = vi.fn().mockResolvedValue({
      ok: true,
      json: async () => ({
        choices: [{ message: { content: 'ok' } }],
        usage: {},
        model: 'test',
      }),
      headers: new Headers(),
      text: async () => '',
    });
    vi.stubGlobal('fetch', mockFetch);

    // Act
    await callOpenRouter({
      messages: [{ role: 'user', content: 'test' }],
      model: 'test',
    });

    // Assert
    const headers = mockFetch.mock.calls[0][1].headers;
    expect(headers['HTTP-Referer']).toBe('https://llmhosts.com');
    expect(headers['X-Title']).toBe('LLMHosts Token Assistant');
  });

  it('should handle res.text() failure in error path', async () => {
    // Arrange
    const mockResponse = {
      ok: false,
      status: 500,
      text: async () => {
        throw new Error('read failed');
      },
      headers: new Headers(),
    };
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue(mockResponse));

    // Act & Assert
    await expect(
      callOpenRouter({
        messages: [{ role: 'user', content: 'test' }],
        model: 'test',
      }),
    ).rejects.toThrow('OpenRouter API error (500)');
  });
});

// =============================================
// callOpenRouterStream
// =============================================

describe('callOpenRouterStream', () => {
  const originalEnv = process.env.OPENROUTER_API_KEY;

  beforeEach(() => {
    vi.clearAllMocks();
    process.env.OPENROUTER_API_KEY = 'test-openrouter-key';
  });

  afterEach(() => {
    vi.restoreAllMocks();
    if (originalEnv !== undefined) {
      process.env.OPENROUTER_API_KEY = originalEnv;
    } else {
      delete process.env.OPENROUTER_API_KEY;
    }
  });

  // ---------- pre-flight errors ----------

  it('should throw when OPENROUTER_API_KEY is not set', async () => {
    delete process.env.OPENROUTER_API_KEY;

    await expect(
      callOpenRouterStream(
        { messages: [{ role: 'user', content: 'Hi' }], model: 'test' },
        async () => {},
      ),
    ).rejects.toThrow('OPENROUTER_API_KEY not configured');
  });

  it('should throw on non-ok response with error message', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: false,
        status: 503,
        text: async () => 'Service Unavailable',
        headers: new Headers(),
      }),
    );

    await expect(
      callOpenRouterStream(
        { messages: [{ role: 'user', content: 'Hi' }], model: 'test' },
        async () => {},
      ),
    ).rejects.toThrow('OpenRouter API error (503): Service Unavailable');
  });

  it('should throw when res.text() fails in error path', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: false,
        status: 502,
        text: async () => {
          throw new Error('body gone');
        },
        headers: new Headers(),
      }),
    );

    await expect(
      callOpenRouterStream(
        { messages: [{ role: 'user', content: 'Hi' }], model: 'test' },
        async () => {},
      ),
    ).rejects.toThrow('OpenRouter API error (502): Unknown error');
  });

  it('should throw when response has no body', async () => {
    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: true,
        body: null,
        headers: new Headers(),
        text: async () => '',
      }),
    );

    await expect(
      callOpenRouterStream(
        { messages: [{ role: 'user', content: 'Hi' }], model: 'test' },
        async () => {},
      ),
    ).rejects.toThrow('OpenRouter returned no response body');
  });

  // ---------- request shape ----------

  it('should send stream: true and stream_options in the request body', async () => {
    const mockFetch = vi.fn().mockResolvedValue({
      ok: true,
      body: createMockSSEStream(['[DONE]']),
      headers: new Headers(),
      text: async () => '',
    });
    vi.stubGlobal('fetch', mockFetch);

    const stream = await callOpenRouterStream(
      {
        messages: [{ role: 'user', content: 'test' }],
        model: 'google/gemini-2.5-flash',
        temperature: 0.8,
        maxTokens: 1024,
      },
      async () => {},
    );
    await drainStream(stream);

    const body = JSON.parse(mockFetch.mock.calls[0][1].body);
    expect(body.stream).toBe(true);
    expect(body.stream_options).toEqual({ include_usage: true });
    expect(body.temperature).toBe(0.8);
    expect(body.max_tokens).toBe(1024);
    expect(body.model).toBe('google/gemini-2.5-flash');
  });

  it('should use default temperature 0.4 and maxTokens 4096', async () => {
    const mockFetch = vi.fn().mockResolvedValue({
      ok: true,
      body: createMockSSEStream(['[DONE]']),
      headers: new Headers(),
      text: async () => '',
    });
    vi.stubGlobal('fetch', mockFetch);

    const stream = await callOpenRouterStream(
      { messages: [{ role: 'user', content: 'test' }], model: 'test' },
      async () => {},
    );
    await drainStream(stream);

    const body = JSON.parse(mockFetch.mock.calls[0][1].body);
    expect(body.temperature).toBe(0.4);
    expect(body.max_tokens).toBe(4096);
  });

  // ---------- streaming token events ----------

  it('should emit token events for each content delta', async () => {
    const sseEvents = [
      JSON.stringify({ choices: [{ delta: { content: 'Hello' } }], model: 'test-model' }),
      JSON.stringify({ choices: [{ delta: { content: ' world' } }] }),
      '[DONE]',
    ];

    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: true,
        body: createMockSSEStream(sseEvents),
        headers: new Headers({ 'x-openrouter-cost': '0.001' }),
        text: async () => '',
      }),
    );

    const stream = await callOpenRouterStream(
      { messages: [{ role: 'user', content: 'test' }], model: 'test-model' },
      async () => {},
    );

    const raw = await drainStream(stream);
    const events = parseSSEEvents(raw);

    // Should have 2 token events + 1 done event
    const tokenEvents = events.filter((e: any) => e.done === false);
    expect(tokenEvents).toHaveLength(2);
    expect((tokenEvents[0] as any).token).toBe('Hello');
    expect((tokenEvents[1] as any).token).toBe(' world');
  });

  it('should emit final done event with metadata', async () => {
    const sseEvents = [
      JSON.stringify({
        choices: [{ delta: { content: 'Hi' } }],
        model: 'google/gemini-2.5-flash',
      }),
      JSON.stringify({
        choices: [{ delta: {} }],
        usage: { prompt_tokens: 15, completion_tokens: 5 },
      }),
      '[DONE]',
    ];

    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: true,
        body: createMockSSEStream(sseEvents),
        headers: new Headers({ 'x-openrouter-cost': '0.002' }),
        text: async () => '',
      }),
    );

    const stream = await callOpenRouterStream(
      { messages: [{ role: 'user', content: 'test' }], model: 'google/gemini-2.5-flash' },
      async () => {},
    );

    const raw = await drainStream(stream);
    const events = parseSSEEvents(raw);
    const doneEvent = events.find((e: any) => e.done === true) as any;

    expect(doneEvent).toBeDefined();
    expect(doneEvent.model).toBe('google/gemini-2.5-flash');
    expect(doneEvent.costUsd).toBe(0.002);
    expect(doneEvent.inputTokens).toBe(15);
    expect(doneEvent.outputTokens).toBe(5);
    expect(doneEvent.latencyMs).toBeGreaterThanOrEqual(0);
  });

  it('should call onComplete with accumulated content and metadata', async () => {
    const sseEvents = [
      JSON.stringify({ choices: [{ delta: { content: 'A' } }], model: 'resolved-model' }),
      JSON.stringify({ choices: [{ delta: { content: 'B' } }] }),
      JSON.stringify({ choices: [{ delta: { content: 'C' } }] }),
      JSON.stringify({ choices: [{ delta: {} }], usage: { prompt_tokens: 50, completion_tokens: 25 } }),
      '[DONE]',
    ];

    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: true,
        body: createMockSSEStream(sseEvents),
        headers: new Headers({ 'x-openrouter-cost': '0.01' }),
        text: async () => '',
      }),
    );

    let completedPayload: StreamCompletePayload | null = null;
    const onComplete = async (p: StreamCompletePayload) => {
      completedPayload = p;
    };

    const stream = await callOpenRouterStream(
      { messages: [{ role: 'user', content: 'test' }], model: 'fallback-model' },
      onComplete,
    );
    await drainStream(stream);

    expect(completedPayload).not.toBeNull();
    expect(completedPayload!.content).toBe('ABC');
    expect(completedPayload!.model).toBe('resolved-model');
    expect(completedPayload!.inputTokens).toBe(50);
    expect(completedPayload!.outputTokens).toBe(25);
    expect(completedPayload!.costUsd).toBe(0.01);
    expect(completedPayload!.latencyMs).toBeGreaterThanOrEqual(0);
  });

  // ---------- model resolution ----------

  it('should extract model from the first chunk that has a model field', async () => {
    const sseEvents = [
      JSON.stringify({ choices: [{ delta: { content: 'x' } }], model: 'first-model' }),
      JSON.stringify({ choices: [{ delta: { content: 'y' } }], model: 'second-model' }),
      '[DONE]',
    ];

    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: true,
        body: createMockSSEStream(sseEvents),
        headers: new Headers(),
        text: async () => '',
      }),
    );

    let completedPayload: StreamCompletePayload | null = null;
    const stream = await callOpenRouterStream(
      { messages: [{ role: 'user', content: 'test' }], model: 'req-model' },
      async (p) => {
        completedPayload = p;
      },
    );
    await drainStream(stream);

    // The last chunk with a model field wins (state updates on every chunk.model)
    expect(completedPayload!.model).toBe('second-model');
  });

  it('should fall back to request model when no chunk contains a model field', async () => {
    const sseEvents = [
      JSON.stringify({ choices: [{ delta: { content: 'ok' } }] }),
      '[DONE]',
    ];

    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: true,
        body: createMockSSEStream(sseEvents),
        headers: new Headers(),
        text: async () => '',
      }),
    );

    let completedPayload: StreamCompletePayload | null = null;
    const stream = await callOpenRouterStream(
      { messages: [{ role: 'user', content: 'test' }], model: 'my-fallback' },
      async (p) => {
        completedPayload = p;
      },
    );
    await drainStream(stream);

    expect(completedPayload!.model).toBe('my-fallback');
  });

  // ---------- usage extraction ----------

  it('should extract usage from the final chunk with usage field', async () => {
    const sseEvents = [
      JSON.stringify({ choices: [{ delta: { content: 'hi' } }], model: 'test' }),
      JSON.stringify({ usage: { prompt_tokens: 200, completion_tokens: 100 } }),
      '[DONE]',
    ];

    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: true,
        body: createMockSSEStream(sseEvents),
        headers: new Headers({ 'x-openrouter-cost': '0.05' }),
        text: async () => '',
      }),
    );

    let completedPayload: StreamCompletePayload | null = null;
    const stream = await callOpenRouterStream(
      { messages: [{ role: 'user', content: 'test' }], model: 'test' },
      async (p) => {
        completedPayload = p;
      },
    );
    await drainStream(stream);

    expect(completedPayload!.inputTokens).toBe(200);
    expect(completedPayload!.outputTokens).toBe(100);
  });

  it('should default to 0 tokens when no usage chunk is received', async () => {
    const sseEvents = [
      JSON.stringify({ choices: [{ delta: { content: 'ok' } }], model: 'test' }),
      '[DONE]',
    ];

    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: true,
        body: createMockSSEStream(sseEvents),
        headers: new Headers(),
        text: async () => '',
      }),
    );

    let completedPayload: StreamCompletePayload | null = null;
    const stream = await callOpenRouterStream(
      { messages: [{ role: 'user', content: 'test' }], model: 'test' },
      async (p) => {
        completedPayload = p;
      },
    );
    await drainStream(stream);

    expect(completedPayload!.inputTokens).toBe(0);
    expect(completedPayload!.outputTokens).toBe(0);
    expect(completedPayload!.costUsd).toBe(0);
  });

  // ---------- content accumulation ----------

  it('should accumulate content across multiple chunks', async () => {
    const sseEvents = [
      JSON.stringify({ choices: [{ delta: { content: 'one ' } }], model: 'm' }),
      JSON.stringify({ choices: [{ delta: { content: 'two ' } }] }),
      JSON.stringify({ choices: [{ delta: { content: 'three' } }] }),
      '[DONE]',
    ];

    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: true,
        body: createMockSSEStream(sseEvents),
        headers: new Headers(),
        text: async () => '',
      }),
    );

    let completedPayload: StreamCompletePayload | null = null;
    const stream = await callOpenRouterStream(
      { messages: [{ role: 'user', content: 'test' }], model: 'm' },
      async (p) => {
        completedPayload = p;
      },
    );

    const raw = await drainStream(stream);
    const events = parseSSEEvents(raw);

    // 3 token events + 1 done event
    expect(events).toHaveLength(4);
    expect(completedPayload!.content).toBe('one two three');
  });

  // ---------- cost header ----------

  it('should include cost from x-openrouter-cost header in done event and onComplete', async () => {
    const sseEvents = [
      JSON.stringify({ choices: [{ delta: { content: 'x' } }], model: 'test' }),
      '[DONE]',
    ];

    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: true,
        body: createMockSSEStream(sseEvents),
        headers: new Headers({ 'x-openrouter-cost': '0.123' }),
        text: async () => '',
      }),
    );

    let completedPayload: StreamCompletePayload | null = null;
    const stream = await callOpenRouterStream(
      { messages: [{ role: 'user', content: 'test' }], model: 'test' },
      async (p) => {
        completedPayload = p;
      },
    );

    const raw = await drainStream(stream);
    const events = parseSSEEvents(raw);
    const doneEvent = events.find((e: any) => e.done === true) as any;

    expect(doneEvent.costUsd).toBe(0.123);
    expect(completedPayload!.costUsd).toBe(0.123);
  });

  // ---------- malformed / edge cases ----------

  it('should skip malformed JSON chunks without crashing', async () => {
    const sseEvents = [
      JSON.stringify({ choices: [{ delta: { content: 'good' } }], model: 'test' }),
      '{this is not valid json',
      JSON.stringify({ choices: [{ delta: { content: ' data' } }] }),
      '[DONE]',
    ];

    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: true,
        body: createMockSSEStream(sseEvents),
        headers: new Headers(),
        text: async () => '',
      }),
    );

    let completedPayload: StreamCompletePayload | null = null;
    const stream = await callOpenRouterStream(
      { messages: [{ role: 'user', content: 'test' }], model: 'test' },
      async (p) => {
        completedPayload = p;
      },
    );

    const raw = await drainStream(stream);
    const events = parseSSEEvents(raw);

    // 2 token events + 1 done (malformed chunk skipped)
    const tokenEvents = events.filter((e: any) => e.done === false);
    expect(tokenEvents).toHaveLength(2);
    expect(completedPayload!.content).toBe('good data');
  });

  it('should handle [DONE] marker without producing a token event', async () => {
    const sseEvents = [
      JSON.stringify({ choices: [{ delta: { content: 'x' } }], model: 'test' }),
      '[DONE]',
    ];

    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: true,
        body: createMockSSEStream(sseEvents),
        headers: new Headers(),
        text: async () => '',
      }),
    );

    const stream = await callOpenRouterStream(
      { messages: [{ role: 'user', content: 'test' }], model: 'test' },
      async () => {},
    );

    const raw = await drainStream(stream);
    const events = parseSSEEvents(raw);

    // No event should contain [DONE] as content
    const doneTokenEvents = events.filter((e: any) => e.token === '[DONE]');
    expect(doneTokenEvents).toHaveLength(0);
  });

  it('should skip empty lines and non-data lines', async () => {
    // Simulate upstream sending blank lines, comments, and empty data among real events
    const encoder = new TextEncoder();
    const rawSSE = [
      '',
      ': this is a comment',
      `data: ${JSON.stringify({ choices: [{ delta: { content: 'ok' } }], model: 'test' })}`,
      '',
      'data: [DONE]',
      '',
    ].join('\n');

    const upstream = new ReadableStream({
      start(controller) {
        controller.enqueue(encoder.encode(rawSSE));
        controller.close();
      },
    });

    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: true,
        body: upstream,
        headers: new Headers(),
        text: async () => '',
      }),
    );

    let completedPayload: StreamCompletePayload | null = null;
    const stream = await callOpenRouterStream(
      { messages: [{ role: 'user', content: 'test' }], model: 'test' },
      async (p) => {
        completedPayload = p;
      },
    );
    await drainStream(stream);

    expect(completedPayload!.content).toBe('ok');
  });

  it('should handle chunks with delta but no content field (role-only deltas)', async () => {
    const sseEvents = [
      JSON.stringify({ choices: [{ delta: { role: 'assistant' } }], model: 'test' }),
      JSON.stringify({ choices: [{ delta: { content: 'text' } }] }),
      '[DONE]',
    ];

    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: true,
        body: createMockSSEStream(sseEvents),
        headers: new Headers(),
        text: async () => '',
      }),
    );

    let completedPayload: StreamCompletePayload | null = null;
    const stream = await callOpenRouterStream(
      { messages: [{ role: 'user', content: 'test' }], model: 'test' },
      async (p) => {
        completedPayload = p;
      },
    );

    const raw = await drainStream(stream);
    const events = parseSSEEvents(raw);
    const tokenEvents = events.filter((e: any) => e.done === false);

    // Only the second chunk has content
    expect(tokenEvents).toHaveLength(1);
    expect(completedPayload!.content).toBe('text');
  });

  it('should handle data split across multiple readable chunks', async () => {
    // Simulate the upstream sending partial SSE lines across chunk boundaries
    const chunk1 = `data: ${JSON.stringify({ choices: [{ delta: { content: 'part1' } }], model: 'test' })}\n\nda`;
    const chunk2 = `ta: ${JSON.stringify({ choices: [{ delta: { content: 'part2' } }] })}\n\ndata: [DONE]\n\n`;

    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: true,
        body: createMultiChunkSSEStream([chunk1, chunk2]),
        headers: new Headers(),
        text: async () => '',
      }),
    );

    let completedPayload: StreamCompletePayload | null = null;
    const stream = await callOpenRouterStream(
      { messages: [{ role: 'user', content: 'test' }], model: 'test' },
      async (p) => {
        completedPayload = p;
      },
    );
    await drainStream(stream);

    expect(completedPayload!.content).toBe('part1part2');
  });

  // ---------- error during streaming ----------

  it('should emit error event and call controller.error on upstream failure', async () => {
    const failingStream = createErrorSSEStream(
      [JSON.stringify({ choices: [{ delta: { content: 'partial' } }], model: 'test' })],
      1,
    );

    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: true,
        body: failingStream,
        headers: new Headers(),
        text: async () => '',
      }),
    );

    const stream = await callOpenRouterStream(
      { messages: [{ role: 'user', content: 'test' }], model: 'test' },
      async () => {},
    );

    const reader = stream.getReader();
    const chunks: string[] = [];
    const decoder = new TextDecoder();

    try {
      while (true) {
        const { done, value } = await reader.read();
        if (done) break;
        chunks.push(decoder.decode(value, { stream: true }));
      }
      // If we get here without error, the stream closed normally (the first chunk
      // was enqueued before the error). Verify we got at least a token event.
      const combined = chunks.join('');
      expect(combined).toContain('"token":"partial"');
    } catch (err: any) {
      // Stream errored as expected
      expect(err.message).toBe('upstream failure');
      // The partial token event and error event should have been enqueued before the error
      const combined = chunks.join('');
      if (combined.length > 0) {
        expect(combined).toContain('data:');
      }
    }
  });

  // ---------- empty stream ----------

  it('should handle an empty stream with only [DONE]', async () => {
    const sseEvents = ['[DONE]'];

    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: true,
        body: createMockSSEStream(sseEvents),
        headers: new Headers({ 'x-openrouter-cost': '0' }),
        text: async () => '',
      }),
    );

    let completedPayload: StreamCompletePayload | null = null;
    const stream = await callOpenRouterStream(
      { messages: [{ role: 'user', content: 'test' }], model: 'test' },
      async (p) => {
        completedPayload = p;
      },
    );

    const raw = await drainStream(stream);
    const events = parseSSEEvents(raw);

    // Only the done event should be emitted
    expect(events).toHaveLength(1);
    const doneEvent = events[0] as any;
    expect(doneEvent.done).toBe(true);
    expect(completedPayload!.content).toBe('');
    expect(completedPayload!.inputTokens).toBe(0);
    expect(completedPayload!.outputTokens).toBe(0);
  });

  it('should handle an upstream that closes with no data at all', async () => {
    const emptyStream = new ReadableStream({
      start(controller) {
        controller.close();
      },
    });

    vi.stubGlobal(
      'fetch',
      vi.fn().mockResolvedValue({
        ok: true,
        body: emptyStream,
        headers: new Headers(),
        text: async () => '',
      }),
    );

    let completedPayload: StreamCompletePayload | null = null;
    const stream = await callOpenRouterStream(
      { messages: [{ role: 'user', content: 'test' }], model: 'test' },
      async (p) => {
        completedPayload = p;
      },
    );

    const raw = await drainStream(stream);
    const events = parseSSEEvents(raw);

    // Should still emit the done event
    expect(events).toHaveLength(1);
    expect((events[0] as any).done).toBe(true);
    expect(completedPayload!.content).toBe('');
  });
});
