import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";

type JsonRecord = Record<string, unknown>;

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

function asRecord(value: unknown, label: string): JsonRecord {
  assertCondition(typeof value === "object" && value !== null && !Array.isArray(value), `${label} must be an object`, value);
  return value as JsonRecord;
}

function asStringArray(value: unknown, label: string): string[] {
  assertCondition(Array.isArray(value) && value.every((item) => typeof item === "string"), `${label} must be a string array`, value);
  return value as string[];
}

function runCli(args: string[], expectStatus: number): JsonRecord {
  const result = spawnSync("bun", ["scripts/cli.ts", ...args], {
    cwd: process.cwd(),
    encoding: "utf8",
    maxBuffer: 4 * 1024 * 1024,
  });
  assertCondition(result.status === expectStatus, `status mismatch for ${args.join(" ")}`, {
    status: result.status,
    stdout: result.stdout.slice(-2000),
    stderr: result.stderr.slice(-2000),
  });
  return asRecord(JSON.parse(result.stdout) as unknown, "cli envelope");
}

function dataOf(envelope: JsonRecord): JsonRecord {
  return asRecord(envelope.data, "data");
}

const contract = dataOf(runCli(["commander", "contract"], 0));
assertCondition(contract.phase === "source-contract", "contract must identify source-contract phase", contract);
assertCondition(contract.serviceId === "host-codex-commander", "contract must expose service id", contract);
assertCondition(contract.daemonImplemented === false, "daemon must not be implemented in phase one", contract);
assertCondition(contract.liveOperationsImplemented === false, "live operations must not be implemented in phase one", contract);
const capabilities = asStringArray(contract.requiredCapabilities, "requiredCapabilities");
for (const expected of [
  "host-codex-process-discovery",
  "ssh-bridge-contract",
  "pty-bridge-contract",
  "stdio-bridge-contract",
  "prompt-guidance-plan",
  "trace-summary-plan",
  "issue-20-board-read-write-entry",
  "issue-46-brief-read-write-entry",
  "pr-closeout-boundary-plan",
  "claudeqq-high-risk-approval-entry",
]) {
  assertCondition(capabilities.includes(expected), `missing required capability ${expected}`, capabilities);
}

const safety = asRecord(contract.safetyBoundary, "safetyBoundary");
assertCondition(safety.phaseOneMutationAllowed === false, "phase one must forbid mutation", safety);
const forbidden = asStringArray(safety.forbiddenWithoutExplicitUserApproval, "forbiddenWithoutExplicitUserApproval");
assertCondition(forbidden.includes("code-queue-backend-restart"), "backend restart must require approval", forbidden);
assertCondition(forbidden.includes("code-queue-task-interrupt"), "task interrupt must require approval", forbidden);
assertCondition(forbidden.includes("code-queue-task-cancel"), "task cancel must require approval", forbidden);
const alwaysForbidden = asStringArray(safety.alwaysForbidden, "alwaysForbidden");
assertCondition(alwaysForbidden.includes("print-token-values"), "contract must forbid token output", alwaysForbidden);

const plan = dataOf(runCli(["commander", "plan", "--dry-run", "--session-id", "primary"], 0));
assertCondition(plan.mutation === false, "plan must be non-mutating", plan);
assertCondition(asRecord(asRecord(plan.processDiscovery, "processDiscovery").startPlan, "startPlan").enabled === false, "start plan must be disabled", plan);
assertCondition(asRecord(plan.bridge, "bridge").mutation === false, "bridge plan must not open bridges", plan);
assertCondition(asRecord(plan.traceSummary, "traceSummary").mutation === false, "trace summary plan must be non-mutating", plan);
assertCondition(asRecord(plan.issueEntries, "issueEntries").mutation === false, "issue entry plan must be non-mutating", plan);
const prCloseout = asRecord(plan.prCloseout, "prCloseout");
assertCondition(prCloseout.mutation === false, "PR closeout plan must be non-mutating", prCloseout);
assertCondition(asRecord(prCloseout.runnerBoundary, "runnerBoundary").maySelfCloseOrMergeOrdinaryPrWithinTaskBoundary === true, "ordinary PR runner self-close/merge boundary must be explicit", prCloseout);
assertCondition(asRecord(prCloseout.unideskCliBoundary, "unideskCliBoundary").mergeSupported === true, "UniDesk REST gh pr merge must be guarded and supported", prCloseout);
assertCondition(asRecord(plan.claudeqqApproval, "claudeqqApproval").mutation === false, "approval plan must be non-mutating", plan);

const planWithoutDryRun = dataOf(runCli(["commander", "plan"], 1));
assertCondition(planWithoutDryRun.error === "dry-run-required", "plan must require dry-run", planWithoutDryRun);

const approval = dataOf(runCli([
  "commander",
  "approval",
  "request",
  "--action",
  "code-queue-task-interrupt",
  "--task-id",
  "task-123",
  "--reason",
  "heartbeat expired",
  "--dry-run",
], 0));
assertCondition(approval.mutation === false, "approval request must be non-mutating", approval);
assertCondition(approval.requiresExplicitUserApproval === true, "approval request must require explicit user approval", approval);
const claudeqq = asRecord(approval.claudeqq, "claudeqq");
assertCondition(claudeqq.mutation === false, "ClaudeQQ preview must not send", claudeqq);
assertCondition(claudeqq.sendImplemented === false, "ClaudeQQ send must not be implemented", claudeqq);

const invalidApproval = dataOf(runCli(["commander", "approval", "request", "--action", "read-token-file", "--dry-run"], 1));
assertCondition(invalidApproval.error === "validation-failed", "unsupported approval action must fail validation", invalidApproval);

const secretReasonResult = spawnSync("bun", [
  "scripts/cli.ts",
  "commander",
  "approval",
  "request",
  "--action",
  "code-queue-backend-restart",
  "--reason",
  "token=ghp_1234567890abcdef",
  "--dry-run",
], {
  cwd: process.cwd(),
  encoding: "utf8",
  maxBuffer: 4 * 1024 * 1024,
});
assertCondition(secretReasonResult.status === 0, "secret-like approval reason command should still return successfully", {
  stdout: secretReasonResult.stdout,
  stderr: secretReasonResult.stderr,
});
assertCondition(!secretReasonResult.stdout.includes("ghp_1234567890abcdef"), "secret-like approval reason must be redacted from stdout", {
  stdout: secretReasonResult.stdout,
});
assertCondition(secretReasonResult.stdout.includes("<redacted>"), "redacted approval reason should disclose redaction marker", {
  stdout: secretReasonResult.stdout,
});

const doc = readFileSync("docs/reference/host-codex-commander.md", "utf8");
for (const snippet of [
  "本地 skeleton 阶段",
  "/health",
  "/api/commander/contract",
  ".state/commander/",
  "trace summary dry-run",
  "approval draft preview",
  "sendImplemented=false",
]) {
  assertCondition(doc.includes(snippet), `reference doc missing snippet: ${snippet}`);
}

process.stdout.write(`${JSON.stringify({
  ok: true,
  checks: [
    "commander contract exposes host Codex service boundary and phase-one no-live-operation flags",
    "dry-run plan covers process discovery, SSH/PTY/stdio bridge, prompt guidance, trace summary, #20/#46 and ClaudeQQ approval",
    "non-dry-run plan is rejected",
    "approval request is dry-run only and rejects unsupported high-risk actions",
    "secret-like approval reasons are redacted from stdout",
    "reference doc states backend restart, task interrupt/cancel, and token-output prohibitions",
  ],
}, null, 2)}\n`);
