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

import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";

import { recoverTaskExecution } from "../recoverTaskExecution.ts";
import { persistWorkerReceiptFailed } from "../workerReceiptDurableHints.ts";
import { resetPmExecutionGovernanceForTests } from "../pmExecutionGovernance.ts";
import { resolveCanonicalTaskRuntimeState } from "../resolveCanonicalTaskRuntimeState.ts";

function recoverableSnapshot(taskId: string) {
  return resolveCanonicalTaskRuntimeState({
    taskId,
    role: "DEV",
    attempts: [{
      attempt_id: "attempt-test",
      task_id: taskId,
      task_path: `${taskId}.md`,
      target_role: "DEV",
      target_agent_id: "DEV-01",
      source: "test",
      mode: "initial",
      status: "session_failed",
      created_at: "2026-06-11T00:00:00.000Z",
      updated_at: "2026-06-11T00:01:00.000Z",
    }],
  });
}

describe("recoverTaskExecution", () => {
  it("does not wake when canonical evidence is in conflict", async () => {
    const root = await mkdtemp(join(tmpdir(), "codeflowmu-recover-conflict-"));
    let wakeCalls = 0;
    try {
      const result = await recoverTaskExecution({
        projectRoot: root,
        taskId: "TASK-20260810-099",
        role: "DEV",
        registry: {
          list: async () => [{ protocol: { agent_id: "DEV-01", role: "DEV", status: "running" } }],
        } as never,
        sessionManager: { listActive: async () => [] } as never,
        wakeExecutor: async () => {
          wakeCalls += 1;
          return { ok: true };
        },
        resolveCanonicalSnapshot: async () =>
          resolveCanonicalTaskRuntimeState({
            taskId: "TASK-20260810-099",
            role: "DEV",
            agentId: "DEV-01",
            lifecycleBucket: "active",
            registry: [{ agent_id: "DEV-01", role: "DEV", status: "running" }],
          }),
      });
      assert.equal(result.reason, "canonical_recovery_denied");
      assert.equal(wakeCalls, 0);
    } finally {
      await rm(root, { recursive: true, force: true }).catch(() => undefined);
    }
  });

  it("clears durable failed mark and in-memory guard callback", async () => {
    const root = await mkdtemp(join(tmpdir(), "codeflowmu-recover-"));
    const taskId = "TASK-20260611-003";
    let guardCleared: string | null = null;
    try {
      await persistWorkerReceiptFailed(root, taskId, "worker_failed_mark");

      const result = await recoverTaskExecution({
        projectRoot: root,
        taskId,
        role: "DEV",
        registry: {
          list: async () => [
            {
              protocol: { agent_id: "DEV-01", role: "DEV", status: "idle" },
            },
          ],
          get: async () => ({
            protocol: { agent_id: "DEV-01", role: "DEV", status: "idle" },
          }),
        } as never,
        sessionManager: {
          listActive: async () => [],
        } as never,
        wakeExecutor: async () => ({ ok: false, error: "stub_wake_failed" }),
        resolveCanonicalSnapshot: async ({ taskId: currentTaskId }) =>
          recoverableSnapshot(currentTaskId),
        clearInMemoryWorkerFailed: (tid) => {
          guardCleared = tid;
        },
      });

      assert.equal(result.ok, false);
      assert.equal(result.status, "failed");
      assert.equal(guardCleared, taskId);
    } finally {
      await rm(root, { recursive: true, force: true }).catch(() => {});
    }
  });

  it("does not recover repeatedly after wake_throttled", async () => {
    resetPmExecutionGovernanceForTests();
    const root = await mkdtemp(join(tmpdir(), "codeflowmu-recover-throttle-"));
    let wakeCalls = 0;
    const opts = {
      projectRoot: root,
      taskId: "TASK-20260615-201",
      role: "DEV",
      registry: {
        list: async () => [{ protocol: { agent_id: "DEV-01", role: "DEV", status: "idle" } }],
        get: async () => ({ protocol: { agent_id: "DEV-01", role: "DEV", status: "idle" } }),
      } as never,
      sessionManager: { listActive: async () => [] } as never,
      wakeExecutor: async () => {
        wakeCalls += 1;
        return { ok: false, delayed: true, remainingMs: 3000, delayedReason: "wake_throttled", reason: "wake_throttled" };
      },
      resolveCanonicalSnapshot: async ({ taskId: currentTaskId }: { taskId: string }) =>
        recoverableSnapshot(currentTaskId),
    };
    try {
      const first = await recoverTaskExecution(opts);
      const second = await recoverTaskExecution(opts);
      assert.equal(first.policy, "PM_STOP");
      assert.equal(first.remainingMs, 3000);
      assert.equal(first.cooldownReason, "wake_throttled");
      assert.equal(second.policy, "PM_STOP");
      assert.equal(second.reason, "wake_throttled");
      assert.equal(wakeCalls, 1);
    } finally {
      await rm(root, { recursive: true, force: true }).catch(() => {});
      resetPmExecutionGovernanceForTests();
    }
  });

  it("revalidates immediately before wake and stops when a live Session appears", async () => {
    resetPmExecutionGovernanceForTests();
    const root = await mkdtemp(join(tmpdir(), "codeflowmu-recover-revalidate-"));
    const taskId = "TASK-20260810-201";
    let snapshotCalls = 0;
    let wakeCalls = 0;
    try {
      const result = await recoverTaskExecution({
        projectRoot: root,
        taskId,
        role: "DEV",
        registry: {
          list: async () => [{ protocol: { agent_id: "DEV-01", role: "DEV", status: "idle" } }],
          get: async () => ({ protocol: { agent_id: "DEV-01", role: "DEV", status: "idle" } }),
        } as never,
        sessionManager: { listActive: async () => [] } as never,
        wakeExecutor: async () => {
          wakeCalls += 1;
          return { ok: true, session_id: "should-not-start" };
        },
        resolveCanonicalSnapshot: async () => {
          snapshotCalls += 1;
          if (snapshotCalls === 1) return recoverableSnapshot(taskId);
          return resolveCanonicalTaskRuntimeState({
            taskId,
            role: "DEV",
            liveSessions: [{
              protocol: {
                session_id: "session-live",
                agent_id: "DEV-01",
                task_id: taskId,
                status: "running",
                runs: [],
              },
              runtime_session_kind: "TASK_BOUND",
              runtime_role: "DEV",
              runtime_attempt_id: "attempt-test",
            } as never],
          });
        },
      });
      assert.equal(result.reason, "canonical_revalidation_blocked");
      assert.equal(snapshotCalls, 2);
      assert.equal(wakeCalls, 0);
    } finally {
      await rm(root, { recursive: true, force: true }).catch(() => undefined);
      resetPmExecutionGovernanceForTests();
    }
  });

  it("uses one cross-process fence for direct concurrent recovery calls", async () => {
    resetPmExecutionGovernanceForTests();
    const root = await mkdtemp(join(tmpdir(), "codeflowmu-recover-fence-"));
    const taskId = "TASK-20260810-202";
    let wakeCalls = 0;
    let allowWakeToFinish!: () => void;
    const wakeCanFinish = new Promise<void>((resolve) => {
      allowWakeToFinish = resolve;
    });
    let markWakeStarted!: () => void;
    const wakeStarted = new Promise<void>((resolve) => {
      markWakeStarted = resolve;
    });
    const opts = {
      projectRoot: root,
      taskId,
      role: "DEV",
      registry: {
        list: async () => [{ protocol: { agent_id: "DEV-01", role: "DEV", status: "idle" } }],
        get: async () => ({ protocol: { agent_id: "DEV-01", role: "DEV", status: "idle" } }),
      } as never,
      sessionManager: { listActive: async () => [] } as never,
      wakeExecutor: async () => {
        wakeCalls += 1;
        markWakeStarted();
        await wakeCanFinish;
        return { ok: true, session_id: "session-new" };
      },
      resolveCanonicalSnapshot: async () => recoverableSnapshot(taskId),
    };
    try {
      const firstPromise = recoverTaskExecution(opts);
      await wakeStarted;
      const second = await recoverTaskExecution(opts);
      assert.equal(second.reason, "recovery_fence_held");
      assert.equal(wakeCalls, 1);
      allowWakeToFinish();
      const first = await firstPromise;
      assert.equal(first.ok, true);
    } finally {
      allowWakeToFinish();
      await rm(root, { recursive: true, force: true }).catch(() => undefined);
      resetPmExecutionGovernanceForTests();
    }
  });
});
