'use strict';

/**
 * Helper scripts for the `run` command
 * @see module:lib/cli/run
 * @module
 * @private
 */

const fs = require('fs');
const path = require('path');
const debug = require('debug')('mocha:cli:run:helpers');
const watchRun = require('./watch-run');
const collectFiles = require('./collect-files');

const cwd = (exports.cwd = process.cwd());

exports.watchRun = watchRun;

/**
 * Exits Mocha when tests + code under test has finished execution (default)
 * @param {number} code - Exit code; typically # of failures
 * @ignore
 * @private
 */
const exitMochaLater = code => {
  process.on('exit', () => {
    process.exitCode = Math.min(code, 255);
  });
};

/**
 * Exits Mocha when Mocha itself has finished execution, regardless of
 * what the tests or code under test is doing.
 * @param {number} code - Exit code; typically # of failures
 * @ignore
 * @private
 */
const exitMocha = code => {
  const clampedCode = Math.min(code, 255);
  let draining = 0;

  // Eagerly set the process's exit code in case stream.write doesn't
  // execute its callback before the process terminates.
  process.exitCode = clampedCode;

  // flush output for Node.js Windows pipe bug
  // https://github.com/joyent/node/issues/6247 is just one bug example
  // https://github.com/visionmedia/mocha/issues/333 has a good discussion
  const done = () => {
    if (!draining--) {
      process.exit(clampedCode);
    }
  };

  const streams = [process.stdout, process.stderr];

  streams.forEach(stream => {
    // submit empty write request and wait for completion
    draining += 1;
    stream.write('', done);
  });

  done();
};

/**
 * Coerce a comma-delimited string (or array thereof) into a flattened array of
 * strings
 * @param {string|string[]} str - Value to coerce
 * @returns {string[]} Array of strings
 * @private
 */
exports.list = str =>
  Array.isArray(str) ? exports.list(str.join(',')) : str.split(/ *, */);

/**
 * `require()` the modules as required by `--require <require>`
 * @param {string[]} requires - Modules to require
 * @private
 */
exports.handleRequires = (requires = []) => {
  requires.forEach(mod => {
    let modpath = mod;
    if (fs.existsSync(mod, {cwd}) || fs.existsSync(`${mod}.js`, {cwd})) {
      modpath = path.resolve(mod);
      debug(`resolved ${mod} to ${modpath}`);
    }
    require(modpath);
    debug(`loaded require "${mod}"`);
  });
};

/**
 * Collect test files and run mocha instance.
 * @param {Mocha} mocha - Mocha instance
 * @param {Options} [opts] - Command line options
 * @param {boolean} [opts.exit] - Whether or not to force-exit after tests are complete
 * @param {Object} fileCollectParams - Parameters that control test
 *   file collection. See `lib/cli/collect-files.js`.
 * @returns {Runner}
 * @private
 */
exports.singleRun = (mocha, {exit}, fileCollectParams) => {
  const files = collectFiles(fileCollectParams);
  debug('running tests with files', files);
  mocha.files = files;
  return mocha.run(exit ? exitMocha : exitMochaLater);
};

/**
 * Actually run tests
 * @param {Mocha} mocha - Mocha instance
 * @param {Object} opts - Command line options
 * @private
 */
exports.runMocha = (mocha, options) => {
  const {
    watch = false,
    extension = [],
    ui = 'bdd',
    exit = false,
    ignore = [],
    file = [],
    recursive = false,
    sort = false,
    spec = []
  } = options;

  const fileCollectParams = {
    ignore,
    extension,
    file,
    recursive,
    sort,
    spec
  };

  if (watch) {
    watchRun(mocha, {ui}, fileCollectParams);
  } else {
    exports.singleRun(mocha, {exit}, fileCollectParams);
  }
};

/**
 * Used for `--reporter` and `--ui`.  Ensures there's only one, and asserts
 * that it actually exists.
 * @todo XXX This must get run after requires are processed, as it'll prevent
 * interfaces from loading.
 * @param {Object} opts - Options object
 * @param {string} key - Resolvable module name or path
 * @param {Object} [map] - An object perhaps having key `key`
 * @private
 */
exports.validatePlugin = (opts, key, map = {}) => {
  if (Array.isArray(opts[key])) {
    throw new TypeError(`"--${key} <${key}>" can only be specified once`);
  }

  const unknownError = () => new Error(`Unknown "${key}": ${opts[key]}`);

  if (!map[opts[key]]) {
    try {
      opts[key] = require(opts[key]);
    } catch (err) {
      if (err.code === 'MODULE_NOT_FOUND') {
        // Try to load reporters from a path (absolute or relative)
        try {
          opts[key] = require(path.resolve(process.cwd(), opts[key]));
        } catch (err) {
          throw unknownError();
        }
      } else {
        throw unknownError();
      }
    }
  }
};
