import { describe, expect, test } from "bun:test";
import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { createRequire } from "node:module";
import { sentinelPublishImageBuildShell } from "./hwlab-node-web-sentinel-cicd-jobs";
import type { SentinelCicdState } from "./hwlab-node-web-sentinel-cicd-shared";
import { renderAgentRunControlPlaneStatusSummary } from "./agentrun/public-exposure";
import { closeoutDeliveryAligned, compactHistoryJson, compactStatusSummary, pacReadOnlyCaptureObservation, renderDebugStep, renderHistory, renderStatus } from "./platform-infra-pipelines-as-code";

const require = createRequire(import.meta.url);
const { parsePacLogRecords } = require("../native/cicd/pac-status-evaluator.cjs") as {
  parsePacLogRecords(value: string): Array<Record<string, unknown>>;
};
const { extractPacSourceObservation } = require("../native/cicd/pac-status-evaluator.cjs") as {
  extractPacSourceObservation(records: Array<Record<string, unknown>>, sourceCommit: string): Record<string, unknown>;
};

describe("PaC 失败证据合同", () => {
  test("真实 sentinel TaskRun 日志保留 typed 首断点", () => {
    const fixture = resolve(import.meta.dir, "fixtures/pac/hwlab-web-probe-sentinel-nc01-r5ww2.log");
    const record = parsePacLogRecords(readFileSync(fixture, "utf8")).find((item) => item.ok === false);
    expect(record).toMatchObject({
      traceId: "6e6e899d3e812891140aedfd974d524e",
      firstBreak: { code: "child-process-exit-nonzero", phase: "image-build" },
      error: { type: "ChildProcessError", code: "EXIT_NONZERO", phase: "image-build", exitCode: 1 },
    });
  });

  test("history、debug-step、status 默认显示 typed 失败字段", () => {
    const fixture = resolve(import.meta.dir, "fixtures/pac/hwlab-web-probe-sentinel-nc01-r5ww2.log");
    const failure = parsePacLogRecords(readFileSync(fixture, "utf8")).find((item) => item.ok === false) ?? {};
    const row = {
      id: "hwlab-web-probe-sentinel-nc01-r5ww2",
      consumer: "sentinel-nc01-v03",
      deliveryClass: "outer-pac-event",
      status: "False",
      traceId: failure.traceId,
      firstBreak: failure.firstBreak,
      error: failure.error,
    };
    const history = renderHistory({ rows: [row], config: {}, historyErrors: [], next: {} }).renderedText;
    const debug = renderDebugStep({ realRun: { found: true, pipelineRun: row, firstBreak: row.firstBreak, error: row.error, traceId: row.traceId }, checks: [], target: {}, next: {} }).renderedText;
    const status = renderStatus({ summary: { latestPipelineRun: row, artifact: row, diagnostics: {} }, consumer: {}, deliveryAuthority: {}, config: {}, coverage: [], target: {}, next: {} }).renderedText;
    for (const output of [history, debug, status]) {
      expect(output).toContain("6e6e899d3e8128");
      expect(output).toContain("child-process-exit-nonzero");
      expect(output).toContain("ChildProcessError");
      expect(output).toContain("image-build");
      expect(output).toContain("EXIT_NONZERO");
      expect(output).toContain("1");
    }
    const compactHistory = compactHistoryJson({ rows: [{ ...row, collector: { matchingTaskCount: 1 } }], detailId: row.id });
    const compactStatus = compactStatusSummary({ latestPipelineRun: row, artifact: row });
    expect(compactHistory).toMatchObject({ rows: [{ traceId: row.traceId, firstBreak: row.firstBreak, error: row.error, collector: { matchingTaskCount: 1 } }] });
    expect(compactStatus).toMatchObject({ artifact: { traceId: row.traceId, firstBreak: row.firstBreak, error: row.error } });
  });

  test("只读 capture timeout 保留阶段、预算、首断点与下一步", () => {
    const observation = pacReadOnlyCaptureObservation({
      action: "history",
      targetId: "NC01",
      consumerId: "agentrun-nc01-v02",
      result: { exitCode: 124, stdout: "", stderr: '{"event":"pac-status-progress","stage":"artifact-evidence","valuesPrinted":false}\nUNIDESK_SSH_RUNTIME_TIMEOUT' },
      parsed: null,
      elapsedMs: 25001,
      timeoutMs: 25000,
      configPath: "config/platform-infra/pipelines-as-code.yaml#observability.readOnlyCapture.timeoutMs",
      next: "bun scripts/cli.ts platform-infra pipelines-as-code history --target NC01 --consumer agentrun-nc01-v02 --limit 5",
    });
    expect(observation).toMatchObject({
      ok: false,
      status: "timeout",
      target: "NC01",
      consumer: "agentrun-nc01-v02",
      stage: "history-remote-observe",
      remoteStage: "artifact-evidence",
      elapsedMs: 25001,
      timeoutMs: 25000,
      configPath: "config/platform-infra/pipelines-as-code.yaml#observability.readOnlyCapture.timeoutMs",
      exitCode: 124,
      firstBreak: { code: "pac-read-timeout", phase: "remote-read" },
    });
    const output = renderHistory({ rows: [], config: {}, historyErrors: [], deliveryBudget: {}, observation, next: {} }).renderedText;
    expect(output).toContain("READ OBSERVATION");
    expect(output).toContain("history-remote-observe");
    expect(output).toContain("pac-read-timeout");
    expect(output).toContain("config/platform-infra/pipelines-as-code.yaml#observability.readOnlyCapture.timeoutMs");
    expect(output).toContain("next-read-only:");
  });

  test("AgentRun status 文本披露 lane 与 PaC 两层 YAML capture 预算", () => {
    const output = renderAgentRunControlPlaneStatusSummary({
      ok: false,
      target: { node: { id: "NC01" }, lane: "nc01-v02" },
      summary: {
        migration: { migrated: true, sourceAuthority: "gitea-pipelines-as-code" },
        pac: {
          readObservation: {
            stage: "status-remote-observe",
            status: "timeout",
            elapsedMs: 25001,
            timeoutMs: 25000,
            configPath: "config/platform-infra/pipelines-as-code.yaml#observability.readOnlyCapture.timeoutMs",
            exitCode: 124,
            firstBreak: { code: "pac-read-timeout" },
            next: "bun scripts/cli.ts platform-infra pipelines-as-code status --full",
          },
        },
        blockers: ["pac-not-ready"],
        warnings: [],
        nextAction: {},
      },
      readCaptureBudget: {
        timeoutMs: 20000,
        configPath: "config/agentrun.yaml.controlPlane.lanes.nc01-v02.status.readOnlyCapture.timeoutMs",
      },
      next: {},
      timings: {},
    }).renderedText;
    expect(output).toContain("PAC READ OBSERVATION");
    expect(output).toContain("config/platform-infra/pipelines-as-code.yaml#observability.readOnlyCapture.timeoutMs");
    expect(output).toContain("AGENTRUN READ CAPTURE BUDGET");
    expect(output).toContain("config/agentrun.yaml.controlPlane.lanes.nc01-v02.status.readOnlyCapture.timeoutMs");
  });

  test("feature-config warning 在 status、history、debug-step 同构有界投影", () => {
    const observation = extractPacSourceObservation([
      {
        event: "pac-delivery-plan",
        sourceCommitId: "a".repeat(40),
        affectedServices: [],
        rolloutServices: [],
        buildServices: [],
        reusedServices: [],
      },
      {
        event: "feature-config-schema-validation",
        warning: true,
        blocking: false,
        code: "feature-config-input-missing",
        schemaPath: "config/feature-config.schema.json",
        errorCount: 1,
        firstError: "rendered workload candidate snapshot is missing",
        mutation: false,
        valuesPrinted: false,
      },
    ]);
    const expected = "feature-config-schema: warning=true blocking=false code=feature-config-input-missing path=config/feature-config.schema.json errors=1 first=rendered workload candidate snapshot is missing";
    const history = renderHistory({ rows: [{ id: "run", sourceObservation: observation }], detailId: "run", config: {}, historyErrors: [], next: {} }).renderedText;
    const debug = renderDebugStep({ realRun: { found: true, pipelineRun: {}, artifact: { sourceObservation: observation } }, checks: [], target: {}, next: {} }).renderedText;
    const status = renderStatus({ summary: { latestPipelineRun: {}, artifact: {}, diagnostics: {}, sourceObservation: observation }, consumer: {}, deliveryAuthority: {}, config: {}, coverage: [], target: {}, next: {} }).renderedText;
    expect(status).toContain(expected);
    expect(debug).toContain(expected);
    expect(history).toContain("featureConfigCode");
    expect(history).toContain("feature-config-input-missing");
    expect(JSON.stringify(observation)).not.toContain("process.env");
  });

  test("collector 以单次有界 selector 拉取 PipelineRun 日志，terminal record 仍限定合同任务", () => {
    const remote = readFileSync(resolve(import.meta.dir, "platform-infra-pipelines-as-code-remote.sh"), "utf8");
    expect(remote).toContain("const contractTaskRuns = matching.filter((item) => taskTerminalRecord(item) !== null)");
    expect(remote).toContain("'--tail=320', '--max-log-requests=20'");
    expect(remote.match(/cp\.spawnSync\('kubectl', \['-n', namespace, 'logs'/gu)).toHaveLength(2);
    expect(remote).toContain("process.env.UNIDESK_PAC_COLLECT_ARTIFACT_LOGS === '1'");
    expect(remote).toContain("`tekton.dev/taskRun=${task.name}`");
    expect(remote).toContain("const pipelineRunsByNamespace = new Map()");
    expect(remote).toContain("const taskRunsByNamespace = new Map()");
    expect(remote).toContain("let mode = 'taskrun-status-only';");
    expect(remote).toContain("['pac-gitops-missing', 'pac-source-observation-inconsistent'].includes(output.code)");
    expect(remote).toContain("artifact.collector?.mode === 'taskrun-status-only'");
    expect(remote).toContain('payload_file="${UNIDESK_PAC_TASKRUN_FILE:-}"');
    expect(remote).toContain('task_file="${UNIDESK_PAC_TASKRUN_FILE:-}"');
    expect(remote).toContain('export UNIDESK_PAC_TASKRUN_FILE="$task_file"');
    expect(remote).not.toContain("for (const item of matching)");
    expect(remote).not.toContain("for (const item of taskRuns)");
    expect(remote).toContain("Object.keys(record(row.firstBreak)).length > 0");
    expect(remote).toContain("traceId: row.traceId || null");
  });

  test("status compact JSON 只投影 admission、bootstrap 与 source observation 有界摘要", () => {
    const compact = compactStatusSummary({
      admissionProvenance: { ready: true, warning: false, blocking: false, reasons: [], policy: { raw: "omitted" } },
      consumerBootstrap: { configured: true, ready: true, warning: false, blocking: false, reasons: [], runner: { raw: "omitted" } },
      sourceObservation: {
        contract: "pac-delivery-plan-v1",
        mode: "delivery",
        valid: true,
        sourceCommit: "a".repeat(40),
        sourceMatched: true,
        reason: "delivery-plan-complete",
        plan: { affectedServiceCount: 2, rolloutServiceCount: 1, buildServiceCount: 1, reusedServiceCount: 1, services: ["large", "raw", "list"] },
        terminal: { raw: "omitted" },
      },
    });
    expect(compact).toMatchObject({
      admissionProvenance: { ready: true, reasons: [] },
      consumerBootstrap: { configured: true, ready: true, reasons: [] },
      sourceObservation: { mode: "delivery", plan: { affectedServiceCount: 2, rolloutServiceCount: 1 } },
    });
    expect(JSON.stringify(compact)).not.toContain("omitted");
    expect(JSON.stringify(compact)).not.toContain("services");
  });

  test("debug-step 默认投影失败 TaskRun、step、exit code 与有界日志", () => {
    const output = renderDebugStep({
      realRun: {
        found: true,
        pipelineRun: { id: "fixture", status: "False" },
        failureEvidence: {
          taskRun: "fixture-build",
          step: "build",
          container: "step-build",
          exitCode: 1,
          terminationReason: "Error",
          logReadOk: true,
          logTail: ["module not found: scripts/missing.ts"],
        },
      },
      checks: [], target: {}, next: {},
    }).renderedText;
    expect(output).toContain("FAILED TASK EVIDENCE");
    expect(output).toContain("fixture-build");
    expect(output).toContain("step-build");
    expect(output).toContain("module not found: scripts/missing.ts");
  });

  test("closeout requires GitOps ancestry and exact runtime digest", () => {
    const digest = `sha256:${"a".repeat(64)}`;
    const base = {
      artifact: { imageStatus: "built", digest },
      argo: { revisionRelation: { relation: "descendant" } },
      runtime: { digest },
    };
    expect(closeoutDeliveryAligned(base)).toBe(true);
    expect(closeoutDeliveryAligned({ ...base, argo: { revisionRelation: { relation: "stale" } } })).toBe(false);
    expect(closeoutDeliveryAligned({ ...base, runtime: { digest: `sha256:${"b".repeat(64)}` } })).toBe(false);
    expect(closeoutDeliveryAligned({ ...base, argo: { revisionRelation: { relation: "retained" } } })).toBe(false);
    expect(closeoutDeliveryAligned({
      sourceObservation: { mode: "no-runtime-change", valid: true },
      diagnostics: { code: "pac-ready-no-runtime-change" },
    })).toBe(true);
  });

  test("sentinel image build 显式等待 socket 与 worker readiness 并清理 daemon", () => {
    const shell = sentinelPublishImageBuildShell({
      spec: {
        nodeId: "NC01",
        lane: "v03",
        buildkit: { sidecarImage: "moby/buildkit:rootless" },
        networkProfile: { imageBuildProxy: { http: "", https: "", all: "", noProxy: [] } },
      },
      image: {
        ref: "registry.example.test/web-probe-sentinel:test",
        monitorWeb: {
          imageBuildPackageMode: "frozen-lockfile",
          imageBuildNetworkMode: "host",
          imageBuildProxySource: "test",
        },
      },
    } as unknown as SentinelCicdState, "sentinel-test");
    expect(shell).toContain("mkdir -p \"$(dirname \"$buildkit_socket\")\"");
    expect(shell).toContain("emit_stage() {");
    expect(shell).toContain('${TRACEPARENT:-}');
    expect(shell).toContain("unset TRACEPARENT TRACESTATE OTEL_EXPORTER_OTLP_ENDPOINT OTEL_EXPORTER_OTLP_TRACES_ENDPOINT OTEL_EXPORTER_OTLP_PROTOCOL OTEL_EXPORTER_OTLP_TRACES_PROTOCOL OTEL_EXPORTER_OTLP_HEADERS OTEL_EXPORTER_OTLP_TRACES_HEADERS");
    expect(shell).toContain("export OTEL_SDK_DISABLED=true OTEL_TRACES_EXPORTER=none");
    expect(shell).toContain("exec rootlesskit buildkitd --addr");
    expect(shell).not.toMatch(/(^|\n)buildkitd --addr/);
    expect(shell).toContain("exec buildctl --addr \"$buildkit_address\" debug workers");
    expect(shell).toContain("exec env HTTP_PROXY=\"$image_build_http_proxy\"");
    expect(shell).toContain("failure_error_type=BuildKitReadinessError");
    expect(shell).toContain("failure_error_code=BUILDKIT_NOT_READY");
    expect(shell).toContain("cleanup_buildkitd");
    expect(shell).not.toContain("buildctl-daemonless.sh build");
    expect(spawnSync("sh", ["-n"], { input: shell, encoding: "utf8" })).toMatchObject({ status: 0, stderr: "" });
  });
});
