import assert from "node:assert/strict";
import { describe, it } from "node:test";

import {
  buildPatrolSummaryFromCache,
  buildWriteReportAckBreakSummary,
  formatReusedToolOutput,
  hasPatrolSessionDataInCache,
  hasWriteReportInterimInCache,
  recordToolDedupeResult,
  resolveToolDedupe,
  shouldBreakPatrolToolLoop,
  shouldBreakWriteReportAckLoop,
  shouldGlobalDedupeTool,
  toolDedupeKey,
} from "../geminiToolDedupe.ts";

describe("geminiToolDedupe", () => {
  it("read_file and grep_files skip global dedupe for workspace paths", () => {
    assert.equal(shouldGlobalDedupeTool("read_file"), false);
    assert.equal(shouldGlobalDedupeTool("grep_files"), false);
    assert.equal(shouldGlobalDedupeTool("list_dir"), true);
    assert.equal(
      shouldGlobalDedupeTool("read_file", {
        path: "fcop/_lifecycle/active/TASK-20260606-013-ADMIN-to-PM.md",
      }),
      true,
    );
    assert.equal(shouldGlobalDedupeTool("list_tasks"), true);
    assert.equal(shouldGlobalDedupeTool("fcop_report"), true);
    assert.equal(shouldGlobalDedupeTool("write_task"), true);
  });

  it("reuses cached output for duplicate read_file in same round", () => {
    const seenThisRound = new Set<string>();
    const seenGlobal = new Set<string>();
    const cache = new Map<string, string>();
    const args = { path: "TASK-20260606-007-ADMIN-to-PM.md" };

    recordToolDedupeResult({
      toolName: "read_file",
      args,
      output: "fcop/_lifecycle/review/TASK-007.md\n1| body",
      seenThisRound,
      seenGlobal,
      cache,
    });

    const second = resolveToolDedupe({
      toolName: "read_file",
      args,
      seenThisRound,
      seenGlobal,
      cache,
    });
    assert.equal(second.action, "reuse");
    if (second.action === "reuse") {
      assert.match(second.output, /TASK-007/);
    }
  });

  it("allows read_file retry next round after failed attempt without cache", () => {
    const seenThisRound = new Set<string>();
    const seenGlobal = new Set<string>();
    const cache = new Map<string, string>();
    const args = { path: "missing.md" };
    const key = `read_file:${JSON.stringify(args)}`;

    // Simulate legacy bug: marked seen before cache on failure
    seenThisRound.add(key);

    const decision = resolveToolDedupe({
      toolName: "read_file",
      args,
      seenThisRound,
      seenGlobal,
      cache,
    });
    assert.equal(decision.action, "execute");
    assert.equal(seenThisRound.has(key), false);
  });

  it("globally dedupes write_task with cached output across rounds", () => {
    const seenThisRound = new Set<string>();
    const seenGlobal = new Set<string>();
    const cache = new Map<string, string>();
    const args = { recipient: "DEV", body: "fix" };

    recordToolDedupeResult({
      toolName: "write_task",
      args,
      output: "TASK written",
      seenThisRound,
      seenGlobal,
      cache,
    });
    seenThisRound.clear();

    const again = resolveToolDedupe({
      toolName: "write_task",
      args,
      seenThisRound,
      seenGlobal,
      cache,
    });
    assert.equal(again.action, "reuse");
  });

  it("dedupes write_task by recipient+references even when body differs", () => {
    const seenThisRound = new Set<string>();
    const seenGlobal = new Set<string>();
    const cache = new Map<string, string>();
    const base = {
      recipient: "DEV",
      references: "TASK-20260606-007",
    };

    recordToolDedupeResult({
      toolName: "write_task",
      args: { ...base, body: "first draft" },
      output: "TASK-20260606-008 written",
      seenThisRound,
      seenGlobal,
      cache,
    });
    seenThisRound.clear();

    const again = resolveToolDedupe({
      toolName: "write_task",
      args: { ...base, body: "second draft with more text" },
      seenThisRound,
      seenGlobal,
      cache,
    });
    assert.equal(again.action, "reuse");
    if (again.action === "reuse") {
      assert.match(
        formatReusedToolOutput("write_task", again.output),
        /请勿重复派单/,
      );
    }
  });

  it("toolDedupeKey maps parent alias to references prefix", () => {
    const a = toolDedupeKey("write_task", {
      recipient: "QA",
      parent: "TASK-20260606-007",
      body: "a",
    });
    const b = toolDedupeKey("write_task", {
      recipient: "QA",
      references: "TASK-20260606-007",
      body: "b",
    });
    assert.equal(a, b);
    assert.equal(a, "write_task:QA:TASK-20260606-007");
  });

  it("formatReusedToolOutput appends read_file hint once", () => {
    const out = formatReusedToolOutput("read_file", "file content");
    assert.match(out, /write_task/);
    assert.equal(formatReusedToolOutput("read_file", out), out);
  });

  it("globally dedupes patrol reads and tells Gemini to summarize", () => {
    const seenThisRound = new Set<string>();
    const seenGlobal = new Set<string>();
    const cache = new Map<string, string>();
    const args = { recipient: "PM" };

    recordToolDedupeResult({
      toolName: "list_tasks",
      args,
      output: "TASK-20260606-001 PM patrol item",
      seenThisRound,
      seenGlobal,
      cache,
    });
    seenThisRound.clear();

    const again = resolveToolDedupe({
      toolName: "list_tasks",
      args,
      seenThisRound,
      seenGlobal,
      cache,
    });
    assert.equal(again.action, "reuse");
    if (again.action === "reuse") {
      const out = formatReusedToolOutput("list_tasks", again.output);
      assert.match(out, /summarize_now/);
      assert.match(out, /already_fetched/);
    }
  });

  it("globally dedupes repeated FCoP read_file calls across rounds", () => {
    const seenThisRound = new Set<string>();
    const seenGlobal = new Set<string>();
    const cache = new Map<string, string>();
    const args = {
      path: "fcop/ledger/views/PM.todo.md",
    };

    recordToolDedupeResult({
      toolName: "read_file",
      args,
      output: "fcop/ledger/views/PM.todo.md\n1| ADMIN rejected task",
      seenThisRound,
      seenGlobal,
      cache,
    });
    seenThisRound.clear();

    const again = resolveToolDedupe({
      toolName: "read_file",
      args,
      seenThisRound,
      seenGlobal,
      cache,
    });
    assert.equal(again.action, "reuse");
    if (again.action === "reuse") {
      const out = formatReusedToolOutput("read_file", again.output);
      assert.match(out, /summarize_now/);
      assert.match(out, /already_fetched/);
    }
  });

  it("reuses cached grep_files output in same round", () => {
    const seenThisRound = new Set<string>();
    const seenGlobal = new Set<string>();
    const cache = new Map<string, string>();
    const args = { pattern: "write_task", path: "packages" };

    recordToolDedupeResult({
      toolName: "grep_files",
      args,
      output: "packages/foo.ts:12: write_task",
      seenThisRound,
      seenGlobal,
      cache,
    });

    const second = resolveToolDedupe({
      toolName: "grep_files",
      args,
      seenThisRound,
      seenGlobal,
      cache,
    });
    assert.equal(second.action, "reuse");
    if (second.action === "reuse") {
      assert.match(second.output, /write_task/);
    }
  });

  it("allows grep_files retry next round after failed attempt without cache", () => {
    const seenThisRound = new Set<string>();
    const seenGlobal = new Set<string>();
    const cache = new Map<string, string>();
    const args = { pattern: "missing-pattern", path: "." };
    const key = `grep_files:${JSON.stringify(args)}`;

    seenThisRound.add(key);

    const decision = resolveToolDedupe({
      toolName: "grep_files",
      args,
      seenThisRound,
      seenGlobal,
      cache,
    });
    assert.equal(decision.action, "execute");
    assert.equal(seenThisRound.has(key), false);
  });

  it("allows fcop_check retry when cached patrol output was a failure", () => {
    const seenThisRound = new Set<string>();
    const seenGlobal = new Set<string>();
    const cache = new Map<string, string>();
    const args = { role: "PM" };
    const key = "fcop_check:patrol";

    recordToolDedupeResult({
      toolName: "fcop_check",
      args,
      output: 'Error: fcop_check() got an unexpected keyword argument \'role\'',
      seenThisRound,
      seenGlobal,
      cache,
    });
    seenThisRound.clear();

    const again = resolveToolDedupe({
      toolName: "fcop_check",
      args,
      seenThisRound,
      seenGlobal,
      cache,
    });
    assert.equal(again.action, "execute");
    assert.equal(seenGlobal.has(key), false);
    assert.equal(cache.has(key), false);
  });

  it("globally dedupes repeated list_dir calls across rounds", () => {
    const seenThisRound = new Set<string>();
    const seenGlobal = new Set<string>();
    const cache = new Map<string, string>();
    const args = { path: "." };

    recordToolDedupeResult({
      toolName: "list_dir",
      args,
      output: ".\n- fcop/\n- packages/\n- workspace/",
      seenThisRound,
      seenGlobal,
      cache,
    });
    seenThisRound.clear();

    const again = resolveToolDedupe({
      toolName: "list_dir",
      args,
      seenThisRound,
      seenGlobal,
      cache,
    });
    assert.equal(again.action, "reuse");
    if (again.action === "reuse") {
      assert.match(again.output, /packages/);
    }
  });

  it("shouldBreakPatrolToolLoop breaks duplicate-only round", () => {
    const cache = new Map<string, string>([
      ["get_team_status:patrol", "PM inbox: 2 tasks"],
    ]);
    assert.equal(
      shouldBreakPatrolToolLoop({
        functionCallCount: 5,
        executedCount: 0,
        reusedCount: 5,
        roundIndex: 1,
        cache,
      }),
      true,
    );
  });

  it("shouldBreakPatrolToolLoop breaks 1 execute + N reuse batch", () => {
    const cache = new Map<string, string>([
      ["get_team_status:patrol", "PM inbox: 2 tasks"],
      ["fcop_check:patrol", "no drift"],
    ]);
    assert.equal(
      shouldBreakPatrolToolLoop({
        functionCallCount: 10,
        executedCount: 1,
        reusedCount: 9,
        roundIndex: 1,
        cache,
      }),
      true,
    );
  });

  it("shouldBreakPatrolToolLoop does not break first round with fresh executes", () => {
    const cache = new Map<string, string>([
      ["get_team_status:patrol", "PM inbox: 2 tasks"],
    ]);
    assert.equal(
      shouldBreakPatrolToolLoop({
        functionCallCount: 2,
        executedCount: 2,
        reusedCount: 0,
        roundIndex: 1,
        cache,
      }),
      false,
    );
  });

  it("shouldBreakPatrolToolLoop defers duplicate-only break on Hot Path until fcop_check or round cap", () => {
    const cache = new Map<string, string>([
      ["fcop_report:patrol", "UNBOUND report"],
    ]);
    assert.equal(
      shouldBreakPatrolToolLoop({
        functionCallCount: 5,
        executedCount: 0,
        reusedCount: 5,
        roundIndex: 1,
        cache,
        adminRejectHotPath: true,
      }),
      false,
    );
    assert.equal(
      shouldBreakPatrolToolLoop({
        functionCallCount: 5,
        executedCount: 0,
        reusedCount: 5,
        roundIndex: 6,
        cache,
        adminRejectHotPath: true,
      }),
      true,
    );
  });

  it("shouldBreakPatrolToolLoop breaks duplicate-only when fcop_check done on Hot Path", () => {
    const cache = new Map<string, string>([
      ["fcop_report:patrol", "UNBOUND report"],
      ['fcop_check:{"role":"PM"}', "no drift"],
    ]);
    assert.equal(
      shouldBreakPatrolToolLoop({
        functionCallCount: 4,
        executedCount: 0,
        reusedCount: 4,
        roundIndex: 1,
        cache,
        adminRejectHotPath: true,
      }),
      true,
    );
  });

  it("buildPatrolSummaryFromCache Hot Path next-steps footer", () => {
    const cache = new Map<string, string>([
      ["fcop_report:patrol", "UNBOUND"],
    ]);
    const summary = buildPatrolSummaryFromCache(cache, {
      uiLang: "zh",
      adminRejectHotPathNextSteps: true,
    });
    assert.match(summary, /Hot Path 未完成/);
    assert.match(summary, /fcop_check/);
    assert.match(summary, /write_report\(status=done\)/);
  });

  it("shouldBreakPatrolToolLoop hard-caps at round 3 when patrol cache exists", () => {
    const cache = new Map<string, string>([
      ["get_team_status:patrol", "PM inbox: 2 tasks"],
    ]);
    assert.equal(
      shouldBreakPatrolToolLoop({
        functionCallCount: 2,
        executedCount: 2,
        reusedCount: 0,
        roundIndex: 3,
        cache,
      }),
      true,
    );
  });

  it("hasPatrolSessionDataInCache detects fcop read_file", () => {
    const cache = new Map<string, string>([
      [
        'read_file:{"path":"fcop/ledger/views/PM.todo.md"}',
        "fcop/ledger/views/PM.todo.md\n1| todo",
      ],
    ]);
    assert.equal(hasPatrolSessionDataInCache(cache), true);
  });

  it("buildPatrolSummaryFromCache includes fcop ledger reads", () => {
    const cache = new Map<string, string>([
      ["get_team_status:patrol", "PM: 1 active"],
      [
        'read_file:{"path":"fcop/ledger/views/PM.todo.md"}',
        "fcop/ledger/views/PM.todo.md\n1| rejected task",
      ],
    ]);
    const summary = buildPatrolSummaryFromCache(cache, { uiLang: "zh" });
    assert.match(summary, /团队状态/);
    assert.match(summary, /PM\.todo\.md|账本/);
  });

  it("buildPatrolSummaryFromCache no_model_text variant uses system-check header", () => {
    const cache = new Map<string, string>([
      ["fcop_check:{\"role\":\"PM\"}", "PM ledger ok"],
    ]);
    const summary = buildPatrolSummaryFromCache(cache, {
      uiLang: "zh",
      variant: "no_model_text",
    });
    assert.match(summary, /系统检查汇总/);
    assert.doesNotMatch(summary, /熔断工具循环/);
  });

  it("dedupes write_report by task_id even when body differs", () => {
    const seenThisRound = new Set<string>();
    const seenGlobal = new Set<string>();
    const cache = new Map<string, string>();
    const base = {
      task_id: "TASK-20260607-010",
      reporter: "PM",
      recipient: "ADMIN",
    };

    recordToolDedupeResult({
      toolName: "write_report",
      args: { ...base, body: "已收到任务，正在分析并派发。" },
      output: "REPORT-20260607-001 written",
      seenThisRound,
      seenGlobal,
      cache,
    });
    seenThisRound.clear();

    const again = resolveToolDedupe({
      toolName: "write_report",
      args: {
        ...base,
        body: "已收到任务，正在分析并准备系统、MCP 工具、团队角色和技能工具的检查报告。",
      },
      seenThisRound,
      seenGlobal,
      cache,
    });
    assert.equal(again.action, "reuse");
    if (again.action === "reuse") {
      assert.match(
        formatReusedToolOutput("write_report", again.output),
        /禁止重复 write_report ack/,
      );
    }
  });

  it("dedupes write_report without task_id by reporter+recipient+bucket", () => {
    const seenThisRound = new Set<string>();
    const seenGlobal = new Set<string>();
    const cache = new Map<string, string>();
    const base = { reporter: "PM", recipient: "ADMIN" };

    recordToolDedupeResult({
      toolName: "write_report",
      args: { ...base, body: "ack 1" },
      output: "REPORT-20260607-001 written",
      seenThisRound,
      seenGlobal,
      cache,
    });
    seenThisRound.clear();

    const again = resolveToolDedupe({
      toolName: "write_report",
      args: { ...base, body: "ack 2 — different wording" },
      seenThisRound,
      seenGlobal,
      cache,
    });
    assert.equal(again.action, "reuse");
    assert.equal(
      toolDedupeKey("write_report", base),
      "write_report:*:PM:ADMIN:interim:",
    );
  });

  it("dedupes write_report with empty reporter/recipient by bucket only", () => {
    const seenThisRound = new Set<string>();
    const seenGlobal = new Set<string>();
    const cache = new Map<string, string>();

    recordToolDedupeResult({
      toolName: "write_report",
      args: { body: "ack 1" },
      output: "REPORT-20260607-002 written",
      seenThisRound,
      seenGlobal,
      cache,
    });
    seenThisRound.clear();

    const again = resolveToolDedupe({
      toolName: "write_report",
      args: { body: "ack 2" },
      seenThisRound,
      seenGlobal,
      cache,
    });
    assert.equal(again.action, "reuse");
    assert.equal(toolDedupeKey("write_report", {}), "write_report:*:*:*:interim:");
  });

  it("toolDedupeKey write_report separates interim vs done buckets", () => {
    const interim = toolDedupeKey("write_report", {
      task_id: "TASK-20260607-010",
      reporter: "PM",
      recipient: "ADMIN",
    });
    const done = toolDedupeKey("write_report", {
      task_id: "TASK-20260607-010",
      reporter: "PM",
      recipient: "ADMIN",
      status: "done",
    });
    assert.equal(interim, "write_report:TASK-20260607-010:PM:ADMIN:interim:");
    assert.match(done, /^write_report:TASK-20260607-010:PM:ADMIN:done:[0-9a-f]{8}$/);
    assert.notEqual(interim, done);
  });

  it("does not dedupe changed terminal report content after rework", () => {
    const seenThisRound = new Set<string>();
    const seenGlobal = new Set<string>();
    const cache = new Map<string, string>();
    const base = {
      task_id: "TASK-20260711-001",
      reporter: "PM",
      recipient: "ADMIN",
      status: "done",
    };

    recordToolDedupeResult({
      toolName: "write_report",
      args: { ...base, body: "first delivery" },
      output: "REPORT-A written",
      seenThisRound,
      seenGlobal,
      cache,
    });
    seenThisRound.clear();

    const revised = resolveToolDedupe({
      toolName: "write_report",
      args: { ...base, body: "second delivery with DEV and QA evidence" },
      seenThisRound,
      seenGlobal,
      cache,
    });
    assert.equal(revised.action, "execute");
  });

  it("dedupes terminal retries with the same client submission id", () => {
    const first = toolDedupeKey("write_report", {
      task_id: "TASK-20260711-001",
      reporter: "PM",
      recipient: "ADMIN",
      status: "done",
      body: "first wording",
      client_submission_id: "submit-2",
    });
    const retry = toolDedupeKey("write_report", {
      task_id: "TASK-20260711-001",
      reporter: "PM",
      recipient: "ADMIN",
      status: "done",
      body: "same request retried with transport formatting",
      client_submission_id: "submit-2",
    });
    assert.equal(first, retry);
  });

  it("shouldBreakWriteReportAckLoop breaks duplicate-only write_report round", () => {
    const cache = new Map<string, string>([
      [
        "write_report:TASK-20260607-010:PM:ADMIN:interim",
        "REPORT written",
      ],
    ]);
    assert.equal(hasWriteReportInterimInCache(cache), true);
    assert.equal(
      shouldBreakWriteReportAckLoop({
        functionCallCount: 3,
        executedCount: 0,
        reusedCount: 3,
        writeReportReuseCount: 3,
        roundIndex: 1,
        cache,
      }),
      true,
    );
  });

  it("buildWriteReportAckBreakSummary mentions next steps", () => {
    const summary = buildWriteReportAckBreakSummary({
      uiLang: "zh",
      toolLog: ["write_report: ok"],
    });
    assert.match(summary, /已 ack|interim report/i);
    assert.match(summary, /write_task|fcop_report/);
  });
});
