import test from "node:test";
import assert from "node:assert/strict";
import { projectConversation } from "./conversation.js";

test("conversation groups a live turn into one user message, activity card, and answer", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "Hello" }, 1000),
    event("turn.started", { turnId: "turn-1", startedAt: 1200 }, 1200),
    event("reasoning.delta", {
      turnId: "turn-1", itemId: "reason-1", text: "Checking"
    }, 1500),
    event("reasoning.delta", {
      turnId: "turn-1", itemId: "reason-1", text: " files"
    }, 1600),
    event("session.output", {
      turnId: "turn-1", kind: "assistant_delta", text: "Hi"
    }, 3000),
    event("turn.completed", {
      turnId: "turn-1", status: "completed", completedAt: 4200, durationMs: 3000
    }, 4200)
  ]);

  assert.equal(turns.length, 1);
  assert.equal(turns[0].userText, "Hello");
  assert.equal(turns[0].assistantText, "Hi");
  assert.equal(turns[0].status, "completed");
  assert.equal(turns[0].activity.items[0].text, "Checking files");
  assert.equal(turns[0].activity.durationMs, 3000);
});

test("conversation replaces cumulative assistant snapshots instead of appending them", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "Preview" }, 1000),
    event("turn.started", { turnId: "turn-1", startedAt: 1100 }, 1100),
    event("session.output", { turnId: "turn-1", itemId: "answer-1", kind: "assistant_snapshot", text: "Partial" }, 1200),
    event("session.output", { turnId: "turn-1", itemId: "answer-1", kind: "assistant_snapshot", text: "Complete answer" }, 1300)
  ]);

  assert.equal(turns[0].assistantText, "Complete answer");
});

test("conversation resumes running after the last pending approval resolves", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "Run checks" }, 1000),
    event("turn.started", { turnId: "turn-approval", startedAt: 1100 }, 1100),
    event("approval.requested", { id: "approval-1", status: "pending" }, 1200),
    event("approval.resolved", { id: "approval-1", status: "approved" }, 1300)
  ]);

  assert.equal(turns[0].status, "running");
  assert.equal(turns[0].approvals[0].status, "approved");
});

test("conversation waits until every pending approval resolves", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "Run checks" }, 1000),
    event("turn.started", { turnId: "turn-approval", startedAt: 1100 }, 1100),
    event("approval.requested", { id: "approval-1", status: "pending" }, 1200),
    event("approval.requested", { id: "approval-2", status: "pending" }, 1250),
    event("approval.resolved", { id: "approval-1", status: "approved" }, 1300)
  ]);

  assert.equal(turns[0].status, "waiting_approval");
  assert.equal(turns[0].approvals.find((approval) => approval.id === "approval-2").status, "pending");
});

test("conversation groups historical follow-up messages into their shared turn", () => {
  const turns = projectConversation([
    event("message.accepted", { turnId: "turn-1", text: "Resize stop button" }, 1000),
    event("message.accepted", { turnId: "turn-1", text: "Make it white" }, 1500),
    event("turn.started", { turnId: "turn-1", startedAt: 1000 }, 1000),
    event("turn.completed", { turnId: "turn-1", status: "completed" }, 2000)
  ]);

  assert.equal(turns.length, 1);
  assert.equal(turns[0].userText, "Resize stop button");
  assert.deepEqual(turns[0].userTexts, ["Resize stop button", "Make it white"]);
  assert.equal(turns[0].status, "completed");
});

test("conversation keeps steering messages in the active turn", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "Start" }, 1000),
    event("turn.started", { turnId: "turn-1", startedAt: 1100 }, 1100),
    event("message.accepted", { text: "Redirect", turnId: "turn-1", steering: true }, 1200),
    event("turn.completed", { turnId: "turn-1", status: "completed" }, 2000)
  ]);

  assert.equal(turns.length, 1);
  assert.deepEqual(turns[0].userTexts, ["Start", "Redirect"]);
  assert.equal(turns[0].status, "completed");
});

test("conversation preserves steer timing relative to activity items", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "Start" }, 1000),
    event("turn.started", { turnId: "turn-1", startedAt: 1100 }, 1100),
    event("activity.started", { turnId: "turn-1", itemId: "before-steer", kind: "commandExecution", summary: "npm test" }, 1200),
    event("message.accepted", { text: "Redirect", turnId: "turn-1", steering: true }, 1300),
    event("activity.started", { turnId: "turn-1", itemId: "after-steer", kind: "commandExecution", summary: "npm run build" }, 1400)
  ]);

  assert.deepEqual(turns[0].userMessages.map((message) => [message.text, message.createdAt]), [
    ["Start", 1000],
    ["Redirect", 1300]
  ]);
  assert.deepEqual(turns[0].activity.items.map((item) => [item.id, item.createdAt]), [
    ["before-steer", 1200],
    ["after-steer", 1400]
  ]);
});

test("conversation records activity updates that arrive after a steering message", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "Start" }, 1000),
    event("turn.started", { turnId: "turn-1", startedAt: 1100 }, 1100),
    event("activity.started", { turnId: "turn-1", itemId: "shared-command", kind: "commandExecution", summary: "npm test" }, 1200),
    event("message.accepted", { text: "Continue", turnId: "turn-1", steering: true }, 1300),
    event("activity.updated", { turnId: "turn-1", itemId: "shared-command", text: "still running" }, 1400),
    event("activity.completed", { turnId: "turn-1", itemId: "shared-command", kind: "commandExecution", status: "completed" }, 1500)
  ]);

  assert.equal(turns[0].activity.items[0].createdAt, 1200);
  assert.equal(turns[0].activity.items[0].updatedAt, 1500);
});

test("conversation records resumed commentary below a steering message", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "Start" }, 1000),
    event("turn.started", { turnId: "turn-1", startedAt: 1100 }, 1100),
    event("activity.started", { turnId: "turn-1", itemId: "progress", kind: "agentMessage", phase: "commentary" }, 1200),
    event("message.accepted", { text: "Continue", turnId: "turn-1", steering: true }, 1300),
    event("session.output", { turnId: "turn-1", itemId: "progress", kind: "assistant_delta", text: "Resumed work" }, 1400)
  ]);

  assert.equal(turns[0].activity.items[0].text, "Resumed work");
  assert.equal(turns[0].activity.items[0].updatedAt, 1400);
});

test("confirmed stopped turns stay interrupted when a later turn starts", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "Slow request" }, 1000),
    event("turn.started", { turnId: "turn-1", startedAt: 1100 }, 1100),
    event("session.status", { status: "interrupted", optimistic: true }, 1200),
    event("turn.completed", { turnId: "turn-1", status: "interrupted", completedAt: 1300 }, 1300),
    event("session.status", { status: "interrupted" }, 1350),
    event("message.accepted", { text: "Answer" }, 1400),
    event("session.status", { status: "running" }, 1500),
    event("turn.started", { turnId: "turn-2", startedAt: 1600 }, 1600)
  ]);

  assert.deepEqual(turns.map((turn) => turn.status), ["interrupted", "running"]);
  assert.equal(turns[0].completedAt, 1300);
});

test("conversation keeps the first visible start time when Codex reports turn start later", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "Hello" }, 1000),
    event("turn.started", { turnId: "turn-1", startedAt: 5000 }, 5000)
  ]);

  assert.equal(turns[0].startedAt, 1000);
});

test("conversation shows an immediate sending state for optimistic submissions", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "Start", optimistic: true }, 1000)
  ]);

  assert.equal(turns.length, 1);
  assert.equal(turns[0].userText, "Start");
  assert.equal(turns[0].status, "sending");
  assert.equal(turns[0].startedAt, undefined);
  assert.equal(turns[0].activity.summary, "전송중");
});

test("conversation marks optimistic submissions as sending before server acceptance", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "Start", optimistic: true }, 1000)
  ]);

  assert.equal(turns.length, 1);
  assert.equal(turns[0].userText, "Start");
  assert.equal(turns[0].status, "sending");
  assert.equal(turns[0].userMessages[0].status, "sending");
  assert.equal(turns[0].activity.summary, "전송중");
});

test("conversation moves from thinking to running after Codex starts the turn", () => {
  const thinking = projectConversation([
    event("message.accepted", { text: "Start", sessionId: "session-1", optimisticAccepted: true }, 1000)
  ]);
  const running = projectConversation([
    event("message.accepted", { text: "Start", sessionId: "session-1", optimisticAccepted: true }, 1000),
    event("turn.started", { turnId: "turn-1", startedAt: 1200 }, 1200)
  ]);

  assert.equal(thinking[0].status, "thinking");
  assert.equal(thinking[0].userMessages[0].status, undefined);
  assert.equal(thinking[0].activity.summary, "생각중");
  assert.equal(running[0].status, "running");
  assert.equal(running[0].activity.summary, "작업중");
});

test("conversation starts elapsed work time when optimistic sending is accepted", () => {
  const turns = projectConversation([
    event("message.accepted", {
      text: "Start",
      sessionId: "session-1",
      optimisticAccepted: true,
      acceptedAt: new Date(4000).toISOString()
    }, 1000)
  ]);

  assert.equal(turns[0].status, "thinking");
  assert.equal(turns[0].userMessages[0].createdAt, 1000);
  assert.equal(turns[0].startedAt, 4000);
});

test("conversation marks an optimistic submitted turn interrupted on local stop", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "Start", optimistic: true }, 1000),
    event("session.status", { status: "interrupted", optimistic: true }, 1100)
  ]);

  assert.equal(turns.length, 1);
  assert.equal(turns[0].userText, "Start");
  assert.equal(turns[0].status, "interrupted");
  assert.equal(turns[0].activity.summary, "작업 중단");
});

test("conversation hides queued waiting bubbles and stops the active turn", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "Start" }, 1000),
    event("turn.started", { turnId: "turn-1", startedAt: 1100 }, 1100),
    event("message.accepted", { text: "Queued follow-up", queued: true, optimistic: true }, 1200),
    event("session.status", { status: "interrupted", optimistic: true }, 1300)
  ]);

  assert.equal(turns.length, 1);
  assert.equal(turns[0].userText, "Start");
  assert.equal(turns[0].status, "interrupted");
});

test("conversation promotes persisted queued placeholders when the queued turn starts", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "Queued follow-up", queued: true, queuedMessageId: "queued-1" }, 1000),
    event("message.accepted", { text: "Queued follow-up", queued: true, queuedMessageId: "queued-1", turnId: "turn-2" }, 2000),
    event("turn.started", { turnId: "turn-2", startedAt: 2100 }, 2100)
  ]);

  assert.equal(turns.length, 1);
  assert.equal(turns[0].userText, "Queued follow-up");
  assert.equal(turns[0].queuedMessageId, "queued-1");
  assert.equal(turns[0].turnId, "turn-2");
  assert.equal(turns[0].status, "running");
});

test("conversation consumes the queued placeholder when that message steers the active turn", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "Start" }, 1000),
    event("turn.started", { turnId: "turn-1", startedAt: 1100 }, 1100),
    event("message.accepted", { text: "Redirect", queued: true, queuedMessageId: "queued-1" }, 1200),
    event("message.accepted", { text: "Redirect", steering: true, queuedMessageId: "queued-1", turnId: "turn-1" }, 1300)
  ]);

  assert.equal(turns.length, 1);
  assert.deepEqual(turns[0].userTexts, ["Start", "Redirect"]);
  assert.equal(turns[0].status, "running");
});

test("conversation restores a turn to running when an optimistic interruption is rejected", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "Start" }, 1000),
    event("turn.started", { turnId: "turn-1", startedAt: 1100 }, 1100),
    event("session.status", { status: "interrupted", optimistic: true }, 1200),
    event("session.status", { status: "running" }, 1300)
  ]);

  assert.equal(turns[0].status, "running");
  assert.equal(turns[0].completedAt, undefined);
});

test("conversation does not reopen a completed turn when the next send is accepted", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "First" }, 1000),
    event("turn.started", { turnId: "turn-1", startedAt: 1100 }, 1100),
    event("turn.completed", { turnId: "turn-1", status: "completed", completedAt: 1200 }, 1200),
    event("session.status", { status: "running" }, 1300),
    event("message.accepted", { text: "Second" }, 1400)
  ]);

  assert.equal(turns[0].status, "completed");
  assert.equal(turns[1].status, "thinking");
});

test("conversation hides internal session events and status output", () => {
  const turns = projectConversation([
    event("session.created", { status: "idle" }, 100),
    event("session.status", { status: "running" }, 200),
    event("message.accepted", { text: "Question" }, 300),
    event("session.output", { kind: "status", text: "Codex turn completed." }, 400)
  ]);

  assert.equal(turns.length, 1);
  assert.equal(turns[0].userText, "Question");
  assert.equal(turns[0].assistantText, "");
  assert.equal(turns[0].status, "completed");
});

test("conversation keeps a freshly submitted optimistic turn sending when an unscoped terminal status arrives", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "New work", optimistic: true }, 1000),
    event("session.output", { kind: "status", text: "Codex turn completed." }, 1010)
  ]);

  assert.equal(turns.length, 1);
  assert.equal(turns[0].userText, "New work");
  assert.equal(turns[0].status, "sending");
  assert.equal(turns[0].activity.summary, "전송중");
});

test("conversation keeps a server-accepted optimistic turn thinking when an unscoped terminal status arrives", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "New work", sessionId: "session-1", optimisticAccepted: true }, 1000),
    event("session.output", { kind: "status", text: "Codex turn completed." }, 1010)
  ]);

  assert.equal(turns.length, 1);
  assert.equal(turns[0].userText, "New work");
  assert.equal(turns[0].status, "thinking");
  assert.equal(turns[0].activity.summary, "생각중");
});

test("conversation shows a user-visible attachment label when image-only messages are sent", () => {
  const turns = projectConversation([
    event("message.accepted", {
      text: "",
      attachmentCount: 1,
      attachments: [{ id: "att-1", name: "photo.jpg", previewUrl: "blob:photo" }]
    }, 1000),
    event("turn.started", { turnId: "turn-image", startedAt: 1100 }, 1100)
  ]);

  assert.equal(turns.length, 1);
  assert.equal(turns[0].userText, "Image attached");
  assert.deepEqual(turns[0].userTexts, ["Image attached"]);
  assert.deepEqual(turns[0].userAttachments, [{ id: "att-1", name: "photo.jpg", previewUrl: "blob:photo" }]);
});

test("conversation rebuilds uploaded image URLs from safe server attachment metadata", () => {
  const turns = projectConversation([
    event("message.accepted", {
      sessionId: "session-1",
      text: "Look",
      attachmentCount: 1,
      attachments: [{ id: "att-1", name: "photo.jpg", mimeType: "image/jpeg" }]
    }, 1000)
  ]);

  assert.equal(turns[0].userAttachments[0].previewUrl, "/api/sessions/session-1/attachments/att-1");
  assert.equal(JSON.stringify(turns).includes("C:\\\\"), false);
});

test("conversation keeps a safe placeholder for a deleted local attachment", () => {
  const turns = projectConversation([{
    id: "deleted-attachment",
    event: "message.accepted",
    data: {
      sessionId: "session-1",
      text: "Old image",
      attachmentCount: 1,
      attachments: [{ id: "att-1", name: "photo.jpg", deleted: true }]
    },
    createdAt: "2026-07-17T00:00:00.000Z"
  }]);

  assert.deepEqual(turns[0].userAttachments[0], { id: "att-1", name: "photo.jpg", deleted: true });
});

test("conversation hides Code Remote Manager bootstrap context prompts", () => {
  const bootstrap = [
    "Continue this Code Remote Manager work in a new account-owned thread.",
    "Use the previous visible conversation as context. Do not expose hidden reasoning, credentials, local auth data, or private paths.",
    "",
    "Previous visible conversation:",
    "User: Old request",
    "Assistant: Old answer",
    "",
    "Current user request:",
    "Archive chats"
  ].join("\n");
  const turns = projectConversation([
    event("message.accepted", { text: bootstrap }, 300),
    event("session.output", { kind: "assistant_delta", text: "Done" }, 400)
  ]);

  assert.equal(turns.length, 1);
  assert.equal(turns[0].userText, "Archive chats");
  assert.deepEqual(turns[0].userTexts, ["Archive chats"]);
  assert.equal(JSON.stringify(turns).includes("Previous visible conversation"), false);
});

test("conversation replaces plans and updates activity items in place", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "Build" }, 1000),
    event("turn.started", { turnId: "turn-2", startedAt: 1100 }, 1100),
    event("plan.updated", {
      turnId: "turn-2", plan: [{ step: "Old", status: "pending" }]
    }, 1200),
    event("plan.updated", {
      turnId: "turn-2", plan: [{ step: "Inspect", status: "completed" }]
    }, 1300),
    event("activity.started", {
      turnId: "turn-2", itemId: "cmd-1", kind: "commandExecution", summary: "npm test"
    }, 1400),
    event("activity.completed", {
      turnId: "turn-2", itemId: "cmd-1", kind: "commandExecution",
      summary: "npm test", status: "completed", durationMs: 250
    }, 1650)
  ]);

  assert.deepEqual(turns[0].activity.plan, [{ step: "Inspect", status: "completed" }]);
  assert.equal(turns[0].activity.items.length, 1);
  assert.equal(turns[0].activity.items[0].status, "completed");
  assert.equal(turns[0].activity.items[0].durationMs, 250);
});

test("conversation replaces streamed command output with the completed aggregate", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "Run" }, 1000),
    event("turn.started", { turnId: "turn-output", startedAt: 1100 }, 1100),
    event("activity.started", { turnId: "turn-output", itemId: "command-1", kind: "commandExecution" }, 1200),
    event("activity.updated", { turnId: "turn-output", itemId: "command-1", text: "partial" }, 1300),
    event("activity.completed", {
      turnId: "turn-output",
      itemId: "command-1",
      kind: "commandExecution",
      status: "failed",
      text: "complete output",
      error: "command failed",
      exitCode: 1
    }, 1400)
  ]);

  assert.equal(turns[0].activity.items[0].text, "complete output");
  assert.equal(turns[0].activity.items[0].error, "command failed");
  assert.equal(turns[0].activity.items[0].exitCode, 1);
});

test("conversation preserves tool call metadata for activity details", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "Check mobile UI" }, 1000),
    event("turn.started", { turnId: "turn-tool", startedAt: 1100 }, 1100),
    event("activity.completed", {
      turnId: "turn-tool",
      itemId: "tool-1",
      kind: "mcpToolCall",
      summary: "node_repl: js",
      status: "completed",
      durationMs: 700,
      toolCall: {
        server: "node_repl",
        tool: "js",
        arguments: { code: "await viewportCap.set({ width: 390, height: 844 })" }
      }
    }, 1800)
  ]);

  assert.deepEqual(turns[0].activity.items[0].toolCall, {
    server: "node_repl",
    tool: "js",
    arguments: { code: "await viewportCap.set({ width: 390, height: 844 })" }
  });
});

test("conversation supports legacy events without turn ids", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "Legacy question" }, 1000),
    event("session.output", { kind: "assistant_delta", text: "Legacy" }, 1200),
    event("session.output", { kind: "assistant_delta", text: " answer" }, 1300)
  ]);

  assert.equal(turns.length, 1);
  assert.equal(turns[0].assistantText, "Legacy answer");
});

test("conversation keeps commentary in activity and final answer in answer bubble", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "Explain" }, 1000),
    event("turn.started", { turnId: "turn-3", startedAt: 1100 }, 1100),
    event("activity.started", {
      turnId: "turn-3", itemId: "comment-1", kind: "agentMessage",
      phase: "commentary", summary: "Working: agentMessage"
    }, 1200),
    event("session.output", {
      turnId: "turn-3", itemId: "comment-1", kind: "assistant_delta",
      text: "파일을 확인합니다."
    }, 1300),
    event("activity.started", {
      turnId: "turn-3", itemId: "final-1", kind: "agentMessage",
      phase: "final_answer", summary: "Working: agentMessage"
    }, 1400),
    event("session.output", {
      turnId: "turn-3", itemId: "final-1", kind: "assistant_delta",
      text: "최종 답변"
    }, 1500)
  ]);

  assert.equal(turns[0].assistantText, "최종 답변");
  assert.equal(turns[0].activity.items.find((item) => item.id === "comment-1").text,
    "파일을 확인합니다.");
});

test("conversation hides empty assistant code fences instead of showing language labels", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "Measure activity width" }, 1000),
    event("session.output", {
      kind: "assistant_delta",
      text: "```js\n```"
    }, 1200),
    event("session.output", {
      kind: "assistant_delta",
      text: "\nActual answer"
    }, 1300)
  ]);

  assert.equal(turns[0].assistantText, "Actual answer");
});

test("conversation removes an entirely empty assistant code fence", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "Measure activity width" }, 1000),
    event("session.output", {
      kind: "assistant_delta",
      text: "```js\n```"
    }, 1200)
  ]);

  assert.equal(turns[0].assistantText, "");
});

test("conversation preserves safe file paths on file change activity", () => {
  const turns = projectConversation([
    event("turn.started", { turnId: "turn-files", startedAt: 1000 }, 1000),
    event("activity.completed", {
      turnId: "turn-files",
      itemId: "files-1",
      kind: "fileChange",
      summary: "Updated 1 file(s)",
      status: "completed",
      paths: ["apps/pwa/src/App.jsx"]
    }, 1200)
  ]);

  assert.deepEqual(turns[0].activity.items[0].paths, ["apps/pwa/src/App.jsx"]);
});

test("conversation promotes completed commentary questions to visible assistant replies", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "Add voice input" }, 1000),
    event("turn.started", { turnId: "turn-question", startedAt: 1100 }, 1100),
    event("activity.started", {
      turnId: "turn-question", itemId: "comment-1", kind: "agentMessage",
      phase: "commentary", summary: "Working: agentMessage"
    }, 1200),
    event("session.output", {
      turnId: "turn-question", itemId: "comment-1", kind: "assistant_delta",
      text: "Want to try it?"
    }, 1300),
    event("turn.completed", { turnId: "turn-question", status: "completed" }, 1400)
  ]);

  assert.equal(turns[0].assistantText, "Want to try it?");
  assert.equal(turns[0].status, "waiting_response");
  assert.equal(turns[0].activity.items.length, 0);
});

test("conversation hides user and final message lifecycle items", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "Question" }, 1000),
    event("activity.started", {
      itemId: "user-1", kind: "userMessage", summary: "Working: userMessage"
    }, 1100),
    event("activity.started", {
      itemId: "final-1", kind: "agentMessage", phase: "final_answer",
      summary: "Working: agentMessage"
    }, 1200),
    event("activity.started", {
      itemId: "comment-1", kind: "agentMessage", phase: "commentary",
      summary: "Working: agentMessage"
    }, 1300)
  ]);

  assert.deepEqual(turns[0].activity.items.map((item) => item.id), ["comment-1"]);
});

test("conversation marks the active turn failed when runtime emits an error", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "Question" }, 1000),
    event("error", { message: "Invalid request" }, 1200)
  ]);

  assert.equal(turns[0].status, "failed");
  assert.equal(turns[0].error, "Invalid request");
  assert.equal(turns[0].activity.summary, "작업 실패");
});

test("conversation marks the active turn failed from terminal session status", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "Question" }, 1000),
    event("turn.started", { turnId: "turn-failed", startedAt: 1100 }, 1100),
    event("session.status", { status: "failed" }, 1200)
  ]);

  assert.equal(turns[0].status, "failed");
  assert.equal(turns[0].activity.summary, "작업 실패");
});

test("conversation marks interrupted turn completion as interrupted", () => {
  const turns = projectConversation([
    event("message.accepted", { text: "Question" }, 1000),
    event("turn.started", { turnId: "turn-interrupted", startedAt: 1100 }, 1100),
    event("turn.completed", { turnId: "turn-interrupted", status: "interrupted" }, 1200)
  ]);

  assert.equal(turns[0].status, "interrupted");
  assert.equal(turns[0].activity.summary, "작업 중단");
});

function event(name, data, time) {
  return {
    id: `${name}-${time}`,
    event: name,
    data,
    createdAt: new Date(time).toISOString()
  };
}
