import { spawnSync } from "node:child_process";
import { writeFileSync, unlinkSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { codexResumeTaskForTest } from "./src/code-queue";
import { findResumeTraceConfirmation, resumeDuplicateDecision, resumeTraceText } from "../src/components/microservices/code-queue/src/resume-confirmation";
import type { QueueTask } from "../src/components/microservices/code-queue/src/types";

type JsonRecord = Record<string, unknown>;

function assertCondition(condition: unknown, message: string, detail: JsonRecord = {}): void {
  if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
}

function nestedRecord(value: unknown, path: string[]): JsonRecord {
  let current: unknown = value;
  for (const key of path) {
    assertCondition(current !== null && typeof current === "object" && !Array.isArray(current), "expected object while traversing JSON", { path, key, current });
    current = (current as JsonRecord)[key];
  }
  assertCondition(current !== null && typeof current === "object" && !Array.isArray(current), "expected nested object", { path, current });
  return current as JsonRecord;
}

function runCli(args: string[], stdin?: string): { status: number | null; stdout: string; stderr: string; json: JsonRecord | null } {
  const result = spawnSync("bun", ["scripts/cli.ts", ...args], {
    cwd: process.cwd(),
    input: stdin,
    encoding: "utf8",
  });
  const stdout = String(result.stdout || "");
  let json: JsonRecord | null = null;
  try {
    json = JSON.parse(stdout) as JsonRecord;
  } catch {
    json = null;
  }
  return { status: result.status, stdout, stderr: String(result.stderr || ""), json };
}

function fixtureTask(): QueueTask {
  const at = "2026-05-23T00:00:00.000Z";
  return {
    id: "codex_resume_fixture",
    queueId: "default",
    queueEnteredAt: at,
    prompt: "base",
    basePrompt: "base",
    referenceTaskIds: [],
    referenceInjection: null,
    providerId: "D601",
    cwd: "/workspace/unidesk",
    model: "gpt-5.5",
    reasoningEffort: null,
    executionMode: "default",
    maxAttempts: 99,
    status: "succeeded",
    createdAt: at,
    updatedAt: at,
    startedAt: at,
    finishedAt: "2026-05-23T00:10:00.000Z",
    readAt: null,
    currentAttempt: 1,
    currentMode: "initial",
    codexThreadId: "thread_resume_fixture",
    activeTurnId: null,
    finalResponse: "done",
    lastError: null,
    lastJudge: null,
    judgeFailCount: 0,
    promptHistory: [],
    output: [],
    events: [],
    attempts: [],
    cancelRequested: false,
    nextPrompt: null,
    nextMode: null,
  };
}

function deterministicResumeId(taskId: string, prompt: string): string {
  return `resume_${Bun.SHA256.hash(`unidesk-code-queue-resume:v1\0${taskId}\0${prompt}`, "hex").slice(0, 24)}`;
}

function assertDryRunPrompt(response: JsonRecord, expectedText: string): void {
  assertCondition(response.ok === true, "CLI dry-run should succeed", response);
  const data = nestedRecord(response.data, []);
  assertCondition(data.dryRun === true, "dry-run response should expose dryRun=true", data);
  const request = nestedRecord(response.data, ["request"]);
  assertCondition(request.method === "POST", "dry-run should expose request method", request);
  assertCondition(request.path === "/api/tasks/codex_test_task/resume", "dry-run should expose resume path", request);
  assertCondition(request.stableProxyPath === "/api/microservices/code-queue/proxy/api/tasks/codex_test_task/resume", "dry-run should expose stable proxy path", request);
  assertCondition(request.resumeId === deterministicResumeId("codex_test_task", expectedText), "dry-run should expose deterministic resumeId", request);
  const prompt = nestedRecord(response.data, ["request", "body", "prompt"]);
  assertCondition(prompt.text === expectedText, "dry-run prompt text mismatch", prompt);
  assertCondition(prompt.chars === expectedText.length, "dry-run prompt char count mismatch", prompt);
  const commands = nestedRecord(response.data, ["commands"]);
  assertCondition(String(commands.run || "").includes(`--resume-id ${String(request.resumeId)}`), "dry-run should expose same resumeId run command", commands);
}

export function runCodeQueueResumeContract(): JsonRecord {
  const positional = runCli(["codex", "resume", "codex_test_task", "fix the PR conflict", "--dry-run"]);
  assertDryRunPrompt(positional.json ?? {}, "fix the PR conflict");
  assertCondition(String(positional.json?.command || "").includes("<prompt:redacted>"), "outer command should redact positional resume prompt", positional.json ?? {});
  assertCondition(!String(positional.json?.command || "").includes("fix the PR conflict"), "outer command must not echo positional resume prompt", positional.json ?? {});

  const stdin = runCli(["codex", "resume", "codex_test_task", "--prompt-stdin", "--dry-run"], "stdin resume prompt\n");
  assertDryRunPrompt(stdin.json ?? {}, "stdin resume prompt\n");

  const promptFile = join(tmpdir(), `unidesk-code-queue-resume-${process.pid}.txt`);
  writeFileSync(promptFile, "file resume prompt", "utf8");
  try {
    const fromFile = runCli(["codex", "resume", "codex_test_task", "--prompt-file", promptFile, "--dry-run"]);
    assertDryRunPrompt(fromFile.json ?? {}, "file resume prompt");
  } finally {
    unlinkSync(promptFile);
  }

  const duplicateSource = runCli(["codex", "resume", "codex_test_task", "positional", "--prompt-stdin", "--dry-run"], "stdin\n");
  assertCondition(duplicateSource.status !== 0, "duplicate prompt source should fail", duplicateSource.json ?? { stdout: duplicateSource.stdout });
  assertCondition(String(nestedRecord(duplicateSource.json, ["error"]).message || "").includes("exactly one prompt source"), "duplicate prompt source error should be explicit", duplicateSource.json ?? {});

  const help = runCli(["codex", "help"]);
  const usage = Array.isArray(nestedRecord(help.json?.data, []).usage) ? nestedRecord(help.json?.data, []).usage as unknown[] : [];
  assertCondition(usage.some((line) => String(line).includes("codex resume <taskId>")), "codex help should list resume", { usage: usage.map(String) });

  let dryRunFetchCount = 0;
  const dryRunDirect = codexResumeTaskForTest("direct_task", ["do not send", "--dry-run"], () => {
    dryRunFetchCount += 1;
    return { ok: true, status: 200, body: { ok: true } };
  });
  assertCondition(dryRunFetchCount === 0, "dry-run must not call stable proxy helper", { dryRunFetchCount, dryRunDirect });

  const longPrompt = `${"x".repeat(480)}-tail-secret-marker`;
  const longDryRun = codexResumeTaskForTest("direct_task", [longPrompt, "--dry-run"], () => {
    throw new Error("dry-run should not fetch");
  }) as JsonRecord;
  const longPreview = nestedRecord(longDryRun, ["request", "body", "prompt"]);
  assertCondition(longPreview.truncated === true, "long dry-run prompt should be truncated", longPreview);
  assertCondition(!String(longPreview.text || "").includes("tail-secret-marker"), "long dry-run must not leak prompt tail", longPreview);

  let fetchPath = "";
  let fetchMethod = "";
  let fetchPrompt = "";
  let fetchResumeId = "";
  const success = codexResumeTaskForTest("direct_task", ["resume this context"], (path, init) => {
    fetchPath = path;
    fetchMethod = String(init?.method || "");
    fetchPrompt = String((init?.body as JsonRecord | undefined)?.prompt || "");
    fetchResumeId = String((init?.body as JsonRecord | undefined)?.resumeId || "");
    return {
      ok: true,
      status: 202,
      body: {
        ok: true,
        accepted: true,
        duplicateSuppressed: false,
        deliveryState: "queued_for_existing_thread",
        resumeId: fetchResumeId,
        turnId: 9,
        reuseOriginalThread: true,
        originalCodexThreadId: "thread_original",
        codexThreadId: "thread_original",
        traceConfirmation: {
          taskId: "direct_task",
          resumeId: fetchResumeId,
          found: true,
          accepted: true,
          deliveryState: "queued_for_existing_thread",
          matchCount: 1,
          trace: { seq: 9, at: "2026-05-23T00:00:09.000Z", method: "turn/resume", resumeId: fetchResumeId, promptChars: 19, promptHash: "hash", promptOmitted: true, source: "output" },
          duplicateSuppressionKey: fetchResumeId,
          promptOmitted: true,
        },
        task: { id: "direct_task", status: "queued", codexThreadId: "thread_original", currentAttempt: 1, currentMode: "initial", prompt: "hidden" },
        queue: { activeTaskIds: [], queuedTaskIds: ["direct_task"] },
      },
    };
  }) as JsonRecord;
  assertCondition(fetchPath === "/api/microservices/code-queue/proxy/api/tasks/direct_task/resume", "non-dry-run should use stable resume path", { fetchPath });
  assertCondition(fetchMethod === "POST", "non-dry-run should POST", { fetchMethod });
  assertCondition(fetchPrompt === "resume this context", "non-dry-run should send raw prompt in body", { fetchPrompt });
  assertCondition(fetchResumeId === deterministicResumeId("direct_task", "resume this context"), "non-dry-run should send deterministic resumeId", { fetchResumeId });
  assertCondition(nestedRecord(success, ["resume"]).accepted === true, "successful resume should report accepted=true", success);
  assertCondition(nestedRecord(success, ["resume"]).reusedCodexThread === true, "successful resume should report thread reuse", success);
  assertCondition(nestedRecord(success, ["resume"]).promptOmitted === true, "successful resume should mark prompt omitted", success);
  assertCondition(nestedRecord(success, ["resume"]).deliveryState === "queued_for_existing_thread", "successful resume should expose delivery state", success);
  assertCondition(!JSON.stringify(success).includes("resume this context"), "successful resume must not echo prompt text", success);

  const explicitResumeId = "resume_manual_12345";
  const duplicateSuppressed = codexResumeTaskForTest("direct_task", ["same prompt", "--resume-id", explicitResumeId], (_path, init) => {
    assertCondition((init?.body as JsonRecord | undefined)?.resumeId === explicitResumeId, "explicit resumeId should be sent unchanged", (init?.body as JsonRecord | undefined) ?? {});
    return {
      ok: true,
      status: 200,
      body: {
        ok: true,
        accepted: true,
        duplicateSuppressed: true,
        deliveryState: "duplicate_suppressed",
        resumeId: explicitResumeId,
        reuseOriginalThread: true,
        originalCodexThreadId: "thread_original",
        codexThreadId: "thread_original",
        traceConfirmation: {
          taskId: "direct_task",
          resumeId: explicitResumeId,
          found: true,
          accepted: true,
          deliveryState: "queued_for_existing_thread",
          matchCount: 1,
          trace: { seq: 11, at: "2026-05-23T00:00:11.000Z", method: "turn/resume", resumeId: explicitResumeId, promptChars: 11, promptHash: "hash2", promptOmitted: true, source: "output" },
          duplicateSuppressionKey: explicitResumeId,
        },
        task: { id: "direct_task", status: "queued", codexThreadId: "thread_original", prompt: "hidden" },
        queue: { queuedTaskIds: ["direct_task"] },
      },
    };
  }) as JsonRecord;
  assertCondition(nestedRecord(duplicateSuppressed, ["resume"]).status === "duplicate_suppressed", "duplicate resume should expose suppression status", duplicateSuppressed);
  assertCondition(nestedRecord(duplicateSuppressed, ["resume"]).duplicateSuppressed === true, "duplicate resume should expose duplicateSuppressed", duplicateSuppressed);

  const conflictPrompt = "changed resume request requested-secret-marker";
  const conflict = codexResumeTaskForTest("direct_task", [conflictPrompt, "--resume-id", explicitResumeId], () => ({
    ok: false,
    status: 409,
    body: {
      ok: false,
      error: "resumeId already exists with a different prompt hash",
      accepted: false,
      deliveryState: "not_accepted",
      resumeId: explicitResumeId,
      existingPromptHash: "old",
      requestedPromptHash: "new",
      traceConfirmation: {
        taskId: "direct_task",
        resumeId: explicitResumeId,
        found: true,
        accepted: true,
        deliveryState: "queued_for_existing_thread",
        matchCount: 1,
        trace: { seq: 11, at: "2026-05-23T00:00:11.000Z", method: "turn/resume", resumeId: explicitResumeId, promptChars: 11, promptHash: "old", promptOmitted: true, source: "output" },
      },
      task: { id: "direct_task", status: "queued", prompt: `${"hidden ".repeat(80)}task-secret-marker` },
    },
  })) as JsonRecord;
  assertCondition(conflict.ok === false, "resumeId conflict should fail", conflict);
  assertCondition(nestedRecord(conflict, ["resume"]).status === "not_accepted", "conflict should expose not_accepted", conflict);
  assertCondition(!JSON.stringify(conflict).includes("requested-secret-marker"), "conflict must not echo requested prompt", conflict);
  assertCondition(!JSON.stringify(conflict).includes("task-secret-marker"), "conflict must not echo full task prompt by default", conflict);

  const runningRejection = codexResumeTaskForTest("running_task", ["use steer instead"], () => ({
    ok: false,
    status: 409,
    body: {
      ok: false,
      error: "task is active: running",
      accepted: false,
      deliveryState: "not_accepted",
      disposition: "use-steer-for-active-task",
      resumeId: deterministicResumeId("running_task", "use steer instead"),
      task: { id: "running_task", status: "running", terminalStatus: null, prompt: "hidden active task" },
    },
  })) as JsonRecord;
  assertCondition(runningRejection.ok === false, "running task resume should fail closed", runningRejection);
  assertCondition(nestedRecord(runningRejection, ["resume"]).reason === "use-steer-for-active-task", "running resume should route to steer", runningRejection);
  assertCondition(String(nestedRecord(runningRejection, ["commands"]).steer || "").includes("codex steer running_task"), "running rejection should provide steer command", runningRejection);

  const notFound = codexResumeTaskForTest("missing_task", ["prompt"], () => ({ ok: false, status: 404, body: { ok: false, error: "task not found" } })) as JsonRecord;
  assertCondition(notFound.ok === false, "missing task resume should fail", notFound);
  assertCondition(nestedRecord(notFound, ["resume"]).status === "not_accepted", "missing task should be not accepted", notFound);

  const task = fixtureTask();
  const resumeId = "resume_contract_12345";
  const prompt = "continue the same PR";
  task.output.push({ seq: 9, at: "2026-05-23T00:00:09.000Z", channel: "user", method: "turn/resume", itemId: resumeId, text: resumeTraceText(resumeId, prompt) });
  const confirmation = findResumeTraceConfirmation(task, resumeId);
  assertCondition(confirmation.found === true && confirmation.accepted === true, "confirmation should find resume trace by resumeId", confirmation as unknown as JsonRecord);
  assertCondition(confirmation.trace?.promptChars === prompt.length, "confirmation should expose prompt chars without prompt text", (confirmation.trace ?? {}) as unknown as JsonRecord);
  assertCondition(!JSON.stringify(confirmation).includes(prompt), "confirmation must not echo prompt text", confirmation as unknown as JsonRecord);
  const duplicate = resumeDuplicateDecision(task, resumeId, prompt);
  assertCondition(duplicate.duplicate === true && duplicate.conflict === false, "same resumeId and prompt should be duplicate-suppressed", duplicate as unknown as JsonRecord);
  const localConflict = resumeDuplicateDecision(task, resumeId, "changed prompt");
  assertCondition(localConflict.duplicate === false && localConflict.conflict === true, "same resumeId with changed prompt should conflict", localConflict as unknown as JsonRecord);
  const newlineResumeId = "resume_contract_newline";
  const newlinePrompt = "keep trailing newline\n";
  task.output.push({ seq: 10, at: "2026-05-23T00:00:10.000Z", channel: "user", method: "turn/resume", itemId: newlineResumeId, text: resumeTraceText(newlineResumeId, newlinePrompt) });
  const newlineDuplicate = resumeDuplicateDecision(task, newlineResumeId, newlinePrompt);
  assertCondition(newlineDuplicate.duplicate === true && newlineDuplicate.conflict === false, "duplicate suppression should use exact prompt hash, including trailing newline", newlineDuplicate as unknown as JsonRecord);

  return {
    ok: true,
    checks: [
      "resume positional/stdin/file dry-runs",
      "bounded disclosure and outer command redaction",
      "non-dry-run sends resumeId and omits prompt from output",
      "terminal resume accepted with thread reuse metadata",
      "duplicate suppression and conflict behavior",
      "running task fails closed with steer command",
      "missing task fails closed",
      "local resume trace confirmation helpers",
      "exact prompt hash survives trailing newline",
    ],
  };
}

if (import.meta.main) {
  process.stdout.write(`${JSON.stringify(runCodeQueueResumeContract(), null, 2)}\n`);
}
