'use strict';

/**

* Main entry point for handling filesystem-based configuration,
* whether that's `mocha.opts` or a config file or `package.json` or whatever.
* @module
*/

const fs = require('fs'); const ansi = require('ansi-colors'); const yargsParser = require('yargs-parser'); const {types, aliases} = require('./run-option-metadata'); const {ONE_AND_DONE_ARGS} = require('./one-and-dones'); const mocharc = require('../mocharc.json'); const {list} = require('./run-helpers'); const {loadConfig, findConfig} = require('./config'); const findUp = require('find-up'); const {deprecate} = require('../utils'); const debug = require('debug')('mocha:cli:options'); const {isNodeFlag} = require('./node-flags');

/**

* The `yargs-parser` namespace
* @external yargsParser
* @see {@link https://npm.im/yargs-parser}
*/

/**

* An object returned by a configured `yargs-parser` representing arguments
* @memberof external:yargsParser
* @interface Arguments
*/

/**

* Base yargs parser configuration
* @private
*/

const YARGS_PARSER_CONFIG = {

'combine-arrays': true,
'short-option-groups': false,
'dot-notation': false

};

/**

* This is the config pulled from the `yargs` property of Mocha's
* `package.json`, but it also disables camel case expansion as to
* avoid outputting non-canonical keynames, as we need to do some
* lookups.
* @private
* @ignore
*/

const configuration = Object.assign({}, YARGS_PARSER_CONFIG, {

'camel-case-expansion': false

});

/**

* This is a really fancy way to:
* - ensure unique values for `array`-type options
* - use its array's last element for `boolean`/`number`/`string`- options given multiple times
* This is passed as the `coerce` option to `yargs-parser`
* @private
* @ignore
*/

const coerceOpts = Object.assign(

types.array.reduce(
  (acc, arg) =>
    Object.assign(acc, {[arg]: v => Array.from(new Set(list(v)))}),
  {}
),
types.boolean
  .concat(types.string, types.number)
  .reduce(
    (acc, arg) =>
      Object.assign(acc, {[arg]: v => (Array.isArray(v) ? v.pop() : v)}),
    {}
  )

);

/**

* We do not have a case when multiple arguments are ever allowed after a flag
* (e.g., `--foo bar baz quux`), so we fix the number of arguments to 1 across
* the board of non-boolean options.
* This is passed as the `narg` option to `yargs-parser`
* @private
* @ignore
*/

const nargOpts = types.array

.concat(types.string, types.number)
.reduce((acc, arg) => Object.assign(acc, {[arg]: 1}), {});

/**

* Wrapper around `yargs-parser` which applies our settings
* @param {string|string[]} args - Arguments to parse
* @param {Object} defaultValues - Default values of mocharc.json
* @param  {...Object} configObjects - `configObjects` for yargs-parser
* @private
* @ignore
*/

const parse = (args = [], defaultValues = {}, …configObjects) => {

// save node-specific args for special handling.
// 1. when these args have a "=" they should be considered to have values
// 2. if they don't, they just boolean flags
// 3. to avoid explicitly defining the set of them, we tell yargs-parser they
//    are ALL boolean flags.
// 4. we can then reapply the values after yargs-parser is done.
const nodeArgs = (Array.isArray(args) ? args : args.split(' ')).reduce(
  (acc, arg) => {
    const pair = arg.split('=');
    let flag = pair[0];
    if (isNodeFlag(flag, false)) {
      flag = flag.replace(/^--?/, '');
      return arg.includes('=')
        ? acc.concat([[flag, pair[1]]])
        : acc.concat([[flag, true]]);
    }
    return acc;
  },
  []
);

const result = yargsParser.detailed(args, {
  configuration,
  configObjects,
  default: defaultValues,
  coerce: coerceOpts,
  narg: nargOpts,
  alias: aliases,
  string: types.string,
  array: types.array,
  number: types.number,
  boolean: types.boolean.concat(nodeArgs.map(pair => pair[0]))
});
if (result.error) {
  console.error(ansi.red(`Error: ${result.error.message}`));
  process.exit(1);
}

// reapply "=" arg values from above
nodeArgs.forEach(([key, value]) => {
  result.argv[key] = value;
});

return result.argv;

};

/**

* - Replaces comments with empty strings
* - Replaces escaped spaces (e.g., 'xxx\ yyy') with HTML space
* - Splits on whitespace, creating array of substrings
* - Filters empty string elements from array
* - Replaces any HTML space with space
* @summary Parses options read from run-control file.
* @private
* @param {string} content - Content read from run-control file.
* @returns {string[]} cmdline options (and associated arguments)
* @ignore
*/

const parseMochaOpts = content =>

content
  .replace(/^#.*$/gm, '')
  .replace(/\\\s/g, '%20')
  .split(/\s/)
  .filter(Boolean)
  .map(value => value.replace(/%20/g, ' '));

/**

* Prepends options from run-control file to the command line arguments.
*
* @deprecated Deprecated in v6.0.0; This function is no longer used internally and will be removed in a future version.
* @public
* @alias module:lib/cli/options
* @see {@link https://mochajs.org/#mochaopts|mocha.opts}
*/

module.exports = function getOptions() {

deprecate(
  'getOptions() is DEPRECATED and will be removed from a future version of Mocha.  Use loadOptions() instead'
);
if (process.argv.length === 3 && ONE_AND_DONE_ARGS.has(process.argv[2])) {
  return;
}

const optsPath =
  process.argv.indexOf('--opts') === -1
    ? mocharc.opts
    : process.argv[process.argv.indexOf('--opts') + 1];

try {
  const options = parseMochaOpts(fs.readFileSync(optsPath, 'utf8'));

  process.argv = process.argv
    .slice(0, 2)
    .concat(options.concat(process.argv.slice(2)));
} catch (ignore) {
  // NOTE: should console.error() and throw the error
}

process.env.LOADED_MOCHA_OPTS = true;

};

/**

* Given filepath in `args.opts`, attempt to load and parse a `mocha.opts` file.
* @param {Object} [args] - Arguments object
* @param {string|boolean} [args.opts] - Filepath to mocha.opts; defaults to whatever's in `mocharc.opts`, or `false` to skip
* @returns {external:yargsParser.Arguments|void} If read, object containing parsed arguments
* @memberof module:lib/cli/options
* @public
*/

const loadMochaOpts = (args = {}) => {

let result;
let filepath = args.opts;
// /dev/null is backwards compat
if (filepath === false || filepath === '/dev/null') {
  return result;
}
filepath = filepath || mocharc.opts;
result = {};
let mochaOpts;
try {
  mochaOpts = fs.readFileSync(filepath, 'utf8');
  debug(`read ${filepath}`);
} catch (err) {
  if (args.opts) {
    throw new Error(`Unable to read ${filepath}: ${err}`);
  }
  // ignore otherwise.  we tried
  debug(`No mocha.opts found at ${filepath}`);
}

// real args should override `mocha.opts` which should override defaults.
// if there's an exception to catch here, I'm not sure what it is.
// by attaching the `no-opts` arg, we avoid re-parsing of `mocha.opts`.
if (mochaOpts) {
  result = parse(parseMochaOpts(mochaOpts));
  debug(`${filepath} parsed succesfully`);
}
return result;

};

module.exports.loadMochaOpts = loadMochaOpts;

/**

* Given path to config file in `args.config`, attempt to load & parse config file.
* @param {Object} [args] - Arguments object
* @param {string|boolean} [args.config] - Path to config file or `false` to skip
* @public
* @memberof module:lib/cli/options
* @returns {external:yargsParser.Arguments|void} Parsed config, or nothing if `args.config` is `false`
*/

const loadRc = (args = {}) => {

if (args.config !== false) {
  const config = args.config || findConfig();
  return config ? loadConfig(config) : {};
}

};

module.exports.loadRc = loadRc;

/**

* Given path to `package.json` in `args.package`, attempt to load config from `mocha` prop.
* @param {Object} [args] - Arguments object
* @param {string|boolean} [args.config] - Path to `package.json` or `false` to skip
* @public
* @memberof module:lib/cli/options
* @returns {external:yargsParser.Arguments|void} Parsed config, or nothing if `args.package` is `false`
*/

const loadPkgRc = (args = {}) => {

let result;
if (args.package === false) {
  return result;
}
result = {};
const filepath = args.package || findUp.sync(mocharc.package);
if (filepath) {
  try {
    const pkg = JSON.parse(fs.readFileSync(filepath, 'utf8'));
    if (pkg.mocha) {
      debug(`'mocha' prop of package.json parsed:`, pkg.mocha);
      result = pkg.mocha;
    } else {
      debug(`no config found in ${filepath}`);
    }
  } catch (err) {
    if (args.package) {
      throw new Error(`Unable to read/parse ${filepath}: ${err}`);
    }
    debug(`failed to read default package.json at ${filepath}; ignoring`);
  }
}
return result;

};

module.exports.loadPkgRc = loadPkgRc;

/**

* Priority list:
*
* 1. Command-line args
* 2. RC file (`.mocharc.js`, `.mocharc.ya?ml`, `mocharc.json`)
* 3. `mocha` prop of `package.json`
* 4. `mocha.opts`
* 5. default configuration (`lib/mocharc.json`)
*
* If a {@link module:lib/cli/one-and-dones.ONE_AND_DONE_ARGS "one-and-done" option} is present in the `argv` array, no external config files will be read.
* @summary Parses options read from `mocha.opts`, `.mocharc.*` and `package.json`.
* @param {string|string[]} [argv] - Arguments to parse
* @public
* @memberof module:lib/cli/options
* @returns {external:yargsParser.Arguments} Parsed args from everything
*/

const loadOptions = (argv = []) => {

let args = parse(argv);
// short-circuit: look for a flag that would abort loading of mocha.opts
if (
  Array.from(ONE_AND_DONE_ARGS).reduce(
    (acc, arg) => acc || arg in args,
    false
  )
) {
  return args;
}

const rcConfig = loadRc(args);
const pkgConfig = loadPkgRc(args);
const optsConfig = loadMochaOpts(args);

if (rcConfig) {
  args.config = false;
  args._ = args._.concat(rcConfig._ || []);
}
if (pkgConfig) {
  args.package = false;
  args._ = args._.concat(pkgConfig._ || []);
}
if (optsConfig) {
  args.opts = false;
  args._ = args._.concat(optsConfig._ || []);
}

args = parse(
  args._,
  mocharc,
  args,
  rcConfig || {},
  pkgConfig || {},
  optsConfig || {}
);

// recombine positional arguments and "spec"
if (args.spec) {
  args._ = args._.concat(args.spec);
  delete args.spec;
}

// make unique
args._ = Array.from(new Set(args._));

return args;

};

module.exports.loadOptions = loadOptions; module.exports.YARGS_PARSER_CONFIG = YARGS_PARSER_CONFIG;