import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { spawnSync } from "node:child_process";
import { diagnosePacRegression, pacFailureFingerprint, renderPacRegressionDiagnosis } from "./platform-infra-pac-regression-diagnosis";

const base = "a".repeat(40);
const failed = "b".repeat(40);

function row(input: {
  id: string;
  consumer?: string;
  status: "True" | "False" | "Unknown";
  at: string;
  commit: string;
  code?: string;
  phase?: string;
}): Record<string, unknown> {
  return {
    id: input.id,
    consumer: input.consumer ?? "agentrun-nc01-v02",
    deliveryAuthorityEligible: true,
    status: input.status,
    triggeredAt: input.at,
    completionTime: input.at,
    commit: input.commit,
    traceId: input.status === "False" ? "1".repeat(32) : null,
    firstBreak: input.status === "False" ? {
      code: input.code ?? "child-process-exit-nonzero",
      phase: input.phase ?? "collect-artifacts",
      reason: "getcwd failed for run 123 at commit bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
    } : null,
    error: input.status === "False" ? { type: "ChildProcessError", code: "EXIT_NONZERO", exitCode: 1 } : null,
  };
}

function diagnose(rows: Record<string, unknown>[], bindings = [
  { id: "agentrun-nc01-v02", repositoryRef: "unidesk-nc01" },
]): Record<string, unknown> {
  return diagnosePacRegression({
    targetId: "NC01",
    consumerId: "agentrun-nc01-v02",
    sourceRepository: "pikasTech/unidesk",
    limitPerConsumer: 20,
    rows,
    consumerBindings: bindings,
  });
}

describe("PaC regression diagnosis", () => {
  test("finds the last known good and first failure in the active streak", () => {
    const result = diagnose([
      row({ id: "fail-2", status: "False", at: "2026-07-16T03:00:00Z", commit: failed }),
      row({ id: "fail-1", status: "False", at: "2026-07-16T02:00:00Z", commit: failed }),
      row({ id: "good-1", status: "True", at: "2026-07-16T01:00:00Z", commit: base }),
    ]);
    expect(result).toMatchObject({ state: "active-regression", incident: true, mutation: false });
    expect(result.window).toMatchObject({ complete: true, failureStreak: 2, lastKnownGood: { id: "good-1" }, firstFailed: { id: "fail-1" } });
    expect(result.sourceRange).toMatchObject({ baseCommit: base, failedCommit: failed, candidateCommit: failed });
    expect(result.rollbackCandidate).toMatchObject({ state: "review-required", automatic: false, requiresReview: true, confidence: "high" });
    expect(JSON.stringify(result)).not.toMatch(/trigger-current|bootstrap|apply|argo sync/iu);
  });

  test("latest successful terminal run reports healthy after an older failure", () => {
    const result = diagnose([
      row({ id: "good-2", status: "True", at: "2026-07-16T03:00:00Z", commit: failed }),
      row({ id: "fail-1", status: "False", at: "2026-07-16T02:00:00Z", commit: failed }),
    ]);
    expect(result).toMatchObject({ state: "healthy", incident: false, mutation: false });
    expect(result.rollbackCandidate).toMatchObject({ state: "insufficient-evidence", automatic: false });
  });

  test("missing successful baseline remains explicit and suggests a bounded wider query", () => {
    const result = diagnose([
      row({ id: "fail-2", status: "False", at: "2026-07-16T03:00:00Z", commit: failed }),
      row({ id: "fail-1", status: "False", at: "2026-07-16T02:00:00Z", commit: failed }),
    ]);
    expect(result).toMatchObject({ state: "active-regression-baseline-missing", incident: true });
    expect(result.window).toMatchObject({ complete: false, lastKnownGood: null });
    expect((result.next as Record<string, unknown>).expandWindow).toContain("--limit 40");
  });

  test("maximum window does not suggest an identical retry", () => {
    const result = diagnosePacRegression({
      targetId: "NC01",
      consumerId: "agentrun-nc01-v02",
      sourceRepository: "pikasTech/agentrun",
      limitPerConsumer: 50,
      rows: [row({ id: "fail-1", status: "False", at: "2026-07-16T02:00:00Z", commit: failed })],
      consumerBindings: [{ id: "agentrun-nc01-v02", repositoryRef: "agentrun-nc01-v02" }],
    });
    expect(result.next).toMatchObject({ expandWindow: null, windowLimitReached: true });
  });

  test("same fingerprint across consumers marks the diagnosis as public service", () => {
    const result = diagnose([
      row({ id: "fail-a", status: "False", at: "2026-07-16T03:00:00Z", commit: failed }),
      row({ id: "fail-b", consumer: "hwlab-nc01-v03", status: "False", at: "2026-07-16T02:30:00Z", commit: failed }),
      row({ id: "good-a", status: "True", at: "2026-07-16T02:00:00Z", commit: base }),
    ], [
      { id: "agentrun-nc01-v02", repositoryRef: "unidesk-nc01" },
      { id: "hwlab-nc01-v03", repositoryRef: "unidesk-nc01" },
    ]);
    expect(result.commonFailure).toMatchObject({ matchingRuns: 2, affectedConsumers: ["agentrun-nc01-v02", "hwlab-nc01-v03"] });
    expect(result.scopeAssessment).toMatchObject({ scope: "public-service", confirmed: true });
    expect(result.rollbackCandidate).toMatchObject({ authorizationRequired: true, automatic: false });
  });

  test("fingerprint normalizes run numbers and commit ids", () => {
    const first = pacFailureFingerprint(row({ id: "a", status: "False", at: "2026-07-16T01:00:00Z", commit: failed }));
    const secondRow = row({ id: "b", status: "False", at: "2026-07-16T02:00:00Z", commit: failed });
    (secondRow.firstBreak as Record<string, unknown>).reason = `getcwd failed for run 999 at commit ${"c".repeat(40)}`;
    expect(pacFailureFingerprint(secondRow)).toMatchObject({ id: (first as Record<string, unknown>).id });
  });

  test("failure summaries redact credential-like values", () => {
    const failedRow = row({ id: "fail-secret", status: "False", at: "2026-07-16T02:00:00Z", commit: failed });
    (failedRow.firstBreak as Record<string, unknown>).reason = "authorization=Bearer exposed token=plain https://user:pass@example.test/path?key=value";
    const result = diagnose([
      failedRow,
      row({ id: "good-1", status: "True", at: "2026-07-16T01:00:00Z", commit: base }),
    ]);
    const serialized = JSON.stringify(result);
    expect(serialized).toContain("[REDACTED]");
    expect(serialized).not.toMatch(/exposed|token=plain|user:pass|key=value/u);
  });

  test("default text is bounded and exposes the recovery window", () => {
    const output = renderPacRegressionDiagnosis(diagnose([
      row({ id: "fail-1", status: "False", at: "2026-07-16T02:00:00Z", commit: failed }),
      row({ id: "good-1", status: "True", at: "2026-07-16T01:00:00Z", commit: base }),
    ]));
    expect(output).toContain("LAST_KNOWN_GOOD: good-1");
    expect(output).toContain("FIRST_FAILED: fail-1");
    expect(output).toContain("MUTATION: false");
    expect(output.split("\n").length).toBeLessThan(24);
  });

  test("read failures remain distinct from an incident and visible in default text", () => {
    const result = diagnose([]);
    const output = renderPacRegressionDiagnosis({
      ...result,
      state: "read-failed",
      incident: null,
      observation: { status: "timeout", elapsedMs: 25001 },
      historyErrors: [{ context: "agentrun-ci:pipelinerun", error: "target read timed out" }],
    });
    expect(output).toContain("STATE: read-failed  INCIDENT: -");
    expect(output).toContain("READ: timeout elapsed-ms=25001 errors=1");
    expect(output).toContain("READ_ERROR: context=agentrun-ci:pipelinerun");
  });

  test("CLI route is discoverable and rejects detail ids before remote access", () => {
    const help = spawnSync("bun", ["scripts/cli.ts", "platform-infra", "pipelines-as-code", "--help", "--json"], { encoding: "utf8" });
    expect(help.status).toBe(0);
    expect(help.stdout).toContain("diagnose-regression");
    const invalid = spawnSync("bun", ["scripts/cli.ts", "platform-infra", "pipelines-as-code", "diagnose-regression", "--target", "NC01", "--consumer", "agentrun-nc01-v02", "--id", "fixture", "--json"], { encoding: "utf8" });
    expect(invalid.status).not.toBe(0);
    expect(invalid.stdout).toContain("diagnose-regression does not accept --id");
    expect(invalid.stderr).not.toContain("read.progress");
  });

  test("target collector uses PipelineRun summaries and exposes no mutation action", () => {
    const remote = readFileSync(resolve(import.meta.dir, "platform-infra-pipelines-as-code-remote.sh"), "utf8");
    const collector = remote.slice(remote.indexOf("regression_rows() {"), remote.indexOf("task_rows() {"));
    expect(collector).toContain("get', 'pipelinerun'");
    expect(collector).toContain("pipelinerun-summary");
    expect(collector).not.toContain("'taskrun'");
    expect(collector).not.toMatch(/kubectl[^\n]+(?:create|apply|patch|delete)|argo[^\n]+sync/iu);
  });
});
