import { expect, test } from "bun:test";
import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import type { GiteaConfig, GiteaMirrorRepository } from "./platform-infra-gitea-config";
import { renderMirrorBootstrap } from "./platform-infra-gitea-render";
import { pacBootstrapYamlMatchFailure, projectPacBootstrapResult, renderPacBootstrap, resolvePacBootstrapMirrorRepository } from "./platform-infra-pipelines-as-code-bootstrap";
import { compactManualReleasePlan, manualReleaseNext, PacReleaseManifestRenderError, parsePacConfigDocument, readPacConfig, renderPacReleaseManifest, runPlatformInfraPipelinesAsCodeCommand, validatePacConfig, validPacConsumers } from "./platform-infra-pipelines-as-code";
import { materializeYamlComposition } from "./yaml-composition";

const mirror: GiteaMirrorRepository = {
  key: "widgets-nc01",
  targetId: "NC01",
  upstream: { repository: "acme/widgets", cloneUrl: "https://github.com/acme/widgets.git", branch: "main", visibility: "public" },
  gitea: { owner: "mirrors", name: "acme-widgets", mirrorMode: "controlled-push", publicRead: true, readUrl: "http://gitea-http.devops-infra.svc.cluster.local:3000/mirrors/acme-widgets.git" },
  gitops: { branch: "main", flushDisposition: "not-a-gitops-branch" },
  snapshot: { prefix: "refs/unidesk/snapshots/widgets-main-nc01" },
  legacyGitMirror: null,
};

function fixtureConfig(repositories: GiteaMirrorRepository[]): GiteaConfig {
  return { sourceAuthority: { repositories } } as unknown as GiteaConfig;
}

function pacConfigDocument(): Record<string, unknown> {
  const source = Bun.YAML.parse(readFileSync(resolve(import.meta.dir, "../../config/platform-infra/pipelines-as-code.yaml"), "utf8")) as Record<string, unknown>;
  return structuredClone(materializeYamlComposition(source, { label: "config/platform-infra/pipelines-as-code.yaml" }).value);
}

test("PaC bootstrap next requires manual release plan after source merge", () => {
  const source = readFileSync(resolve(import.meta.dir, "platform-infra-pipelines-as-code-bootstrap.ts"), "utf8");
  expect(source).toContain("source-pr-merge-then-plan");
  expect(source).toContain("release plan");
  expect(source).not.toContain("由自动链继续交付");
});

test("PaC bootstrap uses one exact YAML-owned Gitea repository", () => {
  const selected = resolvePacBootstrapMirrorRepository(fixtureConfig([mirror]), "nc01", {
    id: "widgets-nc01",
    owner: "mirrors",
    repo: "acme-widgets",
    cloneUrl: `${mirror.gitea.readUrl}/`,
    sourceBranch: "main",
  });
  expect(selected.key).toBe("widgets-nc01");
  const releaseMirror = { ...mirror, key: "widgets-release-nc01", upstream: { ...mirror.upstream, branch: "release" } };
  expect(resolvePacBootstrapMirrorRepository(fixtureConfig([mirror, releaseMirror]), "NC01", {
    id: "widgets-release-nc01",
    owner: "mirrors",
    repo: "acme-widgets",
    cloneUrl: mirror.gitea.readUrl,
    sourceBranch: "release",
  }).key).toBe("widgets-release-nc01");
  expect(() => resolvePacBootstrapMirrorRepository(fixtureConfig([]), "NC01", {
    id: "widgets-nc01",
    owner: "mirrors",
    repo: "acme-widgets",
    cloneUrl: mirror.gitea.readUrl,
    sourceBranch: "main",
  })).toThrow("no exact match");
  expect(() => resolvePacBootstrapMirrorRepository(fixtureConfig([mirror, { ...mirror }]), "NC01", {
    id: "widgets-nc01",
    owner: "mirrors",
    repo: "acme-widgets",
    cloneUrl: mirror.gitea.readUrl,
    sourceBranch: "main",
  })).toThrow("2 exact matches");
});

test("selfmedia consumers share one PaC Repository while keeping branch parameters isolated", () => {
  const config = Bun.YAML.parse(readFileSync(resolve(import.meta.dir, "../../config/platform-infra/pipelines-as-code.yaml"), "utf8")) as Record<string, unknown>;
  const repositories = config.repositories as Array<Record<string, unknown>>;
  const consumers = config.consumers as Array<Record<string, unknown>>;
  const selfmediaRepositories = repositories.filter((item) => item.url === "https://gitea.hwpod.com/mirrors/pikainc-selfmedia");
  expect(selfmediaRepositories).toHaveLength(1);
  const development = consumers.find((item) => item.id === "selfmedia-nc01");
  const production = consumers.find((item) => item.id === "selfmedia-production-nc01");
  expect(development?.repositoryRef).toBe("selfmedia-nc01");
  expect(production?.repositoryRef).toBe("selfmedia-nc01");
  expect(development?.namespace).toBe("selfmedia-ci");
  expect(production?.namespace).toBe("selfmedia-ci");
  expect((selfmediaRepositories[0]?.params as Record<string, unknown>).source_branch).toBe("master");
  expect((production?.params as Record<string, unknown>).source_branch).toBe("release");
  expect((production?.params as Record<string, unknown>).pipeline_name).toBe("selfmedia-production-nc01-pac");
});

test("HWLAB v0.3 and production share one NC01 PaC Repository while keeping lane parameters isolated", () => {
  const config = pacConfigDocument();
  const repositories = config.repositories as Array<Record<string, unknown>>;
  const consumers = config.consumers as Array<Record<string, unknown>>;
  const nc01Repositories = repositories.filter((item) => item.id === "hwlab-nc01-v03" || item.id === "hwlab-nc01-production");
  expect(nc01Repositories.map((item) => item.id)).toEqual(["hwlab-nc01-v03"]);
  const development = consumers.find((item) => item.id === "hwlab-nc01-v03");
  const production = consumers.find((item) => item.id === "hwlab-nc01-production");
  expect(development?.repositoryRef).toBe("hwlab-nc01-v03");
  expect(production?.repositoryRef).toBe("hwlab-nc01-v03");
  expect((nc01Repositories[0]?.params as Record<string, unknown>).source_branch).toBe("v0.3");
  expect(development?.argoBootstrap).toEqual({
    project: "hwlab-v03",
    repoUrl: "http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/HWLAB.git",
    targetRevision: "v0.3-gitops",
    path: "deploy/gitops/node/nc01/runtime-v03",
    destinationNamespace: "hwlab-v03",
    automated: true,
  });
  expect((production?.params as Record<string, unknown>).source_branch).toBe("release");
  expect((production?.params as Record<string, unknown>).gitops_branch).toBe("release-gitops");
  expect((production?.params as Record<string, unknown>).pipeline_name).toBe("hwlab-nc01-production-ci-image-publish");
  expect(readPacConfig({ consumerId: "hwlab-nc01-production" }).validationWarnings).toEqual([]);
});

test("selected PaC consumer rejects duplicate URL authority conflicts before mutation", () => {
  const conflictMutations: Array<[string, (repository: Record<string, unknown>) => void]> = [
    ["namespace", (repository) => { repository.namespace = "other-ci"; }],
    ["SecretRef", (repository) => { repository.secretName = "pac-gitea-hwlab-conflict"; }],
    ["providerType", (repository) => { repository.providerType = "github"; }],
  ];
  for (const [field, mutate] of conflictMutations) {
    const config = structuredClone(readPacConfig({ consumerId: "hwlab-nc01-production" }));
    const sharedRepository = config.repositories.find((repository) => repository.id === "hwlab-nc01-v03");
    const developmentConsumer = config.consumers.find((consumer) => consumer.id === "hwlab-nc01-v03");
    if (sharedRepository === undefined || developmentConsumer === undefined) throw new Error("HWLAB NC01 fixtures are required");
    const duplicateRepository = structuredClone(sharedRepository) as unknown as Record<string, unknown>;
    duplicateRepository.id = "hwlab-nc01-conflict";
    duplicateRepository.name = "hwlab-nc01-conflict";
    mutate(duplicateRepository);
    config.repositories.push(duplicateRepository as never);
    config.consumers.push({ ...structuredClone(developmentConsumer), id: "hwlab-nc01-conflict", repositoryRef: "hwlab-nc01-conflict" });
    expect(() => validatePacConfig(config, new Set(["hwlab-nc01-production"]))).toThrow(field);
  }
});

test("unselected production validation is a non-blocking warning while selected production fails closed", () => {
  const config = structuredClone(readPacConfig({ consumerId: "selfmedia-nc01" }));
  const development = config.consumers.find((consumer) => consumer.id === "selfmedia-nc01");
  const production = config.consumers.find((consumer) => consumer.id === "selfmedia-production-nc01");
  expect(development?.deliveryProvenance).not.toBeNull();
  expect(production?.deliveryProvenance).not.toBeNull();
  if (development?.deliveryProvenance === null || development?.deliveryProvenance === undefined || production?.deliveryProvenance === null || production?.deliveryProvenance === undefined) {
    throw new Error("selfmedia delivery provenance fixtures are required");
  }
  production.params.gitops_secret_name = "invalid-production-secret";
  production.deliveryProvenance.markerValue = development.deliveryProvenance.markerValue;
  config.consumers = [production, development, ...config.consumers.filter((consumer) => consumer !== production && consumer !== development)];

  const warnings = validatePacConfig(config, new Set(["selfmedia-nc01"]));
  const warning = warnings.find((item) => item.consumer === "selfmedia-production-nc01");
  expect(warning).toEqual({
    code: "pac-unselected-consumer-invalid",
    object: { kind: "consumer", id: "selfmedia-production-nc01" },
    consumer: "selfmedia-production-nc01",
    configPath: "config/platform-infra/pipelines-as-code.yaml#consumers.selfmedia-production-nc01",
    blocking: false,
    evidence: expect.stringMatching(/^type=string,length=\d+,sha256=[0-9a-f]{64}$/u),
  });
  expect(() => validatePacConfig(config, new Set(["selfmedia-production-nc01"]))).toThrow();
});

test("unselected parse and repository errors remain warnings while selected objects fail closed", () => {
  const invalidConsumerDocument = pacConfigDocument();
  const invalidConsumer = (invalidConsumerDocument.consumers as Array<Record<string, unknown>>).find((item) => item.id === "selfmedia-production-nc01");
  if (invalidConsumer === undefined) throw new Error("production consumer fixture is required");
  invalidConsumer.namespace = 42;
  const development = parsePacConfigDocument(invalidConsumerDocument, { consumerId: "selfmedia-nc01" });
  expect(development.consumers.some((consumer) => consumer.id === "selfmedia-production-nc01")).toBe(false);
  expect(development.validationWarnings).toContainEqual({
    code: "pac-unselected-consumer-parse-invalid",
    object: { kind: "consumer", id: "selfmedia-production-nc01" },
    consumer: "selfmedia-production-nc01",
    configPath: expect.stringMatching(/#consumers\[\d+\]$/u),
    blocking: false,
    evidence: expect.stringMatching(/^type=string,length=\d+,sha256=[0-9a-f]{64}$/u),
  });
  expect(() => parsePacConfigDocument(invalidConsumerDocument, { consumerId: "selfmedia-production-nc01" })).toThrow("namespace");

  const invalidRepositoryDocument = pacConfigDocument();
  const invalidRepository = (invalidRepositoryDocument.repositories as Array<Record<string, unknown>>).find((item) => item.id === "pikaoa-dev-nc01");
  if (invalidRepository === undefined) throw new Error("pikaoa repository fixture is required");
  invalidRepository.cloneUrl = "https://gitea.example.invalid/mirrors/pikaoa.git";
  const isolated = parsePacConfigDocument(invalidRepositoryDocument, { consumerId: "selfmedia-nc01" });
  expect(isolated.validationWarnings).toContainEqual({
    code: "pac-unselected-repository-invalid",
    object: { kind: "repository", id: "pikaoa-dev-nc01" },
    consumer: null,
    configPath: "config/platform-infra/pipelines-as-code.yaml#repositories.pikaoa-dev-nc01",
    blocking: false,
    evidence: expect.stringMatching(/^type=string,length=\d+,sha256=[0-9a-f]{64}$/u),
  });
  expect(validPacConsumers(isolated).some((consumer) => consumer.repositoryRef === "pikaoa-dev-nc01")).toBe(false);
  expect(() => parsePacConfigDocument(invalidRepositoryDocument, { consumerId: "pikaoa-nc01" })).toThrow("cloneUrl");

  const duplicateSelectedRepository = pacConfigDocument();
  const repositories = duplicateSelectedRepository.repositories as Array<Record<string, unknown>>;
  const selfmediaRepository = repositories.find((item) => item.id === "selfmedia-nc01");
  if (selfmediaRepository === undefined) throw new Error("selfmedia repository fixture is required");
  repositories.push(structuredClone(selfmediaRepository));
  expect(() => parsePacConfigDocument(duplicateSelectedRepository, { consumerId: "selfmedia-nc01" })).toThrow("must match exactly one YAML object");
});

test("Gitea mirror bootstrap defaults to a visible dry-run instead of an empty refusal", () => {
  const rendered = renderMirrorBootstrap({
    ok: true,
    action: "platform-infra-gitea-mirror-bootstrap",
    mode: "dry-run",
    mutation: false,
    target: { id: "NC01" },
    repositories: [{ key: "widgets-nc01", giteaOwner: "mirrors", giteaRepo: "acme-widgets" }],
    blockers: [],
    next: { confirm: "bun scripts/cli.ts platform-infra gitea mirror bootstrap --target NC01 --repo widgets-nc01 --confirm" },
  }).renderedText;
  expect(rendered).toContain("MODE     MUTATION  TARGET");
  expect(rendered).toContain("widgets-nc01");
  expect(rendered).toContain("--repo widgets-nc01 --confirm");
  expect(rendered).not.toContain("missing-confirm");
});

test("PaC apply checks the Gitea repository before dry-run return and cluster mutation", () => {
  const remote = readFileSync(resolve(import.meta.dir, "platform-infra-pipelines-as-code-remote.sh"), "utf8");
  const source = readFileSync(resolve(import.meta.dir, "platform-infra-pipelines-as-code.ts"), "utf8");
  const action = remote.indexOf("apply_action() {");
  const preflight = remote.indexOf("if ! repository_preflight", action);
  const dryRun = remote.indexOf('if [ "$UNIDESK_PAC_DRY_RUN" = "1" ]', action);
  const release = remote.indexOf("  apply_release", action);
  const admission = remote.indexOf("    apply_admission_manifest", release);
  const rbac = remote.indexOf("    rbac_tmp=$(mktemp)", admission);
  expect(action).toBeGreaterThanOrEqual(0);
  expect(preflight).toBeGreaterThan(action);
  expect(dryRun).toBeGreaterThan(preflight);
  expect(release).toBeGreaterThan(dryRun);
  expect(admission).toBeGreaterThan(release);
  expect(rbac).toBeGreaterThan(admission);
  expect(remote).toContain('"admission":{"applied":%s,"ready":%s}');
  expect(remote).toContain('"admissionProvenance":%s');
  expect(remote).toContain('"error":"gitea-repository-missing"');
  expect(remote).toContain("remove_automatic_webhooks");
  expect(remote).not.toContain("ensure_webhook");
  expect(remote).not.toContain('"active":true');
  const statusAction = remote.slice(remote.indexOf("status_action() {"), remote.indexOf("history_action() {"));
  const bootstrapReadyAssignment = statusAction.split("\n").find((line) => line.includes("bootstrap_ready=$("));
  expect(bootstrapReadyAssignment).toBeDefined();
  expect(remote).toContain("status_progress() {");
  expect(remote).toContain('stepTimeoutSeconds');
  expect(remote).toContain('pac_read kubectl');
  expect(remote).toContain('export UNIDESK_PAC_ARGO_FILE="$argo_file"');
  expect(remote).toContain('get pipelinerun -l "pipelinesascode.tekton.dev/repository=$UNIDESK_PAC_REPOSITORY_NAME"');
  expect(remote).toContain('"error":"pac-pipelinerun-read-failed"');
  expect(remote).not.toContain('get pipelinerun -l "pipelinesascode.tekton.dev/repository=$UNIDESK_PAC_REPOSITORY_NAME" -o json >"$payload_file" 2>/dev/null ||');
  expect(remote).toContain('kubectl auth can-i --list');
  expect(remote).not.toContain('for verb in create patch update delete deletecollection');
  expect(source).toContain("Math.ceil(pac.observability.readOnlyCapture.timeoutMs / 1_000)");
  expect(source).not.toContain("Math.floor(pac.observability.readOnlyCapture.timeoutMs / 8_000)");
  expect(statusAction).toContain("status_progress artifact-evidence");
  expect(statusAction).toContain("status_progress diagnostics");
  expect(statusAction).toContain("status_progress complete");
  expect(statusAction).toContain("['pac-gitops-missing', 'pac-source-observation-inconsistent'].includes(output.code)");
  expect(statusAction).toContain("output.code = 'pac-artifact-log-evidence-deferred'");
  expect(statusAction).toContain("blocking: false");
  expect(statusAction).toContain('[ "$bootstrap_ready" = "true" ]');
  expect(statusAction).toContain("output.code = 'pac-consumer-bootstrap-not-ready'");
  expect(statusAction).toContain("output.ok = false");
  expect(statusAction).toContain("code: 'pac-admission-provenance-not-ready'");
  expect(statusAction).toContain("blocking: false");
  for (const fixture of [
    { state: { configured: false, ready: null }, expected: true },
    { state: { configured: true, ready: true }, expected: true },
    { state: { configured: true, ready: false }, expected: false },
  ]) {
    const smoke = spawnSync("sh", ["-eu", "-c", [
      "consumer_bootstrap=$UNIDESK_PAC_BOOTSTRAP_STATE",
      bootstrapReadyAssignment!.trim(),
      `printf '{"ok":%s}\\n' "$( [ "$bootstrap_ready" = "true" ] && echo true || echo false )"`,
    ].join("\n")], {
      encoding: "utf8",
      env: { ...process.env, UNIDESK_PAC_BOOTSTRAP_STATE: JSON.stringify(fixture.state) },
    });
    expect(smoke).toMatchObject({ status: 0, stderr: "" });
    expect(JSON.parse(smoke.stdout)).toEqual({ ok: fixture.expected });
  }
});

test("PaC automatic webhook disable is target-scoped and does not touch Kubernetes", async () => {
  const plan = await runPlatformInfraPipelinesAsCodeCommand({} as never, ["automatic-webhooks", "disable", "--target", "NC01"]);
  expect(plan).toMatchObject({ ok: true, mutation: false, mode: "plan", target: { id: "NC01" }, plan: { automaticWebhookEnabled: false } });
  expect(Number((plan as any).plan.repositoryCount)).toBeGreaterThan(1);
  const remote = readFileSync(resolve(import.meta.dir, "platform-infra-pipelines-as-code-remote.sh"), "utf8");
  const actionStart = remote.indexOf("disable_automatic_webhooks_action()");
  const action = remote.slice(actionStart, remote.indexOf("manual_release_plan_json()", actionStart));
  expect(action).toContain("gitea_api DELETE");
  expect(action).not.toContain("kubectl");
  expect(action).not.toContain("ensure_webhook");
});

test("PaC top-level help enumerates every exposed manual-release control", async () => {
  const help = await runPlatformInfraPipelinesAsCodeCommand({} as never, ["--help"]);
  expect(String((help as any).command)).toContain("automatic-webhooks");
  expect(JSON.stringify(help)).toContain("automatic-webhooks disable --target <NODE> [--confirm]");
});

test("bounded HWLAB build worker wrapper preserves heredoc terminators", () => {
  const source = readFileSync(resolve(import.meta.dir, "../native/hwlab/runtime-pipeline-resource-governance.mjs"), "utf8");
  expect(source).toContain("workerScript,");
  expect(source).not.toContain("workerScript.split('\\n').map((line) => `  ${line}`).join('\\n')");
  expect(source).toContain('export HWLAB_TEKTON_RESULTS_DIR="/workspace/service-results-tekton/$service_id"');
  expect(source).toContain('process.env.HWLAB_TEKTON_RESULTS_DIR + "/" + name');
  expect(source).toContain('--tekton-results-dir "$HWLAB_TEKTON_RESULTS_DIR"');
  expect(source).toContain("contains an unsupported Tekton result path context");
});

test("L2/L3 release exposes plan first and trigger only sends the PaC webhook", async () => {
  await expect(runPlatformInfraPipelinesAsCodeCommand({} as never, [
    "release", "trigger", "--target", "NC01", "--consumer", "hwlab-nc01-v03",
    "--source-commit", "a".repeat(40),
  ])).rejects.toThrow("release trigger requires --confirm after reviewing release plan");
  await expect(runPlatformInfraPipelinesAsCodeCommand({} as never, [
    "release", "trigger", "--target", "NC01", "--consumer", "hwlab-nc01-v03",
    "--source-commit", "a".repeat(40), "--confirm",
  ])).rejects.toThrow("release trigger requires --plan-identity from the reviewed release plan");
  const remote = readFileSync(resolve(import.meta.dir, "platform-infra-pipelines-as-code-remote.sh"), "utf8");
  const planStart = remote.indexOf("manual_release_plan_json()");
  const actionStart = remote.indexOf("manual_release_action()");
  const planAction = remote.slice(planStart, actionStart);
  const triggerAction = remote.slice(actionStart, remote.indexOf("repository_manifest()", actionStart));
  expect(triggerAction).toContain("manual_release_plan_json");
  expect(triggerAction).toContain("X-Gitea-Event-Type: push");
  expect(planAction).toContain("imageBuildCount");
  expect(planAction).toContain("envReuse");
  expect(planAction).toContain("requestedScope");
  expect(planAction).toContain("service.envChangedPaths");
  expect(planAction).toContain("service.codeChangedPaths");
  expect(planAction).toContain("rangeBaseline");
  expect(planAction).toContain("base_source=catalog-source");
  expect(planAction.indexOf("restore-artifact-catalog.mjs")).toBeLessThan(planAction.indexOf("base_source=catalog-source"));
  expect(planAction).toContain("expandedServices");
  expect(planAction).toContain("buildServices");
  expect(planAction).toContain("rolloutServices");
  expect(planAction).toContain("--verify-reuse-registry");
  expect(planAction).toContain("--release-target");
  expect(planAction).toContain("planIdentity");
  expect(planAction).toContain('gitops_read_url=$(cluster_service_url "$UNIDESK_PAC_MANUAL_GITOPS_READ_URL")');
  expect(planAction).toContain('HWLAB_GIT_READ_URL="$gitops_read_url"');
  expect(planAction).not.toContain('HWLAB_GIT_READ_URL="$UNIDESK_PAC_REPOSITORY_URL"');
  const pac = readPacConfig({ consumerId: "hwlab-nc01-v03", selectDefault: true });
  expect(pac.repositories.find((repository) => repository.id === "hwlab-nc01-v03")?.params.gitops_read_url)
    .toBe("http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/HWLAB.git");
  expect(triggerAction).toContain("release-plan-scope-expanded");
  expect(triggerAction).toContain("release-plan-empty-scope");
  expect(triggerAction).toContain("release-plan-identity-changed");
  expect(triggerAction).toContain("unidesk-release-plan-v1.");
  expect(triggerAction).not.toContain("kubectl create");
  expect(triggerAction).not.toContain("PipelineRun");
  const sourceArtifactRenderer = readFileSync(resolve(import.meta.dir, "platform-infra-pipelines-as-code-source-artifact.ts"), "utf8");
  expect(sourceArtifactRenderer).toContain('if (name === "release-base-commit") return { name, value: "{{ body.before }}" }');
  expect(sourceArtifactRenderer).toContain('if (name === "reviewed-plan") return { name, value: "{{ body.head_commit.message }}" }');
});

test("PaC release scoped help stays on the manual plan-before-trigger path", async () => {
  const root = await runPlatformInfraPipelinesAsCodeCommand({} as never, ["--help"]);
  const release = await runPlatformInfraPipelinesAsCodeCommand({} as never, ["release", "--help"]);
  const plan = await runPlatformInfraPipelinesAsCodeCommand({} as never, ["release", "plan", "--help"]);
  const trigger = await runPlatformInfraPipelinesAsCodeCommand({} as never, ["release", "trigger", "--help"]);
  expect(release).toMatchObject({ command: "platform-infra pipelines-as-code release", mutation: false });
  expect(JSON.stringify(root)).toContain("范围非空");
  expect(JSON.stringify(release)).toContain("scopeReview.status=exact");
  expect(JSON.stringify(release)).toContain("终态 no-op");
  expect(JSON.stringify(release)).toContain("rangeBaseline.catalogAligned=true");
  expect(JSON.stringify(plan)).toContain("release plan --target <NODE>");
  expect(JSON.stringify(plan)).toContain("envReuse");
  expect(JSON.stringify(plan)).toContain("计划非空");
  expect(JSON.stringify(trigger)).toContain("release trigger --target <NODE>");
  expect(JSON.stringify(trigger)).toContain("--confirm");
  expect(JSON.stringify(trigger)).toContain("--plan-identity");
  expect(JSON.stringify(trigger)).toContain("先执行 release plan");
  expect(JSON.stringify(trigger)).toContain("无需 trigger");
});

test("PaC failed or expanded plan never points next to trigger", () => {
  const identity = `sha256:${"d".repeat(64)}`;
  const args = ["plan", "NC01", "hwlab-nc01-v03", "a".repeat(40), "b".repeat(40), identity] as const;
  expect(manualReleaseNext(...args, { planSucceeded: false, scopeExpanded: false, scopeEmpty: false, catalogBaseCommit: null, remoteError: false })).toContain("then rerun");
  const catalogBase = "c".repeat(40);
  const expanded = manualReleaseNext(...args, { planSucceeded: true, scopeExpanded: true, scopeEmpty: false, catalogBaseCommit: catalogBase, remoteError: false });
  expect(expanded).toContain(`--base-commit ${catalogBase}`);
  expect(expanded).not.toContain(`--base-commit ${args[4]}`);
  expect(manualReleaseNext(...args, { planSucceeded: true, scopeExpanded: false, scopeEmpty: false, catalogBaseCommit: null, remoteError: false })).toContain("release trigger");
  expect(manualReleaseNext(...args, { planSucceeded: true, scopeExpanded: false, scopeEmpty: false, catalogBaseCommit: null, remoteError: false })).toContain(`--plan-identity ${identity}`);
  const noOp = manualReleaseNext(...args, { planSucceeded: true, scopeExpanded: false, scopeEmpty: true, catalogBaseCommit: null, remoteError: false });
  expect(noOp).toContain("no release trigger required");
  expect(noOp).not.toContain("release trigger --target");
  const observed = manualReleaseNext("trigger", args[1], args[2], args[3], args[4], args[5], { planSucceeded: true, scopeExpanded: false, scopeEmpty: false, catalogBaseCommit: null, remoteError: false });
  expect(observed).toContain("delivery-observe");
  expect(observed).toContain("a".repeat(40));
  expect(observed).not.toContain("closeout");
  const remote = readFileSync(resolve(import.meta.dir, "platform-infra-pipelines-as-code-remote.sh"), "utf8");
  const action = remote.slice(remote.indexOf("manual_release_plan_json()"), remote.indexOf("repository_manifest()"));
  expect(action).not.toContain('DOMAIN_PLAN="$domain_plan"');
  expect(action).not.toContain('PLAN_JSON="$plan"');
  expect(action).not.toContain('PAYLOAD="$payload"');
  expect(action).toContain('node - "$changed_paths_file" "$domain_plan_file"');
  expect(action).toContain('--data-binary "@$payload_file"');
});

test("PaC default release plan projection keeps review fields without changed-path payload", () => {
  const source = readFileSync(resolve(import.meta.dir, "platform-infra-pipelines-as-code.ts"), "utf8");
  const projector = source.slice(source.indexOf("function compactManualReleasePlan"), source.indexOf("export function manualReleaseNext"));
  expect(projector).toContain("requestedScope");
  expect(projector).toContain("planIdentity");
  expect(projector).toContain("registryProbe");
  expect(projector).toContain("envReuse");
  expect(projector).toContain("buildServices");
  expect(projector).toContain("rolloutServices");
  expect(projector).toContain("affectedServices");
  expect(projector).toContain("scopeReview");
  expect(projector).not.toContain("changedPaths:");
});

test("PaC compact release plan keeps bounded registry probe results", () => {
  const compact = compactManualReleasePlan({
    registryProbe: {
      enabled: true,
      services: [
        { serviceId: "api", status: "present", image: "registry/api:large", digest: `sha256:${"a".repeat(64)}` },
        { serviceId: "worker", status: "missing", image: "registry/worker:large", digest: null },
      ],
    },
  });
  expect(compact.registryProbe).toEqual({
    enabled: true,
    serviceCount: 2,
    statusCounts: { present: 1, missing: 1 },
    exceptions: [
      { serviceId: "worker", status: "missing" },
    ],
  });
  expect(JSON.stringify(compact)).not.toContain("registry/api:large");
  expect(JSON.stringify(compact)).not.toContain("sha256:aaaaaaaa");
});

test("PaC release manifest renders YAML-owned watcher startup backlog probes", () => {
  const pac = readPacConfig({ consumerId: "hwlab-nc01-v03", selectDefault: true });
  const source = readFileSync(resolve(import.meta.dir, "fixtures/pac/release-leading-comment-only.yaml"), "utf8");
  const manifest = renderPacReleaseManifest(pac, source);
  const documents = manifest.split(/^---\s*$/mu).map((item) => item.trim()).filter(Boolean).map((item) => Bun.YAML.parse(item) as any);
  expect(documents).toHaveLength(2);
  const watcher = documents[0].spec.template.spec.containers[0];
  expect(watcher.startupProbe).toEqual(pac.release.workloads.watcher.startupProbe);
  expect(watcher.readinessProbe).toEqual(pac.release.workloads.watcher.readinessProbe);
  expect(watcher.livenessProbe).toEqual(pac.release.workloads.watcher.livenessProbe);
  expect(documents[1].kind).toBe("Service");
});

test("PaC release manifest rejects resources without typed object identity", () => {
  const pac = readPacConfig({ consumerId: "hwlab-nc01-v03", selectDefault: true });
  const source = readFileSync(resolve(import.meta.dir, "fixtures/pac/release-missing-object-identity.yaml"), "utf8");
  try {
    renderPacReleaseManifest(pac, source);
    throw new Error("expected release manifest renderer to reject the invalid resource");
  } catch (error) {
    expect(error).toBeInstanceOf(PacReleaseManifestRenderError);
    expect(error).toMatchObject({
      code: "pac-release-manifest-object-identity-invalid",
      documentIndex: 2,
      missingFields: ["apiVersion", "kind"],
    });
  }
});

test("platform bootstrap help advertises the composite entry before low-level apply", async () => {
  const help = await runPlatformInfraPipelinesAsCodeCommand({} as never, ["help", "platform-bootstrap"]);
  const usage = (help as Record<string, unknown>).usage as string[];
  expect(usage[0]).toContain("pipelines-as-code bootstrap");
  expect(usage).toContain("bun scripts/cli.ts platform-infra pipelines-as-code bootstrap --target <NODE> --consumer <consumer> --confirm");
});

test("PaC bootstrap projects typed stages and compact JSON", () => {
  const result = {
    ok: true,
    action: "platform-infra-pipelines-as-code-bootstrap",
    mutation: false,
    mode: "dry-run",
    target: { id: "NC01" },
    consumer: { id: "widgets", node: "NC01", lane: "v01" },
    repository: { id: "widgets", namespace: "ci" },
    mirrorRepository: { key: mirror.key, upstreamRepository: mirror.upstream.repository, upstreamBranch: mirror.upstream.branch, owner: mirror.gitea.owner, repo: mirror.gitea.name },
    githubPreflight: { ok: true, mutation: false, repositories: [{ key: mirror.key, code: "github-upstream-available", ok: true }] },
    giteaBootstrap: { ok: true, mutation: false, mode: "dry-run", blockers: [] },
    pipelinesAsCodeApply: { ok: true, mutation: false, mode: "dry-run", remote: { ok: true, preflight: { repositoryExists: true } } },
  };
  const projection = projectPacBootstrapResult(result);
  expect(projection.ok).toBe(true);
  expect(JSON.stringify(projection)).not.toContain("pipelinesAsCodeApply");
  expect((projection.stages as Record<string, unknown>[]).map((item) => item.id)).toEqual(["gitea-init", "pac-controller", "pac-admission", "tekton-consumer", "gitops-argo"]);
  const rendered = renderPacBootstrap(result).renderedText;
  expect(rendered).toContain("PRECONDITIONS");
  expect(rendered).toContain("yaml-repository-exact-match");
  expect(rendered).toContain("confirm:");
});

test("PaC bootstrap keeps admission drift as a non-blocking warning", () => {
  const result = {
    ok: true,
    action: "platform-infra-pipelines-as-code-bootstrap",
    mutation: true,
    mode: "confirmed",
    target: { id: "NC01" },
    consumer: { id: "widgets", node: "NC01", lane: "v01" },
    repository: { id: "widgets", namespace: "ci" },
    mirrorRepository: { key: mirror.key, upstreamRepository: mirror.upstream.repository, upstreamBranch: mirror.upstream.branch, owner: mirror.gitea.owner, repo: mirror.gitea.name },
    githubPreflight: { ok: true, mutation: false, repositories: [{ key: mirror.key, code: "github-upstream-available", ok: true }] },
    giteaBootstrap: { ok: true, mutation: false, mode: "skipped", blockers: [] },
    pipelinesAsCodeApply: {
      ok: true,
      mutation: true,
      mode: "confirmed",
      remote: {
        ok: true,
        admission: { applied: true, ready: false },
        admissionProvenance: { policyName: "unidesk-pac-delivery-provenance-v2", reasons: ["policy-generation-not-observed"] },
      },
    },
  };
  const projection = projectPacBootstrapResult(result);
  expect(projection.ok).toBe(true);
  expect((projection.stages as Record<string, unknown>[]).find((item) => item.id === "pac-admission")).toMatchObject({ ok: true, state: "warning", blocking: false });
  expect(projection.warnings).toEqual([expect.objectContaining({ code: "pac-admission-provenance-not-ready", blocking: false })]);
});

test("PaC bootstrap returns fix-yaml next for zero YAML matches", () => {
  const result = pacBootstrapYamlMatchFailure("NC01", "widgets", new Error("cannot resolve repository: no exact match"));
  expect(projectPacBootstrapResult(result)).toBe(result);
  expect((result.blockers as Record<string, unknown>[])[0]?.code).toBe("yaml-repository-no-match");
  expect(result.next).toEqual({
    kind: "fix-yaml",
    command: "检查 config/platform-infra/gitea.yaml 与 config/platform-infra/pipelines-as-code.yaml 的 target、owner/name 和 read URL 精确匹配",
  });
});

test("PaC bootstrap returns fix-yaml next for multiple YAML matches", () => {
  const result = pacBootstrapYamlMatchFailure("NC01", "widgets", new Error("cannot resolve repository: 2 exact matches"));
  expect(result.mutation).toBe(false);
  expect((result.blockers as Record<string, unknown>[])[0]?.code).toBe("yaml-repository-multiple-matches");
  expect((result.next as Record<string, unknown>).kind).toBe("fix-yaml");
});

test("PaC bootstrap distinguishes GitHub access failure from Gitea and PaC stages", () => {
  const projection = projectPacBootstrapResult({
    ok: false,
    action: "platform-infra-pipelines-as-code-bootstrap",
    mutation: false,
    mode: "dry-run",
    target: { id: "NC01" },
    consumer: { id: "widgets", node: "NC01", lane: "v01" },
    repository: { id: "widgets", namespace: "ci" },
    mirrorRepository: { key: mirror.key, upstreamRepository: mirror.upstream.repository, upstreamBranch: mirror.upstream.branch, owner: mirror.gitea.owner, repo: mirror.gitea.name },
    githubPreflight: { ok: false, mutation: false, repositories: [{ key: mirror.key, code: "github-repository-not-found-or-no-access", errorTail: "repository unavailable" }] },
    giteaBootstrap: { ok: false, mutation: false, mode: "skipped", githubPreflight: { ok: false, repositories: [{ code: "github-repository-not-found-or-no-access", errorTail: "repository unavailable" }] } },
    pipelinesAsCodeApply: { ok: false, mutation: false, mode: "skipped" },
  });
  expect((projection.blockers as Record<string, unknown>[])).toEqual([{ code: "github-repository-not-found-or-no-access", stage: "github-upstream", reason: "repository unavailable" }]);
  expect((projection.preconditions as Record<string, unknown>[])[2]).toMatchObject({ ok: null, code: "gitea-bootstrap-skipped" });
  expect((projection.stages as Record<string, unknown>[]).map((item) => item.state)).toEqual(["skipped", "skipped", "skipped", "skipped", "skipped"]);
  expect((projection.next as Record<string, unknown>).kind).toBe("read-only-status");
});
