import { spawnSync } from "node:child_process";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { codexPromptLiveAuthorizationLintForTest } from "./src/code-queue";

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): JsonRecord {
  assertCondition(typeof value === "object" && value !== null && !Array.isArray(value), "expected JSON object", { value });
  return value as JsonRecord;
}

function nestedRecord(value: unknown, path: string[]): JsonRecord {
  let current: unknown = value;
  for (const key of path) {
    current = asRecord(current)[key];
  }
  return asRecord(current);
}

function stringArray(value: unknown): string[] {
  return Array.isArray(value) ? value.map((item) => String(item)) : [];
}

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,
  };
}

const readOnlyPrompt = `
DEV test class: read-only
允许的 live mutation: none
禁止动作: prod mutation, secret values, database patch, Code Queue backend restart, interrupt/cancel.
closeout fields: report class, live mutation false, commands, target environment, evidence, residual risk.

Task: update docs and run contract tests only.
Validation: git diff --check and bun scripts/code-queue-prompt-lint-contract-test.ts.
`;

const unclassifiedM3SmokePrompt = `
Run the HWLAB M3 smoke on DEV and verify res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1.
Do not touch production.
`;

const liveReadPrompt = `
DEV test class: live-read
允许的 live mutation: none
禁止动作: no POST/PUT/PATCH/DELETE, no deploy, no task trigger, no prod mutation.
closeout fields: report class, live mutation false, endpoints read, namespace/service, evidence, residual risk.

Read live DEV health and status only: GET /health and kubectl get pods.
`;

const liveMutatingPrompt = `
DEV test class: live-mutating
允许的 live mutation: run exactly one DEV HWLAB M3 operation for res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1, record operation/audit/evidence ids, then observe recovery.
禁止动作: no prod mutation, no secret values, no database manual patch, no Code Queue backend restart, no unrelated interrupt/cancel.
closeout fields: report class, live mutation true, command summary, DEV target, operation/audit/evidence ids, rollback observation, residual risk.
`;

const secretBearingPrompt = `
DEV test class: live-mutating
允许的 live mutation: POST /api/run?token=ds-secret-must-not-print once in DEV only.
禁止动作: no prod mutation, no database patch.
closeout fields: report class, live mutation true, command summary and evidence id.
`;

function assertLintShape(lint: JsonRecord): void {
  assertCondition(lint.dryRun === true, "lint must be dry-run", lint);
  assertCondition(lint.mutation === false, "lint must be non-mutating", lint);
  assertCondition(asRecord(lint.policy).printsPromptText === false, "lint policy must not print full prompt", lint);
  assertCondition(asRecord(lint.promptShape).textEchoed === false, "lint shape must not echo prompt text", lint);
  assertCondition(Array.isArray(lint.signals), "lint must expose signals", lint);
  const json = JSON.stringify(lint);
  assertCondition(!json.includes("ds-secret-must-not-print"), "lint must not print secret marker", lint);
}

export function runCodeQueuePromptLintContract(): JsonRecord {
  const readOnly = asRecord(codexPromptLiveAuthorizationLintForTest(readOnlyPrompt));
  assertLintShape(readOnly);
  assertCondition(readOnly.ok === true, "well-formed read-only prompt should pass", readOnly);
  assertCondition(readOnly.declaredClass === "read-only", "read-only prompt should declare read-only", readOnly);
  assertCondition(readOnly.effectiveClass === "read-only", "read-only effective class mismatch", readOnly);
  assertCondition(readOnly.requiredClass === "read-only", "read-only required class mismatch", readOnly);
  assertCondition(readOnly.dispatchDisposition === "ready", "read-only prompt should be dispatch-ready", readOnly);

  const liveRead = asRecord(codexPromptLiveAuthorizationLintForTest(liveReadPrompt));
  assertLintShape(liveRead);
  assertCondition(liveRead.ok === true, "well-formed live-read prompt should pass", liveRead);
  assertCondition(liveRead.declaredClass === "live-read", "live-read prompt should declare live-read", liveRead);
  assertCondition(liveRead.requiredClass === "live-read", "live-read required class mismatch", liveRead);
  assertCondition(liveRead.dispatchDisposition === "ready", "live-read prompt should be dispatch-ready", liveRead);

  const unclassifiedM3 = asRecord(codexPromptLiveAuthorizationLintForTest(unclassifiedM3SmokePrompt));
  assertLintShape(unclassifiedM3);
  assertCondition(unclassifiedM3.ok === false, "unclassified M3 smoke should fail lint", unclassifiedM3);
  assertCondition(unclassifiedM3.declaredClass === null, "unclassified prompt should have no declared class", unclassifiedM3);
  assertCondition(unclassifiedM3.effectiveClass === "read-only", "unclassified prompt should default to read-only", unclassifiedM3);
  assertCondition(unclassifiedM3.requiredClass === "live-mutating", "M3 smoke should require live-mutating", unclassifiedM3);
  assertCondition(unclassifiedM3.dispatchDisposition === "needs-authorization", "unclassified live mutation should need authorization", unclassifiedM3);
  assertCondition(stringArray(unclassifiedM3.missingOrContradictory).some((item) => item.includes("missing DEV test class")), "unclassified prompt should report missing class", unclassifiedM3);

  const liveMutating = asRecord(codexPromptLiveAuthorizationLintForTest(liveMutatingPrompt));
  assertLintShape(liveMutating);
  assertCondition(liveMutating.ok === true, "well-formed live-mutating prompt should pass", liveMutating);
  assertCondition(liveMutating.declaredClass === "live-mutating", "live-mutating prompt should declare live-mutating", liveMutating);
  assertCondition(liveMutating.requiredClass === "live-mutating", "live-mutating required class mismatch", liveMutating);
  assertCondition(liveMutating.liveMutationAuthorized === true, "live-mutating prompt should be authorized when allowed mutation is enumerated", liveMutating);

  const secretBearing = asRecord(codexPromptLiveAuthorizationLintForTest(secretBearingPrompt));
  assertLintShape(secretBearing);
  assertCondition(secretBearing.requiredClass === "live-mutating", "secret-bearing live mutation should still classify", secretBearing);
  assertCondition(!JSON.stringify(secretBearing).includes("ds-secret-must-not-print"), "prompt lint evidence must redact secret-looking values", secretBearing);

  const tmp = mkdtempSync(join(tmpdir(), "unidesk-code-queue-prompt-lint-"));
  const promptFile = join(tmp, "prompt.md");
  writeFileSync(promptFile, liveMutatingPrompt, "utf8");
  try {
    const cliLint = runCli(["codex", "prompt-lint", "--prompt-file", promptFile]);
    assertCondition(cliLint.status === 0 && cliLint.json?.ok === true, "prompt-lint CLI should succeed for authorized live-mutating prompt", cliLint.json ?? { stdout: cliLint.stdout });
    const lintData = nestedRecord(cliLint.json?.data, []);
    assertCondition(lintData.dryRun === true && lintData.mutation === false, "prompt-lint CLI should be dry-run and non-mutating", lintData);
    assertCondition(lintData.declaredClass === "live-mutating", "prompt-lint CLI should classify live-mutating", lintData);
    assertCondition(!JSON.stringify(lintData).includes("run exactly one DEV HWLAB M3 operation"), "prompt-lint CLI should not echo full prompt text", lintData);
  } finally {
    rmSync(tmp, { recursive: true, force: true });
  }

  const submitDryRun = runCli(["codex", "submit", "--prompt-stdin", "--dry-run"], unclassifiedM3SmokePrompt);
  assertCondition(submitDryRun.status === 0 && submitDryRun.json?.ok === true, "submit dry-run should still succeed for commander review", submitDryRun.json ?? { stdout: submitDryRun.stdout });
  const submitPromptLint = nestedRecord(submitDryRun.json?.data, ["promptLint"]);
  assertCondition(submitPromptLint.dispatchDisposition === "needs-authorization", "submit dry-run should embed prompt lint authorization blocker", submitPromptLint);
  assertCondition(submitPromptLint.requiredClass === "live-mutating", "submit dry-run lint should require live-mutating", submitPromptLint);

  const steerDryRun = runCli(["codex", "steer", "codex_test_task", "--prompt-stdin", "--dry-run"], unclassifiedM3SmokePrompt);
  assertCondition(steerDryRun.status === 0 && steerDryRun.json?.ok === true, "steer dry-run should succeed for commander review", steerDryRun.json ?? { stdout: steerDryRun.stdout });
  const steerPromptLint = nestedRecord(steerDryRun.json?.data, ["promptLint"]);
  assertCondition(steerPromptLint.dispatchDisposition === "needs-authorization", "steer dry-run should embed prompt lint authorization blocker", steerPromptLint);

  const help = runCli(["codex", "help"]);
  assertCondition(help.status === 0 && help.json?.ok === true, "codex help should succeed", help.json ?? { stdout: help.stdout });
  const helpData = nestedRecord(help.json?.data, []);
  const usage = stringArray(helpData.usage);
  assertCondition(usage.some((line) => line.includes("codex prompt-lint")), "codex help should list prompt-lint", helpData);
  const authorizationHelp = nestedRecord(helpData, ["promptLiveAuthorization"]);
  assertCondition(stringArray(authorizationHelp.classes).includes("live-mutating"), "help should document live-mutating class", authorizationHelp);
  assertCondition(authorizationHelp.defaultWhenMissing === "read-only", "help should document read-only default", authorizationHelp);

  return {
    ok: true,
    checks: [
      "prompt-lint classifies read-only/live-read/live-mutating prompts",
      "unclassified HWLAB M3 smoke defaults read-only but requires live-mutating authorization",
      "prompt-lint evidence redacts secret-looking values",
      "prompt-lint CLI is dry-run, non-mutating, and does not echo full prompt text",
      "submit --dry-run embeds prompt live-authorization lint",
      "steer --dry-run embeds prompt live-authorization lint",
      "codex help documents prompt-lint and authorization classes",
    ],
  };
}

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