import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it, vi } from "vitest";
import {
  EXPECTED_CRITERION_IDS,
  proofUnits,
  semanticReadiness,
  validateCriterionRegistry,
} from "../golive/criterion-registry.js";
import {
  atomicTransitionJourney,
  loadJourneyManifestAtLeast,
  manifestFingerprint,
  parseJourneyManifest,
  reconcilePersistedClosure,
} from "../golive/journey-manifest.js";
import {
  executeBarrierSchedule,
  executionSchedule,
  hasExactDependencyChain,
  isCausallyAdvancedApproval,
  isPristinePreApproval,
  lifecycleHandlerFor,
  main,
  proofEnvironment,
  statusSummary,
  validateBarrierTopology,
  validateExecutionPlan,
  withJourneyMutationLock,
  validateStructure,
} from "../golive/run.js";
import {
  authoritativeRemoteHead,
  canonicalGitHubRepository,
  commandOutput,
} from "../golive/repository-identity.js";
import {
  acceptanceState,
  loadProofReceiptLedger,
  persistProofReceipts,
  reconcileAcceptanceFile,
} from "../golive/acceptance-state.js";
const repository = {
  owner: "platform-modules",
  name: "awp",
  remoteUrl: "https://example.invalid/platform-modules/awp.git",
  checkoutPath: process.cwd(),
  defaultBranch: "main",
};
const rawSeed = {
  schemaVersion: 1,
  stage: "seed",
  journeyKey: "journey-1",
  runId: "run-1",
  generation: 0,
  seedFingerprint: "seed-fingerprint",
  repository,
  agentAccountId: "account-1",
  agentModel: "model-1",
} as const;
const seed = parseJourneyManifest(rawSeed);
const owner = {
  ...rawSeed,
  stage: "owner-created" as const,
  generation: 1,
  priorGenerationHash: manifestFingerprint(seed),
  projectId: "project-1",
  planId: "plan-1",
  planRevisionId: "revision-1",
  taskIds: ["task-1", "task-2", "task-3"],
  authoringReceipt: "receipt-1",
};
const approved = {
  ...owner,
  stage: "approved" as const,
  generation: 2,
  priorGenerationHash: manifestFingerprint(parseJourneyManifest(owner)),
  factoryRunId: "factory-1",
  firstTaskId: "task-1",
  approvalReceipt: "receipt-2",
};
describe("GOLIVE orchestrator contracts", () => {
  it("keeps an immutable exact AC set and separate structure/readiness/acceptance counters", () => {
    expect(Object.isFrozen(EXPECTED_CRITERION_IDS)).toBe(true);
    expect(EXPECTED_CRITERION_IDS).toHaveLength(30);
    expect(() => validateCriterionRegistry()).not.toThrow();
    expect(statusSummary()).toBe(
      `registry structure: 30/30\nsemantic readiness: ${semanticReadiness().ready}/30\nowner acceptance: 0/30`,
    );
  });
  it("separates structurally ready proof migrations from owner acceptance", () => {
    for (const id of ["AC-12", "AC-14", "AC-22", "AC-23", "AC-28", "AC-29"] as const)
      expect(proofUnits.find((u) => u.criterionIds.includes(id))?.migrationStatus).toBe("ready");
    expect(semanticReadiness()).toMatchObject({ ready: 30, total: 30, pending: [] });
    const authoring = proofUnits.find((unit) => unit.unitId === "owner-authoring")!;
    const approval = proofUnits.find((unit) => unit.unitId === "owner-approval")!;
    expect(authoring.runtimeInputs).toEqual([
      "AWP_WEB_URL",
      "AWP_PLAYWRIGHT_MODULE",
      "AWP_TEST_POSTGRES_URL",
      "AWP_JOURNEY_MANIFEST_PATH",
      "AWP_OPERATOR_PASSWORD",
    ]);
    expect(approval.runtimeInputs).toEqual(authoring.runtimeInputs);
    expect(proofUnits.find((unit) => unit.unitId === "self-project")?.inputSelectors).toEqual(
      expect.arrayContaining([
        "projectId",
        "planId",
        "planRevisionId",
        "taskIds",
        "factoryRunId",
        "firstTaskId",
        "repository.checkoutPath",
      ]),
    );
    const liveTransitions = proofUnits.find((unit) => unit.unitId === "live-transitions")!;
    expect(liveTransitions.inputSelectors).toEqual(
      expect.arrayContaining(["projectId", "planId", "planRevisionId", "taskIds"]),
    );
    expect(liveTransitions.inputSelectors).not.toContain("factoryRunId");
    expect(liveTransitions.runtimeInputs).toContain("AWP_OPERATOR_PASSWORD");
    expect(() => validateExecutionPlan()).not.toThrow();
  });
  it("validates staged all-or-nothing identity", () => {
    expect(seed.stage).toBe("seed");
    expect(() => parseJourneyManifest({ ...rawSeed, projectId: "guessed" })).toThrow(
      /rejects product-generated/,
    );
    expect(() => parseJourneyManifest({ ...owner, taskIds: [] })).toThrow(/taskIds/);
    expect(() => parseJourneyManifest({ ...approved, factoryRunId: "" })).toThrow(/factoryRunId/);
  });
  it("atomically advances and stage-gates persisted identity", () => {
    const dir = mkdtempSync(join(tmpdir(), "awp-journey-"));
    const path = join(dir, "journey.json");
    writeFileSync(path, JSON.stringify(seed));
    atomicTransitionJourney(path, seed, owner);
    expect(loadJourneyManifestAtLeast(path, "owner-created").stage).toBe("owner-created");
    expect(() => loadJourneyManifestAtLeast(path, "approved")).toThrow(
      /approved identity required/,
    );
  });
  it("accepts only an absent or exactly-one complete persisted recovery closure", () => {
    expect(reconcilePersistedClosure("authoring", [])).toBeUndefined();
    expect(reconcilePersistedClosure("authoring", [{ status: "complete", value: owner }])).toBe(
      owner,
    );
    expect(() =>
      reconcilePersistedClosure("authoring", [
        { status: "complete", value: owner },
        { status: "complete", value: owner },
      ]),
    ).toThrow(/ambiguous/);
    expect(() =>
      reconcilePersistedClosure("authoring", [
        { status: "partial", reason: "Project exists without its Plan closure" },
      ]),
    ).toThrow(/partial/);
    expect(() =>
      reconcilePersistedClosure("approval", [
        { status: "mismatched", reason: "FactoryRun model differs" },
      ]),
    ).toThrow(/mismatched/);
  });
  it("builds fresh allowlisted environments and fails each missing input", () => {
    const manifest = parseJourneyManifest(approved);
    const unit = {
      ...proofUnits[0]!,
      inputSelectors: [
        "repository.remoteUrl",
        "repository.checkoutPath",
        "projectId",
        "taskIds",
        "factoryRunId",
      ],
      runtimeInputs: ["AWP_WEB_URL"],
    };
    const env = proofEnvironment(unit, manifest, { AWP_WEB_URL: "http://localhost" });
    expect(env).toMatchObject({
      AWP_REPOSITORY_REMOTE_URL: repository.remoteUrl,
      AWP_REPOSITORY_CHECKOUT_PATH: repository.checkoutPath,
      AWP_PROJECT_ID: "project-1",
      AWP_TASK_IDS: "task-1,task-2,task-3",
      AWP_FACTORY_RUN_ID: "factory-1",
      AWP_WEB_URL: "http://localhost",
    });
    expect(env.AWP_PROJECT_REPOSITORY).toBeUndefined();
    expect(env.AWP_GOLIVE_EXECUTION).toBeUndefined();
    expect(() => proofEnvironment(unit, manifest, {})).toThrow(/missing runtime input/);
  });
  it("models causal start/await barriers and deduplicated shared units", () => {
    const schedule = executionSchedule();
    expect(new Set(schedule.map((s) => s.unitId)).size).toBe(schedule.length);
    expect(schedule.find((s) => s.unitId === "ui-loads")?.requiredManifestStage).toBe("seed");
    expect(schedule.find((s) => s.unitId === "owner-authoring")?.requiredManifestStage).toBe(
      "seed",
    );
    expect(schedule.find((s) => s.unitId === "owner-approval")?.requiredManifestStage).toBe(
      "owner-created",
    );
    expect(lifecycleHandlerFor("owner-authoring")).toBeTypeOf("function");
    expect(lifecycleHandlerFor("owner-approval")).toBeTypeOf("function");
    expect(lifecycleHandlerFor("unmigrated-proof")).toBeUndefined();
    for (const inherited of ["toString", "__proto__", "constructor"])
      expect(lifecycleHandlerFor(inherited)).toBeUndefined();
    const ac13 = schedule.find((s) => s.criterionIds.includes("AC-13"))!;
    const ac23 = schedule.find((s) => s.criterionIds.includes("AC-23"))!;
    expect(ac13.startBarrier).toBe("P8");
    expect(ac13.awaitBarrier).toBe("P8-complete");
    expect(ac23.startBarrier).toBe("P2");
    expect(ac23.awaitBarrier).toBe("P8-complete");
    expect(ac23.requiredManifestStage).toBe("owner-created");
    expect(schedule.find((s) => s.criterionIds.includes("AC-03"))?.startBarrier).toBe("P9");
  });
  it("executes P4 proofs concurrently while keeping P10 self closure ordered", async () => {
    expect(() => validateBarrierTopology()).not.toThrow();
    const selected = proofUnits.filter((unit) =>
      [
        "k3s-agent-run",
        "changeset",
        "control-plane-restart",
        "self-project",
        "self-change",
        "golive-progress",
      ].includes(unit.unitId),
    );
    const events: string[] = [];
    await executeBarrierSchedule(
      selected,
      async (unit) => {
        events.push(`start:${unit.unitId}`);
        if (unit.startBarrier === "P4") {
          await new Promise((resolveDelay) =>
            setTimeout(resolveDelay, unit.awaitBarrier === "P4-complete" ? 5 : 15),
          );
        }
        events.push(`finish:${unit.unitId}`);
        return unit.unitId;
      },
      async (unit) => {
        events.push(`complete:${unit.unitId}`);
      },
    );
    const p4Starts = selected
      .filter((unit) => unit.startBarrier === "P4")
      .map((unit) => events.indexOf(`start:${unit.unitId}`));
    const firstP4Complete = Math.min(
      ...selected
        .filter((unit) => unit.startBarrier === "P4")
        .map((unit) => events.indexOf(`complete:${unit.unitId}`)),
    );
    expect(Math.max(...p4Starts)).toBeLessThan(firstP4Complete);
    expect(events.indexOf("complete:k3s-agent-run")).toBeLessThan(
      events.indexOf("complete:changeset"),
    );
    expect(events.indexOf("complete:self-project")).toBeLessThan(
      events.indexOf("start:self-change"),
    );
    expect(events.indexOf("complete:self-change")).toBeLessThan(
      events.indexOf("start:golive-progress"),
    );
  });

  it("persists run-scoped proof receipts and repairs only receipt-backed checkboxes", () => {
    const dir = mkdtempSync(join(tmpdir(), "awp-acceptance-"));
    const acceptancePath = join(dir, "GOLIVE.md");
    const ledgerPath = join(dir, "journey.proof-receipts.json");
    const source = `${EXPECTED_CRITERION_IDS.map((id) => `- [ ] ${id}: proof`).join("\n")}\n\nCurrent: **0 / 30 (0%) re-proven after the I1 drift audit**.\n`;
    writeFileSync(acceptancePath, source);
    let ledger = persistProofReceipts(ledgerPath, seed, {
      unitId: "ui-loads",
      proofId: "ui-loads-ac-01",
      criterionIds: ["AC-01"],
      output: "AC-01 PASS",
      completedAt: "2026-08-24T00:00:00.000Z",
    });
    reconcileAcceptanceFile(acceptancePath, ledger);
    expect(acceptanceState(readFileSync(acceptancePath, "utf8")).checked).toEqual(
      new Set(["AC-01"]),
    );

    ledger = persistProofReceipts(ledgerPath, seed, {
      unitId: "control-plane-postgres",
      proofId: "control-plane-postgres-ac-02",
      criterionIds: ["AC-02"],
      output: "AC-02 PASS",
      completedAt: "2026-08-24T00:00:01.000Z",
    });
    // Simulate a crash after the durable receipt but before GOLIVE.md was updated.
    expect(acceptanceState(readFileSync(acceptancePath, "utf8")).checked).toEqual(
      new Set(["AC-01"]),
    );
    reconcileAcceptanceFile(acceptancePath, loadProofReceiptLedger(ledgerPath, seed));
    expect(acceptanceState(readFileSync(acceptancePath, "utf8")).checked).toEqual(
      new Set(["AC-01", "AC-02"]),
    );
    expect(readFileSync(acceptancePath, "utf8")).toContain("Current: **2 / 30 (7%)");

    writeFileSync(
      acceptancePath,
      readFileSync(acceptancePath, "utf8").replace("- [ ] AC-03:", "- [x] AC-03:"),
    );
    expect(() => reconcileAcceptanceFile(acceptancePath, ledger)).toThrow(
      /AC-03 is checked without this run's durable receipt/,
    );
  });

  it("strictly canonicalizes repository identity before parser normalization", () => {
    for (const remote of [
      "HTTPS://GITHUB.COM/Owner/Repository.git",
      "SSH://git@GitHub.Com/Owner/Repository.git",
      "git@GITHUB.com:Owner/Repository.git",
    ])
      expect(canonicalGitHubRepository(remote)).toBe("github.com/owner/repository");
    for (const remote of [
      " https://github.com/owner/repository",
      "https://github.com/owner/repository\n",
      "https://github.com/owner/../repository",
      "https://github.com/owner/%2e%2e/repository",
      "https://github.com/owner%2frepository",
      "https://github.com/owner%5crepository",
      "https://github.com/owner\\repository",
      "https://github.com//owner/repository",
      "https://user@github.com/owner/repository",
      "ssh://root@github.com/owner/repository",
      "https://github.com:443/owner/repository",
      "https://github.com/owner/repository?ref=main",
      "https://github.com/owner/repository#main",
      "https://example.com/owner/repository",
    ])
      expect(() => canonicalGitHubRepository(remote)).toThrow();
    expect(commandOutput("value\n")).toBe("value");
    expect(commandOutput("value\r\n")).toBe("value");
    expect(commandOutput("value\n\n")).toBe("value\n");
    expect(
      authoritativeRemoteHead(`ref: refs/heads/Main\tHEAD\n${"a".repeat(40)}\tHEAD\n`),
    ).toEqual({
      branchRef: "refs/heads/Main",
      oid: "a".repeat(40),
    });
    expect(() =>
      authoritativeRemoteHead(`ref: refs/heads/main\tHEAD\n${"a".repeat(40)}\tHEAD\nextra\n`),
    ).toThrow();
    expect(() => canonicalGitHubRepository(commandOutput("https://github.com/o/r\n\n"))).toThrow();
  });
  it("fails recovery closed unless task identity, state, and ordered edges are exact", () => {
    const ids = ["task-1", "task-2", "task-3"];
    const edges = [
      ["task-2", "task-1"],
      ["task-3", "task-2"],
    ] as const;
    const pristine = ids.map((id, index) => [id, index === 0 ? "planned" : "blocked"] as const);
    expect(hasExactDependencyChain(ids, edges)).toBe(true);
    expect(isPristinePreApproval("draft", pristine, ids, edges)).toBe(true);
    for (const wrongEdges of [
      [["task-2", "task-1"]],
      [
        ["task-3", "task-2"],
        ["task-2", "task-1"],
      ],
      [
        ["task-2", "task-1"],
        ["task-3", "task-1"],
      ],
      [...edges, ["task-1", "task-3"]],
    ] as const)
      expect(hasExactDependencyChain(ids, wrongEdges)).toBe(false);
    for (const mutation of [
      pristine.map(([id, status], index) => [id, index === 1 ? "completed" : status] as const),
      pristine.map(([id, status], index) => [id, index === 2 ? "running" : status] as const),
      pristine.map(([id, status], index) => [id, index === 2 ? "failed" : status] as const),
      [["__proto__", "planned"], pristine[1]!, pristine[2]!] as const,
    ])
      expect(isPristinePreApproval("draft", mutation, ids, edges)).toBe(false);
    expect(isPristinePreApproval("approved", pristine, ids, edges)).toBe(false);
  });
  it("serializes owner lifecycle mutation with a crash-releasing kernel lock", async () => {
    const dir = mkdtempSync(join(tmpdir(), "awp-owner-lock-"));
    const manifestPath = join(dir, "journey.json");
    let releaseFirst!: () => void;
    let markStarted!: () => void;
    const started = new Promise<void>((resolveStarted) => {
      markStarted = resolveStarted;
    });
    const held = new Promise<void>((resolveHeld) => {
      releaseFirst = resolveHeld;
    });
    const first = withJourneyMutationLock(manifestPath, async () => {
      markStarted();
      await held;
      return "first";
    });
    await started;
    await expect(withJourneyMutationLock(manifestPath, async () => "unsafe")).rejects.toThrow(
      /already in progress/,
    );
    releaseFirst();
    await expect(first).resolves.toBe("first");
    await expect(withJourneyMutationLock(manifestPath, async () => "reacquired")).resolves.toBe(
      "reacquired",
    );
  });

  it("accepts only valid causal advancement after approval", () => {
    const ids = ["task-1", "task-2", "task-3"] as const;
    const queued = [
      [ids[0], "dispatched"],
      [ids[1], "blocked"],
      [ids[2], "blocked"],
    ] as const;
    expect(
      isCausallyAdvancedApproval(
        "approved",
        "queued",
        "Automatic dependency-legal dispatch after Plan approval",
        queued,
        ids,
      ),
    ).toBe(true);
    expect(
      isCausallyAdvancedApproval(
        "approved",
        "running",
        "Plan graph still has active or dependency-blocked Tasks",
        [
          [ids[0], "executing"],
          [ids[1], "blocked"],
          [ids[2], "blocked"],
        ],
        ids,
      ),
    ).toBe(true);
    expect(
      isCausallyAdvancedApproval(
        "completed",
        "completed",
        "All Tasks in the PlanRevision completed through trusted merge",
        ids.map((id) => [id, "completed"] as const),
        ids,
      ),
    ).toBe(true);
    expect(isCausallyAdvancedApproval("approved", "completed", "wrong", queued, ids)).toBe(false);
    expect(
      isCausallyAdvancedApproval(
        "approved",
        "queued",
        "Automatic dependency-legal dispatch after Plan approval",
        [queued[0], [ids[1], "planned"], queued[2]],
        ids,
      ),
    ).toBe(false);
  });

  it("validation cannot claim acceptance or enable execution", () => {
    const log = vi.spyOn(console, "log").mockImplementation(() => undefined);
    expect(() => main(["--validate-bindings"])).not.toThrow();
    expect(log).toHaveBeenCalledWith(statusSummary());
    expect(() => main([])).toThrow(/explicit --execute/);
    expect(() => main(["--execute", "--validate-bindings"])).toThrow(/separate commands/);
    log.mockRestore();
    expect(() => validateStructure()).not.toThrow();
  });
});
