#!/usr/bin/env node

"use strict";

/**
 * This wrapper executable checks for known node flags and appends them when found,
 * before invoking the "real" executable (`lib/cli/cli.js`)
 *
 * @module bin/mocha
 * @private
 */

const os = require("node:os");
const { loadOptions } = require("../lib/cli/options");
const {
  unparseNodeFlags,
  isNodeFlag,
  impliesNoTimeouts,
} = require("../lib/cli/node-flags");
const unparse = require("yargs-unparser");
const debug = require("debug")("mocha:cli:mocha");
const { aliases } = require("../lib/cli/run-option-metadata");

const mochaArgs = {};
const nodeArgs = {};
const SIGNAL_OFFSET = 128;
let hasInspect = false;

const opts = loadOptions(process.argv.slice(2));
debug("loaded opts", opts);

/**
 * Given option/command `value`, disable timeouts if applicable
 * @param {string} [value] - Value to check
 * @ignore
 */
const disableTimeouts = (value) => {
  if (impliesNoTimeouts(value)) {
    debug("option %s disabled timeouts", value);
    mochaArgs.timeout = 0;
  }
};

/**
 * If `value` begins with `v8-` and is not explicitly `v8-options`, remove prefix
 * @param {string} [value] - Value to check
 * @returns {string} `value` with prefix (maybe) removed
 * @ignore
 */
const trimV8Option = (value) =>
  value !== "v8-options" && /^v8-/.test(value) ? value.slice(3) : value;

// sort options into "node" and "mocha" buckets
Object.keys(opts).forEach((opt) => {
  if (isNodeFlag(opt)) {
    nodeArgs[trimV8Option(opt)] = opts[opt];
  } else {
    mochaArgs[opt] = opts[opt];
  }
});

// disable 'timeout' for debugFlags
Object.keys(nodeArgs).forEach((opt) => disableTimeouts(opt));
mochaArgs["node-option"] &&
  mochaArgs["node-option"].forEach((opt) => disableTimeouts(opt));

// Native debugger handling
// see https://nodejs.org/api/debugger.html#debugger_debugger
// look for 'inspect' that would launch this debugger,
// remove it from Mocha's opts and prepend it to Node's opts.
// A deprecation warning will be printed by node, if applicable.
// (mochaArgs._ are "positional" arguments, not prefixed with - or --)
if (mochaArgs._) {
  const i = mochaArgs._.findIndex((val) => val === "inspect");
  if (i > -1) {
    mochaArgs._.splice(i, 1);
    disableTimeouts("inspect");
    hasInspect = true;
  }
}

if (mochaArgs["node-option"] || Object.keys(nodeArgs).length || hasInspect) {
  const { spawn } = require("node:child_process");
  const mochaPath = require.resolve("../lib/cli/cli.js");

  const nodeArgv =
    (mochaArgs["node-option"] &&
      mochaArgs["node-option"].map((v) => "--" + v)) ||
    unparseNodeFlags(nodeArgs);

  if (hasInspect) nodeArgv.unshift("inspect");
  delete mochaArgs["node-option"];

  debug("final node argv", nodeArgv);

  const args = [].concat(
    nodeArgv,
    mochaPath,
    unparse(mochaArgs, { alias: aliases }),
  );

  debug(
    "forking child process via command: %s %s",
    process.execPath,
    args.join(" "),
  );

  const proc = spawn(process.execPath, args, {
    stdio: "inherit",
  });

  proc.on("exit", (code, signal) => {
    process.on("exit", () => {
      if (signal) {
        signal =
          typeof signal === "string" ? os.constants.signals[signal] : signal;
        if (mochaArgs["posix-exit-codes"] === true) {
          process.exitCode = SIGNAL_OFFSET + signal;
        }
        process.kill(process.pid, signal);
      } else {
        process.exit(Math.min(code, mochaArgs["posix-exit-codes"] ? 1 : 255));
      }
    });
  });

  // terminate children.
  process.on("SIGINT", () => {
    // XXX: a previous comment said this would abort the runner, but I can't see that it does
    // anything with the default runner.
    debug("main process caught SIGINT");
    proc.kill("SIGINT");
    // if running in parallel mode, we will have a proper SIGINT handler, so the below won't
    // be needed.
    if (!args.parallel || args.jobs < 2) {
      // win32 does not support SIGTERM, so use next best thing.
      if (os.platform() === "win32") {
        proc.kill("SIGKILL");
      } else {
        // using SIGKILL won't cleanly close the output streams, which can result
        // in cut-off text or a befouled terminal.
        debug("sending SIGTERM to child process");
        proc.kill("SIGTERM");
      }
    }
  });
} else {
  debug("running Mocha in-process");
  require("../lib/cli/cli").main([], mochaArgs);
}
