#!/usr/bin/env node

import { spawnSync } from "node:child_process";
import { repoRoot, runMoonbitTool } from "./lib/moonbit-tool-runner.mjs";

const smokeCatalogTool = "tools/moui/validate_smoke_catalog";

const usage = () => {
  console.log(`Usage: node scripts/smoke-gate.mjs [options]

Runs or previews suites from the checked-in MoUI smoke gate catalog.

Options:
  --manifest PATH      Smoke gate catalog path. Default: smoke/gates.json
  --tier TIER          Select suites from daily, nightly, or release.
  --suite ID           Select a suite id. Repeatable.
  --platform PLATFORM  Replace <macos|web|windows|linux> placeholders.
  --dry-run            Print the selected suite plan. This is the default.
  --run                Execute selected non-manual suites.
  --allow-manual       Allow commands marked manual to execute with --run.
  --json               Print the selected suite plan as JSON.
  -h, --help           Show this help.`);
};

const parseArgs = argv => {
  const options = {
    manifest: "smoke/gates.json",
    tier: null,
    suites: [],
    platform: null,
    mode: "dry-run",
    allowManual: false,
    json: false,
  };
  for (let index = 0; index < argv.length;) {
    const arg = argv[index];
    switch (arg) {
      case "--manifest":
        if (index + 1 >= argv.length) throw new Error("missing value for --manifest");
        options.manifest = argv[index + 1];
        index += 2;
        break;
      case "--tier":
        if (index + 1 >= argv.length) throw new Error("missing value for --tier");
        options.tier = argv[index + 1];
        index += 2;
        break;
      case "--suite":
        if (index + 1 >= argv.length) throw new Error("missing value for --suite");
        options.suites.push(argv[index + 1]);
        index += 2;
        break;
      case "--platform":
        if (index + 1 >= argv.length) throw new Error("missing value for --platform");
        options.platform = argv[index + 1];
        index += 2;
        break;
      case "--dry-run":
        options.mode = "dry-run";
        index += 1;
        break;
      case "--run":
        options.mode = "run";
        index += 1;
        break;
      case "--allow-manual":
        options.allowManual = true;
        index += 1;
        break;
      case "--json":
        options.json = true;
        index += 1;
        break;
      case "-h":
      case "--help":
        options.mode = "help";
        index += 1;
        break;
      default:
        throw new Error(`unknown argument: ${arg}`);
    }
  }
  return options;
};

const fail = (message, code = 2) => {
  console.error(message);
  process.exit(code);
};

const shellQuote = arg => /^[A-Za-z0-9_./:=@+-]+$/.test(arg) ? arg : JSON.stringify(arg);

const commandToString = argv => argv.map(shellQuote).join(" ");

const planArgs = (options, forceJson = false) => {
  const args = [
    "--repo-root",
    repoRoot,
    "--manifest",
    options.manifest,
    "--gate-plan",
    options.mode === "run" ? "--run" : "--dry-run",
  ];
  if (forceJson || options.json) args.push("--json");
  if (options.tier !== null) args.push("--tier", options.tier);
  for (const suite of options.suites) args.push("--suite", suite);
  if (options.platform !== null) args.push("--platform", options.platform);
  return args;
};

const runPlanner = (options, { forceJson = false, capture = false } = {}) => {
  const result = runMoonbitTool(smokeCatalogTool, planArgs(options, forceJson), {
    encoding: "utf8",
    exitOnFailure: false,
    suppressSuccessStdout: capture,
  });
  if (result.status !== 0 || result.error) {
    process.exit(result.status ?? 1);
  }
  return result.stdout ?? "";
};

const loadRunPlan = options => {
  const output = runPlanner(options, { forceJson: true, capture: true });
  return JSON.parse(output);
};

const runCommand = (label, argv) => {
  console.log(`\n==> ${label}`);
  console.log(commandToString(argv));
  const result = spawnSync(argv[0], argv.slice(1), {
    cwd: repoRoot,
    env: process.env,
    stdio: "inherit",
  });
  if (result.error) {
    throw result.error;
  }
  if (result.signal !== null) {
    fail(`${label} terminated by signal ${result.signal}`, 1);
  }
  if (result.status !== 0) {
    process.exit(result.status ?? 1);
  }
};

const runSuite = (suite, options) => {
  for (const command of suite.commands) {
    if (command.mode === "manual" && !options.allowManual) {
      fail(`${suite.id} contains manual commands; pass --allow-manual to run them`);
    }
    runCommand(`${suite.id}`, command.argv);
  }
  if (suite.result?.type === "manifest") {
    runCommand(`${suite.id} result validator`, suite.result.validator);
  }
};

const main = () => {
  let options;
  try {
    options = parseArgs(process.argv.slice(2));
  } catch (error) {
    console.error(error.message);
    usage();
    process.exit(2);
  }
  if (options.mode === "help") {
    usage();
    return;
  }
  if (options.mode === "dry-run") {
    runPlanner(options);
    return;
  }
  const plan = loadRunPlan(options);
  for (const suite of plan.suites) {
    runSuite(suite, options);
  }
};

main();