import { spawnSync } from "node:child_process";
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { createCommanderRequestHandler, type RuntimeConfig } from "../src/components/microservices/host-codex-commander/src/index";
import { commanderHealth, summarizeCommanderTrace } from "../src/components/microservices/host-codex-commander/src/state";

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 asRecordArray(value: unknown, label: string): JsonRecord[] {
  assertCondition(Array.isArray(value) && value.every((item) => typeof item === "object" && item !== null && !Array.isArray(item)), `${label} must be object array`, 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 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),
  });
  assertCondition(result.stdout.trim().length > 0, `command produced no stdout: ${args.join(" ")}`);
  return asRecord(JSON.parse(result.stdout) as unknown, "cli envelope");
}

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

async function readJson(response: Response): Promise<JsonRecord> {
  return asRecord(await response.json() as unknown, "response body");
}

const sessionId = `no-daemon-smoke-contract-${process.pid}`;
const liveSessionPath = join(process.cwd(), ".state", "commander", "sessions", `${sessionId}.json`);
assertCondition(!existsSync(liveSessionPath), "precondition failed: smoke session path should not already exist", liveSessionPath);

const smoke = dataOf(runCli(["commander", "smoke", "--dry-run", "--session-id", sessionId], 0));
assertCondition(smoke.ok === true, "smoke must succeed", smoke);
assertCondition(smoke.phase === "source-contract", "smoke must remain source-contract phase", smoke);
assertCondition(smoke.mode === "dry-run", "smoke must report dry-run mode", smoke);
assertCondition(smoke.mutation === false, "smoke must be non-mutating", smoke);

const noDaemon = asRecord(smoke.noDaemonSmokeContract, "noDaemonSmokeContract");
for (const flag of [
  "startsDaemon",
  "startsPtyBridge",
  "startsStdioBridge",
  "opensSshBridge",
  "sendsClaudeqq",
  "restartsServices",
  "interruptsTasks",
  "cancelsTasks",
  "deploys",
  "runsFullCheckOrE2e",
]) {
  assertCondition(noDaemon[flag] === false, `${flag} must be false`, noDaemon);
}
assertCondition(asStringArray(noDaemon.allowedCommands, "allowedCommands").includes("bun scripts/host-codex-commander-no-daemon-smoke-contract-test.ts"), "smoke should name this lightweight contract", noDaemon);

const validationPlan = asRecordArray(smoke.validationPlan, "validationPlan");
const surfaces = validationPlan.map((item) => item.surface);
for (const expected of [
  "health endpoint",
  "state file",
  "trace summary dry-run",
  "approval draft preview",
  "SSH bridge boundary",
]) {
  assertCondition(surfaces.includes(expected), `missing validation surface ${expected}`, surfaces);
}
for (const item of validationPlan) {
  assertCondition(asStringArray(item.expectedEvidence, "expectedEvidence").length > 0, "each validation item must define evidence", item);
  assertCondition(asStringArray(item.noRuntimeSideEffects, "noRuntimeSideEffects").length > 0, "each validation item must define no-side-effect boundary", item);
}

const smokeWithoutDryRun = dataOf(runCli(["commander", "smoke", "--session-id", sessionId], 1));
assertCondition(smokeWithoutDryRun.error === "dry-run-required", "smoke must require --dry-run", smokeWithoutDryRun);
assertCondition(!existsSync(liveSessionPath), "smoke CLI must not write live commander state", liveSessionPath);

const tmp = mkdtempSync(join(tmpdir(), "host-codex-commander-smoke-"));
try {
  const runtime: RuntimeConfig = {
    rootDir: tmp,
    host: "127.0.0.1",
    port: 4261,
    logFile: join(tmp, "logs", "commander.jsonl"),
    serviceId: "host-codex-commander",
    stateRoot: tmp,
    sessionId,
  };
  const health = commanderHealth(runtime, "2026-05-21T00:00:00.000Z");
  assertCondition(health.ok === true && health.service === "host-codex-commander", "health helper must expose service metadata", health);
  assertCondition(health.stateRoot === tmp, "health helper must use temp state root", health);

  const handler = createCommanderRequestHandler(runtime);
  const healthBody = await readJson(await handler(new Request("http://localhost/health")));
  assertCondition(healthBody.ok === true, "short-lived handler health route must succeed without Bun.serve", healthBody);

  const trace = summarizeCommanderTrace({
    taskId: "task-smoke",
    sessionId,
    traceJsonl: [
      JSON.stringify({ seq: 1, kind: "message", status: "running", summary: "checking token=ghp_1234567890abcdef" }),
      JSON.stringify({ seq: 2, kind: "event", status: "attention_required", text: "needs approval" }),
    ].join("\n"),
    taskSummary: "summary password=secret",
  });
  assertCondition(trace.taskId === "task-smoke", "trace summary must preserve task id", trace);
  assertCondition(trace.sessionId === sessionId, "trace summary must preserve session id", trace);
  assertCondition(trace.lastSeq === 2, "trace summary must compute last seq", trace);
  assertCondition(trace.status === "attention_required", "trace summary must derive attention_required status", trace);
  assertCondition(trace.redactionsApplied >= 2, "trace summary must redact mock secrets", trace);
} finally {
  rmSync(tmp, { recursive: true, force: true });
}

const approval = dataOf(runCli([
  "commander",
  "approval",
  "request",
  "--action",
  "code-queue-task-cancel",
  "--reason",
  "token=ghp_1234567890abcdef",
  "--dry-run",
], 0));
const claudeqq = asRecord(approval.claudeqq, "claudeqq");
assertCondition(claudeqq.mutation === false, "approval preview must not mutate ClaudeQQ", claudeqq);
assertCondition(claudeqq.sendImplemented === false, "approval preview must not implement sending", claudeqq);
assertCondition(!JSON.stringify(approval).includes("ghp_1234567890abcdef"), "approval preview must redact secret-like reason", approval);

const doc = readFileSync("docs/reference/host-codex-commander.md", "utf8");
for (const snippet of [
  "commander smoke --dry-run",
  "无 daemon smoke contract",
  "health endpoint",
  "SSH bridge boundary",
]) {
  assertCondition(doc.includes(snippet), `reference doc missing snippet: ${snippet}`);
}

process.stdout.write(`${JSON.stringify({
  ok: true,
  checks: [
    "commander smoke --dry-run is non-mutating and dry-run required",
    "no-daemon smoke contract forbids daemon, SSH/PTY/stdio bridge, ClaudeQQ send, restart, interrupt, cancel, deploy, and full e2e",
    "health endpoint and trace summary are validated through short-lived source-level helpers",
    "approval draft preview remains sendImplemented=false and redacted",
    "reference doc describes the dev validation surfaces and no-daemon boundary",
  ],
}, null, 2)}\n`);
