import type { ChatCompletionTool } from "openai/resources/chat/completions";
import { describe, it, expect, beforeAll, afterAll } from "vitest";

import { startServer, stopServer, getTestClient } from "./setup";

describe("Tool Calls", () => {
  beforeAll(async () => {
    await startServer({
      TOOL_CALL_PROBABILITY: "1.0", // Always make tool calls for deterministic tests
    });
  });

  afterAll(async () => {
    await stopServer();
  });

  const weatherTool: ChatCompletionTool = {
    type: "function",
    function: {
      name: "get_weather",
      description: "Get the weather for a location",
      parameters: {
        type: "object",
        properties: {
          location: { type: "string", description: "City name" },
          unit: { type: "string", enum: ["celsius", "fahrenheit"] },
        },
        required: ["location"],
      },
    },
  };

  const calculatorTool: ChatCompletionTool = {
    type: "function",
    function: {
      name: "calculator",
      description: "Perform a calculation",
      parameters: {
        type: "object",
        properties: {
          operation: {
            type: "string",
            enum: ["add", "subtract", "multiply", "divide"],
          },
          a: { type: "number" },
          b: { type: "number" },
        },
        required: ["operation", "a", "b"],
      },
    },
  };

  describe("Non-streaming", () => {
    it("should return a tool call when tools are provided", async () => {
      const client = getTestClient();

      const response = await client.chat.completions.create({
        model: "gpt-4o",
        messages: [{ role: "user", content: "What's the weather?" }],
        tools: [weatherTool],
        tool_choice: "required",
      });

      expect(response.choices[0].finish_reason).toBe("tool_calls");
      expect(response.choices[0].message.tool_calls).toBeDefined();
      expect(response.choices[0].message.tool_calls).toHaveLength(1);

      const toolCall = response.choices[0].message.tool_calls![0];
      expect(toolCall.id).toMatch(/^call_/);
      expect(toolCall.type).toBe("function");
      expect(toolCall.function.name).toBe("get_weather");

      // Arguments should be valid JSON
      const args = JSON.parse(toolCall.function.arguments);
      expect(args).toHaveProperty("location");
      expect(typeof args.location).toBe("string");
    });

    it("should generate arguments matching the schema", async () => {
      const client = getTestClient();

      const response = await client.chat.completions.create({
        model: "gpt-4o",
        messages: [{ role: "user", content: "Calculate something" }],
        tools: [calculatorTool],
        tool_choice: "required",
      });

      const toolCall = response.choices[0].message.tool_calls![0];
      expect(toolCall.function.name).toBe("calculator");

      const args = JSON.parse(toolCall.function.arguments);
      expect(args).toHaveProperty("operation");
      expect(args).toHaveProperty("a");
      expect(args).toHaveProperty("b");
      expect(["add", "subtract", "multiply", "divide"]).toContain(
        args.operation
      );
      expect(typeof args.a).toBe("number");
      expect(typeof args.b).toBe("number");
    });

    it("should pick from multiple tools", async () => {
      const client = getTestClient();

      const response = await client.chat.completions.create({
        model: "gpt-4o",
        messages: [{ role: "user", content: "Do something" }],
        tools: [weatherTool, calculatorTool],
        tool_choice: "required",
      });

      const toolCall = response.choices[0].message.tool_calls![0];
      expect(["get_weather", "calculator"]).toContain(toolCall.function.name);
    });

    it("should not return tool calls when tool_choice is none", async () => {
      const client = getTestClient();

      const response = await client.chat.completions.create({
        model: "gpt-4o",
        messages: [{ role: "user", content: "Hello" }],
        tools: [weatherTool],
        tool_choice: "none",
      });

      expect(response.choices[0].finish_reason).toBe("stop");
      expect(response.choices[0].message.tool_calls).toBeUndefined();
      expect(response.choices[0].message.content).toBeTruthy();
    });
  });

  describe("Advanced Schema Features", () => {
    it("should handle complex nested schemas with formats", async () => {
      const client = getTestClient();

      const complexTool: ChatCompletionTool = {
        type: "function",
        function: {
          name: "create_user",
          description: "Create a new user",
          parameters: {
            type: "object",
            properties: {
              email: { type: "string", format: "email" },
              website: { type: "string", format: "uri" },
              birthDate: { type: "string", format: "date" },
              age: { type: "integer", minimum: 18, maximum: 120 },
              score: {
                type: "number",
                minimum: 0,
                maximum: 100,
                multipleOf: 0.5,
              },
              role: { type: "string", enum: ["admin", "user", "guest"] },
              tags: {
                type: "array",
                items: { type: "string" },
                minItems: 1,
                maxItems: 5,
              },
              address: {
                type: "object",
                properties: {
                  street: { type: "string" },
                  city: { type: "string" },
                  zipCode: { type: "string", pattern: "^[0-9]{5}$" },
                },
                required: ["city"],
              },
            },
            required: ["email", "role"],
          },
        },
      };

      const response = await client.chat.completions.create({
        model: "gpt-4o",
        messages: [{ role: "user", content: "Create a user" }],
        tools: [complexTool],
        tool_choice: "required",
      });

      const toolCall = response.choices[0].message.tool_calls![0];
      const args = JSON.parse(toolCall.function.arguments);

      // Required fields should be present
      expect(args).toHaveProperty("email");
      expect(args).toHaveProperty("role");

      // Email should look like an email
      expect(args.email).toMatch(/@/);

      // Role should be from enum
      expect(["admin", "user", "guest"]).toContain(args.role);

      // Age should be within bounds (if present)
      if (args.age !== undefined) {
        expect(args.age).toBeGreaterThanOrEqual(18);
        expect(args.age).toBeLessThanOrEqual(120);
        expect(Number.isInteger(args.age)).toBe(true);
      }

      // Tags should be an array (if present)
      if (args.tags !== undefined) {
        expect(Array.isArray(args.tags)).toBe(true);
        expect(args.tags.length).toBeGreaterThanOrEqual(1);
        expect(args.tags.length).toBeLessThanOrEqual(5);
      }

      // Address should have required city (if present)
      if (args.address !== undefined) {
        expect(args.address).toHaveProperty("city");
      }
    });

    it("should handle oneOf schemas", async () => {
      const client = getTestClient();

      const oneOfTool: ChatCompletionTool = {
        type: "function",
        function: {
          name: "send_message",
          description: "Send a message",
          parameters: {
            type: "object",
            properties: {
              recipient: {
                oneOf: [
                  { type: "string", format: "email" },
                  {
                    type: "object",
                    properties: { userId: { type: "integer" } },
                    required: ["userId"],
                  },
                ],
              },
            },
            required: ["recipient"],
          },
        },
      };

      const response = await client.chat.completions.create({
        model: "gpt-4o",
        messages: [{ role: "user", content: "Send a message" }],
        tools: [oneOfTool],
        tool_choice: "required",
      });

      const toolCall = response.choices[0].message.tool_calls![0];
      const args = JSON.parse(toolCall.function.arguments);

      // recipient should be either a string or an object with userId
      expect(args).toHaveProperty("recipient");
      const recipient = args.recipient;
      const isString = typeof recipient === "string";
      const isUserIdObject =
        typeof recipient === "object" &&
        recipient !== null &&
        "userId" in recipient;
      expect(isString || isUserIdObject).toBe(true);
    });
  });

  describe("Streaming", () => {
    it("should stream tool calls correctly", async () => {
      const client = getTestClient();

      const stream = await client.chat.completions.create({
        model: "gpt-4o",
        messages: [{ role: "user", content: "What's the weather?" }],
        tools: [weatherTool],
        tool_choice: "required",
        stream: true,
      });

      let toolCallId: string | null = null;
      let toolCallName: string | null = null;
      let toolCallArgs = "";
      let hasFinishReason = false;

      for await (const chunk of stream) {
        const delta = chunk.choices[0]?.delta;

        if (delta?.tool_calls) {
          for (const tc of delta.tool_calls) {
            if (tc.id) {
              toolCallId = tc.id;
            }
            if (tc.function?.name) {
              toolCallName = tc.function.name;
            }
            if (tc.function?.arguments) {
              toolCallArgs += tc.function.arguments;
            }
          }
        }

        if (chunk.choices[0]?.finish_reason === "tool_calls") {
          hasFinishReason = true;
        }
      }

      expect(toolCallId).toMatch(/^call_/);
      expect(toolCallName).toBe("get_weather");
      expect(hasFinishReason).toBe(true);

      // Combined arguments should be valid JSON
      const args = JSON.parse(toolCallArgs);
      expect(args).toHaveProperty("location");
    });
  });
});
