import fs from "node:fs";
import * as os from "node:os";
import type { WebSearchResultBlock } from "@anthropic-ai/sdk/resources.mjs";
import type { ToolName, ToolRequestId } from "@magenta/core";
import lodash from "lodash";
import { expect, it } from "vitest";
import { $, within } from "zx";
import { getcwd } from "../nvim/nvim.ts";
import { withDriver } from "../test/preamble.ts";
import { pollUntil } from "../utils/async.ts";
import type { HomeDir, UnresolvedFilePath } from "../utils/files.ts";
import { resolveFilePath } from "../utils/files.ts";
import { LOGO } from "./thread-view.ts";

/** Sanitize display buffer text for stable snapshots by removing dynamic content */
function sanitizeDisplayForSnapshot(text: string): string {
  // Replace timing info like "exit code 0 (16ms)" with stable placeholder
  return text.replace(/\((\d+)ms\)/g, "(<timing>ms)");
}

/** Replace dynamic thread IDs and timing info in messages with a placeholder for stable snapshots */
function sanitizeMessagesForSnapshot<T>(messages: T): T {
  let json = JSON.stringify(messages);
  // Replace thread IDs like 019bab33-8c7c-76a9-bc7c-9f94103502c8 with placeholder
  json = json.replace(
    /\/tmp\/magenta\/threads\/[a-f0-9-]+\//g,
    "/tmp/magenta/threads/<thread-id>/",
  );
  // Replace timing info like "exit code 0 (16ms)" with stable placeholder
  json = json.replace(/\((\d+)ms\)/g, "(<timing>ms)");
  return JSON.parse(json) as T;
}

it("chat render and a few updates", async () => {
  await withDriver({}, async (driver) => {
    await driver.showSidebar();
    await driver.inputMagentaText("Can you run a simple command for me?");
    await driver.send();

    const stream = await driver.mockAnthropic.awaitPendingStream();
    const toolRequestId = "test-bash-command" as ToolRequestId;

    stream.respond({
      stopReason: "tool_use",
      text: "Sure, let me run a simple bash command for you.",
      toolRequests: [
        {
          status: "ok",
          value: {
            id: toolRequestId,
            toolName: "bash_command" as ToolName,
            input: { command: "echo 'Hello from bash!'" },
          },
        },
      ],
    });

    // Check that the buffer contains the expected content during tool execution
    await driver.assertDisplayBufferContains(
      "Can you run a simple command for me?",
    );
    await driver.assertDisplayBufferContains(
      "Sure, let me run a simple bash command for you.",
    );

    // After the tool executes
    await driver.assertDisplayBufferContains("Hello from bash!");
  });
});

it("new-thread creates fresh thread", async () => {
  await withDriver({}, async (driver) => {
    await driver.showSidebar();
    await driver.inputMagentaText("Can you look at my list of buffers?");
    await driver.send();

    const stream = await driver.mockAnthropic.awaitPendingStream();

    stream.respond({
      stopReason: "end_turn",
      text: "Sure, let me use the list_buffers tool.",
      toolRequests: [],
    });

    await driver.assertDisplayBufferContains(
      "Can you look at my list of buffers?",
    );
    await driver.assertDisplayBufferContains(
      "Sure, let me use the list_buffers tool.",
    );

    await driver.magenta.command("new-thread");
    await driver.assertDisplayBufferContains(LOGO.split("\n")[0]);
  });
});

it("getMessages correctly interleaves tool requests and responses", async () => {
  await withDriver({}, async (driver) => {
    // Create a more complex conversation with multiple tool uses
    await driver.showSidebar();
    await driver.inputMagentaText("Can you help me with my code?");
    await driver.send();

    const request1 = await driver.mockAnthropic.awaitPendingStream();

    const toolRequestId1 = "tool-1" as ToolRequestId;
    const toolRequestId2 = "tool-2" as ToolRequestId;

    // First response with bash_command tool use
    request1.respond({
      stopReason: "tool_use",
      text: "I'll help you. Let me check your project first.",
      toolRequests: [
        {
          status: "ok",
          value: {
            id: toolRequestId1,
            toolName: "bash_command" as ToolName,
            input: { command: "echo 'Project files summary'" },
          },
        },
      ],
    });

    await driver.assertDisplayBufferContains("Project files summary");

    const request2 = await driver.mockAnthropic.awaitPendingStream();
    request2.respond({
      stopReason: "tool_use",
      text: "Now let me check your project structure.",
      toolRequests: [
        {
          status: "ok",
          value: {
            id: toolRequestId2,
            toolName: "bash_command" as ToolName,
            input: { command: "echo 'Project structure summary'" },
          },
        },
      ],
    });

    await driver.assertDisplayBufferContains("Project structure summary");

    // Final part of the assistant's response
    const request3 = await driver.mockAnthropic.awaitPendingStream();
    request3.respond({
      stopReason: "end_turn",
      text: "Based on these results, I can help you.",
      toolRequests: [],
    });

    // Verify all parts of the conversation are present
    await driver.assertDisplayBufferContains("Can you help me with my code?");
    await driver.assertDisplayBufferContains(
      "I'll help you. Let me check your project first.",
    );
    await driver.assertDisplayBufferContains(
      "Now let me check your project structure.",
    );
    await driver.assertDisplayBufferContains(
      "Based on these results, I can help you.",
    );

    const thread = driver.magenta.chat.getActiveThread();
    const messages = thread.getMessages();

    expect(messages.length).toBe(8);
    expect(
      messages.flatMap((m) => m.content.map((b) => `${m.role}:${b.type}`)),
    ).toEqual([
      "user:text",
      "user:system_reminder",
      "assistant:text",
      "assistant:tool_use",
      "user:tool_result",
      "user:system_reminder", // system reminder after first tool response
      "assistant:text",
      "assistant:tool_use",
      "user:tool_result",
      "user:system_reminder", // system reminder after second tool response
      "assistant:text",
    ]);
  });
});

it("handles errors during streaming response", async () => {
  await withDriver({}, async (driver) => {
    await driver.showSidebar();
    await driver.inputMagentaText("Test error handling during response");
    await driver.send();

    const stream = await driver.mockAnthropic.awaitPendingStream();

    // Simulate an error during streaming
    const errorMessage = "Simulated error during streaming";
    stream.respondWithError(new Error(errorMessage));

    // After a non-retryable error, the agent's pre-submit history is rolled
    // back via truncateMessages. The failed user message and the error are
    // rendered as standalone blocks so the user can see what they tried to
    // send and why it failed; the input buffer is re-populated for easy
    // resubmission.
    await driver.assertDisplayBufferContains(
      "Test error handling during response",
    );
    await driver.assertDisplayBufferContains("Error");
    await driver.assertDisplayBufferContains(errorMessage);

    await driver.assertInputBufferContains(
      "Test error handling during response",
    );
  });
});

it("forks a thread with multiple messages into a new thread", async () => {
  await withDriver({}, async (driver) => {
    // 1. Open the sidebar
    await driver.showSidebar();

    // 2. Create a thread with multiple messages
    await driver.inputMagentaText("What is the capital of France?");
    await driver.send();

    // Wait for the request and respond
    const request1 = await driver.mockAnthropic.awaitPendingStream({
      message: "initial request",
    });
    request1.respond({
      stopReason: "end_turn",
      text: "The capital of France is Paris.",
      toolRequests: [],
    });

    // Add a second message
    await driver.inputMagentaText("What about Germany?");
    await driver.send();

    const request2 = await driver.mockAnthropic.awaitPendingStream({
      message: "followup request",
    });
    request2.respond({
      stopReason: "end_turn",
      text: "The capital of Germany is Berlin.",
      toolRequests: [],
    });

    // Get the original thread ID before forking
    const originalThreadId = driver.magenta.chat.state.activeThreadId;

    // 3. Fork the thread by pressing F on the assistant's response.
    await driver.pressOnDisplayMessage("The capital of France is Paris.", "F");

    // 4. Wait for the new thread to become active.
    await pollUntil(() => {
      if (driver.magenta.chat.state.activeThreadId === originalThreadId) {
        throw new Error("Still on original thread");
      }
    });

    // Send a follow-up message on the forked thread.
    await driver.inputMagentaText("Tell me about Italy");
    await driver.send();

    const stream = await driver.mockAnthropic.awaitPendingStream({
      message: "forked thread request",
    });

    // Verify the cloned messages are present (from the original thread)
    // Plus the new user message "Tell me about Italy"
    expect(sanitizeMessagesForSnapshot(stream.messages)).toMatchSnapshot(
      "fork-cloned-messages",
    );

    // 5. Respond to the forked thread
    stream.respond({
      stopReason: "end_turn",
      text: "Italy's capital is Rome. It's known for its rich history, art, and cuisine.",
      toolRequests: [],
    });

    // 6. Verify the new thread is now active
    const newThread = driver.magenta.chat.getActiveThread();
    expect(newThread.id).not.toBe(originalThreadId);

    // 7. Verify the display shows the forked conversation
    await driver.assertDisplayBufferContains("Tell me about Italy");
    await driver.assertDisplayBufferContains("Italy's capital is Rome");

    // 8. Verify the forked thread has the full conversation history
    const messages = newThread.getMessages();
    expect(messages).toMatchSnapshot("forked-thread-messages");
  });
});

it("processes @diag keyword to include diagnostics in message", {
  timeout: 10000,
}, async () => {
  await withDriver({}, async (driver) => {
    // Create a file with syntax errors to generate diagnostics
    await driver.editFile("test.ts");
    await driver.showSidebar();

    // Wait for diagnostics to be available
    await pollUntil(
      async () => {
        const diagnostics = (await driver.nvim.call("nvim_exec_lua", [
          `return vim.diagnostic.get(nil)`,
          [],
        ])) as unknown[];

        if (diagnostics.length === 0) {
          throw new Error("No diagnostics available yet");
        }
      },
      { timeout: 5000 },
    );

    // Send a message with @diag keyword
    await driver.inputMagentaText("Help me fix this issue @diag");
    await driver.send();

    const stream = await driver.mockAnthropic.awaitPendingStream();
    stream.respond({
      stopReason: "end_turn",
      text: "I can see the diagnostics you've provided. Let me help you fix the issue.",
      toolRequests: [],
    });

    // Verify the original message is displayed
    await driver.assertDisplayBufferContains("Help me fix this issue @diag");

    // Verify the diagnostics are appended as a separate content block
    await driver.assertDisplayBufferContains("Current diagnostics:");
    await driver.assertDisplayBufferContains(
      "Property 'd' does not exist on type",
    );
    await driver.assertDisplayBufferContains("test.ts");

    // Check the thread message structure
    const thread = driver.magenta.chat.getActiveThread();
    const messages = thread.getMessages();

    // Should have user message and assistant response
    expect(messages.length).toBe(2);

    // The user message should have four content blocks: original text + diagnostics + system_reminder + checkpoint
    expect(messages[0].content.length).toBe(3);
    const content0 = messages[0].content[0];
    expect(content0.type).toBe("text");
    expect((content0 as Extract<typeof content0, { type: "text" }>).text).toBe(
      "Help me fix this issue @diag",
    );
    const content1 = messages[0].content[1];
    expect(content1.type).toBe("text");
    expect(
      (content1 as Extract<typeof content1, { type: "text" }>).text,
    ).toContain("Current diagnostics:");
    expect(
      (content1 as Extract<typeof content1, { type: "text" }>).text,
    ).toContain("Property 'd' does not exist on type");
    expect(messages[0].content[2].type).toBe("system_reminder");
  });
});

it("processes @diagnostics keyword to include diagnostics in message", {
  timeout: 10000,
}, async () => {
  await withDriver({}, async (driver) => {
    // Create a file with syntax errors to generate diagnostics
    await driver.editFile("test.ts");
    await driver.showSidebar();

    // Wait for diagnostics to be available
    await pollUntil(
      async () => {
        const diagnostics = (await driver.nvim.call("nvim_exec_lua", [
          `return vim.diagnostic.get(nil)`,
          [],
        ])) as unknown[];

        if (diagnostics.length === 0) {
          throw new Error("No diagnostics available yet");
        }
      },
      { timeout: 5000 },
    );

    // Send a message with @diagnostics keyword
    await driver.inputMagentaText("Check these @diagnostics please");
    await driver.send();

    const stream = await driver.mockAnthropic.awaitPendingStream();
    stream.respond({
      stopReason: "end_turn",
      text: "I can see the diagnostics. Let me analyze them for you.",
      toolRequests: [],
    });

    // Verify the original message is displayed
    await driver.assertDisplayBufferContains("Check these @diagnostics please");

    // Verify the diagnostics are appended as a separate content block
    await driver.assertDisplayBufferContains("Current diagnostics:");
    await driver.assertDisplayBufferContains(
      "Property 'd' does not exist on type",
    );

    // Check the thread message structure
    const thread = driver.magenta.chat.getActiveThread();
    const messages = thread.getMessages();

    // Should have user message and assistant response
    expect(messages.length).toBe(2);

    // The user message should have three content blocks: original text + diagnostics + system_reminder
    expect(messages[0].content.length).toBe(3);
    const content0 = messages[0].content[0];
    expect(content0.type).toBe("text");
    expect((content0 as Extract<typeof content0, { type: "text" }>).text).toBe(
      "Check these @diagnostics please",
    );
    const content1 = messages[0].content[1];
    expect(content1.type).toBe("text");
    expect(
      (content1 as Extract<typeof content1, { type: "text" }>).text,
    ).toContain("Current diagnostics:");
  });
});
it("processes @qf keyword to include quickfix list in message", {
  timeout: 10000,
}, async () => {
  await withDriver({}, async (driver) => {
    // Create some test quickfix entries
    await driver.nvim.call("nvim_command", [
      "call setqflist([" +
        "{'filename': 'test1.ts', 'lnum': 10, 'col': 5, 'text': 'Error: undefined variable'}," +
        "{'filename': 'test2.js', 'lnum': 25, 'col': 12, 'text': 'Warning: unused import'}" +
        "])",
    ]);

    await driver.showSidebar();

    // Send a message with @qf keyword
    await driver.inputMagentaText("Help me fix these issues @qf");
    await driver.send();

    const stream = await driver.mockAnthropic.awaitPendingStream();
    stream.respond({
      stopReason: "end_turn",
      text: "I can see the quickfix list you've provided. Let me help you fix these issues.",
      toolRequests: [],
    });

    // Verify the original message is displayed
    await driver.assertDisplayBufferContains("Help me fix these issues @qf");

    // Verify the quickfix list is appended as a separate content block
    await driver.assertDisplayBufferContains("Current quickfix list:");
    await driver.assertDisplayBufferContains("Error: undefined variable");
    await driver.assertDisplayBufferContains("Warning: unused import");
    await driver.assertDisplayBufferContains("test1.ts:10:5");
    await driver.assertDisplayBufferContains("test2.js:25:12");

    // Check the thread message structure
    const thread = driver.magenta.chat.getActiveThread();
    const messages = thread.getMessages();

    // Should have user message and assistant response
    expect(messages.length).toBe(2);

    // The user message should have three content blocks: original text + quickfix list + system_reminder
    expect(messages[0].content.length).toBe(3);
    const content0 = messages[0].content[0];
    expect(content0.type).toBe("text");
    expect((content0 as Extract<typeof content0, { type: "text" }>).text).toBe(
      "Help me fix these issues @qf",
    );
    const content1 = messages[0].content[1];
    expect(content1.type).toBe("text");
    expect(
      (content1 as Extract<typeof content1, { type: "text" }>).text,
    ).toContain("Current quickfix list:");
    expect(
      (content1 as Extract<typeof content1, { type: "text" }>).text,
    ).toContain("Error: undefined variable");
    expect(
      (content1 as Extract<typeof content1, { type: "text" }>).text,
    ).toContain("Warning: unused import");
  });
});

it("processes @quickfix keyword to include quickfix list in message", {
  timeout: 10000,
}, async () => {
  await withDriver({}, async (driver) => {
    // Create some test quickfix entries
    await driver.nvim.call("nvim_command", [
      "call setqflist([" +
        "{'filename': 'error.py', 'lnum': 42, 'col': 1, 'text': 'SyntaxError: invalid syntax'}," +
        "{'filename': 'warning.js', 'lnum': 15, 'col': 8, 'text': 'Unused variable'}" +
        "])",
    ]);

    await driver.showSidebar();

    // Send a message with @quickfix keyword
    await driver.inputMagentaText("Check these @quickfix entries");
    await driver.send();

    const stream = await driver.mockAnthropic.awaitPendingStream();
    stream.respond({
      stopReason: "end_turn",
      text: "I can see the quickfix entries. Let me analyze them for you.",
      toolRequests: [],
    });

    // Verify the original message is displayed
    await driver.assertDisplayBufferContains("Check these @quickfix entries");

    // Verify the quickfix list is appended as a separate content block
    await driver.assertDisplayBufferContains("Current quickfix list:");
    await driver.assertDisplayBufferContains("SyntaxError: invalid syntax");
    await driver.assertDisplayBufferContains("Unused variable");
    await driver.assertDisplayBufferContains("error.py:42:1");
    await driver.assertDisplayBufferContains("warning.js:15:8");

    // Check the thread message structure
    const thread = driver.magenta.chat.getActiveThread();
    const messages = thread.getMessages();

    // Should have user message and assistant response
    expect(messages.length).toBe(2);

    // The user message should have three content blocks: original text + quickfix list + system_reminder
    expect(messages[0].content.length).toBe(3);
    const content0 = messages[0].content[0];
    expect(content0.type).toBe("text");
    expect((content0 as Extract<typeof content0, { type: "text" }>).text).toBe(
      "Check these @quickfix entries",
    );
    const content1 = messages[0].content[1];
    expect(content1.type).toBe("text");
    expect(
      (content1 as Extract<typeof content1, { type: "text" }>).text,
    ).toContain("Current quickfix list:");
    expect(
      (content1 as Extract<typeof content1, { type: "text" }>).text,
    ).toContain("SyntaxError: invalid syntax");
  });
});

it("handles empty quickfix list with @qf command", {
  timeout: 10000,
}, async () => {
  await withDriver({}, async (driver) => {
    // Clear quickfix list
    await driver.nvim.call("nvim_command", ["call setqflist([])"]);

    await driver.showSidebar();

    // Send a message with @qf keyword
    await driver.inputMagentaText("Any issues to fix? @qf");
    await driver.send();

    const stream = await driver.mockAnthropic.awaitPendingStream();
    stream.respond({
      stopReason: "end_turn",
      text: "I can see the quickfix list is empty. No issues to fix right now!",
      toolRequests: [],
    });

    // Verify the original message is displayed
    await driver.assertDisplayBufferContains("Any issues to fix? @qf");

    // Verify the empty quickfix list is handled properly
    await driver.assertDisplayBufferContains("Current quickfix list:");

    // Check the thread message structure
    const thread = driver.magenta.chat.getActiveThread();
    const messages = thread.getMessages();

    // Should have user message and assistant response
    expect(messages.length).toBe(2);

    // The user message should have three content blocks: original text + empty quickfix list + system_reminder
    expect(messages[0].content.length).toBe(3);
    const content1 = messages[0].content[1];
    expect(content1.type).toBe("text");
    expect((content1 as Extract<typeof content1, { type: "text" }>).text).toBe(
      "Current quickfix list:\n",
    );
  });
});

it("processes @buf keyword to include buffers list in message", {
  timeout: 10000,
}, async () => {
  await withDriver({}, async (driver) => {
    // Create some test buffers
    await driver.editFile("poem.txt");
    await driver.editFile("poem2.txt");
    await driver.showSidebar();

    // Send a message with @buf keyword
    await driver.inputMagentaText("Help me organize my files @buf");
    await driver.send();

    const stream = await driver.mockAnthropic.awaitPendingStream();
    stream.respond({
      stopReason: "end_turn",
      text: "I can see the buffers you have open. Let me help you organize them.",
      toolRequests: [],
    });

    // Verify the original message is displayed
    await driver.assertDisplayBufferContains("Help me organize my files @buf");

    // Verify the buffers list is appended as a separate content block
    await driver.assertDisplayBufferContains("Current buffers list:");
    await driver.assertDisplayBufferContains("poem.txt");
    await driver.assertDisplayBufferContains("active poem2.txt");

    // Check the thread message structure
    const thread = driver.magenta.chat.getActiveThread();
    const messages = thread.getMessages();

    // Should have user message and assistant response
    expect(messages.length).toBe(2);

    // The user message should have three content blocks: original text + buffers list + system_reminder
    expect(messages[0].content.length).toBe(3);
    const content0 = messages[0].content[0];
    expect(content0.type).toBe("text");
    expect((content0 as Extract<typeof content0, { type: "text" }>).text).toBe(
      "Help me organize my files @buf",
    );
    const content1 = messages[0].content[1];
    expect(content1.type).toBe("text");
    expect(
      (content1 as Extract<typeof content1, { type: "text" }>).text,
    ).toContain("Current buffers list:");
    expect(
      (content1 as Extract<typeof content1, { type: "text" }>).text,
    ).toContain("poem.txt");
    expect(
      (content1 as Extract<typeof content1, { type: "text" }>).text,
    ).toContain("active poem2.txt");
  });
});

it("processes @buffers keyword to include buffers list in message", {
  timeout: 10000,
}, async () => {
  await withDriver({}, async (driver) => {
    // Create some test buffers
    await driver.editFile("poem.txt");
    await driver.editFile("poem2.txt");
    await driver.showSidebar();

    // Send a message with @buffers keyword
    await driver.inputMagentaText("Show me my current @buffers");
    await driver.send();

    const stream = await driver.mockAnthropic.awaitPendingStream();
    stream.respond({
      stopReason: "end_turn",
      text: "I can see your current buffers. Here's what you have open.",
      toolRequests: [],
    });

    // Verify the original message is displayed
    await driver.assertDisplayBufferContains("Show me my current @buffers");

    // Verify the buffers list is appended as a separate content block
    await driver.assertDisplayBufferContains("Current buffers list:");
    await driver.assertDisplayBufferContains("poem.txt");
    await driver.assertDisplayBufferContains("active poem2.txt");

    // Check the thread message structure
    const thread = driver.magenta.chat.getActiveThread();
    const messages = thread.getMessages();

    // Should have user message and assistant response
    expect(messages.length).toBe(2);

    // The user message should have three content blocks: original text + buffers list + system_reminder
    expect(messages[0].content.length).toBe(3);
    const content0 = messages[0].content[0];
    expect(content0.type).toBe("text");
    expect((content0 as Extract<typeof content0, { type: "text" }>).text).toBe(
      "Show me my current @buffers",
    );
    const content1 = messages[0].content[1];
    expect(content1.type).toBe("text");
    expect(
      (content1 as Extract<typeof content1, { type: "text" }>).text,
    ).toContain("Current buffers list:");
    expect(
      (content1 as Extract<typeof content1, { type: "text" }>).text,
    ).toContain("poem.txt");
  });
});

it("handles empty buffers list with @buf command", {
  timeout: 10000,
}, async () => {
  await withDriver({}, async (driver) => {
    await driver.showSidebar();

    // Send a message with @buf keyword
    await driver.inputMagentaText("What files do I have open? @buf");
    await driver.send();

    const stream = await driver.mockAnthropic.awaitPendingStream();
    stream.respond({
      stopReason: "end_turn",
      text: "I can see your current buffers. It looks like you have minimal files open.",
      toolRequests: [],
    });

    // Verify the original message is displayed
    await driver.assertDisplayBufferContains("What files do I have open? @buf");

    // Verify the buffers list is handled properly
    await driver.assertDisplayBufferContains("Current buffers list:");

    // Check the thread message structure
    const thread = driver.magenta.chat.getActiveThread();
    const messages = thread.getMessages();

    // Should have user message and assistant response
    expect(messages.length).toBe(2);

    // The user message should have three content blocks: original text + buffers list + system_reminder
    expect(messages[0].content.length).toBe(3);
    const content1 = messages[0].content[1];
    expect(content1.type).toBe("text");
    expect(
      (content1 as Extract<typeof content1, { type: "text" }>).text,
    ).toContain("Current buffers list:");
  });
});

it("processes @diff command to include git diff in message", {
  timeout: 10000,
}, async () => {
  await withDriver({}, async (driver) => {
    // First, initialize git and commit the file so we can create a diff
    const cwd = await getcwd(driver.nvim);
    await within(async () => {
      $.cwd = cwd;
      // stage the file
      await $`git add poem.txt`;
      // add an unstaged change
      await $`echo 'modified content' >> poem.txt`;
    });

    await driver.showSidebar();

    // Send a message with @diff command
    await driver.inputMagentaText("Show me changes in @diff:poem.txt");
    await driver.send();

    const stream = await driver.mockAnthropic.awaitPendingStream();

    // Check that the request messages contain the git diff
    const userMessage = stream.messages.find((msg) => msg.role === "user");
    expect(userMessage).toBeDefined();
    const userContent = userMessage!.content;
    expect(Array.isArray(userContent)).toBe(true);
    if (!Array.isArray(userContent)) throw new Error("Expected array");
    expect(userContent.length).toBeGreaterThan(1);

    // Find the diff content block in the request
    const diffContent = userContent.find(
      (content) =>
        content.type === "text" &&
        content.text.includes("Git diff for `poem.txt`:") &&
        content.text.includes("modified content"),
    );
    expect(diffContent).toBeDefined();

    stream.respond({
      stopReason: "end_turn",
      text: "I can see the git diff you've provided. Let me analyze the changes.",
      toolRequests: [],
    });

    // Verify the original message is displayed
    await driver.assertDisplayBufferContains(
      "Show me changes in @diff:poem.txt",
    );

    // Verify git diff content is included
    await driver.assertDisplayBufferContains("Git diff for `poem.txt`:");
    await driver.assertDisplayBufferContains("modified content");
  });
});

it("processes @staged command to include staged diff in message", {
  timeout: 10000,
}, async () => {
  await withDriver({}, async (driver) => {
    // First, initialize git and commit the file so we can create a staged diff
    const cwd = await getcwd(driver.nvim);
    await within(async () => {
      $.cwd = cwd;
      await $`echo 'staged content' >> poem2.txt`;
      await $`git add poem2.txt`;
    });

    await driver.showSidebar();

    // Send a message with @staged command
    await driver.inputMagentaText("Review staged changes @staged:poem2.txt");
    await driver.send();

    const stream = await driver.mockAnthropic.awaitPendingStream();

    // Check that the request messages contain the staged diff
    const userMessage = stream.messages.find((msg) => msg.role === "user");
    expect(userMessage).toBeDefined();
    const userContent = userMessage!.content;
    expect(Array.isArray(userContent)).toBe(true);
    if (!Array.isArray(userContent)) throw new Error("Expected array");
    expect(userContent.length).toBeGreaterThan(1);

    // Find the staged diff content block in the request
    const stagedContent = userContent.find(
      (content) =>
        content.type === "text" &&
        content.text.includes("Staged diff for `poem2.txt`:") &&
        content.text.includes("staged content"),
    );
    expect(stagedContent).toBeDefined();

    stream.respond({
      stopReason: "end_turn",
      text: "I can see the staged changes you've provided. Let me review them.",
      toolRequests: [],
    });

    // Verify the original message is displayed
    await driver.assertDisplayBufferContains(
      "Review staged changes @staged:poem2.txt",
    );

    // Verify staged diff content is included
    await driver.assertDisplayBufferContains("Staged diff for `poem2.txt`:");
    await driver.assertDisplayBufferContains("staged content");
  });
});

it("handles @file commands", { timeout: 10000 }, async () => {
  await withDriver({}, async (driver) => {
    // Create test files
    await driver.editFile("poem.txt");
    await driver.editFile("poem2.txt");
    await driver.showSidebar();

    // Send a message with multiple @file commands
    await driver.inputMagentaText(
      "Compare these files @file:poem.txt and @file:poem2.txt",
    );
    await driver.send();

    const stream = await driver.mockAnthropic.awaitPendingStream();

    // Check that the request messages contain context updates for the added files
    const userMessage = stream.messages.find((msg) => msg.role === "user");
    expect(userMessage).toBeDefined();
    const userContent = userMessage!.content;
    expect(Array.isArray(userContent)).toBe(true);
    if (!Array.isArray(userContent)) throw new Error("Expected array");

    // Look for context updates in the request messages - they should appear as text content
    // containing the file contents
    const contextContent = userContent.find(
      (content) =>
        content.type === "text" &&
        (content.text.includes("poem.txt") ||
          content.text.includes("poem2.txt")),
    );
    expect(contextContent).toBeDefined();

    stream.respond({
      stopReason: "end_turn",
      text: "I can see both files you've added to context. Let me compare them.",
      toolRequests: [],
    });

    // Verify the original message is displayed
    await driver.assertDisplayBufferContains(
      "Compare these files @file:poem.txt and @file:poem2.txt",
    );

    // Verify both files were added to context manager
    const thread = driver.magenta.chat.getActiveThread();
    const contextManager = thread.contextManager;
    const files = contextManager.files;

    // Check that both files are in the context
    const hasPoem1 = Object.keys(files).some((path) =>
      path.includes("poem.txt"),
    );
    const hasPoem2 = Object.keys(files).some((path) =>
      path.includes("poem2.txt"),
    );
    expect(hasPoem1).toBe(true);
    expect(hasPoem2).toBe(true);
  });
});

it("handles @file command with non-existent file", {
  timeout: 10000,
}, async () => {
  await withDriver({}, async (driver) => {
    await driver.showSidebar();

    // Send a message with @file command for non-existent file
    await driver.inputMagentaText("Help with @file:nonexistent.txt");
    await driver.send();

    const stream = await driver.mockAnthropic.awaitPendingStream();
    stream.respond({
      stopReason: "end_turn",
      text: "I see there was an error adding that file to context.",
      toolRequests: [],
    });

    // Verify the original message is displayed
    await driver.assertDisplayBufferContains("Help with @file:nonexistent.txt");

    // Verify error message is included
    await driver.assertDisplayBufferContains("Error adding file to context");
    await driver.assertDisplayBufferContains("nonexistent.txt");

    // Check the thread message structure
    const thread = driver.magenta.chat.getActiveThread();
    const messages = thread.getMessages();

    // Should have user message and assistant response
    expect(messages.length).toBe(2);

    // The user message should have multiple content blocks including error
    expect(messages[0].content.length).toBeGreaterThan(1);

    // The user message should have multiple content blocks including error
    expect(messages[0].content.length).toBeGreaterThan(1);

    // Find the error content block
    const errorContent = messages[0].content.find(
      (content) =>
        content.type === "text" &&
        content.text.includes("Error adding file to context"),
    );
    expect(errorContent).toBeDefined();
  });
});

it.skip("display multiple edits to the same file, and edit details", async () => {
  await withDriver({}, async (driver) => {
    await driver.showSidebar();
    await driver.inputMagentaText(`Update the poem in the file poem.txt`);
    await driver.send();

    const stream = await driver.mockAnthropic.awaitPendingStream();
    stream.respond({
      stopReason: "tool_use",
      text: "ok, I will try to rewrite the poem in that file",
      toolRequests: [
        {
          status: "ok",
          value: {
            id: "id1" as ToolRequestId,
            toolName: "replace" as ToolName,
            input: {
              filePath: "poem.txt" as UnresolvedFilePath,
              find: `Moonlight whispers through the trees,
Silver shadows dance with ease.
Stars above like diamonds bright,
Paint their stories in the night.`,
              replace: `Replace 1
Replace 2`,
            },
          },
        },
      ],
    });
    await driver.assertDisplayBufferContains(`\
# user:
Update the poem in the file poem.txt`);

    await driver.assertDisplayBufferContains(`\
# assistant:
ok, I will try to rewrite the poem in that file
✏️✅ Replace [[ -4 / +2 ]] in \`poem.txt\`
\`\`\`diff
-Moonlight whispers through the trees,
-Silver shadows dance with ease.
-Stars above like diamonds bright,
-Paint their stories in the night.
\\ No newline at end of file
+Replace 1
+Replace 2
\\ No newline at end of file

\`\`\``);

    await driver.assertDisplayBufferContains(`\
Edits:
- \`poem.txt\` (1 edits). [± diff snapshot]`);

    await driver.triggerDisplayBufferKeyOnContent("diff snapshot", "<CR>");

    await driver.assertDisplayBufferContains(`\
# assistant:
ok, I will try to rewrite the poem in that file
✏️✅ Replace [[ -4 / +2 ]] in \`poem.txt\``);

    // Go back to main view
    await driver.triggerDisplayBufferKeyOnContent("diff snapshot", "<CR>");

    await driver.assertDisplayBufferContains(`\
# assistant:
ok, I will try to rewrite the poem in that file
✏️✅ Replace [[ -4 / +2 ]] in \`poem.txt\``);
  });
});

it("displays deleted context updates correctly", async () => {
  await withDriver({}, async (driver) => {
    await driver.showSidebar();

    // Create a temporary file for testing
    const cwd = await getcwd(driver.nvim);
    const tempFilePath = resolveFilePath(
      cwd,
      "temp-delete-test.txt" as UnresolvedFilePath,
      os.homedir() as HomeDir,
    );
    const tempContent = "temporary file content\nfor testing deletion";
    await fs.promises.writeFile(tempFilePath, tempContent);

    // Add file to context
    await driver.addContextFiles("temp-delete-test.txt");

    // Verify file is in context (with pending whole-file send since never read by agent)
    await driver.assertDisplayBufferContains(`- \`temp-delete-test.txt\``);

    // Delete the file from disk
    await fs.promises.unlink(tempFilePath);

    // Send a message to trigger context update
    await driver.inputMagentaText("What happened to the file?");
    await driver.send();

    const stream = await driver.mockAnthropic.awaitPendingStream();

    // Check that the request contains the file deletion update
    stream.messages.find(
      (msg) =>
        msg.role === "user" &&
        typeof msg.content === "object" &&
        lodash.some(
          msg.content,
          (b) =>
            b.type === "text" &&
            b.text.includes("temp-delete-test.txt") &&
            b.text.includes("This file has been deleted"),
        ),
    );

    stream.respond({
      stopReason: "end_turn",
      text: "I can see the file has been deleted from context.",
      toolRequests: [],
    });

    // Verify the display shows the deletion indicator - check pieces separately
    await driver.assertDisplayBufferContains("# user:");
    await driver.assertDisplayBufferContains("Context Updates:");
    await driver.assertDisplayBufferContains(
      "`temp-delete-test.txt` [ deleted ]",
    );
    await driver.assertDisplayBufferContains("What happened to the file?");
    await driver.assertDisplayBufferContains("# assistant:");
    await driver.assertDisplayBufferContains(
      "I can see the file has been deleted from context.",
    );
  });
});

it("handles web search results and citations together", async () => {
  await withDriver({}, async (driver) => {
    await driver.showSidebar();
    await driver.inputMagentaText(
      `Compare TypeScript and JavaScript for large projects`,
    );
    await driver.send();

    const stream = await driver.mockAnthropic.awaitPendingStream();

    // Stream server tool use (web search)
    stream.streamServerToolUse("search_1", "web_search", {
      query: "TypeScript vs JavaScript large projects",
    });

    // Stream web search result
    stream.streamWebSearchToolResult("search_1", [
      {
        type: "web_search_result",
        title: "TypeScript vs JavaScript: Which Is Better for Your Project?",
        url: "https://example.com/typescript-vs-javascript",
        encrypted_content: "",
        page_age: "3 months ago",
      },
    ] as WebSearchResultBlock[]);

    // Stream text content
    stream.streamText(
      "TypeScript offers significant advantages for large projects compared to JavaScript.",
    );

    // Finish the response
    stream.finishResponse("end_turn");

    // Verify content pieces separately to allow for system reminder
    await driver.assertDisplayBufferContains("# user:");
    await driver.assertDisplayBufferContains(
      "Compare TypeScript and JavaScript for large projects",
    );
    await driver.assertDisplayBufferContains("# assistant:");
    await driver.assertDisplayBufferContains(
      "🔍 Searching TypeScript vs JavaScript large projects...",
    );
    await driver.assertDisplayBufferContains("🌐 1 search result");

    await driver.assertDisplayBufferContains(
      "TypeScript offers significant advantages for large projects compared to JavaScript.",
    );
    await driver.assertDisplayBufferContains("Stopped (end_turn)");
  });
});

it("handles thinking and redacted thinking blocks", async () => {
  await withDriver({}, async (driver) => {
    await driver.showSidebar();
    await driver.inputMagentaText(
      "What should I consider when designing a database schema?",
    );
    await driver.send();

    const stream = await driver.mockAnthropic.awaitPendingStream();

    // Stream thinking block
    stream.streamThinking("abc\ndef\nghi");

    // Stream redacted thinking block
    stream.streamRedactedThinking(
      "This thinking contains sensitive information that has been redacted.",
    );

    stream.finishResponse("end_turn");

    // Assert initial collapsed state of thinking block - check pieces separately
    await driver.assertDisplayBufferContains("# user:");
    await driver.assertDisplayBufferContains(
      "What should I consider when designing a database schema?",
    );
    await driver.assertDisplayBufferContains("# assistant:");
    await driver.assertDisplayBufferContains("💭 [Thinking]");
    await driver.assertDisplayBufferContains("💭 [Redacted Thinking]");

    // Test expanding the thinking block
    await driver.triggerDisplayBufferKeyOnContent("💭 [Thinking]", "=");

    // Verify expanded thinking block - check pieces separately
    await driver.assertDisplayBufferContains("# user:");
    await driver.assertDisplayBufferContains(
      "What should I consider when designing a database schema?",
    );
    await driver.assertDisplayBufferContains("# assistant:");
    await driver.assertDisplayBufferContains("💭 [Thinking]");
    await driver.assertDisplayBufferContains("abc");
    await driver.assertDisplayBufferContains("def");
    await driver.assertDisplayBufferContains("ghi");
    await driver.assertDisplayBufferContains("💭 [Redacted Thinking]");

    // Test collapsing the thinking block
    await driver.triggerDisplayBufferKeyOnContent("💭 [Thinking]", "=");

    // Verify collapsed thinking block again - check pieces separately
    await driver.assertDisplayBufferContains("# user:");
    await driver.assertDisplayBufferContains(
      "What should I consider when designing a database schema?",
    );
    await driver.assertDisplayBufferContains("# assistant:");
    await driver.assertDisplayBufferContains("💭 [Thinking]");
    await driver.assertDisplayBufferContains("💭 [Redacted Thinking]");

    // Send a followup message to test that thinking blocks are included in context
    await driver.inputMagentaText("Can you elaborate on normalization?");
    await driver.send();

    const followupStream = await driver.mockAnthropic.awaitPendingStream();

    // Verify that the followup request includes both thinking blocks in messages
    const assistantMessage = followupStream.messages.find(
      (msg) => msg.role === "assistant",
    );
    expect(assistantMessage).toBeTruthy();
    const assistantContent = assistantMessage!.content;
    expect(Array.isArray(assistantContent)).toBe(true);
    if (!Array.isArray(assistantContent)) throw new Error("Expected array");
    expect(assistantContent).toHaveLength(2);

    // Check thinking block is included with full content
    const thinkingContent = assistantContent[0];
    expect(thinkingContent.type).toBe("thinking");
    expect(
      (thinkingContent as Extract<typeof thinkingContent, { type: "thinking" }>)
        .thinking,
    ).toEqual("abc\ndef\nghi");

    const redactedThinkingContent = assistantContent[1];
    expect(redactedThinkingContent.type).toBe("redacted_thinking");
    expect(
      (
        redactedThinkingContent as Extract<
          typeof redactedThinkingContent,
          { type: "redacted_thinking" }
        >
      ).data,
    ).toBe(
      "This thinking contains sensitive information that has been redacted.",
    );
  });
});

it("handles streaming thinking blocks correctly", async () => {
  await withDriver({}, async (driver) => {
    await driver.showSidebar();
    await driver.inputMagentaText("Explain how async/await works");
    await driver.send();

    const stream = await driver.mockAnthropic.awaitPendingStream();
    const thinkingIndex = stream.nextBlockIndex();

    // Start streaming thinking block
    stream.emitEvent({
      type: "content_block_start",
      index: thinkingIndex,
      content_block: { type: "thinking", thinking: "", signature: "" },
    });

    // Add thinking content in multiple chunks to test streaming
    stream.emitEvent({
      type: "content_block_delta",
      index: thinkingIndex,
      delta: {
        type: "thinking_delta",
        thinking:
          "I need to explain async/await.\n\nThis is a JavaScript feature that makes asynchronous code look synchronous.",
      },
    });

    // Assert that during streaming, we see the preview with last line
    await driver.assertDisplayBufferContains(
      "💭 [Thinking] This is a JavaScript feature that makes asynchronous code look synchronous.",
    );

    // Add more content to the thinking block
    stream.emitEvent({
      type: "content_block_delta",
      index: thinkingIndex,
      delta: {
        type: "thinking_delta",
        thinking: "\n\nIt's built on top of Promises.",
      },
    });

    // Assert that the preview now shows the new last line
    await driver.assertDisplayBufferContains(
      "💭 [Thinking] It's built on top of Promises.",
    );
  });
});

it("shows EDL script preview while streaming", async () => {
  await withDriver({}, async (driver) => {
    await driver.showSidebar();
    await driver.inputMagentaText("Edit a file for me");
    await driver.send();

    const stream = await driver.mockAnthropic.awaitPendingStream();
    const toolIndex = stream.nextBlockIndex();

    // Start streaming an edl tool_use block
    stream.emitEvent({
      type: "content_block_start",
      index: toolIndex,
      content_block: {
        type: "tool_use",
        id: "edl-preview-test",
        name: "edl",
        input: {},
        caller: { type: "direct" as const },
      },
    });

    // Stream partial input JSON with escaped newlines
    stream.emitEvent({
      type: "content_block_delta",
      index: toolIndex,
      delta: {
        type: "input_json_delta",
        partial_json: '{"script": "file `src/utils.ts`\\nselect',
      },
    });

    // Assert the display shows a file summary and the streaming tail
    await driver.assertDisplayBufferContains("📝 edl: editing 1 file:");
    await driver.assertDisplayBufferContains("src/utils.ts");
    await driver.assertDisplayBufferContains("select");

    // Stream more of the script
    stream.emitEvent({
      type: "content_block_delta",
      index: toolIndex,
      delta: {
        type: "input_json_delta",
        partial_json: " /oldFunc/\\nextend_forward",
      },
    });

    await driver.assertDisplayBufferContains("extend_forward");
  });
});
it("shows bash_command preview while streaming", async () => {
  await withDriver({}, async (driver) => {
    await driver.showSidebar();
    await driver.inputMagentaText("Run a command for me");
    await driver.send();

    const stream = await driver.mockAnthropic.awaitPendingStream();
    const toolIndex = stream.nextBlockIndex();

    stream.emitEvent({
      type: "content_block_start",
      index: toolIndex,
      content_block: {
        type: "tool_use",
        id: "bash-preview-test",
        name: "bash_command",
        input: {},
        caller: { type: "direct" as const },
      },
    });

    stream.emitEvent({
      type: "content_block_delta",
      index: toolIndex,
      delta: {
        type: "input_json_delta",
        partial_json: '{"command": "echo hello',
      },
    });

    await driver.assertDisplayBufferContains("⚡");
    await driver.assertDisplayBufferContains("echo hello");

    stream.emitEvent({
      type: "content_block_delta",
      index: toolIndex,
      delta: {
        type: "input_json_delta",
        partial_json: ' && echo world"}',
      },
    });

    await driver.assertDisplayBufferContains("echo world");
  });
});
it("handles @async messages by queueing them and sending on next tool response", async () => {
  await withDriver({}, async (driver) => {
    driver.mockSandbox.setState({ status: "unsupported", reason: "disabled" });
    await driver.showSidebar();

    // First, send a regular message that will use a bash command
    await driver.inputMagentaText("Can you run a command?");
    await driver.send();

    const request1 = await driver.mockAnthropic.awaitPendingStream();
    const toolRequestId = "bash-tool" as ToolRequestId;

    // Respond with bash_command tool use - this will block on user approval (sandbox disabled)
    request1.respond({
      stopReason: "tool_use",
      text: "I'll run the command.",
      toolRequests: [
        {
          status: "ok",
          value: {
            id: toolRequestId,
            toolName: "bash_command" as ToolName,
            input: { command: "cat .secret" },
          },
        },
      ],
    });

    // Wait for approval dialog to fully render
    await driver.assertDisplayBufferContains("> YES");

    // Now send an @async message while the tool is waiting for approval
    await driver.inputMagentaText("@async This should be queued");
    await driver.send();

    // Wait for the pending message indicator to appear in the display
    await driver.assertDisplayBufferContains("pending message");

    // Approve the file read to complete the tool execution
    await driver.triggerDisplayBufferKeyOnContent("> YES", "<CR>");

    // Wait for file read to complete
    await driver.assertDisplayBufferContains("✅ ");

    // Handle the auto-response after tool completion
    const stream2 = await driver.mockAnthropic.awaitPendingStream();

    // Verify the message structure follows the expected pattern
    const messagePattern = stream2.messages.flatMap((m) =>
      typeof m.content === "string"
        ? "stringmessage"
        : m.content.map((c) => `${m.role}:${c.type}`),
    );

    expect(
      messagePattern,
      "tool_use immediately followed by tool_result",
    ).toEqual([
      "user:text",
      "user:text", // system_reminder converted to text
      "assistant:text",
      "assistant:tool_use",
      "user:tool_result",
      "user:text",
      "user:text", // system_reminder converted to text
    ]);
    expect(sanitizeMessagesForSnapshot(stream2.messages)).toMatchSnapshot();
  });
});

it("handles @async messages and sends them on end turn", async () => {
  await withDriver({}, async (driver) => {
    await driver.showSidebar();

    // Send a regular message
    await driver.inputMagentaText("Tell me about TypeScript");
    await driver.send();

    const request1 = await driver.mockAnthropic.awaitPendingStream();

    // Send @async message while first request is in flight
    await driver.inputMagentaText("@async Also tell me about JavaScript");
    await driver.send();

    // Verify message is queued
    const thread = driver.magenta.chat.getActiveThread();
    expect(thread.core.state.pendingMessages).toHaveLength(1);
    expect(thread.core.state.pendingMessages[0].text).toBe(
      "Also tell me about JavaScript",
    );

    // Respond to first request with end_turn - this should trigger sending queued messages
    request1.respond({
      stopReason: "end_turn",
      text: "TypeScript is a typed superset of JavaScript.",
      toolRequests: [],
    });

    // Now the queued message should be sent automatically
    const request2 = await driver.mockAnthropic.awaitPendingStream();
    expect(request2.messages).toMatchSnapshot();
  });
});

it("should process custom commands in messages", async () => {
  await withDriver(
    {
      options: {
        customCommands: [
          {
            name: "@nedit",
            text: "DO NOT MAKE ANY EDITS TO CODE",
            description: "Disable all code editing functionality",
          },
        ],
      },
    },
    async (driver) => {
      await driver.showSidebar();
      await driver.waitForChatReady();

      await driver.inputMagentaText("@nedit Please help with this task");
      await driver.send();

      // Wait for the message to be processed and displayed
      await driver.assertDisplayBufferContains("DO NOT MAKE ANY EDITS TO CODE");
    },
  );
});

it("renders successive tool uses with single assistant header and inline metadata", async () => {
  await withDriver({}, async (driver) => {
    await driver.showSidebar();
    await driver.inputMagentaText("Read two files for me");
    await driver.send();

    const stream1 = await driver.mockAnthropic.awaitPendingStream();

    // First tool use
    stream1.respond({
      stopReason: "tool_use",
      text: "I'll read the first file.",
      toolRequests: [
        {
          status: "ok",
          value: {
            id: "tool-1" as ToolRequestId,
            toolName: "get_file" as ToolName,
            input: { filePath: "poem.txt" as UnresolvedFilePath },
          },
        },
      ],
    });

    // Wait for first tool to complete (auto-approved for poem.txt)
    await driver.assertDisplayBufferContains("✅ ");

    // Second request - thinking then another tool use
    const stream2 = await driver.mockAnthropic.awaitPendingStream();
    stream2.streamThinking("Let me read the second file now.");
    stream2.respond({
      stopReason: "tool_use",
      text: "",
      toolRequests: [
        {
          status: "ok",
          value: {
            id: "tool-2" as ToolRequestId,
            toolName: "get_file" as ToolName,
            input: { filePath: "poem2.txt" as UnresolvedFilePath },
          },
        },
      ],
    });

    // Wait for second tool to complete
    await driver.assertDisplayBufferContains("✅ ");

    // Third request - final response
    const stream3 = await driver.mockAnthropic.awaitPendingStream();
    stream3.respond({
      stopReason: "end_turn",
      text: "I've read both files for you.",
      toolRequests: [],
    });

    await driver.assertDisplayBufferContains("I've read both files for you.");

    // Get the display buffer and verify the format via snapshot
    const displayText = sanitizeDisplayForSnapshot(
      await driver.getDisplayBufferText(),
    );

    // Snapshot the full display to verify:
    // 1. Only ONE "# assistant:" header for the entire turn
    // 2. System reminders and checkpoints are inline (📋 [System Reminder]🏁 [Checkpoint])
    // 3. No blank lines between tool results and metadata
    expect(displayText).toMatchSnapshot("successive-tool-uses-display");
  });
});

it("followup user message text is visible after tool-use cycle", async () => {
  await withDriver({}, async (driver) => {
    await driver.showSidebar();

    // Send initial message
    await driver.inputMagentaText("Read a file for me");
    await driver.send();

    const stream1 = await driver.mockAnthropic.awaitPendingStream();
    const toolRequestId = "read-file-1" as ToolRequestId;

    // Respond with a tool use
    stream1.respond({
      stopReason: "tool_use",
      text: "I'll read the file for you.",
      toolRequests: [
        {
          status: "ok",
          value: {
            id: toolRequestId,
            toolName: "get_file" as ToolName,
            input: { filePath: "./poem.txt" as UnresolvedFilePath },
          },
        },
      ],
    });

    // Wait for tool to auto-execute and complete
    await driver.assertDisplayBufferContains("poem.txt");

    // Auto-respond after tool completion
    const stream2 = await driver.mockAnthropic.awaitPendingStream();
    stream2.respond({
      stopReason: "end_turn",
      text: "I've read the file. It contains a poem about moonlight.",
      toolRequests: [],
    });

    await driver.assertDisplayBufferContains("poem about moonlight");

    // Now send a followup message
    await driver.inputMagentaText("Now edit the poem to be about sunshine");
    await driver.send();

    const stream3 = await driver.mockAnthropic.awaitPendingStream();
    stream3.respond({
      stopReason: "end_turn",
      text: "I'll edit the poem for you.",
      toolRequests: [],
    });

    // Verify the followup user message text is visible in the display
    await driver.assertDisplayBufferContains(
      "Now edit the poem to be about sunshine",
    );

    // Verify the assistant response to the followup is also visible
    await driver.assertDisplayBufferContains("I'll edit the poem for you.");

    // Snapshot the full display for verification
    const displayText = sanitizeDisplayForSnapshot(
      await driver.getDisplayBufferText(),
    );
    expect(displayText).toMatchSnapshot("followup-message-after-tool-use");
  });
});

it("followup user message text is visible with context updates", async () => {
  await withDriver({}, async (driver) => {
    await driver.showSidebar();

    // Add a file to context and send initial message
    await driver.addContextFiles("poem.txt");
    await driver.inputMagentaText("Help me with this poem");
    await driver.send();

    const stream1 = await driver.mockAnthropic.awaitPendingStream();
    stream1.respond({
      stopReason: "end_turn",
      text: "I can see the poem. What would you like me to do?",
      toolRequests: [],
    });

    await driver.assertDisplayBufferContains("What would you like me to do?");

    // Modify the file externally to trigger a context update on next message
    const cwd = await getcwd(driver.nvim);
    await fs.promises.writeFile(
      `${cwd}/poem.txt`,
      "sunshine poem\nwith extra lines",
    );

    // Send a followup message - this should include context updates
    await driver.inputMagentaText("Now make the poem longer please");
    await driver.send();

    const stream2 = await driver.mockAnthropic.awaitPendingStream();
    stream2.respond({
      stopReason: "end_turn",
      text: "I'll make the poem longer.",
      toolRequests: [],
    });

    // Verify the followup user message text is visible in the display
    await driver.assertDisplayBufferContains("Now make the poem longer please");

    // Verify the assistant response to the followup is also visible
    await driver.assertDisplayBufferContains("I'll make the poem longer.");

    // Snapshot the full display for verification
    const displayText = sanitizeDisplayForSnapshot(
      await driver.getDisplayBufferText(),
    );
    expect(displayText).toMatchSnapshot("followup-with-context-updates");
  });
});
it("expands context update diff with = binding", async () => {
  await withDriver({}, async (driver) => {
    await driver.showSidebar();

    await driver.addContextFiles("poem.txt");
    await driver.inputMagentaText("Help me with this poem");
    await driver.send();

    const stream1 = await driver.mockAnthropic.awaitPendingStream();
    stream1.respond({
      stopReason: "end_turn",
      text: "What would you like me to do?",
      toolRequests: [],
    });

    await driver.assertDisplayBufferContains("What would you like me to do?");

    const cwd = await getcwd(driver.nvim);
    await fs.promises.writeFile(
      `${cwd}/poem.txt`,
      "sunshine poem\nwith extra lines",
    );

    await driver.inputMagentaText("Now make the poem longer please");
    await driver.send();

    const stream2 = await driver.mockAnthropic.awaitPendingStream();
    stream2.respond({
      stopReason: "end_turn",
      text: "I'll make the poem longer.",
      toolRequests: [],
    });

    await driver.assertDisplayBufferContains("Context Updates:");

    // Expand the diff using the "=" binding on the file line
    const pos = await driver.assertDisplayBufferContains(
      "`poem.txt` [ +2 / -4 ]",
    );
    await driver.triggerDisplayBufferKey(pos, "=");

    await driver.assertDisplayBufferContains("+with extra lines");
  });
});
it("handles malformed tool_use by sending error tool_result and continuing", async () => {
  await withDriver({}, async (driver) => {
    await driver.showSidebar();
    await driver.inputMagentaText("Please read a file");
    await driver.send();

    const stream = await driver.mockAnthropic.awaitPendingStream();
    const toolRequestId = "tool-malformed-1" as ToolRequestId;

    // Stream a malformed get_file tool_use (missing filePath)
    stream.streamText("Let me read that file.");
    stream.streamToolUse(toolRequestId, "get_file" as ToolName, {});
    stream.finishResponse("tool_use");

    // The thread should send an error tool_result and auto-continue
    const stream2 = await driver.mockAnthropic.awaitPendingStream();

    // Verify the messages sent include the error tool_result
    const providerMessages = stream2.getProviderMessages();
    const toolResultMsg = providerMessages.find(
      (m) =>
        m.role === "user" && m.content.some((b) => b.type === "tool_result"),
    );
    expect(toolResultMsg).toBeDefined();
    const toolResultBlock = toolResultMsg!.content.find(
      (b) => b.type === "tool_result",
    );
    expect(toolResultBlock).toBeDefined();
    if (toolResultBlock?.type === "tool_result") {
      expect(toolResultBlock.result.status).toBe("error");
    }

    // The agent should be able to recover
    stream2.respond({
      stopReason: "end_turn",
      text: "Sorry, I made an error with that tool call.",
      toolRequests: [],
    });

    await driver.assertDisplayBufferContains(
      "Sorry, I made an error with that tool call.",
    );
  });
});
