import { describe, it, expect, vi } from 'vitest';
import { OpenAIClient } from './openai.js';

const { mockCreate, mockOpenAIConstructor } = vi.hoisted(() => {
  const mockCreate = vi.fn().mockResolvedValue({
    choices: [{ message: { content: 'Hello from GPT!' }, finish_reason: 'stop' }],
    usage: { prompt_tokens: 10, completion_tokens: 5 },
  });
  const mockOpenAIConstructor = vi.fn().mockImplementation(() => ({
    chat: {
      completions: {
        create: mockCreate,
      },
    },
  }));
  return { mockCreate, mockOpenAIConstructor };
});

vi.mock('openai', () => ({
  default: mockOpenAIConstructor,
}));

describe('OpenAIClient', () => {
  it('sets request timeout and disables SDK retries', () => {
    new OpenAIClient({
      apiKey: 'test-key',
      model: 'gpt-4o',
    });

    expect(mockOpenAIConstructor).toHaveBeenCalledWith(expect.objectContaining({
      timeout: 20_000,
      maxRetries: 0,
    }));
  });

  it('sends messages and returns response', async () => {
    const client = new OpenAIClient({
      apiKey: 'test-key',
      model: 'gpt-4o',
    });

    const response = await client.chat({
      messages: [{ role: 'user', content: 'Hello' }],
    });

    expect(response.content).toBe('Hello from GPT!');
    expect(response.stopReason).toBe('end_turn');
    expect(response.usage.inputTokens).toBe(10);
    expect(response.usage.outputTokens).toBe(5);
  });
});

describe('OpenAIClient tool use', () => {
  it('passes tools to API and parses tool_calls response', async () => {
    mockCreate.mockResolvedValueOnce({
      choices: [{
        message: {
          content: null,
          tool_calls: [{
            id: 'call_1',
            type: 'function',
            function: { name: 'shell.exec', arguments: '{"command":"ls"}' },
          }],
        },
        finish_reason: 'tool_calls',
      }],
      usage: { prompt_tokens: 20, completion_tokens: 15 },
    });

    const client = new OpenAIClient({
      apiKey: 'test-key',
      model: 'gpt-4o',
    });

    const response = await client.chat({
      messages: [{ role: 'user', content: 'list files' }],
      tools: [{
        name: 'shell.exec',
        description: 'Run shell command',
        input_schema: { type: 'object', properties: { command: { type: 'string' } }, required: ['command'] },
      }],
    });

    expect(response.stopReason).toBe('tool_use');
    expect(response.toolCalls).toHaveLength(1);
    const firstToolCall = response.toolCalls?.[0];
    expect(firstToolCall).toEqual({
      id: 'call_1',
      name: 'shell.exec',
      args: { command: 'ls' },
    });
  });

  it('maps finish_reason "tool_calls" with empty tool_calls to end_turn', async () => {
    mockCreate.mockResolvedValueOnce({
      choices: [{
        message: {
          content: 'I tried to call a tool but none matched.',
          tool_calls: [],
        },
        finish_reason: 'tool_calls',
      }],
      usage: { prompt_tokens: 15, completion_tokens: 10 },
    });

    const client = new OpenAIClient({
      apiKey: 'test-key',
      model: 'gpt-4o',
    });

    const response = await client.chat({
      messages: [{ role: 'user', content: 'do something' }],
    });

    expect(response.stopReason).toBe('end_turn');
    expect(response.toolCalls).toBeUndefined();
  });

  it('maps finish_reason "length" to max_tokens', async () => {
    mockCreate.mockResolvedValueOnce({
      choices: [{
        message: { content: 'Truncated output...' },
        finish_reason: 'length',
      }],
      usage: { prompt_tokens: 100, completion_tokens: 4096 },
    });

    const client = new OpenAIClient({
      apiKey: 'test-key',
      model: 'gpt-4o',
    });

    const response = await client.chat({
      messages: [{ role: 'user', content: 'write a long essay' }],
    });

    expect(response.stopReason).toBe('max_tokens');
  });

  it('retries with max_completion_tokens when provider rejects max_tokens', async () => {
    const initialCallCount = mockCreate.mock.calls.length;
    mockCreate
      .mockRejectedValueOnce(new Error(
        "400 Unsupported parameter: 'max_tokens' is not supported with this model. Use 'max_completion_tokens' instead.",
      ))
      .mockResolvedValueOnce({
        choices: [{ message: { content: 'Hello from GPT-5.2!' }, finish_reason: 'stop' }],
        usage: { prompt_tokens: 11, completion_tokens: 6 },
      });

    const client = new OpenAIClient({
      apiKey: 'test-key',
      model: 'gpt-5.2',
    });

    const response = await client.chat({
      messages: [{ role: 'user', content: 'Hello' }],
    });

    expect(response.content).toBe('Hello from GPT-5.2!');
    expect(mockCreate.mock.calls.length - initialCallCount).toBe(2);

    const firstArgs = mockCreate.mock.calls[initialCallCount]?.[0] as Record<string, unknown>;
    expect(firstArgs.max_tokens).toBeDefined();

    const secondArgs = mockCreate.mock.calls[initialCallCount + 1]?.[0] as Record<string, unknown>;
    expect(secondArgs.max_tokens).toBeUndefined();
    expect(secondArgs.max_completion_tokens).toBeDefined();
  });

  it('rewrites Z.AI 401 errors with actionable auth guidance', async () => {
    mockCreate.mockRejectedValueOnce({
      status: 401,
      message: '401 You have insufficient permissions for this operation. Missing scopes: model.request.',
    });

    const client = new OpenAIClient({
      apiKey: 'zai-key',
      model: 'glm-4.7',
      baseURL: 'https://api.z.ai/api/paas/v4',
    });

    await expect(client.chat({
      messages: [{ role: 'user', content: 'hello' }],
    })).rejects.toThrow(/Z\.AI authentication failed \(401\)/);
  });

  it('rewrites Z.AI textual 401 errors when SDK status is absent', async () => {
    mockCreate.mockRejectedValueOnce(new Error(
      '401 You have insufficient permissions for this operation. Missing scopes: model.request.',
    ));

    const client = new OpenAIClient({
      apiKey: 'zai-key',
      model: 'glm-4.7',
      baseURL: 'https://api.z.ai/api/paas/v4',
    });

    await expect(client.chat({
      messages: [{ role: 'user', content: 'hello' }],
    })).rejects.toThrow(/The key lacks `model\.request` scope/);
  });

  it('passes OpenAI response_format json_schema when requested', async () => {
    const client = new OpenAIClient({
      apiKey: 'test-key',
      model: 'gpt-5.2',
    });

    await client.chat({
      messages: [{ role: 'user', content: 'emit json' }],
      responseFormat: {
        type: 'json_schema',
        name: 'council_ideation',
        schema: {
          type: 'object',
          additionalProperties: false,
          required: ['ideas'],
          properties: {
            ideas: { type: 'array', items: { type: 'object' } },
          },
        },
        strict: true,
      },
    });

    const args = mockCreate.mock.calls.at(-1)?.[0] as Record<string, unknown>;
    const responseFormat = args.response_format as Record<string, unknown>;
    expect(responseFormat.type).toBe('json_schema');

    const jsonSchema = responseFormat.json_schema as Record<string, unknown>;
    expect(jsonSchema.name).toBe('council_ideation');
    expect(jsonSchema.strict).toBe(true);
  });
});
