import { describe, it, expect, vi, beforeEach } from "vitest";

const mockCreate = vi.fn();
const mockList = vi.fn();
const mockRetrieve = vi.fn();

vi.mock("openai", () => {
  return {
    default: class MockOpenAI {
      chat = {
        completions: {
          create: mockCreate,
        },
      };
      models = {
        list: mockList,
        retrieve: mockRetrieve,
      };
    },
  };
});

import { OpenAiApi } from "./openai";

const mockLogger = {
  debug: vi.fn(),
  info: vi.fn(),
  warn: vi.fn(),
  error: vi.fn(),
};

const mockEnv = { NODE_ENV: "test", services: {} };
const mockProvider = {
  _id: "provider-openai",
  name: "OpenAI Compatible",
  sdk: "openai" as const,
  baseUrl: "https://example.test/v1",
  apiKey: "test-key",
};

async function* streamChunks(chunks: unknown[]) {
  for (const chunk of chunks) {
    yield chunk;
  }
}

describe("OpenAiApi", () => {
  let api: OpenAiApi;

  beforeEach(() => {
    vi.clearAllMocks();
    api = new OpenAiApi(mockEnv as any, mockProvider as any, mockLogger as any);
  });

  it("rejects an empty streaming and fallback response", async () => {
    mockCreate
      .mockResolvedValueOnce(streamChunks([
        { choices: [{ delta: {}, finish_reason: "stop" }] },
      ]))
      .mockResolvedValueOnce({
        choices: [{ message: { content: "", tool_calls: [] }, finish_reason: "stop" }],
      });

    await expect(api.chat(
      {
        provider: mockProvider as any,
        modelId: "test-model",
        params: { reasoning: false, temperature: 0.8, topP: 0.9, topK: 40, numPredict: -1, numCtx: 131072, maxCompletionTokens: 16384 },
      },
      { userPrompt: "Hello", context: [], tools: [] },
      vi.fn(),
    )).rejects.toThrow("Provider returned an empty chat response");
  });

  it("assembles streamed tool-call argument fragments and returns them", async () => {
    mockCreate.mockResolvedValueOnce(streamChunks([
      {
        choices: [{
          delta: {
            tool_calls: [{
              index: 0,
              id: "call_1",
              type: "function",
              function: { name: "file_read", arguments: '{"path"' },
            }],
          },
          finish_reason: null,
        }],
      },
      {
        choices: [{
          delta: {
            tool_calls: [{
              index: 0,
              function: { arguments: ':"index.html"}' },
            }],
          },
          finish_reason: "tool_calls",
        }],
      },
    ]));

    const streamCallback = vi.fn();
    const response = await api.chat(
      {
        provider: mockProvider as any,
        modelId: "test-model",
        params: { reasoning: false, temperature: 0.8, topP: 0.9, topK: 40, numPredict: -1, numCtx: 131072, maxCompletionTokens: 16384 },
      },
      { userPrompt: "Read index.html", context: [], tools: [] },
      streamCallback,
    );

    // Tool calls are returned, not executed
    expect(response.toolCalls).toBeDefined();
    expect(response.toolCalls!.length).toBe(1);
    expect(response.toolCalls![0].function.name).toBe("file_read");
    expect(response.toolCalls![0].function.arguments).toBe('{"path":"index.html"}');
    expect(mockCreate).toHaveBeenCalledTimes(1);
  });

  it("falls back to non-streaming response when stream has no deltas", async () => {
    mockCreate
      .mockResolvedValueOnce(streamChunks([
        { choices: [{ delta: {}, finish_reason: "stop" }] },
      ]))
      .mockResolvedValueOnce({
        choices: [{ message: { content: "Fallback answer", tool_calls: [] }, finish_reason: "stop" }],
      });
    const streamCallback = vi.fn();

    const response = await api.chat(
      {
        provider: mockProvider as any,
        modelId: "test-model",
        params: { reasoning: false, temperature: 0.8, topP: 0.9, topK: 40, numPredict: -1, numCtx: 131072, maxCompletionTokens: 16384 },
      },
      { userPrompt: "Hello", context: [], tools: [] },
      streamCallback,
    );

    expect(response.response).toBe("Fallback answer");
    expect(streamCallback).toHaveBeenCalledWith({ type: "response", data: "Fallback answer" });
  });

  it("returns conversational responses without forcing another iteration", async () => {
    mockCreate.mockResolvedValueOnce(streamChunks([
      { choices: [{ delta: { content: "I can talk this through." }, finish_reason: "stop" }] },
    ]));
    const streamCallback = vi.fn();

    const response = await api.chat(
      {
        provider: mockProvider as any,
        modelId: "test-model",
        params: { reasoning: false, temperature: 0.8, topP: 0.9, topK: 40, numPredict: -1, numCtx: 131072, maxCompletionTokens: 16384 },
      },
      {
        userPrompt: "Don't edit code. Just talk to me.",
        context: [],
        tools: [{
          name: "file_edit",
          category: "file",
          definition: {
            type: "function" as const,
            function: {
              name: "file_edit",
              description: "Edit file",
              parameters: { type: "object", properties: {} },
            },
          },
          execute: vi.fn(),
        }],
      },
      streamCallback,
    );

    expect(response.response).toBe("I can talk this through.");
    expect(mockCreate).toHaveBeenCalledTimes(1);
  });
});
