import { execFileSync, spawnSync } from "node:child_process";
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { describe, expect, it } from "vitest";
import {
  assertTrustedWorkflowHarness,
  FULL_RELEASE_WAIT_POLL_INTERVAL_MS,
  FULL_RELEASE_WAIT_TIMEOUT_MINUTES,
  parseArgs,
  releaseProfileForTarget,
  releaseEvidenceVerificationArgs,
  releaseEvidenceVerifierPath,
  resolveRemoteTargetRefSha,
  shouldDeleteTemporaryWorkflowRef,
} from "../../scripts/full-release-validation-at-sha.mts";

const SCRIPT_PATH = resolve("scripts/full-release-validation-at-sha.mjs");

function runGit(cwd: string, args: string[]): string {
  return execFileSync("git", args, {
    cwd,
    encoding: "utf8",
    stdio: ["ignore", "pipe", "ignore"],
  }).trim();
}

function createDispatchFixture(options: { workflowSource?: string } = {}) {
  const root = mkdtempSync(join(tmpdir(), "openclaw-release-dispatch-"));
  const origin = join(root, "origin.git");
  const checkout = join(root, "checkout");
  const binDir = join(root, "bin");
  const gitCallsPath = join(root, "git-calls.jsonl");
  const ghCallsPath = join(root, "gh-calls.jsonl");
  const releaseRef = "release/2026.8.1";
  mkdirSync(checkout);
  mkdirSync(binDir);
  writeFileSync(gitCallsPath, "");
  writeFileSync(ghCallsPath, "");

  execFileSync("git", ["init", "--bare", origin], { stdio: "ignore" });
  execFileSync("git", ["init", "-b", "main"], { cwd: checkout, stdio: "ignore" });
  runGit(checkout, ["config", "user.email", "release-test@openclaw.invalid"]);
  runGit(checkout, ["config", "user.name", "OpenClaw Release Test"]);
  mkdirSync(join(checkout, ".github", "workflows"), { recursive: true });
  mkdirSync(join(checkout, "scripts"), { recursive: true });
  writeFileSync(join(checkout, "package.json"), '{"version":"2026.8.1"}\n');
  writeFileSync(
    join(checkout, ".github", "workflows", "full-release-validation.yml"),
    options.workflowSource ??
      `name: Full Release Validation
on:
  workflow_dispatch:
    inputs:
      expected_sha:
        required: false
`,
  );
  writeFileSync(
    join(checkout, "scripts", "release-ci-summary.mjs"),
    'console.log(JSON.stringify({ valid: true, current: { runId: "123" }, root: { runId: "123" }, evidenceReuse: false }));\n',
  );
  runGit(checkout, ["add", "."]);
  runGit(checkout, ["commit", "-m", "test: trusted workflow"]);
  const workflowSha = runGit(checkout, ["rev-parse", "HEAD"]);
  runGit(checkout, ["remote", "add", "origin", origin]);
  runGit(checkout, ["push", "-u", "origin", "main"]);
  runGit(checkout, ["checkout", "-b", releaseRef]);
  writeFileSync(join(checkout, "target.txt"), "release target\n");
  runGit(checkout, ["add", "target.txt"]);
  runGit(checkout, ["commit", "-m", "test: release target"]);
  const targetSha = runGit(checkout, ["rev-parse", "HEAD"]);
  runGit(checkout, ["push", "-u", "origin", releaseRef]);
  runGit(checkout, ["checkout", "main"]);

  const gitPath = join(binDir, "git");
  writeFileSync(
    gitPath,
    `#!${process.execPath}
const fs = require("node:fs");
const { spawnSync } = require("node:child_process");
const args = process.argv.slice(2);
fs.appendFileSync(process.env.MOCK_GIT_CALLS, JSON.stringify(args) + "\\n");
const result = spawnSync("git", args, {
  env: { ...process.env, PATH: process.env.MOCK_REAL_PATH },
  stdio: "inherit",
});
process.exit(result.status ?? 1);
`,
  );
  chmodSync(gitPath, 0o755);

  const ghPath = join(binDir, "gh");
  writeFileSync(
    ghPath,
    `#!${process.execPath}
const fs = require("node:fs");
const args = process.argv.slice(2);
fs.appendFileSync(process.env.MOCK_GH_CALLS, JSON.stringify(args) + "\\n");
if (args[0] === "workflow" && args[1] === "run") {
  console.log("https://github.com/openclaw/openclaw/actions/runs/123");
} else if (args[0] === "api" && args.at(-1).endsWith("/actions/runs/123")) {
  console.log(JSON.stringify({ status: "completed", conclusion: "success", head_sha: process.env.MOCK_WORKFLOW_SHA }));
} else {
  console.error("unexpected gh call: " + args.join(" "));
  process.exit(2);
}
`,
  );
  chmodSync(ghPath, 0o755);

  const run = (extraArgs: string[] = []) =>
    spawnSync(
      process.execPath,
      [SCRIPT_PATH, "--sha", targetSha, "--target-ref", releaseRef, ...extraArgs],
      {
        cwd: checkout,
        encoding: "utf8",
        env: {
          ...process.env,
          MOCK_GH_CALLS: ghCallsPath,
          MOCK_GIT_CALLS: gitCallsPath,
          MOCK_REAL_PATH: process.env.PATH,
          MOCK_WORKFLOW_SHA: workflowSha,
          PATH: `${binDir}:${process.env.PATH}`,
        },
      },
    );
  const readCalls = (path: string): string[][] =>
    readFileSync(path, "utf8")
      .trim()
      .split("\n")
      .filter(Boolean)
      .map((line) => JSON.parse(line) as string[]);

  return {
    checkout,
    cleanup: () => rmSync(root, { force: true, recursive: true }),
    ghCallsPath,
    gitCallsPath,
    origin,
    readCalls,
    releaseRef,
    run,
    targetSha,
    workflowSha,
  };
}

describe("full-release-validation-at-sha", () => {
  it("parses release validation dispatch args", () => {
    expect(
      parseArgs([
        "--sha",
        "abc123",
        "--workflow-sha",
        "origin/main",
        "--target-ref",
        "release/2026.7.1",
        "--keep-branch",
        "--dry-run",
        "-f",
        "provider=anthropic",
        "--",
        "mode=linux",
      ]),
    ).toMatchObject({
      dryRun: true,
      keepBranch: true,
      inputs: {
        mode: "linux",
        provider: "anthropic",
        reuse_evidence: "true",
        fail_fast: "false",
      },
      sha: "abc123",
      targetRef: "release/2026.7.1",
      workflowSha: "origin/main",
    });
  });

  it("accepts documented -f assignments after the option separator", () => {
    expect(
      parseArgs(["--", "-f", "release_profile=full", "-fmode=linux", "provider=anthropic"]).inputs,
    ).toMatchObject({
      mode: "linux",
      provider: "anthropic",
      release_profile: "full",
    });
    expect(() => parseArgs(["--", "-f"])).toThrow("-f requires a value");
  });

  it("infers the release profile from the target package version", () => {
    const readVersion = (version: string) => () => JSON.stringify({ version });

    expect(releaseProfileForTarget("a".repeat(40), readVersion("2026.7.1-beta.4"))).toBe("beta");
    expect(releaseProfileForTarget("a".repeat(40), readVersion("2026.7.1-alpha.4"))).toBe("beta");
    expect(releaseProfileForTarget("a".repeat(40), readVersion("2026.7.1"))).toBe("stable");
    expect(releaseProfileForTarget("a".repeat(40), readVersion("2026.7.1-1"))).toBe("stable");
  });

  it("keeps release context separate from the exact target SHA", () => {
    const source = readFileSync("scripts/full-release-validation-at-sha.mts", "utf8");
    expect(source).toContain("ref: targetBranch");
    expect(source).toContain("target_context_ref: targetContextRef");
    expect(source).toContain(
      'args.inputs.allow_unreleased_changelog ??= args.targetRef ? "false" : "true"',
    );
  });

  it("rejects missing option values", () => {
    expect(() => parseArgs(["--sha", "--dry-run"])).toThrow("--sha requires a value");
    expect(() => parseArgs(["--sha", "-h"])).toThrow("--sha requires a value");
    expect(() => parseArgs(["--workflow-sha", "--dry-run"])).toThrow(
      "--workflow-sha requires a value",
    );
    expect(() => parseArgs(["--workflow-sha", "-h"])).toThrow("--workflow-sha requires a value");
    expect(() => parseArgs(["--target-ref", "--dry-run"])).toThrow("--target-ref requires a value");
    expect(() => parseArgs(["-f", "--dry-run"])).toThrow("-f requires a value");
    expect(() => parseArgs(["-f", "-h"])).toThrow("-f requires a value");
  });

  it("accepts only canonical release branch or tag context", () => {
    expect(parseArgs(["--target-ref", "extended-stable/2026.6.33"]).targetRef).toBe(
      "extended-stable/2026.6.33",
    );
    expect(parseArgs(["--target-ref", "v2026.7.1-beta.5"]).targetRef).toBe("v2026.7.1-beta.5");
    expect(parseArgs(["--target-ref", "v2026.7.1"]).targetRef).toBe("v2026.7.1");
    expect(() => parseArgs(["--target-ref", "feature/not-release"])).toThrow(
      "canonical OpenClaw release branch or tag",
    );
  });

  it("resolves annotated release tags through their peeled commit", () => {
    const calls: string[][] = [];
    const sha = resolveRemoteTargetRefSha("v2026.7.1-beta.5", (args) => {
      calls.push(args);
      return `b6387afd6d2e0f43c2ae98d2d124dbc277f03cca\t${args.at(-1)}`;
    });
    expect(sha).toBe("b6387afd6d2e0f43c2ae98d2d124dbc277f03cca");
    expect(calls).toEqual([["ls-remote", "--tags", "origin", "refs/tags/v2026.7.1-beta.5^{}"]]);
  });

  it("falls back to the direct ref for lightweight release tags", () => {
    const calls: string[][] = [];
    const sha = resolveRemoteTargetRefSha("v2026.7.1", (args) => {
      calls.push(args);
      return args.at(-1)?.endsWith("^{}")
        ? ""
        : "0123456789abcdef0123456789abcdef01234567\trefs/tags/v2026.7.1";
    });
    expect(sha).toBe("0123456789abcdef0123456789abcdef01234567");
    expect(calls).toEqual([
      ["ls-remote", "--tags", "origin", "refs/tags/v2026.7.1^{}"],
      ["ls-remote", "--tags", "origin", "refs/tags/v2026.7.1"],
    ]);
  });

  it("allows exact-target reuse to be disabled for a forced fresh run", () => {
    expect(parseArgs(["-f", "reuse_evidence=false"]).inputs.reuse_evidence).toBe("false");
    expect(() => parseArgs(["-f", "reuse_evidence=maybe"])).toThrow(
      "reuse_evidence must be true or false",
    );
    expect(parseArgs(["-f", "fail_fast=true"]).inputs.fail_fast).toBe("true");
    expect(() => parseArgs(["-f", "fail_fast=maybe"])).toThrow("fail_fast must be true or false");
    expect(() => parseArgs(["-f", "release_profile=minimum"])).toThrow(
      "release_profile must be beta, stable, or full",
    );
    expect(() => parseArgs(["-f", "allow_unreleased_changelog=maybe"])).toThrow(
      "allow_unreleased_changelog must be true or false",
    );
  });

  it("reserves immutable candidate identity inputs for the resolved --sha", () => {
    expect(() => parseArgs(["-f", "ref=other"])).toThrow("reserves the ref input");
    expect(() => parseArgs(["--", "ref=other"])).toThrow("reserves the ref input");
    expect(() => parseArgs(["-f", `expected_sha=${"a".repeat(40)}`])).toThrow(
      "reserves expected_sha",
    );
    expect(() => parseArgs(["--", `expected_sha=${"a".repeat(40)}`])).toThrow(
      "reserves expected_sha",
    );
  });

  it("validates direct and reused runs through the strict evidence verifier", () => {
    expect(releaseEvidenceVerificationArgs("123")).toEqual([
      "--validate-run",
      "123",
      "--trusted-workflow-ref",
      "main",
      "--json",
    ]);
    expect(() => releaseEvidenceVerificationArgs("")).toThrow("positive decimal");
  });

  it("polls the exact workflow run without GraphQL quota use", () => {
    const source = readFileSync("scripts/full-release-validation-at-sha.mts", "utf8");
    expect(FULL_RELEASE_WAIT_TIMEOUT_MINUTES).toBe(720);
    expect(FULL_RELEASE_WAIT_POLL_INTERVAL_MS).toBe(45_000);
    expect(source).toContain("actions/runs/${parentRunId}");
    expect(source).toContain("workflowRun.head_sha !== workflowSha");
    expect(source).toContain("return suite;");
    expect(source).toContain("Date.now() + FULL_RELEASE_WAIT_TIMEOUT_MINUTES * 60_000");
    expect(source).toContain("const remainingMs = deadline - Date.now();");
    expect(source).toContain("Math.min(FULL_RELEASE_WAIT_POLL_INTERVAL_MS, remainingMs)");
    expect(source).toContain(
      "Timed out after ${FULL_RELEASE_WAIT_TIMEOUT_MINUTES} minutes waiting for Full Release Validation",
    );
    expect(source).not.toContain("attempt < 480");
    expect(source).not.toContain('"graphql"');
    expect(source).not.toContain('["run", "watch"');
  });

  it("bounds GitHub reads without applying a timeout to workflow dispatch", () => {
    const source = readFileSync("scripts/full-release-validation-at-sha.mts", "utf8");
    expect(source).toContain("timeout: GH_READ_TIMEOUT_MS");
    expect(source.match(/GH_READ_OPTIONS/gu)).toHaveLength(3);
    expect(source).toContain('const dispatchOutput = run("gh", dispatchArgs');
  });

  it("rejects incomplete trusted release harnesses before dispatch", () => {
    const workflowPath = ".github/workflows/full-release-validation.yml";
    const verifierPath = "scripts/release-ci-summary.mjs";
    const checked: string[] = [];
    expect(
      assertTrustedWorkflowHarness(
        "a".repeat(40),
        (relativePath) => {
          checked.push(relativePath);
          return relativePath === workflowPath || relativePath === verifierPath;
        },
        () => "on:\n  workflow_dispatch:\n    inputs:\n      expected_sha: {}\n",
      ),
    ).toBe(verifierPath);
    expect(checked).toEqual([workflowPath, verifierPath]);
    expect(() => assertTrustedWorkflowHarness("a".repeat(40), () => false)).toThrow(workflowPath);
    expect(() =>
      assertTrustedWorkflowHarness(
        "a".repeat(40),
        (relativePath) => relativePath === workflowPath,
        () => "on:\n  workflow_dispatch:\n    inputs:\n      expected_sha: {}\n",
      ),
    ).toThrow("supported release evidence verifier");
    expect(() =>
      assertTrustedWorkflowHarness(
        "b".repeat(40),
        () => true,
        () => "on:\n  workflow_dispatch:\n    inputs: {}\n",
      ),
    ).toThrow(`Tooling SHA ${"b".repeat(40)} is missing workflow_dispatch input expected_sha`);

    const source = readFileSync("scripts/full-release-validation-at-sha.mts", "utf8");
    expect(source.indexOf("assertTrustedWorkflowHarness(workflowSha);")).toBeLessThan(
      source.indexOf('run("git", ["push", "origin", `${workflowSha}:${remoteBranchRef}`]'),
    );
  });

  it("retains a failed parent workflow ref for GitHub reruns", () => {
    expect(
      shouldDeleteTemporaryWorkflowRef({
        dryRun: false,
        evidenceVerified: false,
        keepBranch: false,
        parentConclusion: "failure",
      }),
    ).toBe(false);
    expect(
      shouldDeleteTemporaryWorkflowRef({
        dryRun: false,
        evidenceVerified: true,
        keepBranch: false,
        parentConclusion: "success",
      }),
    ).toBe(true);
    expect(
      shouldDeleteTemporaryWorkflowRef({
        dryRun: true,
        evidenceVerified: false,
        keepBranch: false,
        parentConclusion: "",
      }),
    ).toBe(true);
    expect(
      shouldDeleteTemporaryWorkflowRef({
        dryRun: false,
        evidenceVerified: false,
        keepBranch: false,
        parentConclusion: "success",
      }),
    ).toBe(false);
  });

  it("pushes an exact target ref, dispatches it, prints the run URL, and cleans both refs", () => {
    const fixture = createDispatchFixture();
    try {
      const result = fixture.run();
      expect(result.status, result.stderr).toBe(0);
      const gitCalls = fixture.readCalls(fixture.gitCallsPath);
      const ghCalls = fixture.readCalls(fixture.ghCallsPath);
      const targetPush = gitCalls.find(
        (args) => args[0] === "push" && args[2]?.includes(":refs/heads/validation/target-"),
      );
      expect(targetPush?.[2]).toMatch(
        new RegExp(
          `^${fixture.targetSha}:refs/heads/validation/target-${fixture.targetSha.slice(0, 12)}-[0-9]+$`,
          "u",
        ),
      );
      const targetBranch = targetPush?.[2]?.split(":refs/heads/")[1];
      const workflowPush = gitCalls.find(
        (args) => args[0] === "push" && args[2]?.includes(":refs/heads/release-ci/"),
      );
      const workflowBranch = workflowPush?.[2]?.split(":refs/heads/")[1];
      expect(workflowPush?.[2]).toMatch(
        new RegExp(
          `^${fixture.workflowSha}:refs/heads/release-ci/${fixture.workflowSha.slice(0, 12)}-[0-9]+$`,
          "u",
        ),
      );
      const dispatch = ghCalls.find((args) => args[0] === "workflow" && args[1] === "run");
      expect(dispatch).toEqual(
        expect.arrayContaining([
          "--ref",
          workflowBranch,
          "-f",
          `ref=${targetBranch}`,
          "-f",
          `expected_sha=${fixture.targetSha}`,
          "-f",
          `target_context_ref=${fixture.releaseRef}`,
        ]),
      );
      expect(result.stdout).toContain(`Validation SHA: ${fixture.targetSha}`);
      expect(result.stdout).toContain(`Tooling SHA: ${fixture.workflowSha}`);
      expect(result.stdout).toContain(
        "Parent run: https://github.com/openclaw/openclaw/actions/runs/123",
      );
      expect(result.stdout.indexOf("Parent run:")).toBeLessThan(
        result.stdout.indexOf("Parent run status:"),
      );
      expect(gitCalls).toContainEqual([
        "push",
        "origin",
        `:refs/heads/${workflowBranch}`,
        `:refs/heads/${targetBranch}`,
      ]);
      expect(runGit(fixture.origin, ["for-each-ref", "--format=%(refname)", "refs/heads"])).toBe(
        "refs/heads/main\nrefs/heads/release/2026.8.1",
      );
    } finally {
      fixture.cleanup();
    }
  });

  it("rejects pinned old-schema tooling before either remote ref is pushed", () => {
    const fixture = createDispatchFixture({
      workflowSource: "name: Full Release Validation\non:\n  workflow_dispatch:\n",
    });
    try {
      const result = fixture.run(["--workflow-sha", fixture.workflowSha]);
      expect(result.status).toBe(1);
      expect(result.stderr).toContain(`Tooling SHA ${fixture.workflowSha}`);
      expect(result.stderr).toContain("missing workflow_dispatch input expected_sha");
      expect(fixture.readCalls(fixture.gitCallsPath).filter((args) => args[0] === "push")).toEqual(
        [],
      );
      expect(readFileSync(fixture.ghCallsPath, "utf8")).toBe("");
    } finally {
      fixture.cleanup();
    }
  });

  it("keeps both temporary refs with --keep-branch", () => {
    const fixture = createDispatchFixture();
    try {
      const result = fixture.run(["--keep-branch"]);
      expect(result.status, result.stderr).toBe(0);
      const gitCalls = fixture.readCalls(fixture.gitCallsPath);
      expect(
        gitCalls.some(
          (args) => args[0] === "push" && args.slice(2).some((value) => value.startsWith(":")),
        ),
      ).toBe(false);
      const remoteRefs = runGit(fixture.origin, [
        "for-each-ref",
        "--format=%(refname)",
        "refs/heads/release-ci",
        "refs/heads/validation",
      ]).split("\n");
      expect(remoteRefs).toHaveLength(2);
      expect(remoteRefs).toEqual(
        expect.arrayContaining([
          expect.stringMatching(/^refs\/heads\/release-ci\//u),
          expect.stringMatching(/^refs\/heads\/validation\/target-/u),
        ]),
      );
    } finally {
      fixture.cleanup();
    }
  });

  it("fails clearly before dispatch when the target SHA is absent after the named fetch", () => {
    const fixture = createDispatchFixture();
    try {
      const missingSha = "f".repeat(40);
      const result = spawnSync(
        process.execPath,
        [SCRIPT_PATH, "--sha", missingSha, "--target-ref", fixture.releaseRef],
        {
          cwd: fixture.checkout,
          encoding: "utf8",
          env: {
            ...process.env,
            MOCK_GH_CALLS: fixture.ghCallsPath,
            MOCK_GIT_CALLS: fixture.gitCallsPath,
            MOCK_REAL_PATH: process.env.PATH,
            MOCK_WORKFLOW_SHA: fixture.workflowSha,
            PATH: `${join(fixture.checkout, "..", "bin")}:${process.env.PATH}`,
          },
        },
      );
      expect(result.status).toBe(1);
      const failedReasons = result.stderr
        .trim()
        .split("\n")
        .filter((line) => line.startsWith("[full-release-validation] FAILED:"));
      expect(failedReasons).toEqual([
        `[full-release-validation] FAILED: Target SHA ${missingSha} is not available locally after fetching ${fixture.releaseRef}`,
      ]);
      expect(result.stderr.trim().split("\n").at(-1)).toBe(
        "[full-release-validation] FAILED (exit 1)",
      );
      expect(readFileSync(fixture.ghCallsPath, "utf8")).toBe("");
    } finally {
      fixture.cleanup();
    }
  });

  it("supports current and legacy verifier locations in trusted workflow checkouts", () => {
    const root = mkdtempSync(join(tmpdir(), "openclaw-release-verifier-path-"));
    try {
      const legacy = join(
        root,
        ".agents",
        "skills",
        "release-openclaw-ci",
        "scripts",
        "release-ci-summary.mjs",
      );
      mkdirSync(join(legacy, ".."), { recursive: true });
      writeFileSync(legacy, "");
      expect(releaseEvidenceVerifierPath(root)).toBe(legacy);

      const current = join(root, "scripts", "release-ci-summary.mjs");
      mkdirSync(join(current, ".."), { recursive: true });
      writeFileSync(current, "");
      expect(releaseEvidenceVerifierPath(root)).toBe(current);
    } finally {
      rmSync(root, { force: true, recursive: true });
    }
  });
});
