import assert from "node:assert/strict";
import { existsSync, readFileSync, readdirSync } from "node:fs";
import path from "node:path";
import { describe, it } from "node:test";

import { runtimeAlertManager } from "../../alerts/RuntimeAlertManager.ts";
import { invokeFcopToolOnce } from "../FcopMcpOneShot.ts";
import {
  buildModelNotFoundFailureDetail,
  decideGeminiRetryOnError,
  processFunctionResponseRound,
  runAuthorityToolSmoke,
  runWriteReportTaskIdGuardSmoke,
} from "./googleAdapterStepASmokeHarness.ts";

const REPO_ROOT = path.resolve(
  import.meta.dirname,
  "../../../../..",
);
const FCOP_ROOT = path.join(REPO_ROOT, "fcop");
const FCOP_LIFECYCLE = path.join(FCOP_ROOT, "_lifecycle");
const LIFECYCLE_STAGES = ["inbox", "active", "review", "done", "archive"] as const;

function listLifecycleTaskFiles(): string[] {
  const files: string[] = [];
  for (const stage of LIFECYCLE_STAGES) {
    const dir = path.join(FCOP_LIFECYCLE, stage);
    if (!existsSync(dir)) continue;
    for (const name of readdirSync(dir)) {
      if (/^TASK-.*\.md$/i.test(name)) {
        files.push(path.join(dir, name));
      }
    }
  }
  return files;
}

/** write_task 先落 inbox；runtime 可能立刻 dispatch 到 active，故按 marker 跨阶段查找。 */
function findTaskLandEvidence(marker: string): {
  filePath: string;
  landedInInbox: boolean;
  stillInInbox: boolean;
} | null {
  for (const filePath of listLifecycleTaskFiles()) {
    const text = readFileSync(filePath, "utf-8");
    if (!text.includes(marker)) continue;
    const stillInInbox = filePath.includes(`${path.sep}_lifecycle${path.sep}inbox${path.sep}`);
    const landedInInbox =
      stillInInbox ||
      /\nto:\s*inbox\b/m.test(text) ||
      /from:\s*inbox\b/m.test(text);
    return { filePath, landedInInbox, stillInInbox };
  }
  return null;
}

function eventPayloads(
  events: Array<{ event_type: string; payload?: unknown }>,
  type: string,
): unknown[] {
  return events.filter((e) => e.event_type === type).map((e) => e.payload);
}

describe("Google FCoP Adapter Step A smoke", () => {
  it("V1: DEV-01 write_task → AUTHORITY_DENIED, no invoke, events + alert + failed settle", async () => {
    runtimeAlertManager.resetForTests();
    let mcpCalled = false;

    const result = await runAuthorityToolSmoke({
      agentId: "DEV-01",
      toolName: "write_task",
      args: { recipient: "QA", body: "smoke should not land" },
      invokeTool: async () => {
        mcpCalled = true;
        return "should-not-run";
      },
      onAlert: (p) => {
        runtimeAlertManager.ingest({
          ...p,
          affected_agent: "DEV-01",
        });
      },
    });

    assert.equal(result.settledStatus, "failed");
    assert.equal(mcpCalled, false);
    assert.equal(result.toolInvoked, false);

    const toolResults = eventPayloads(result.events, "sdk.tool_result");
    assert.ok(toolResults.length >= 1);
    const failedResult = toolResults.find((p) => {
      const raw = (p as { raw?: { status?: string } })?.raw;
      return raw?.status === "failed";
    });
    assert.ok(failedResult, "expected sdk.tool_result failed");

    const statuses = eventPayloads(result.events, "sdk.status");
    const deniedStatus = statuses.find((p) => {
      const raw = (p as { raw?: { code?: string } })?.raw;
      return raw?.code === "AUTHORITY_DENIED";
    });
    assert.ok(deniedStatus, "expected sdk.status AUTHORITY_DENIED");

    assert.equal(result.alerts.length, 1);
    assert.equal(result.alerts[0]!.code, "AUTHORITY_DENIED");
    assert.equal(result.alerts[0]!.severity, "P1");
    assert.equal(result.alerts[0]!.category, "role_boundary");

    const snapshot = runtimeAlertManager.getSnapshot();
    assert.ok(
      snapshot.active.some(
        (a) => a.code === "AUTHORITY_DENIED" && a.category === "role_boundary",
      ),
    );

    assert.ok(result.sdkFailureDetail);
    const detail = JSON.stringify(result.sdkFailureDetail);
    assert.match(detail, /AUTHORITY_DENIED|权限|write_task/i);

    const settled = await new Promise<{ status: string }>((resolve) => {
      void result;
      resolve({ status: result.settledStatus });
    });
    assert.equal(settled.status, "failed");
  });

  it("V1b: DEV-01 create_task同样拦截", async () => {
    const result = await runAuthorityToolSmoke({
      agentId: "DEV-01",
      toolName: "create_task",
      args: { recipient: "DEV", body: "noop" },
    });
    assert.equal(result.settledStatus, "failed");
    assert.equal(result.alerts[0]?.code, "AUTHORITY_DENIED");
  });

  it("V2: PM-01 write_task authority 允许且不触发 AUTHORITY_DENIED", async () => {
    const result = await runAuthorityToolSmoke({
      agentId: "PM-01",
      toolName: "write_task",
      args: { recipient: "DEV", body: "step-a smoke allowed path" },
    });

    assert.equal(result.settledStatus, "finished");
    assert.equal(result.alerts.length, 0);
    const statuses = eventPayloads(result.events, "sdk.status");
    assert.ok(
      !statuses.some((p) => {
        const raw = (p as { raw?: { code?: string } })?.raw;
        return raw?.code === "AUTHORITY_DENIED";
      }),
    );

    const toolResults = eventPayloads(result.events, "sdk.tool_result");
    const okResult = toolResults.find((p) => {
      const raw = (p as { raw?: { status?: string } })?.raw;
      return raw?.status === "success";
    });
    assert.ok(okResult);
  });

  it("V2b (optional): PM-01 真实 write_task 落盘 inbox", async (t) => {
    if (process.env.CODEFLOW_STEP_A_FCOP !== "1") {
      t.skip("set CODEFLOW_STEP_A_FCOP=1 to run live FCoP write_task");
      return;
    }
    if (!existsSync(path.join(FCOP_ROOT, "fcop.json"))) {
      t.skip("fcop project not found");
      return;
    }

    const beforePaths = new Set(listLifecycleTaskFiles());
    const marker = `step-a-smoke-${Date.now()}`;

    const result = await runAuthorityToolSmoke({
      agentId: "PM-01",
      toolName: "write_task",
      args: {
        recipient: "DEV",
        body: `# Step A smoke\n\n${marker}\n\n## Acceptance\n- inbox file exists`,
        thread_key: "google-gemini-smoke-step-a",
      },
      invokeTool: (tool, args) =>
        invokeFcopToolOnce("python", REPO_ROOT, tool, args, 120_000),
    });

    assert.equal(result.settledStatus, "finished");
    assert.equal(result.toolInvoked, true);

    const evidence = findTaskLandEvidence(marker);
    assert.ok(evidence, `expected TASK file containing marker ${marker}`);
    assert.ok(
      evidence.landedInInbox,
      `expected write_task to land in inbox first (file=${evidence.filePath})`,
    );
    const afterPaths = listLifecycleTaskFiles();
    const newTask = afterPaths.find((p) => !beforePaths.has(p) && p === evidence.filePath);
    assert.ok(
      newTask ?? afterPaths.some((p) => !beforePaths.has(p)),
      "expected a new TASK file under fcop/_lifecycle/",
    );
  });

  it("V3: MODEL_NOT_FOUND 不重试、不 backoff", () => {
    const err = Object.assign(new Error("models/gemini-fake-404 is not found"), {
      status: 404,
    });

    const d1 = decideGeminiRetryOnError(err, 0, 3);
    assert.deepEqual(d1, { action: "no_retry_break", reason: "MODEL_NOT_FOUND" });

    const d2 = decideGeminiRetryOnError(err, 0, 3);
    assert.notEqual(d2.action, "retry");

    const detail = buildModelNotFoundFailureDetail(
      err,
      "gemini-fake-404",
      "PM-01",
      "sess-smoke",
    );
    assert.equal(detail.runtime_provider, "google");
    const blob = JSON.stringify(detail);
    assert.match(blob, /MODEL_NOT_FOUND/i);
  });

  it("V3b: 503/429 才 increment retryCount", () => {
    const err503 = new Error("503 Service Unavailable");
    const retry503 = decideGeminiRetryOnError(err503, 0, 3);
    assert.deepEqual(retry503, { action: "retry", nextRetryCount: 1 });

    const err429 = new Error("429 Resource exhausted rate limit");
    const retry429 = decideGeminiRetryOnError(err429, 1, 3);
    assert.deepEqual(retry429, { action: "retry", nextRetryCount: 2 });
  });

  it("V4: 第一次 mismatch 可 repair 并 push", () => {
    // build 用 tr.id ?? call.id → 错位 id；repair 用 call.id ?? tr.id → 可修复
    const modelParts = [
      {
        functionCall: { name: "read_task", id: "fc-1", args: { task_id: "T1" } },
      },
    ];
    const toolResults = [
      { name: "read_task", id: "fc-wrong", response: { ok: true, data: "x" } },
    ];

    const r1 = processFunctionResponseRound({
      modelParts,
      toolResults,
      strikes: 0,
      round: 1,
    });

    assert.equal(r1.strikes, 1);
    assert.equal(r1.broken, false);
    assert.equal(r1.chatHistoryPushed, true);
    assert.equal(r1.align.ok, true);
    assert.equal(r1.alert, undefined);
    const frId = (
      r1.turn!.parts[0] as { functionResponse: { id?: string } }
    ).functionResponse.id;
    assert.equal(frId, "fc-1");
  });

  it("V4b: 第二次仍失败 → FUNCTION_RESPONSE_MISALIGNED，不 push", () => {
    const modelParts = [
      {
        functionCall: { name: "read_task", id: "fc-a", args: {} },
      },
      {
        functionCall: { name: "grep_files", id: "fc-b", args: {} },
      },
    ];
    const toolResults = [
      { name: "read_task", id: "fc-wrong", response: { ok: false } },
    ];

    const r1 = processFunctionResponseRound({
      modelParts,
      toolResults,
      strikes: 0,
      round: 1,
    });
    assert.equal(r1.strikes, 1);
    assert.equal(r1.broken, false);

    const r2 = processFunctionResponseRound({
      modelParts,
      toolResults,
      strikes: r1.strikes,
      round: 2,
    });

    assert.equal(r2.strikes, 2);
    assert.equal(r2.broken, true);
    assert.equal(r2.chatHistoryPushed, false);
    assert.equal(r2.statusCode, "FUNCTION_RESPONSE_MISALIGNED");
    assert.equal(r2.alert?.severity, "P0");
    assert.equal(r2.alert?.code, "FUNCTION_RESPONSE_MISALIGNED");
  });

  it("V4c: repair 后仍不对齐则直接 strike=2 熔断", () => {
    const modelParts = [
      { functionCall: { name: "read_task", id: "fc-1", args: {} } },
    ];
    const toolResults = [
      { name: "evil_tool", id: "fc-1", response: { ok: false } },
    ];
    // id 相同但 name 错位：repair 按 id 匹配后仍保留 tr.name，validate 继续失败
    const misalignedTurn = {
      role: "user" as const,
      parts: [
        {
          functionResponse: {
            name: "evil_tool",
            id: "fc-1",
            response: { ok: false },
          },
        },
      ],
    };

    const r = processFunctionResponseRound({
      modelParts,
      toolResults,
      strikes: 0,
      round: 3,
      initialUserTurn: misalignedTurn,
    });

    assert.equal(r.strikes, 2);
    assert.equal(r.broken, true);
    assert.equal(r.chatHistoryPushed, false);
    assert.equal(r.statusCode, "FUNCTION_RESPONSE_MISALIGNED");
    assert.equal(r.alert?.severity, "P0");
  });

  it("V5: TASK-20260607-029 run spec — write_report 缺 task_id 时 MCP 前 autofill", () => {
    const pinned = "TASK-20260607-029";
    const taskFilepath =
      "fcop/_lifecycle/active/TASK-20260607-029-ADMIN-to-PM.md";
    const promptWithoutTaskId =
      "你是 PM。请阅读任务正文并完成 Hot Path 交付，最后 write_report 回 ADMIN。";

    const r = runWriteReportTaskIdGuardSmoke({
      sendSpec: {
        pinnedTaskId: pinned,
        taskFilepath,
        frontmatterTaskId: pinned,
        promptText: promptWithoutTaskId,
      },
      writeReportArgs: {
        reporter: "PM",
        recipient: "ADMIN",
        body: "巡检完成",
        status: "done",
      },
    });

    assert.equal(r.pinnedResolution.pinnedTaskId, pinned);
    assert.equal(r.pinnedResolution.source, "spec_pinnedTaskId");
    assert.equal(r.guardResult.action, "repair");
    assert.equal(r.wouldInvokeMcp, true);
    assert.equal(r.effectiveArgs.task_id, pinned);
  });

  it("V5b: 仅 taskFilepath 显式传入时仍解析 pinned（不依赖 prompt）", () => {
    const pinned = "TASK-20260607-029";
    const taskFilepath =
      "fcop/_lifecycle/active/TASK-20260607-029-ADMIN-to-PM.md";

    const r = runWriteReportTaskIdGuardSmoke({
      sendSpec: {
        taskFilepath,
        promptText: "你是 PM，完成工作后 write_report。",
      },
      writeReportArgs: {
        reporter: "PM",
        recipient: "ADMIN",
        body: "done",
        status: "done",
      },
    });

    assert.equal(r.pinnedResolution.pinnedTaskId, pinned);
    assert.equal(r.pinnedResolution.source, "spec_taskFilepath");
    assert.equal(r.effectiveArgs.task_id, pinned);
  });

  it("V5c: spec pinned 优先于 prompt 中其它 TASK 前缀", () => {
    const pinned = "TASK-20260607-029";
    const r = runWriteReportTaskIdGuardSmoke({
      sendSpec: {
        pinnedTaskId: pinned,
        promptText: "参考 TASK-20260101-001 的历史格式写 report",
      },
      writeReportArgs: {
        reporter: "PM",
        recipient: "ADMIN",
        body: "x",
        status: "done",
      },
    });

    assert.equal(r.pinnedResolution.source, "spec_pinnedTaskId");
    assert.equal(r.effectiveArgs.task_id, pinned);
  });
});
