import test from "node:test";
import assert from "node:assert/strict";
import { activityFiles, activityItemsBetween, activityLogCount, activityTitle, groupConsecutiveCommands, groupTechnicalActivity, technicalGroupLabel, toolCallLabel, visibleActivityItems, visibleActivityText, withFileChangeCount } from "./activity-card.js";

test("activity updated after a follow-up appears below that message", () => {
  const item = { id: "shared-command", createdAt: 1200, updatedAt: 1500 };

  assert.deepEqual(activityItemsBetween([item], 1000, 1300), []);
  assert.deepEqual(activityItemsBetween([item], 1300), [item]);
});

test("activity file rows preserve patch paths missing from the current git changes", () => {
  const items = [{ kind: "fileChange", paths: ["useChanges.js", "committed.js"], diffs: [{ path: "committed.js", diff: "+old" }] }];
  const files = [
    { path: "useChanges.js", additions: 3, deletions: 1 },
    { path: "unrelated.js", additions: 99, deletions: 0 }
  ];

  assert.deepEqual(activityFiles(items, files), [
    files[0],
    { path: "committed.js", status: "modified", diff: "+old" }
  ]);
  assert.deepEqual(activityFiles([{ kind: "fileChange" }], files), []);
});

test("activity file rows recover redacted paths from the current changed files when counts match", () => {
  const files = [{ path: "apps/pwa/vite-hosts.js", additions: 2, deletions: 1 }];

  assert.deepEqual(activityFiles([{ kind: "fileChange", summary: "Updated 1 file(s)", paths: ["[private-path]"] }], files), files);
  assert.deepEqual(activityFiles([{ kind: "fileChange", summary: "Updated 2 file(s)", paths: ["[private-path]"] }], files), [
    { path: "[private-path]", status: "modified" }
  ]);
});

test("activity file rows use snapshot diff paths when public paths were redacted", () => {
  const items = [{
    kind: "fileChange",
    summary: "Updated 1 file(s)",
    paths: ["[private-path]"],
    diffs: [{ path: "apps/backend/src/sessions.js", diff: "+snapshot" }]
  }];

  assert.deepEqual(activityFiles(items, []), [
    { path: "apps/backend/src/sessions.js", status: "modified", diff: "+snapshot" }
  ]);
});

test("activity file rows prefer snapshot diff paths over current files when public paths were redacted", () => {
  const items = [{
    kind: "fileChange",
    summary: "Updated 1 file(s)",
    paths: ["[private-path]"],
    diffs: [{ path: "apps/backend/src/sessions.js", diff: "+snapshot" }]
  }];
  const files = [{ path: "apps/pwa/src/App.jsx", additions: 1, deletions: 0 }];

  assert.deepEqual(activityFiles(items, files), [
    { path: "apps/backend/src/sessions.js", status: "modified", diff: "+snapshot" }
  ]);
});

test("activity file rows preserve saved diffs whose own paths were redacted", () => {
  const items = [{
    kind: "fileChange",
    summary: "Updated 3 file(s)",
    paths: ["[private-path]"],
    diffs: [
      { path: "[private-path]", diff: "+first snapshot" },
      { path: "[private-path]", diff: "+another private snapshot" },
      { path: "[redacted]", diff: "+second snapshot" }
    ]
  }];
  const currentFiles = [{ path: "unrelated.js", additions: 1, deletions: 0 }];

  assert.deepEqual(activityFiles(items, currentFiles), [
    { path: "[private-path]", status: "modified", diff: "+first snapshot" },
    { path: "[private-path]", status: "modified", diff: "+another private snapshot" },
    { path: "[redacted]", status: "modified", diff: "+second snapshot" }
  ]);
});

test("activity card exposes commentary and reasoning summaries", () => {
  assert.equal(visibleActivityText({ kind: "agentMessage", phase: "commentary", text: "Live log" }), "Live log");
  assert.equal(visibleActivityText({ kind: "reasoning", text: "Safe summary" }), "Safe summary");
  assert.equal(visibleActivityText({ kind: "commandExecution", text: "safe output" }), "safe output");
  assert.equal(visibleActivityText({ kind: "mcpToolCall", error: "lookup failed" }), "lookup failed");
});

test("activity log count ignores workspace-wide file counts without patch activity", () => {
  assert.equal(activityLogCount({
    activity: {
      plan: [{ step: "one" }, { step: "two" }],
      items: [{ id: "command" }],
      fileCount: 2
    },
    approvals: [{ status: "pending" }, { status: "approved" }],
    error: "failed"
  }), 5);
});

test("activity log count counts file activity once", () => {
  assert.equal(activityLogCount({
    activity: {
      plan: [],
      items: [{ id: "files", kind: "fileChange", status: "completed" }],
      fileCount: 2
    },
    approvals: []
  }), 1);
});

test("empty interrupted activity cards show stopped instead of preparing", () => {
  assert.equal(activityTitle({ status: "running" }, 0), "Starting work");
  assert.equal(activityTitle({ status: "interrupted" }, 0), "Stopped");
  assert.equal(activityTitle({ status: "interrupted" }, 2), "Work log (2)");
});

test("activity labels use the active language translator", () => {
  const t = (key, values = {}) => ({
    "activity.log": `Work log (${values.count})`,
    "activity.completed": "Completed",
    "activity.command": "Command",
    "activity.filesChanged": `${values.count} files changed`,
    "activity.commandsRun": `${values.count} commands run`,
    "activity.tool": "Tool"
  })[key];

  assert.equal(activityTitle({ status: "completed" }, 2, t), "Work log (2)");
  assert.equal(technicalGroupLabel([
    { kind: "fileChange", summary: "Updated 2 file(s)", status: "completed" },
    { kind: "commandExecution", status: "completed" }
  ], t), "2 files changed, 1 commands run Completed");
  assert.equal(toolCallLabel({ kind: "mcpToolCall" }, t), "Tool");
});

test("activity log shows public reasoning summaries and omits empty commentary rows", () => {
  const command = { id: "command", kind: "commandExecution", summary: "npm test" };
  const commentary = { id: "commentary", kind: "agentMessage", phase: "commentary", summary: "Working: agentMessage", text: "Live update" };

  assert.deepEqual(visibleActivityItems([
    { id: "reasoning", kind: "reasoning", summary: "Thinking", text: "internal summary" },
    { id: "empty", kind: "agentMessage", phase: "commentary", summary: "Working: agentMessage", text: "" },
    commentary,
    command
  ]), [
    { id: "reasoning", kind: "reasoning", summary: "Thinking", text: "internal summary" },
    commentary,
    command
  ]);
});

test("activity log groups only consecutive commands with identical summaries", () => {
  const first = { id: "command-1", kind: "commandExecution", summary: "npm test" };
  const second = { id: "command-2", kind: "commandExecution", summary: "npm test" };
  const commentary = { id: "commentary", kind: "agentMessage", phase: "commentary", text: "Checking" };
  const third = { id: "command-3", kind: "commandExecution", summary: "npm test" };

  assert.deepEqual(groupConsecutiveCommands([first, second, commentary, third]), [
    { type: "commandGroup", key: "command-1", summary: "npm test", items: [first, second] },
    { type: "item", key: "commentary", item: commentary },
    { type: "item", key: "command-3", item: third }
  ]);
});

test("activity log groups consecutive commands from the same executable region", () => {
  const first = { id: "command-1", kind: "commandExecution", summary: ">_ \"C:\\\\Windows\\\\System32\\\\WindowsPowerShell\\\\v1.0\\\\powershell.exe\" -NoProfile -Command npm test" };
  const second = { id: "command-2", kind: "commandExecution", summary: ">_ C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -NoProfile -Command npm run build" };
  const third = { id: "command-3", kind: "commandExecution", summary: ">_ \"C:\\\\Windows\\\\System32\\\\WindowsPowerShell\\\\v1.0\\\\powershell.exe\" -NoLogo" };

  assert.deepEqual(groupConsecutiveCommands([first, second, third]), [
    { type: "commandGroup", key: "command-1", summary: first.summary, items: [first, second, third] }
  ]);
});

test("activity log leaves commands without visible summaries separate", () => {
  const first = { id: "command-1", kind: "commandExecution" };
  const second = { id: "command-2", kind: "commandExecution" };

  assert.deepEqual(groupConsecutiveCommands([first, second]), [
    { type: "item", key: "command-1", item: first },
    { type: "item", key: "command-2", item: second }
  ]);
});

test("activity log groups adjacent commands and file changes while preserving commentary boundaries", () => {
  const command = { id: "command-1", kind: "commandExecution", summary: "npm test", status: "completed" };
  const fileChange = { id: "files-1", kind: "fileChange", summary: "Updated 2 file(s)", status: "completed" };
  const commentary = { id: "commentary", kind: "agentMessage", phase: "commentary", text: "검증 중" };
  const laterCommand = { id: "command-2", kind: "commandExecution", summary: "npm run build", status: "running" };

  assert.deepEqual(groupTechnicalActivity([command, fileChange, commentary, laterCommand]), [
    { type: "technicalGroup", key: "command-1", items: [command, fileChange] },
    { type: "item", key: "commentary", item: commentary },
    { type: "technicalGroup", key: "command-2", items: [laterCommand] }
  ]);
});

test("technical activity label reports file and command counts with aggregate status", () => {
  assert.equal(technicalGroupLabel([
    { kind: "fileChange", summary: "Updated 2 file(s)", status: "completed" },
    { kind: "commandExecution", status: "completed" }
  ]), "2 files changed, 1 commands run Completed");

  assert.equal(technicalGroupLabel([
    { kind: "commandExecution", status: "completed" },
    { kind: "commandExecution", status: "running" }
  ]), "2 commands run Working");

  assert.equal(technicalGroupLabel([
    { kind: "commandExecution", status: "completed" },
    { kind: "commandExecution", status: "failed" }
  ]), "2 commands run · 1 Failed");
});

test("patch paths replace the workspace-wide changed-file count without duplicating rows", () => {
  const command = { id: "command", kind: "commandExecution", status: "completed" };
  const firstChange = { id: "files-1", kind: "fileChange", summary: "Updating files", status: "completed", paths: ["src/first.js", "src/shared.js"] };
  const duplicateChange = { id: "files-2", kind: "fileChange", summary: "Updating files", status: "completed", paths: ["src/second.jsx", "src/shared.js"] };

  assert.deepEqual(withFileChangeCount([command, firstChange, duplicateChange], 14, "completed"), [
    command,
    { ...firstChange, summary: "Updated 3 files", paths: ["src/first.js", "src/shared.js", "src/second.jsx"] }
  ]);
  assert.deepEqual(withFileChangeCount([command], 14, "running"), [command]);
});

test("tool calls render as direct activity rows so details open in one tap", () => {
  const tool = { id: "tool-1", kind: "commandExecution", summary: "PowerShell", status: "completed", toolCall: { tool: "js" } };

  assert.deepEqual(groupTechnicalActivity([tool]), [
    { type: "item", key: "tool-1", item: tool }
  ]);
});

test("nested shell commands use their user-facing command summary instead of the internal js tool name", () => {
  assert.equal(toolCallLabel({
    kind: "commandExecution",
    summary: "PowerShell 명령",
    toolCall: { tool: "js" }
  }), "PowerShell 명령");
  assert.equal(toolCallLabel({ kind: "mcpToolCall", toolCall: { server: "node_repl", tool: "js" } }), "node_repl Tool");
});

test("tool-only js summaries use a generic label instead of exposing the internal tool name", () => {
  assert.equal(toolCallLabel({ kind: "mcpToolCall", summary: "js", toolCall: {} }), "Tool");
});
