/**
 * A worker process.  Consumes {@link module:reporters/parallel-buffered} reporter.
 * @module worker
 * @private
 */

"use strict";

/**
 * @typedef {import('../types.d.ts').BufferedEvent} BufferedEvent
 * @typedef {import('../types.d.ts').MochaOptions} MochaOptions
 */

const {
  createInvalidArgumentTypeError,
  createInvalidArgumentValueError,
} = require("../errors");
const workerpool = require("workerpool");
const Mocha = require("../mocha");
const { handleRequires, validateLegacyPlugin } = require("../cli/run-helpers");
const d = require("debug");
const debug = d.debug(`mocha:parallel:worker:${process.pid}`);
const isDebugEnabled = d.enabled(`mocha:parallel:worker:${process.pid}`);
const { serialize } = require("./serializer");
const { setInterval, clearInterval } = global;

let rootHooks;

if (workerpool.isMainThread) {
  throw new Error(
    "This script is intended to be run as a worker (by the `workerpool` package).",
  );
}

/**
 * Initializes some stuff on the first call to {@link run}.
 *
 * Handles `--require` and `--ui`.  Does _not_ handle `--reporter`,
 * as only the `Buffered` reporter is used.
 *
 * **This function only runs once per worker**; it overwrites itself with a no-op
 * before returning.
 *
 * @param {MochaOptions} argv - Command-line options
 */
let bootstrap = async (argv) => {
  // globalSetup and globalTeardown do not run in workers
  const plugins = await handleRequires(argv.require, {
    ignoredPlugins: ["mochaGlobalSetup", "mochaGlobalTeardown"],
  });
  validateLegacyPlugin(argv, "ui", Mocha.interfaces);

  rootHooks = plugins.rootHooks;
  bootstrap = () => {};
  debug("bootstrap(): finished with args: %O", argv);
};

/**
 * Runs a single test file in a worker thread.
 * @param {string} filepath - Filepath of test file
 * @param {string} [serializedOptions] - **Serialized** options. This string will be eval'd!
 * @see https://npm.im/serialize-javascript
 * @returns {Promise<{failures: number, events: BufferedEvent[]}>} - Test
 * failure count and list of events.
 */
async function run(filepath, serializedOptions = "{}") {
  if (!filepath) {
    throw createInvalidArgumentTypeError(
      'Expected a non-empty "filepath" argument',
      "file",
      "string",
    );
  }

  debug("run(): running test file %s", filepath);

  if (typeof serializedOptions !== "string") {
    throw createInvalidArgumentTypeError(
      "run() expects second parameter to be a string which was serialized by the `serialize-javascript` module",
      "serializedOptions",
      "string",
    );
  }
  let argv;
  try {
    // eslint-disable-next-line no-eval
    argv = eval("(" + serializedOptions + ")");
  } catch (err) {
    throw createInvalidArgumentValueError(
      "run() was unable to deserialize the options",
      "serializedOptions",
      serializedOptions,
    );
  }

  const opts = Object.assign({ ui: "bdd" }, argv, {
    // if this was true, it would cause infinite recursion.
    parallel: false,
    // this doesn't work in parallel mode
    forbidOnly: true,
    // it's useful for a Mocha instance to know if it's running in a worker process.
    isWorker: true,
  });

  await bootstrap(opts);

  opts.rootHooks = rootHooks;

  const mocha = new Mocha(opts).addFile(filepath);

  try {
    await mocha.loadFilesAsync();
  } catch (err) {
    debug("run(): could not load file %s: %s", filepath, err);
    throw err;
  }

  return new Promise((resolve, reject) => {
    let debugInterval;
    /* istanbul ignore next */
    if (isDebugEnabled) {
      debugInterval = setInterval(() => {
        debug("run(): still running %s...", filepath);
      }, 5000).unref();
    }
    mocha.run((result) => {
      // Runner adds these; if we don't remove them, we'll get a leak.
      process.removeAllListeners("uncaughtException");
      process.removeAllListeners("unhandledRejection");

      try {
        const serialized = serialize(result);
        debug(
          "run(): completed run with %d test failures; returning to main process",
          typeof result.failures === "number" ? result.failures : 0,
        );
        resolve(serialized);
      } catch (err) {
        // TODO: figure out exactly what the sad path looks like here.
        // rejection should only happen if an error is "unrecoverable"
        debug("run(): serialization failed; rejecting: %O", err);
        reject(err);
      } finally {
        clearInterval(debugInterval);
      }
    });
  });
}

// this registers the `run` function.
workerpool.worker({ run });

debug("started worker process");

// for testing
exports.run = run;
