import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { createServer as createNetServer } from "node:net";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import {
  buildWindowsDevUpdateToolchainCheckScript,
  buildWindowsFreshShellVersionCheckScript,
  buildWindowsPathBootstrapScript,
  canConnectToLoopbackPort,
  buildDiscordSmokeGuildsConfig,
  buildRealUpdateEnv,
  isImmutableReleaseRef,
  looksLikeReleaseVersionRef,
  normalizeRequestedRef,
  normalizeWindowsCommandShimPath,
  normalizeWindowsInstalledCliPath,
  parseArgs,
  packageHasScript,
  readInstalledVersion,
  readRunnerOverrideEnv,
  resolveExplicitBaselineVersion,
  resolveDevUpdateVerificationRef,
  resolveInstalledPrefixDirFromCliPath,
  resolvePublishedInstallerUrl,
  resolveRequestedSuites,
  resolveRunnerMatrix,
  resolveStaticFileContentType,
  shouldExerciseManagedGatewayLifecycleAfterInstall,
  shouldSkipInstallerDaemonHealthCheck,
  shouldStopManagedGatewayBeforeManualFallback,
  shouldRunMainChannelDevUpdate,
  shouldUseManagedGatewayForInstallerRuntime,
  shouldUseManagedGatewayService,
  verifyDevUpdateStatus,
} from "../../scripts/openclaw-cross-os-release-checks.ts";

describe("scripts/openclaw-cross-os-release-checks", () => {
  it("treats explicit empty-string args as values instead of boolean flags", () => {
    expect(parseArgs(["--ubuntu-runner", "", "--mode", "both"])).toEqual({
      "ubuntu-runner": "",
      mode: "both",
    });
  });

  it("detects release refs and keeps branch refs out of release-only logic", () => {
    expect(looksLikeReleaseVersionRef("2026.4.5")).toBe(true);
    expect(looksLikeReleaseVersionRef("refs/tags/v2026.4.5-beta.1")).toBe(true);
    expect(looksLikeReleaseVersionRef("v2026.4.5-beta.1")).toBe(true);
    expect(looksLikeReleaseVersionRef("v2026.4.7-1")).toBe(true);
    expect(looksLikeReleaseVersionRef("main")).toBe(false);
    expect(looksLikeReleaseVersionRef("codex/cross-os-release-checks")).toBe(false);
  });

  it("normalizes full Git refs before suite and update decisions", () => {
    expect(normalizeRequestedRef(" refs/heads/main ")).toBe("main");
    expect(normalizeRequestedRef("refs/tags/v2026.4.14")).toBe("v2026.4.14");
    expect(isImmutableReleaseRef("refs/tags/test-tag")).toBe(true);
    expect(resolveRequestedSuites("both", "refs/tags/v2026.4.14")).toEqual([
      "packaged-fresh",
      "installer-fresh",
      "packaged-upgrade",
    ]);
    expect(resolveRequestedSuites("both", "refs/tags/test-tag")).toEqual([
      "packaged-fresh",
      "installer-fresh",
      "packaged-upgrade",
    ]);
    expect(shouldRunMainChannelDevUpdate("refs/heads/main")).toBe(true);
    expect(shouldRunMainChannelDevUpdate("refs/tags/main")).toBe(false);
  });

  it("skips the dev-update suite for immutable release refs", () => {
    expect(resolveRequestedSuites("both", "v2026.4.5")).toEqual([
      "packaged-fresh",
      "installer-fresh",
      "packaged-upgrade",
    ]);
  });

  it("skips dev-update for non-main branch validation refs", () => {
    expect(resolveRequestedSuites("both", "codex/cross-os-release-checks")).toEqual([
      "packaged-fresh",
      "installer-fresh",
      "packaged-upgrade",
    ]);
  });

  it("keeps dev-update enabled for main validation refs", () => {
    expect(resolveRequestedSuites("both", "main")).toEqual([
      "packaged-fresh",
      "installer-fresh",
      "packaged-upgrade",
      "dev-update",
    ]);
  });

  it("skips dev-update for pinned commit refs", () => {
    expect(resolveRequestedSuites("both", "08753a1d793c040b101c8a26c43445dbbab14995")).toEqual([
      "packaged-fresh",
      "installer-fresh",
      "packaged-upgrade",
    ]);
  });

  it("builds a suite-aware runner matrix with the beefy Windows default", () => {
    const matrix = resolveRunnerMatrix({
      mode: "both",
      ref: "main",
      ubuntuRunner: "",
      windowsRunner: "",
      macosRunner: "",
      varUbuntuRunner: "",
      varWindowsRunner: "",
      varMacosRunner: "",
    });

    expect(matrix.include).toHaveLength(12);
    expect(matrix.include).toContainEqual(
      expect.objectContaining({
        os_id: "windows",
        runner: "blacksmith-32vcpu-windows-2025",
        suite: "dev-update",
        lane: "upgrade",
      }),
    );
    expect(matrix.include).toContainEqual(
      expect.objectContaining({
        os_id: "ubuntu",
        suite: "installer-fresh",
        lane: "fresh",
      }),
    );
  });

  it("can rebuild the Windows PATH with or without current-process entries", () => {
    expect(buildWindowsPathBootstrapScript()).toContain("@($userPath, $machinePath, $env:Path)");
    const persistedOnlyScript = buildWindowsPathBootstrapScript({
      includeCurrentProcessPath: false,
    });
    expect(persistedOnlyScript).toContain("@($userPath, $machinePath)");
    expect(persistedOnlyScript).not.toContain("@($userPath, $machinePath, $env:Path)");
  });

  it("prefers the freshly installed Windows CLI under npm's prefix before PATH lookup", () => {
    const script = buildWindowsFreshShellVersionCheckScript({
      expectedNeedle: "2026.4.14",
    });
    expect(script).toContain(buildWindowsPathBootstrapScript());
    expect(script).not.toContain(
      buildWindowsPathBootstrapScript({ includeCurrentProcessPath: false }),
    );
    expect(script).toContain("Get-Command npm.cmd -ErrorAction SilentlyContinue");
    expect(script).toContain('$env:Path = "$npmPrefix;$env:Path"');
    expect(script).toContain("(Join-Path $npmPrefix 'openclaw.cmd')");
    expect(script).toContain("$cmd = Get-Command openclaw -ErrorAction Stop");
  });

  it("keeps Windows dev-update toolchain checks compatible with setup-node PATH shims", () => {
    const script = buildWindowsDevUpdateToolchainCheckScript();
    expect(script).toContain(buildWindowsPathBootstrapScript());
    expect(script).not.toContain(
      buildWindowsPathBootstrapScript({ includeCurrentProcessPath: false }),
    );
    expect(script).toContain("$pnpmPath = Resolve-CommandPath 'pnpm'");
    expect(script).toContain("$corepackPath = Resolve-CommandPath 'corepack'");
    expect(script).toContain("$npmPath = Resolve-CommandPath 'npm'");
  });

  it("prefers workflow-injected runner override env names over legacy ones", () => {
    expect(
      readRunnerOverrideEnv({
        VAR_UBUNTU_RUNNER: "workflow-linux",
        VAR_WINDOWS_RUNNER: "workflow-windows",
        VAR_MACOS_RUNNER: "workflow-macos",
        OPENCLAW_RELEASE_CHECKS_UBUNTU_RUNNER: "legacy-linux",
        OPENCLAW_RELEASE_CHECKS_WINDOWS_RUNNER: "legacy-windows",
        OPENCLAW_RELEASE_CHECKS_MACOS_RUNNER: "legacy-macos",
      }),
    ).toEqual({
      varUbuntuRunner: "workflow-linux",
      varWindowsRunner: "workflow-windows",
      varMacosRunner: "workflow-macos",
    });
  });

  it("falls back to legacy runner override env names when workflow vars are blank", () => {
    expect(
      readRunnerOverrideEnv({
        VAR_UBUNTU_RUNNER: "",
        VAR_WINDOWS_RUNNER: " ",
        VAR_MACOS_RUNNER: "",
        OPENCLAW_RELEASE_CHECKS_UBUNTU_RUNNER: "legacy-linux",
        OPENCLAW_RELEASE_CHECKS_WINDOWS_RUNNER: "legacy-windows",
        OPENCLAW_RELEASE_CHECKS_MACOS_RUNNER: "legacy-macos",
      }),
    ).toEqual({
      varUbuntuRunner: "legacy-linux",
      varWindowsRunner: "legacy-windows",
      varMacosRunner: "legacy-macos",
    });
  });

  it("serves installer scripts as UTF-8 text and package payloads as binary", () => {
    expect(resolveStaticFileContentType("scripts/install.sh")).toBe("text/plain; charset=utf-8");
    expect(resolveStaticFileContentType("scripts/install.ps1")).toBe("text/plain; charset=utf-8");
    expect(resolveStaticFileContentType("openclaw-2026.4.14.tgz")).toBe("application/octet-stream");
  });

  it("uses the published installer URLs for native installer lanes", () => {
    expect(resolvePublishedInstallerUrl("darwin")).toBe("https://openclaw.ai/install.sh");
    expect(resolvePublishedInstallerUrl("linux")).toBe("https://openclaw.ai/install.sh");
    expect(resolvePublishedInstallerUrl("win32")).toBe("https://openclaw.ai/install.ps1");
  });

  it("uses managed gateway services only on native Windows runners", () => {
    expect(shouldUseManagedGatewayService("win32")).toBe(true);
    expect(shouldUseManagedGatewayService("darwin")).toBe(false);
    expect(shouldUseManagedGatewayService("linux")).toBe(false);
  });

  it("keeps the Windows installer runtime on the manual gateway after managed lifecycle checks", () => {
    expect(shouldExerciseManagedGatewayLifecycleAfterInstall("win32")).toBe(true);
    expect(shouldUseManagedGatewayForInstallerRuntime("win32")).toBe(false);
    expect(shouldExerciseManagedGatewayLifecycleAfterInstall("darwin")).toBe(false);
    expect(shouldUseManagedGatewayForInstallerRuntime("darwin")).toBe(false);
  });

  it("stops the managed gateway before the manual fallback only on Windows", () => {
    expect(shouldStopManagedGatewayBeforeManualFallback("win32")).toBe(true);
    expect(shouldStopManagedGatewayBeforeManualFallback("darwin")).toBe(false);
    expect(shouldStopManagedGatewayBeforeManualFallback("linux")).toBe(false);
  });

  it("skips daemon health during installed onboarding only on native Windows", () => {
    expect(shouldSkipInstallerDaemonHealthCheck("win32")).toBe(true);
    expect(shouldSkipInstallerDaemonHealthCheck("darwin")).toBe(false);
    expect(shouldSkipInstallerDaemonHealthCheck("linux")).toBe(false);
  });

  it("normalizes Windows installed CLI paths to the cmd shim", () => {
    expect(
      normalizeWindowsInstalledCliPath(
        String.raw`C:\Users\runner\AppData\Roaming\npm\openclaw.ps1`,
      ),
    ).toBe(String.raw`C:\Users\runner\AppData\Roaming\npm\openclaw.cmd`);
    expect(
      normalizeWindowsInstalledCliPath(
        String.raw`C:\Users\runner\AppData\Roaming\npm\openclaw.cmd`,
      ),
    ).toBe(String.raw`C:\Users\runner\AppData\Roaming\npm\openclaw.cmd`);
  });

  it("normalizes generic Windows PowerShell shims to cmd shims", () => {
    expect(normalizeWindowsCommandShimPath(String.raw`C:\Program Files\nodejs\pnpm.ps1`)).toBe(
      String.raw`C:\Program Files\nodejs\pnpm.cmd`,
    );
    expect(normalizeWindowsCommandShimPath(String.raw`C:\Program Files\nodejs\corepack.ps1`)).toBe(
      String.raw`C:\Program Files\nodejs\corepack.cmd`,
    );
    expect(normalizeWindowsCommandShimPath(String.raw`C:\Program Files\nodejs\node.exe`)).toBe(
      String.raw`C:\Program Files\nodejs\node.exe`,
    );
  });

  it("derives the installed prefix from resolved CLI paths", () => {
    expect(
      resolveInstalledPrefixDirFromCliPath(
        String.raw`C:\Users\runner\AppData\Roaming\npm\openclaw.ps1`,
        "win32",
      ),
    ).toBe(String.raw`C:\Users\runner\AppData\Roaming\npm`);
    expect(
      resolveInstalledPrefixDirFromCliPath("/Users/runner/.npm-global/bin/openclaw", "darwin"),
    ).toBe("/Users/runner/.npm-global");
  });

  it("detects whether a managed gateway listener is still reachable on loopback", async () => {
    const server = createNetServer();
    await new Promise((resolvePromise) => {
      server.listen(0, "127.0.0.1", resolvePromise);
    });
    const address = server.address();
    const port = typeof address === "object" && address ? address.port : 0;
    expect(await canConnectToLoopbackPort(port)).toBe(true);
    await new Promise((resolvePromise) => {
      server.close(resolvePromise);
    });
    expect(await canConnectToLoopbackPort(port)).toBe(false);
  });

  it("writes Discord smoke config using the strict guild channel schema", () => {
    expect(buildDiscordSmokeGuildsConfig("guild-123", "channel-456")).toEqual({
      "guild-123": {
        channels: {
          "channel-456": {
            enabled: true,
            requireMention: false,
          },
        },
      },
    });
  });

  it("keeps the dev-update lane for main only", () => {
    expect(shouldRunMainChannelDevUpdate("main")).toBe(true);
    expect(shouldRunMainChannelDevUpdate("08753a1d793c040b101c8a26c43445dbbab14995")).toBe(false);
    expect(shouldRunMainChannelDevUpdate(" codex/cross-os-release-checks-full-native-e2e ")).toBe(
      false,
    );
    expect(shouldRunMainChannelDevUpdate("v2026.4.14")).toBe(false);
  });

  it("verifies main dev updates against the prepared source sha when available", () => {
    expect(resolveDevUpdateVerificationRef("main")).toBe("main");
    expect(
      resolveDevUpdateVerificationRef("main", "08753a1d793c040b101c8a26c43445dbbab14995"),
    ).toBe("08753a1d793c040b101c8a26c43445dbbab14995");
    expect(
      resolveDevUpdateVerificationRef(
        "refs/heads/main",
        "08753a1d793c040b101c8a26c43445dbbab14995",
      ),
    ).toBe("08753a1d793c040b101c8a26c43445dbbab14995");
    expect(resolveDevUpdateVerificationRef("codex/cross-os-release-checks-full-native-e2e")).toBe(
      "codex/cross-os-release-checks-full-native-e2e",
    );
  });

  it("drops the bundled plugin postinstall disable flag for real updater calls", () => {
    expect(
      buildRealUpdateEnv({
        FOO: "bar",
        OPENCLAW_DISABLE_BUNDLED_PLUGIN_POSTINSTALL: "1",
      }),
    ).toEqual({
      FOO: "bar",
    });
  });

  it("only treats pinned baseline specs as exact installer version assertions", () => {
    expect(resolveExplicitBaselineVersion("")).toBe("");
    expect(resolveExplicitBaselineVersion("openclaw@latest")).toBe("");
    expect(resolveExplicitBaselineVersion("openclaw@2026.4.10")).toBe("2026.4.10");
    expect(resolveExplicitBaselineVersion("2026.4.10")).toBe("2026.4.10");
  });

  it("reads an installed baseline version without requiring build metadata", () => {
    const prefixDir = mkdtempSync(join(tmpdir(), "openclaw-cross-os-installed-version-"));
    try {
      const packageRoot =
        process.platform === "win32"
          ? join(prefixDir, "node_modules", "openclaw")
          : join(prefixDir, "lib", "node_modules", "openclaw");
      mkdirSync(packageRoot, { recursive: true });
      writeFileSync(
        join(packageRoot, "package.json"),
        JSON.stringify({
          name: "openclaw",
          version: "2026.4.10",
        }),
        "utf8",
      );

      expect(readInstalledVersion(prefixDir)).toBe("2026.4.10");
    } finally {
      rmSync(prefixDir, { recursive: true, force: true });
    }
  });

  it("treats missing package scripts as optional in older refs", () => {
    const packageRoot = mkdtempSync(join(tmpdir(), "openclaw-cross-os-scripts-"));
    try {
      writeFileSync(
        join(packageRoot, "package.json"),
        JSON.stringify({
          name: "openclaw",
          scripts: {
            build: "pnpm build",
          },
        }),
        "utf8",
      );

      expect(packageHasScript(packageRoot, "build")).toBe(true);
      expect(packageHasScript(packageRoot, "ui:build")).toBe(false);
    } finally {
      rmSync(packageRoot, { recursive: true, force: true });
    }
  });

  it("accepts a git main dev-channel update status payload", () => {
    expect(() =>
      verifyDevUpdateStatus(
        JSON.stringify({
          update: {
            installKind: "git",
            git: {
              branch: "main",
            },
          },
          channel: {
            value: "dev",
          },
        }),
      ),
    ).not.toThrow();
  });

  it("accepts a git dev-channel payload for a requested non-main branch", () => {
    expect(() =>
      verifyDevUpdateStatus(
        JSON.stringify({
          update: {
            installKind: "git",
            git: {
              branch: "codex/cross-os-release-checks-full-native-e2e",
              sha: "08753a1d793c040b101c8a26c43445dbbab14995",
            },
          },
          channel: {
            value: "dev",
          },
        }),
        { ref: "codex/cross-os-release-checks-full-native-e2e" },
      ),
    ).not.toThrow();
  });

  it("accepts a git dev-channel payload pinned to a prepared source sha", () => {
    expect(() =>
      verifyDevUpdateStatus(
        JSON.stringify({
          update: {
            installKind: "git",
            git: {
              branch: "main",
              sha: "08753a1d793c040b101c8a26c43445dbbab14995",
            },
          },
          channel: {
            value: "dev",
          },
        }),
        { ref: "08753a1d793c040b101c8a26c43445dbbab14995" },
      ),
    ).not.toThrow();
  });

  it("accepts uppercase requested commit shas when update status reports lowercase", () => {
    expect(() =>
      verifyDevUpdateStatus(
        JSON.stringify({
          update: {
            installKind: "git",
            git: {
              sha: "08753a1d793c040b101c8a26c43445dbbab14995",
            },
          },
          channel: {
            value: "dev",
          },
        }),
        { ref: "08753A1D793C040B101C8A26C43445DBBAB14995" },
      ),
    ).not.toThrow();
  });

  it("rejects update status payloads that are not on dev/main git", () => {
    expect(() =>
      verifyDevUpdateStatus(
        JSON.stringify({
          update: {
            installKind: "package",
            git: {
              branch: "release",
            },
          },
          channel: {
            value: "stable",
          },
        }),
      ),
    ).toThrow("git install");
  });
});
