import { spawnSync } from "node:child_process";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { lintCommanderPrompt } from "./src/commander-prompt-lint";

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 string array`, value);
  return value as string[];
}

function runCli(args: string[], stdin?: string): { status: number | null; stdout: string; stderr: string; envelope: JsonRecord } {
  const result = spawnSync("bun", ["scripts/cli.ts", ...args], {
    cwd: process.cwd(),
    input: stdin,
    encoding: "utf8",
    maxBuffer: 4 * 1024 * 1024,
  });
  assertCondition(String(result.stdout || "").trim().length > 0, `command produced no stdout: ${args.join(" ")}`, {
    status: result.status,
    stderr: String(result.stderr || ""),
  });
  return {
    status: result.status,
    stdout: String(result.stdout || ""),
    stderr: String(result.stderr || ""),
    envelope: asRecord(JSON.parse(String(result.stdout || "")) as unknown, "cli envelope"),
  };
}

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

const completePrompt = `
UniDesk#20 #118 / commander prompt boundary lint

You are a D601 Code Queue GPT-5.5 runner. Work in UniDesk only.

PR and Git authorization:
- You may create and update a head branch and PR, rebase/update from origin/master, resolve conflicts, and self-merge/close the ordinary PR when checks pass and the task boundary is satisfied.

Artifact authorization:
- You may use repo-owned CI/CD, publish, or equivalent controlled build paths to build/publish DEV images or artifacts, and must report commit, image tag, digest, artifact report, and validation evidence.

Rollout boundary:
- DEV deploy apply, rollout, and live health verification are owned by the host commander unless this prompt explicitly contains ROLLOUT_OK.
- Without explicit ROLLOUT_OK, do not acquire the DEV CD lock, run deploy apply, run rollout restart, or compete with host commander live verification.

Forbidden:
- No PROD mutation, no reading or printing secrets/tokens/credentials, no manual database/DB writes, and no destructive rollback.
`;

const incompletePromptWithSecret = `
UniDesk#20 #118
Task: implement the lint. token=ghp_prompt_lint_contract_secret
Please make code changes and tests.
`;

const lint = lintCommanderPrompt(completePrompt);
assertCondition(lint.ok === true, "complete GPT-5.5 PR prompt should pass lint", lint);
assertCondition(lint.missingClauses.length === 0, "complete prompt should have no missing clauses", lint);
assertCondition(lint.suggestedPatchSnippet === "", "passing prompt should not include patch snippet", lint);
assertCondition(lint.policy.advisoryOnly === true, "lint must be advisory only", lint);
assertCondition(lint.policy.changesCodexSubmitDefault === false, "lint must not change codex submit default", lint);
assertCondition(JSON.stringify(lint).includes("promptShape"), "lint should expose prompt shape metadata", lint);
assertCondition(!JSON.stringify(lint).includes("self-merge/close the ordinary PR"), "direct lint result must not echo full prompt", lint);

const failingLint = lintCommanderPrompt(incompletePromptWithSecret);
assertCondition(failingLint.ok === false, "incomplete prompt should fail lint", failingLint);
assertCondition(failingLint.riskLevel === "high", "incomplete prompt should be high risk", failingLint);
for (const expected of [
  "pr-self-merge-rebase-authorization",
  "artifact-build-publish-authorization",
  "host-owned-dev-rollout",
  "runner-rollout-forbidden-without-rollout-ok",
  "prod-secret-db-rollback-boundary",
]) {
  assertCondition(failingLint.missingClauses.includes(expected), `missing expected clause id ${expected}`, failingLint);
}
assertCondition(failingLint.suggestedPatchSnippet.includes("ROLLOUT_OK"), "snippet should mention ROLLOUT_OK", failingLint);
assertCondition(!JSON.stringify(failingLint).includes("ghp_prompt_lint_contract_secret"), "lint output must not echo secret-like prompt text", failingLint);

const tmp = mkdtempSync(join(tmpdir(), "host-codex-commander-prompt-lint-"));
try {
  const promptFile = join(tmp, "prompt.md");
  writeFileSync(promptFile, incompletePromptWithSecret, "utf8");
  const fileRun = runCli(["commander", "prompt-lint", "--kind", "gpt55-pr", "--prompt-file", promptFile]);
  assertCondition(fileRun.status === 0, "commander prompt-lint is advisory and should exit 0 even when lint ok=false", fileRun);
  assertCondition(fileRun.envelope.ok === true, "CLI envelope should remain ok for advisory lint result", fileRun.envelope);
  const fileData = dataOf(fileRun.envelope);
  assertCondition(fileData.ok === false, "lint data ok should reflect missing clauses", fileData);
  assertCondition(asStringArray(fileData.missingClauses, "missingClauses").includes("host-owned-dev-rollout"), "file lint should report rollout clause", fileData);
  assertCondition(String(fileData.suggestedPatchSnippet || "").includes("DEV deploy apply"), "file lint should include bounded patch snippet", fileData);
  assertCondition(!fileRun.stdout.includes("ghp_prompt_lint_contract_secret"), "file lint stdout must not echo prompt secret", fileRun.stdout);

  const stdinRun = runCli(["commander", "prompt-lint", "--kind", "gpt55-pr", "--stdin"], completePrompt);
  assertCondition(stdinRun.status === 0 && stdinRun.envelope.ok === true, "stdin lint should exit successfully", stdinRun.envelope);
  const stdinData = dataOf(stdinRun.envelope);
  assertCondition(stdinData.ok === true, "stdin lint data should pass for complete prompt", stdinData);
  assertCondition(asStringArray(stdinData.missingClauses, "missingClauses").length === 0, "stdin lint should have no missing clauses", stdinData);
  assertCondition(!stdinRun.stdout.includes("Artifact authorization:"), "stdin lint must not echo full prompt", stdinRun.stdout);
} finally {
  rmSync(tmp, { recursive: true, force: true });
}

const submitDryRun = runCli(["codex", "submit", "--prompt-stdin", "--queue", "prompt-lint-contract", "--dry-run"], incompletePromptWithSecret);
assertCondition(submitDryRun.status === 0 && submitDryRun.envelope.ok === true, "codex submit --dry-run should not be gated by commander prompt-lint", submitDryRun.envelope);
const submitData = dataOf(submitDryRun.envelope);
assertCondition(asRecord(submitData.request, "submit request").prompt !== undefined, "submit dry-run should keep its existing prompt review behavior", submitData);

const helpRun = runCli(["commander", "--help"]);
assertCondition(helpRun.status === 0 && helpRun.envelope.ok === true, "commander help should succeed", helpRun.envelope);
const helpData = dataOf(helpRun.envelope);
assertCondition(asStringArray(helpData.usage, "help usage").some((line) => line.includes("commander prompt-lint")), "commander help should list prompt-lint", helpData);
assertCondition(asRecord(helpData.promptLint, "promptLint").gate === "advisory-only; not a business PR gate and not a Code Queue submit admission change", "help should document advisory-only gate", helpData);

const doc = readFileSync("docs/reference/host-codex-commander.md", "utf8");
for (const snippet of [
  "commander prompt-lint --kind gpt55-pr",
  "missingClauses",
  "suggestedPatchSnippet",
  "不是业务 PR 门禁",
]) {
  assertCondition(doc.includes(snippet), `reference doc missing snippet: ${snippet}`);
}

process.stdout.write(`${JSON.stringify({
  ok: true,
  checks: [
    "complete GPT-5.5 PR prompt passes commander prompt-lint",
    "missing PR/artifact/DEV rollout/ROLLOUT_OK/PROD-secret-DB-rollback clauses are reported as high risk",
    "prompt-lint supports --prompt-file and --stdin",
    "prompt-lint output does not echo full prompt or secret-like prompt text",
    "commander prompt-lint remains advisory and does not gate codex submit --dry-run",
    "commander help and host commander reference document the advisory lint entry",
  ],
}, null, 2)}\n`);
