import { assert } from "convex-helpers";
import { describe, expect, it } from "vitest";
import { toUIMessages } from "./UIMessages.js";
import type { MessageDoc } from "./validators.js";

// Helper to create a base message doc
function baseMessageDoc(overrides: Partial<MessageDoc> = {}): MessageDoc {
  return {
    _id: "msg1",
    _creationTime: Date.now(),
    order: 0,
    stepOrder: 0,
    status: "success",
    threadId: "thread1",
    tool: false,
    ...overrides,
  };
}

describe("toUIMessages", () => {
  it("handles user message", () => {
    const messages = [
      baseMessageDoc({
        message: {
          role: "user",
          content: "Hello!",
        },
        text: "Hello!",
      }),
    ];
    const uiMessages = toUIMessages(messages);
    expect(uiMessages).toHaveLength(1);
    expect(uiMessages[0].role).toBe("user");
    expect(uiMessages[0].text).toBe("Hello!");
    expect(uiMessages[0].parts[0]).toEqual({ type: "text", text: "Hello!" });
  });

  it("handles assistant message", () => {
    const messages = [
      baseMessageDoc({
        message: {
          role: "assistant",
          content: "Hi, how can I help?",
        },
        text: "Hi, how can I help?",
      }),
    ];
    const uiMessages = toUIMessages(messages);
    expect(uiMessages).toHaveLength(1);
    expect(uiMessages[0].role).toBe("assistant");
    expect(uiMessages[0].text).toBe("Hi, how can I help?");
    expect(uiMessages[0].parts[0]).toEqual({
      type: "text",
      text: "Hi, how can I help?",
      state: "done",
    });
  });

  it("handles multiple messages", () => {
    const messages = [
      baseMessageDoc({
        message: {
          role: "user",
          content: "Hello!",
        },
        text: "Hello!",
      }),
      baseMessageDoc({
        message: {
          role: "assistant",
          content: [
            {
              type: "reasoning",
              text: "I'm thinking...",
            },
            {
              type: "redacted-reasoning",
              data: "asdfasdfasdf",
            },
            {
              type: "text",
              text: "I'm thinking...",
            },
            {
              type: "file",
              mimeType: "text/plain",
              data: "https://example.com/file.txt",
            },
            {
              type: "tool-call",
              toolName: "myTool",
              toolCallId: "call1",
              input: "an arg",
              args: "an arg",
            },
          ],
        },
        tool: true,
        reasoning: "I'm thinking...",
        text: "I'm thinking...",
      }),
      baseMessageDoc({
        message: {
          role: "tool",
          content: [
            {
              type: "tool-result",
              toolCallId: "call1",
              toolName: "myTool",
              output: {
                type: "text",
                value: "42",
              },
            },
          ],
        },
        tool: true,
      }),
    ];
    const uiMessages = toUIMessages(messages);
    expect(uiMessages).toHaveLength(2);
    expect(uiMessages[0].role).toBe("user");
    expect(uiMessages[0].parts.filter((p) => p.type === "text")).toHaveLength(
      1,
    );
    expect(uiMessages[1].role).toBe("assistant");
    expect(
      uiMessages[1].parts.filter((p) => p.type === "tool-myTool"),
    ).toHaveLength(1);
    expect(
      uiMessages[1].parts.filter((p) => p.type === "tool-myTool")[0],
    ).toMatchObject({
      type: "tool-myTool",
      toolCallId: "call1",
      state: "output-available",
      output: "42",
    });
  });

  it("handles multiple text and reasoning parts", () => {
    const messages = [
      baseMessageDoc({
        message: {
          role: "assistant",
          content: [
            {
              type: "reasoning",
              text: "I'm thinking...",
            },
            {
              type: "text",
              text: "Here's one idea.",
            },
            {
              type: "reasoning",
              text: "I'm thinking...",
            },
            {
              type: "text",
              text: "Here's another idea.",
            },
          ],
        },
        reasoning: "I'm thinking... I'm thinking...",
        text: "Here's one idea. Here's another idea.",
      }),
    ];
    const uiMessages = toUIMessages(messages);
    expect(uiMessages).toHaveLength(1);
    expect(uiMessages[0].role).toBe("assistant");
    expect(uiMessages[0].text).toBe("Here's one idea. Here's another idea.");
    expect(uiMessages[0].parts.filter((p) => p.type === "reasoning")).toEqual([
      {
        providerOptions: undefined,
        state: "done",
        text: "I'm thinking...",
        type: "reasoning",
      },
      {
        providerOptions: undefined,
        state: "done",
        text: "I'm thinking...",
        type: "reasoning",
      },
    ]);
    expect(uiMessages[0].parts[0].type).toBe("reasoning");
    assert(uiMessages[0].parts[0].type === "reasoning");
    expect(uiMessages[0].parts[0].text).toBe("I'm thinking...");
    expect(uiMessages[0].parts[1].type).toBe("text");
    assert(uiMessages[0].parts[1].type === "text");
    expect(uiMessages[0].parts[1].text).toBe("Here's one idea.");
    expect(uiMessages[0].parts[2].type).toBe("reasoning");
    assert(uiMessages[0].parts[2].type === "reasoning");
    expect(uiMessages[0].parts[2].text).toBe("I'm thinking...");

    expect(uiMessages[0].parts.filter((p) => p.type === "text")).toHaveLength(
      2,
    );
    expect(uiMessages[0].parts.filter((p) => p.type === "text")[0].text).toBe(
      "Here's one idea.",
    );
    expect(uiMessages[0].parts.filter((p) => p.type === "text")[1].text).toBe(
      "Here's another idea.",
    );
  });

  it("combines text from between messages", () => {
    const messages = [
      baseMessageDoc({
        message: {
          role: "assistant",
          content: [
            {
              type: "reasoning",
              text: "I'm thinking...",
            },
            {
              type: "text",
              text: "I'm going to ask a question.",
            },
            {
              type: "tool-call",
              input: "What's the meaning of life?",
              args: "What's the meaning of life?",
              toolCallId: "call1",
              toolName: "myTool",
            },
          ],
        },
        reasoning: "I'm thinking...",
        text: "Here's one idea.",
        tool: true,
        order: 1,
        stepOrder: 1,
      }),
      baseMessageDoc({
        message: {
          role: "tool",
          content: [
            {
              type: "tool-result",
              toolCallId: "call1",
              toolName: "myTool",
              output: {
                type: "text",
                value: "42",
              },
            },
          ],
        },
        text: "",
        tool: true,
        order: 1,
        stepOrder: 2,
      }),
      baseMessageDoc({
        message: {
          role: "assistant",
          content: [
            {
              type: "reasoning",
              text: "Thinking again...",
            },
            {
              type: "text",
              text: "Ok now I know.",
            },
          ],
        },
        text: "One last thing.",
        order: 1,
        stepOrder: 3,
      }),
    ];
    const uiMessages = toUIMessages(messages);
    expect(uiMessages).toHaveLength(1);
    expect(uiMessages[0].role).toBe("assistant");
    expect(uiMessages[0].text).toBe(
      "I'm going to ask a question. Ok now I know.",
    );
  });

  it("handles system message", () => {
    const messages = [
      baseMessageDoc({
        message: {
          role: "system",
          content: "System message here",
        },
        text: "System message here",
      }),
    ];
    const uiMessages = toUIMessages(messages);
    expect(uiMessages).toHaveLength(1);
    expect(uiMessages[0].role).toBe("system");
    expect(uiMessages[0].text).toBe("System message here");
    expect(uiMessages[0].parts[0]).toEqual({
      type: "text",
      text: "System message here",
      state: "done",
      providerMetadata: undefined,
    });
  });

  it("handles wrapped JSON tool output", () => {
    const messages = [
      baseMessageDoc({
        message: {
          role: "assistant",
          content: [
            {
              type: "tool-call",
              toolName: "myTool",
              toolCallId: "call1",
              input: { query: "test" },
              args: { query: "test" },
            },
          ],
        },
        tool: true,
      }),
      baseMessageDoc({
        message: {
          role: "tool",
          content: [
            {
              type: "tool-result",
              toolName: "myTool",
              toolCallId: "call1",
              output: {
                type: "json",
                value: { data: "wrapped result", success: true },
              },
            },
          ],
        },
        tool: true,
      }),
    ];
    const uiMessages = toUIMessages(messages);
    expect(uiMessages).toHaveLength(1);
    const toolPart = uiMessages[0].parts.find((p) => p.type === "tool-myTool");
    expect(toolPart).toMatchObject({
      type: "tool-myTool",
      toolCallId: "call1",
      state: "output-available",
      input: { query: "test" },
      output: { data: "wrapped result", success: true }, // Should be unwrapped
    });
  });

  it("handles tool call", () => {
    const messages = [
      baseMessageDoc({
        message: {
          role: "assistant",
          content: [
            {
              type: "tool-call",
              toolName: "myTool",
              toolCallId: "call1",
              input: "hi",
              args: "hi",
            },
          ],
        },
        text: "",
      }),
    ];
    const uiMessages = toUIMessages(messages);
    expect(uiMessages).toHaveLength(1);
    expect(uiMessages[0].role).toBe("assistant");
    expect(
      uiMessages[0].parts.filter((p) => p.type === "tool-myTool")[0],
    ).toMatchObject({
      type: "tool-myTool",
      toolCallId: "call1",
      input: "hi",
      state: "input-available",
    });
  });

  it("handles tool result", () => {
    const messages = [
      baseMessageDoc({
        tool: true,
        message: {
          role: "assistant",
          content: [
            {
              type: "tool-call",
              toolName: "myTool",
              toolCallId: "call1",
              input: "",
              args: "",
            },
          ],
        },
        text: "",
      }),
      baseMessageDoc({
        message: {
          role: "tool",
          content: [
            {
              type: "tool-result",
              toolCallId: "call1",
              toolName: "myTool",
              result: "42",
            },
          ],
        },
        text: "",
      }),
    ];
    const uiMessages = toUIMessages(messages);
    expect(uiMessages).toHaveLength(1);
    expect(uiMessages[0].role).toBe("assistant");
    // Should have a tool-invocation part
    expect(uiMessages[0].parts.some((p) => p.type === "tool-myTool")).toBe(
      true,
    );
  });

  it("does not duplicate text content", () => {
    const messages = [
      baseMessageDoc({
        message: {
          role: "assistant",
          content: "Hello!",
        },
        text: "Hello!",
      }),
    ];
    const uiMessages = toUIMessages(messages);
    // There should only be one text part
    const textParts = uiMessages[0].parts.filter((p) => p.type === "text");
    expect(textParts).toHaveLength(1);
    expect(textParts[0].text).toBe("Hello!");
  });

  it("sets text field correctly when message has many parts with text part as final part", () => {
    const messages = [
      baseMessageDoc({
        message: {
          role: "assistant",
          content: [
            {
              type: "reasoning",
              text: "Let me think about this...",
            },
            {
              type: "tool-call",
              toolName: "calculator",
              toolCallId: "call1",
              input: { operation: "add", a: 2, b: 3 },
              args: { operation: "add", a: 2, b: 3 },
            },
            {
              type: "file",
              mimeType: "application/json",
              data: "some-file-data",
            },
            {
              type: "text",
              text: "Here's my final answer.",
            },
          ],
        },
        text: "Here's my final answer.",
        reasoning: "Let me think about this...",
      }),
    ];

    const uiMessages = toUIMessages(messages);

    expect(uiMessages).toHaveLength(1);
    expect(uiMessages[0].role).toBe("assistant");
    expect(uiMessages[0].text).toBe("Here's my final answer.");

    // Verify that the text part exists in parts
    const textParts = uiMessages[0].parts.filter((p) => p.type === "text");
    expect(textParts).toHaveLength(1);
    expect(textParts[0].text).toBe("Here's my final answer.");
  });

  // Add more tests for array content, tool calls, etc. as needed

  it("should update tool call state from input-available to output-available", () => {
    const messages = [
      baseMessageDoc({
        message: {
          role: "assistant",
          content: [
            {
              type: "tool-call",
              toolName: "calculator",
              toolCallId: "call1",
              input: { operation: "add", a: 1, b: 2 },
              args: { operation: "add", a: 1, b: 2 },
            },
          ],
        },
        tool: true,
      }),
      baseMessageDoc({
        message: {
          role: "tool",
          content: [
            {
              type: "tool-result",
              toolCallId: "call1",
              toolName: "calculator",
              result: { sum: 3 },
            },
          ],
        },
        tool: true,
      }),
    ];

    const uiMessages = toUIMessages(messages);

    // Should have one assistant message
    expect(uiMessages).toHaveLength(1);
    expect(uiMessages[0].role).toBe("assistant");

    // Should have a single tool-calculator part (not separate tool-call and tool-result parts)
    const toolParts = uiMessages[0].parts.filter(
      (p) => p.type === "tool-calculator",
    );
    expect(toolParts).toHaveLength(1);

    const toolPart = toolParts[0];
    expect(toolPart).toMatchObject({
      type: "tool-calculator",
      toolCallId: "call1",
      state: "output-available",
      input: { operation: "add", a: 1, b: 2 },
      output: { sum: 3 },
    });

    // Should NOT have a tool-call part (which is what currently happens)
    const toolCallParts = uiMessages[0].parts.filter(
      (p) => p.type === "tool-call",
    );
    expect(toolCallParts).toHaveLength(0);
  });
  it("sets text field correctly in multi-step tool-call + final-text flow", () => {
    const messages = [
      baseMessageDoc({
        text: "what time is it in paris?",
        message: {
          role: "user",
          content: "what time is it in paris?",
        },
      }),
      baseMessageDoc({
        tool: true,
        finishReason: "tool-calls",
        text: "",
        stepOrder: 1,
        message: {
          role: "assistant",
          content: [
            {
              type: "reasoning",
              text: "**Finding the Time**\n\nI've pinpointed the core task: obtaining the current time in Paris. It involves using the `dateTime` tool. I've identified \"Europe/Paris\" as the necessary timezone identifier to provide to the tool. My next step is to test the tool.\n\n\n",
            },
            {
              input: {
                timezone: "Europe/Paris",
              },
              args: {
                timezone: "Europe/Paris",
              },
              type: "tool-call",
              toolName: "dateTime",
              toolCallId: "tool_0_dateTime",
            },
          ],
        },
        reasoning:
          "**Finding the Time**\n\nI've pinpointed the core task: obtaining the current time in Paris. It involves using the `dateTime` tool. I've identified \"Europe/Paris\" as the necessary timezone identifier to provide to the tool. My next step is to test the tool.\n\n\n",
        reasoningDetails: [
          {
            text: "**Finding the Time**\n\nI've pinpointed the core task: obtaining the current time in Paris. It involves using the `dateTime` tool. I've identified \"Europe/Paris\" as the necessary timezone identifier to provide to the tool. My next step is to test the tool.\n\n\n",
            type: "reasoning",
          },
        ],
        warnings: [],
      }),
      baseMessageDoc({
        tool: true,
        stepOrder: 2,
        message: {
          role: "tool",
          content: [
            {
              type: "tool-result",
              toolCallId: "tool_0_dateTime",
              toolName: "dateTime",
              result: {
                type: "json",
                value: {
                  day: "20",
                  hours: 16,
                  minutes: 3,
                  month: "August",
                  year: "2025",
                },
              },
            },
          ],
        },
        sources: [],
      }),
      baseMessageDoc({
        finishReason: "stop",
        text: "The time in Paris, France is 4:03 PM on August 20, 2025.",
        stepOrder: 3,
        message: {
          role: "assistant",
          content: [
            {
              type: "text",
              text: "The time in Paris, France is 4:03 PM on August 20, 2025.",
            },
          ],
        },
        reasoningDetails: [],
        sources: [],
        warnings: [],
      }),
    ];

    const uiMessages = toUIMessages(messages);

    expect(uiMessages).toHaveLength(2);
    expect(uiMessages[0].role).toBe("user");
    expect(uiMessages[0].text).toBe("what time is it in paris?");
    expect(uiMessages[1].role).toBe("assistant");
    expect(uiMessages[1].text).toBe(
      "The time in Paris, France is 4:03 PM on August 20, 2025.",
    );
  });

  it("handles messages in reverse order (assistant response, then tool response, then tool call)", () => {
    const messages = [
      // Final assistant response comes first (stepOrder 3) - no tool flag since it's the final message
      baseMessageDoc({
        _id: "msg3",
        order: 1,
        stepOrder: 3,
        finishReason: "stop",
        text: "The result is 42.",
        tool: false, // This is the final message without tool calls
        message: {
          role: "assistant",
          content: [
            {
              type: "text",
              text: "The result is 42.",
            },
          ],
        },
      }),
      // Tool result comes second (stepOrder 2)
      baseMessageDoc({
        _id: "msg2",
        order: 1,
        stepOrder: 2,
        tool: true,
        message: {
          role: "tool",
          content: [
            {
              type: "tool-result",
              toolCallId: "call1",
              toolName: "calculator",
              output: {
                type: "text",
                value: "42",
              },
            },
          ],
        },
      }),
      // Tool call comes last (stepOrder 1)
      baseMessageDoc({
        _id: "msg1",
        order: 1,
        stepOrder: 1,
        tool: true,
        text: "",
        message: {
          role: "assistant",
          content: [
            {
              type: "tool-call",
              toolName: "calculator",
              toolCallId: "call1",
              input: { operation: "add", a: 40, b: 2 },
              args: { operation: "add", a: 40, b: 2 },
            },
          ],
        },
      }),
    ];

    const uiMessages = toUIMessages(messages);

    expect(uiMessages).toHaveLength(1);
    expect(uiMessages[0].role).toBe("assistant");

    // Should concatenate text from all messages (only the final response has text)
    expect(uiMessages[0].text).toBe("The result is 42.");

    // Should use first message's fields (msg1 with stepOrder 1)
    expect(uiMessages[0].id).toBe("msg1");
    expect(uiMessages[0].order).toBe(1);
    expect(uiMessages[0].stepOrder).toBe(1);

    // Should have both tool call and result parts
    const toolParts = uiMessages[0].parts.filter(
      (p) => p.type === "tool-calculator",
    );
    expect(toolParts).toHaveLength(1);
    expect(toolParts[0]).toMatchObject({
      type: "tool-calculator",
      toolCallId: "call1",
      state: "output-available",
      input: { operation: "add", a: 40, b: 2 },
      output: "42",
    });

    // Should also have text part
    const textParts = uiMessages[0].parts.filter((p) => p.type === "text");
    expect(textParts).toHaveLength(1);
    expect(textParts[0].text).toBe("The result is 42.");
  });

  it("shows output-error state when tool result has isError: true (issue #162)", () => {
    const messages = [
      // Tool call
      baseMessageDoc({
        _id: "msg1",
        order: 1,
        stepOrder: 1,
        tool: true,
        message: {
          role: "assistant",
          content: [
            {
              type: "tool-call",
              toolName: "generateImage",
              toolCallId: "call1",
              input: { id: "invalid-id" },
              args: { id: "invalid-id" },
            },
          ],
        },
      }),
      // Tool result with error
      baseMessageDoc({
        _id: "msg2",
        order: 1,
        stepOrder: 2,
        tool: true,
        message: {
          role: "tool",
          content: [
            {
              type: "tool-result",
              toolCallId: "call1",
              toolName: "generateImage",
              output: {
                type: "text",
                value:
                  'ArgumentValidationError: Value does not match validator.\nPath: .id\nValue: "invalid-id"\nValidator: v.id("images")',
              },
              isError: true,
            },
          ],
        },
      }),
    ];

    const uiMessages = toUIMessages(messages);

    expect(uiMessages).toHaveLength(1);
    expect(uiMessages[0].role).toBe("assistant");

    const toolParts = uiMessages[0].parts.filter(
      (p) => p.type === "tool-generateImage",
    );
    expect(toolParts).toHaveLength(1);

    const toolPart = toolParts[0] as any;
    expect(toolPart.toolCallId).toBe("call1");
    // Should show output-error, not output-available
    expect(toolPart.state).toBe("output-error");
    expect(toolPart.output).toContain("ArgumentValidationError");
  });

  it("shows output-error when tool result has isError: true without tool call present (issue #162)", () => {
    // This simulates the case where the tool-call message wasn't saved
    const messages = [
      baseMessageDoc({
        _id: "msg1",
        order: 1,
        stepOrder: 1,
        tool: true,
        message: {
          role: "tool",
          content: [
            {
              type: "tool-result",
              toolCallId: "call1",
              toolName: "generateImage",
              output: {
                type: "text",
                value: "Error: Something went wrong",
              },
              isError: true,
            },
          ],
        },
      }),
    ];

    const uiMessages = toUIMessages(messages);

    expect(uiMessages).toHaveLength(1);
    expect(uiMessages[0].role).toBe("assistant");

    const toolParts = uiMessages[0].parts.filter(
      (p) => p.type === "tool-generateImage",
    );
    expect(toolParts).toHaveLength(1);

    const toolPart = toolParts[0] as any;
    expect(toolPart.state).toBe("output-error");
  });

  describe("userId preservation", () => {
    it("preserves userId in user messages", () => {
      const messages = [
        baseMessageDoc({
          userId: "user123",
          message: {
            role: "user",
            content: "Hello!",
          },
          text: "Hello!",
        }),
      ];
      const uiMessages = toUIMessages(messages);
      expect(uiMessages).toHaveLength(1);
      expect(uiMessages[0].role).toBe("user");
      expect(uiMessages[0].userId).toBe("user123");
    });

    it("preserves userId in system messages", () => {
      const messages = [
        baseMessageDoc({
          userId: "user456",
          message: {
            role: "system",
            content: "System prompt",
          },
          text: "System prompt",
        }),
      ];
      const uiMessages = toUIMessages(messages);
      expect(uiMessages).toHaveLength(1);
      expect(uiMessages[0].role).toBe("system");
      expect(uiMessages[0].userId).toBe("user456");
    });

    it("preserves userId in assistant messages", () => {
      const messages = [
        baseMessageDoc({
          userId: "user789",
          message: {
            role: "assistant",
            content: "Hi there!",
          },
          text: "Hi there!",
        }),
      ];
      const uiMessages = toUIMessages(messages);
      expect(uiMessages).toHaveLength(1);
      expect(uiMessages[0].role).toBe("assistant");
      expect(uiMessages[0].userId).toBe("user789");
    });

    it("preserves userId from first message in grouped assistant messages", () => {
      const messages = [
        baseMessageDoc({
          _id: "msg1",
          userId: "userA",
          order: 1,
          stepOrder: 1,
          tool: true,
          message: {
            role: "assistant",
            content: [
              {
                type: "tool-call",
                toolName: "myTool",
                toolCallId: "call1",
                input: {},
                args: {},
              },
            ],
          },
          text: "",
        }),
        baseMessageDoc({
          _id: "msg2",
          userId: "userA",
          order: 1,
          stepOrder: 2,
          tool: true,
          message: {
            role: "tool",
            content: [
              {
                type: "tool-result",
                toolCallId: "call1",
                toolName: "myTool",
                output: { type: "text", value: "result" },
              },
            ],
          },
          text: "",
        }),
        baseMessageDoc({
          _id: "msg3",
          userId: "userA",
          order: 1,
          stepOrder: 3,
          message: {
            role: "assistant",
            content: "Done!",
          },
          text: "Done!",
        }),
      ];
      const uiMessages = toUIMessages(messages);
      expect(uiMessages).toHaveLength(1);
      expect(uiMessages[0].role).toBe("assistant");
      expect(uiMessages[0].userId).toBe("userA");
    });

    it("handles undefined userId gracefully", () => {
      const messages = [
        baseMessageDoc({
          // No userId provided
          message: {
            role: "user",
            content: "Hello!",
          },
          text: "Hello!",
        }),
      ];
      const uiMessages = toUIMessages(messages);
      expect(uiMessages).toHaveLength(1);
      expect(uiMessages[0].userId).toBeUndefined();
    });
  });

  describe("tool approval workflow", () => {
    it("sets state to approval-requested when tool-approval-request is present", () => {
      const messages = [
        baseMessageDoc({
          _id: "msg1",
          order: 1,
          stepOrder: 1,
          tool: true,
          message: {
            role: "assistant",
            content: [
              {
                type: "tool-call",
                toolName: "dangerousTool",
                toolCallId: "call1",
                input: { action: "delete" },
                args: { action: "delete" },
              },
              {
                type: "tool-approval-request",
                approvalId: "approval1",
                toolCallId: "call1",
              },
            ],
          },
        }),
      ];

      const uiMessages = toUIMessages(messages);

      expect(uiMessages).toHaveLength(1);
      const toolPart = uiMessages[0].parts.find(
        (p) => p.type === "tool-dangerousTool",
      ) as any;
      expect(toolPart).toBeDefined();
      expect(toolPart.state).toBe("approval-requested");
      expect(toolPart.approval).toEqual({ id: "approval1" });
    });

    it("sets state to approval-responded when tool-approval-response with approved: true", () => {
      const messages = [
        baseMessageDoc({
          _id: "msg1",
          order: 1,
          stepOrder: 1,
          tool: true,
          message: {
            role: "assistant",
            content: [
              {
                type: "tool-call",
                toolName: "dangerousTool",
                toolCallId: "call1",
                input: { action: "delete" },
                args: { action: "delete" },
              },
              {
                type: "tool-approval-request",
                approvalId: "approval1",
                toolCallId: "call1",
              },
            ],
          },
        }),
        baseMessageDoc({
          _id: "msg2",
          order: 1,
          stepOrder: 2,
          tool: true,
          message: {
            role: "tool",
            content: [
              {
                type: "tool-approval-response",
                approvalId: "approval1",
                approved: true,
                reason: "User confirmed",
              },
            ],
          },
        }),
      ];

      const uiMessages = toUIMessages(messages);

      expect(uiMessages).toHaveLength(1);
      const toolPart = uiMessages[0].parts.find(
        (p) => p.type === "tool-dangerousTool",
      ) as any;
      expect(toolPart).toBeDefined();
      expect(toolPart.state).toBe("approval-responded");
      expect(toolPart.approval).toEqual({
        id: "approval1",
        approved: true,
        reason: "User confirmed",
      });
    });

    it("sets state to output-denied when tool-approval-response with approved: false", () => {
      const messages = [
        baseMessageDoc({
          _id: "msg1",
          order: 1,
          stepOrder: 1,
          tool: true,
          message: {
            role: "assistant",
            content: [
              {
                type: "tool-call",
                toolName: "dangerousTool",
                toolCallId: "call1",
                input: { action: "delete" },
                args: { action: "delete" },
              },
              {
                type: "tool-approval-request",
                approvalId: "approval1",
                toolCallId: "call1",
              },
            ],
          },
        }),
        baseMessageDoc({
          _id: "msg2",
          order: 1,
          stepOrder: 2,
          tool: true,
          message: {
            role: "tool",
            content: [
              {
                type: "tool-approval-response",
                approvalId: "approval1",
                approved: false,
                reason: "User declined the operation",
              },
            ],
          },
        }),
      ];

      const uiMessages = toUIMessages(messages);

      expect(uiMessages).toHaveLength(1);
      const toolPart = uiMessages[0].parts.find(
        (p) => p.type === "tool-dangerousTool",
      ) as any;
      expect(toolPart).toBeDefined();
      expect(toolPart.state).toBe("output-denied");
      expect(toolPart.approval).toEqual({
        id: "approval1",
        approved: false,
        reason: "User declined the operation",
      });
    });

    it("sets state to output-denied when tool-result has execution-denied output", () => {
      const messages = [
        baseMessageDoc({
          _id: "msg1",
          order: 1,
          stepOrder: 1,
          tool: true,
          message: {
            role: "assistant",
            content: [
              {
                type: "tool-call",
                toolName: "dangerousTool",
                toolCallId: "call1",
                input: { action: "delete" },
                args: { action: "delete" },
              },
            ],
          },
        }),
        baseMessageDoc({
          _id: "msg2",
          order: 1,
          stepOrder: 2,
          tool: true,
          message: {
            role: "tool",
            content: [
              {
                type: "tool-result",
                toolCallId: "call1",
                toolName: "dangerousTool",
                output: {
                  type: "execution-denied",
                  reason: "Tool execution was denied by the user",
                },
              },
            ],
          },
        }),
      ];

      const uiMessages = toUIMessages(messages);

      expect(uiMessages).toHaveLength(1);
      const toolPart = uiMessages[0].parts.find(
        (p) => p.type === "tool-dangerousTool",
      ) as any;
      expect(toolPart).toBeDefined();
      expect(toolPart.state).toBe("output-denied");
      expect(toolPart.approval).toEqual({
        id: "",
        approved: false,
        reason: "Tool execution was denied by the user",
      });
    });

    it("handles full approval flow: request → approved → executed → output-available", () => {
      const messages = [
        baseMessageDoc({
          _id: "msg1",
          order: 1,
          stepOrder: 1,
          tool: true,
          message: {
            role: "assistant",
            content: [
              {
                type: "tool-call",
                toolName: "dangerousTool",
                toolCallId: "call1",
                input: { action: "delete" },
                args: { action: "delete" },
              },
              {
                type: "tool-approval-request",
                approvalId: "approval1",
                toolCallId: "call1",
              },
            ],
          },
        }),
        baseMessageDoc({
          _id: "msg2",
          order: 1,
          stepOrder: 2,
          tool: true,
          message: {
            role: "tool",
            content: [
              {
                type: "tool-approval-response",
                approvalId: "approval1",
                approved: true,
              },
            ],
          },
        }),
        baseMessageDoc({
          _id: "msg3",
          order: 1,
          stepOrder: 3,
          tool: true,
          message: {
            role: "tool",
            content: [
              {
                type: "tool-result",
                toolCallId: "call1",
                toolName: "dangerousTool",
                output: {
                  type: "json",
                  value: { deleted: true },
                },
              },
            ],
          },
        }),
      ];

      const uiMessages = toUIMessages(messages);

      expect(uiMessages).toHaveLength(1);
      const toolPart = uiMessages[0].parts.find(
        (p) => p.type === "tool-dangerousTool",
      ) as any;
      expect(toolPart).toBeDefined();
      // After tool-result, state should be output-available
      expect(toolPart.state).toBe("output-available");
      expect(toolPart.output).toEqual({ deleted: true });
      // approval should still be preserved from earlier
      expect(toolPart.approval).toEqual({
        id: "approval1",
        approved: true,
        reason: undefined,
      });
    });
  });
});
