import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { mapConversationMessagesToAiSdk } from "./provider-message-mapping.js";
import {
  PLAN_UPDATE_TOOL_NAME,
  prepareProviderToolNameBindings,
  toProviderHistoryToolName,
  toProviderToolName,
} from "./provider-tool-names.js";

const tempDirs: string[] = [];

afterEach(() => {
  while (tempDirs.length > 0) {
    rmSync(tempDirs.pop() as string, { force: true, recursive: true });
  }
});

describe("mapConversationMessagesToAiSdk", () => {
  it("materializes path-backed image and file prompt parts", async () => {
    const dir = mkdtempSync(`${tmpdir()}/best-agent-provider-message-mapping-`);
    tempDirs.push(dir);
    const imagePath = join(dir, "diagram.png");
    const filePath = join(dir, "report.pdf");
    writeFileSync(imagePath, Buffer.from([1, 2, 3, 4]));
    writeFileSync(filePath, Buffer.from([5, 6, 7, 8]));

    const messages = await mapConversationMessagesToAiSdk([
      {
        role: "user",
        content: "[diagram.png](/tmp/diagram.png)\nExplain the report.",
        contentParts: [
          { type: "text", text: "Image first.\n" },
          { type: "image", path: imagePath, mediaType: "image/png", filename: "diagram.png" },
          { type: "text", text: "\nThen the file.\n" },
          { type: "file", path: filePath, mediaType: "application/pdf", filename: "report.pdf" },
        ],
      },
    ]);

    expect(messages).toHaveLength(1);
    expect(messages[0]).toMatchObject({
      role: "user",
      content: [
        { type: "text", text: "Image first.\n" },
        { type: "image", mediaType: "image/png" },
        { type: "text", text: "\nThen the file.\n" },
        { type: "file", filename: "report.pdf", mediaType: "application/pdf" },
      ],
    });

    const content = (messages[0] as { content: Array<Record<string, unknown>> }).content;
    expect(content[1]?.image).toBeInstanceOf(Uint8Array);
    expect(content[3]?.data).toBeInstanceOf(Uint8Array);
  });

  it("converts unreadable image attachments to model-visible error text", async () => {
    const dir = mkdtempSync(`${tmpdir()}/best-agent-provider-message-mapping-empty-`);
    tempDirs.push(dir);
    const emptyPath = join(dir, "empty.png");
    writeFileSync(emptyPath, "");

    const messages = await mapConversationMessagesToAiSdk([
      {
        role: "user",
        content: "Look at these images.",
        contentParts: [
          {
            type: "image",
            path: "/tmp/does-not-exist-image.png",
            mediaType: "image/png",
          },
          {
            type: "image",
            image: "data:image/png;base64,",
            mediaType: "image/png",
            filename: "empty-data.png",
          },
          {
            type: "image",
            path: emptyPath,
            mediaType: "image/png",
          },
        ],
      },
    ]);

    expect(messages).toEqual([
      {
        role: "user",
        content: [
          {
            type: "text",
            text: expect.stringContaining("path does not exist: /tmp/does-not-exist-image.png"),
          },
          {
            type: "text",
            text: expect.stringContaining(
              'Cannot read image attachment "empty-data.png" (image data is empty or corrupted)',
            ),
          },
          {
            type: "text",
            text: expect.stringContaining(`path is not a readable file: ${emptyPath}`),
          },
        ],
      },
    ]);
  });

  it("converts unreadable file attachments to model-visible error text", async () => {
    const messages = await mapConversationMessagesToAiSdk([
      {
        role: "user",
        content: "Read this file.",
        contentParts: [
          {
            type: "file",
            path: "/tmp/does-not-exist-report.pdf",
            mediaType: "application/pdf",
            filename: "report.pdf",
          },
        ],
      },
    ]);

    expect(messages).toEqual([
      {
        role: "user",
        content: [
          {
            type: "text",
            text: expect.stringContaining(
              'Cannot read file attachment "report.pdf" (path does not exist: /tmp/does-not-exist-report.pdf)',
            ),
          },
        ],
      },
    ]);
  });

  it("maps assistant tool calls and tool results structurally", async () => {
    const messages = await mapConversationMessagesToAiSdk([
      {
        role: "assistant",
        content: "",
        toolCalls: [
          {
            input: { path: "package.json" },
            toolCallId: "call_read",
            toolName: "read-file",
          },
        ],
      },
      {
        role: "tool",
        content:
          "fatal: could not open '/dev/null' for reading and writing: Operation not permitted",
        failureKind: "execution-failed",
        outcome: "error",
        toolCallId: "call_read",
        toolName: "read-file",
      },
    ]);

    expect(messages).toEqual([
      {
        role: "assistant",
        content: [
          {
            type: "tool-call",
            input: { path: "package.json" },
            toolCallId: "call_read",
            toolName: "read-file",
          },
        ],
      },
      {
        role: "tool",
        content: [
          {
            type: "tool-result",
            toolCallId: "call_read",
            toolName: "read-file",
            output: {
              type: "error-text",
              value:
                "fatal: could not open '/dev/null' for reading and writing: Operation not permitted",
            },
          },
        ],
      },
    ]);
  });

  it("maps runtime observations as provider-visible observations, not assistant text", async () => {
    const messages = await mapConversationMessagesToAiSdk([
      {
        code: "PROVIDER_NO_ACTION",
        content: "Runtime observation [ERROR PROVIDER_NO_ACTION]:\nChoose one action.",
        role: "runtime",
        severity: "error",
      },
    ]);

    expect(messages).toEqual([
      {
        role: "user",
        content: "Runtime observation [ERROR PROVIDER_NO_ACTION]:\nChoose one action.",
      },
    ]);
  });

  it("maps runtime MCP tool names in provider follow-up history", async () => {
    const runtimeName = "mcp:docs:search";
    const providerName = toProviderToolName("mcp_docs_search_abcd1234");

    const messages = await mapConversationMessagesToAiSdk(
      [
        {
          role: "assistant",
          content: "",
          toolCalls: [
            {
              input: { input: "query" },
              toolCallId: "call_1",
              toolName: runtimeName,
            },
          ],
        },
        {
          role: "tool",
          content: "result",
          outcome: "success",
          toolCallId: "call_1",
          toolName: runtimeName,
        },
      ],
      [{ providerName, runtimeName }],
    );

    expect(JSON.stringify(messages)).toContain(providerName);
    expect(JSON.stringify(messages)).not.toContain(runtimeName);
  });

  it("maps runtime MCP tool names in provider generate history", async () => {
    const messages = await mapConversationMessagesToAiSdk(
      [
        {
          role: "assistant",
          content: "",
          toolCalls: [
            {
              input: { input: "query" },
              toolCallId: "call_1",
              toolName: "mcp:docs:search",
            },
          ],
        },
        {
          role: "tool",
          content: "result",
          outcome: "success",
          toolCallId: "call_1",
          toolName: "mcp:docs:search",
        },
      ],
      prepareProviderToolNameBindings([{ name: "mcp:docs:search" }]),
    );

    const serialized = JSON.stringify(messages);
    expect(serialized).not.toContain("mcp:docs:search");
    expect(serialized).toContain("mcp_docs_search_");
  });

  it("prefers persisted provider tool names in history", async () => {
    const messages = await mapConversationMessagesToAiSdk([
      {
        role: "assistant",
        content: "",
        toolCalls: [
          {
            input: { input: "query" },
            providerToolName: "mcp_docs_search_saved",
            toolCallId: "call_1",
            toolName: "mcp:docs:search",
          },
        ],
      },
      {
        role: "tool",
        content: "result",
        outcome: "success",
        providerToolName: "mcp_docs_search_saved",
        toolCallId: "call_1",
        toolName: "mcp:docs:search",
      },
    ]);

    const serialized = JSON.stringify(messages);
    expect(serialized).toContain("mcp_docs_search_saved");
    expect(serialized).not.toContain("mcp:docs:search");
  });

  it("ignores invalid persisted provider tool names in history", async () => {
    const runtimeName = "mcp:docs:search";
    const [binding] = prepareProviderToolNameBindings([{ name: runtimeName }]);
    expect(binding).toBeDefined();

    const messages = await mapConversationMessagesToAiSdk(
      [
        {
          role: "assistant",
          content: "",
          toolCalls: [
            {
              input: { input: "query" },
              providerToolName: runtimeName,
              toolCallId: "call_1",
              toolName: runtimeName,
            },
          ],
        },
        {
          role: "tool",
          content: "result",
          outcome: "success",
          providerToolName: runtimeName,
          toolCallId: "call_1",
          toolName: runtimeName,
        },
      ],
      [binding!],
    );

    const serialized = JSON.stringify(messages);
    expect(serialized).toContain(binding!.providerName);
    expect(serialized).not.toContain(runtimeName);
  });

  it("ignores reserved persisted provider tool names in history", async () => {
    const runtimeName = "plugin:plan_update";
    const [binding] = prepareProviderToolNameBindings([{ name: runtimeName }]);
    expect(binding).toBeDefined();

    const messages = await mapConversationMessagesToAiSdk(
      [
        {
          role: "assistant",
          content: "",
          toolCalls: [
            {
              input: {},
              providerToolName: PLAN_UPDATE_TOOL_NAME,
              toolCallId: "call_1",
              toolName: runtimeName,
            },
          ],
        },
        {
          role: "tool",
          content: "result",
          outcome: "success",
          providerToolName: PLAN_UPDATE_TOOL_NAME,
          toolCallId: "call_1",
          toolName: runtimeName,
        },
      ],
      [binding!],
    );

    const serialized = JSON.stringify(messages);
    expect(serialized).toContain(binding!.providerName);
    expect(serialized).not.toContain(PLAN_UPDATE_TOOL_NAME);
  });

  it("does not use reserved control names as fallback provider history tool names", () => {
    const toolName = toProviderHistoryToolName(PLAN_UPDATE_TOOL_NAME, []);

    expect(toolName).not.toBe(PLAN_UPDATE_TOOL_NAME);
    expect(toolName).toMatch(/^__best_agent_plan_update_[A-Za-z0-9]+$/u);
  });
});
