"use strict";

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

const { readFileSync } = require("node:fs");
const pc = require("picocolors");
const { parseMochaArgs } = require("./parse-args.js");
const { ONE_AND_DONE_ARGS } = require("./one-and-dones.js");
const mocharc = require("../mocharc.json");
const { loadConfig, findConfig } = require("./config.cjs");
const { sync } = require("find-up");
const debugModule = require("debug");
const { createUnparsableFileError, isMochaError } = require("../errors.js");

const debug = debugModule("mocha:cli:options");

/**
 * The `parseArgs` function
 * @external parseArgs
 * @see {@link https://nodejs.org/api/util.html#utilparseargsconfig}
 */

/**
 * An object representing arguments parsed from the CLI
 * @memberof external:parseArgs
 * @interface Arguments
 */

/**
 * Wrapper around `parseMochaArgs` which applies Mocha's CLI parsing behavior.
 * @param {string|string[]} args - Arguments to parse
 * @param {Object} defaultValues - Default values of mocharc.json
 * @param  {...Object} configObjects - Objects to merge into parsed arguments
 * @private
 * @ignore
 */
const parse = (args = [], defaultValues = {}, ...configObjects) => {
  try {
    return parseMochaArgs(args, defaultValues, ...configObjects);
  } catch (err) {
    if (isMochaError(err)) {
      throw err;
    }
    console.error(pc.red(`Error: ${err.message}`));
    process.exit(1);
  }
};

/**
 * 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
 * @alias module:lib/cli.loadRc
 * @returns {external:parseArgs.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) : {};
  }
};

/**
 * Given path to `package.json` in `args.package`, attempt to load config from `mocha` prop.
 * If `args.package` is falsy, attempts to load from default package location (`./package.json`)
 * @param {Object} [args] - Arguments object
 * @param {string|boolean} [args.config] - Path to `package.json` or `false` to use default
 * @public
 * @alias module:lib/cli.loadPkgRc
 * @returns {external:parseArgs.Arguments|void} Parsed config. Throws if unparsableF. Empty object if file not found.
 */
const loadPkgRc = (args = {}) => {
  let result;
  if (args.package === false) {
    return result;
  }
  result = {};
  const filepath = args.package || sync(mocharc.package);
  if (filepath) {
    let configData;
    try {
      configData = readFileSync(filepath, "utf8");
    } catch (err) {
      // If `args.package` was explicitly specified, throw an error
      if (filepath == args.package) {
        throw createUnparsableFileError(
          `Unable to read ${filepath}: ${err}`,
          filepath,
        );
      } else {
        debug("failed to read default package.json at %s; ignoring", filepath);
        return result;
      }
    }
    try {
      const pkg = JSON.parse(configData);
      if (pkg.mocha) {
        debug("`mocha` prop of package.json parsed: %O", pkg.mocha);
        result = pkg.mocha;
      } else {
        debug("no config found in %s", filepath);
      }
    } catch (err) {
      // If JSON failed to parse, throw an error.
      throw createUnparsableFileError(
        `Unable to parse ${filepath}: ${err}`,
        filepath,
      );
    }
  }
  return result;
};

/**
 * Priority list:
 *
 * 1. Command-line args
 * 2. `MOCHA_OPTIONS` environment variable.
 * 3. RC file (`.mocharc.c?js`, `.mocharc.ya?ml`, `mocharc.json`)
 * 4. `mocha` prop of `package.json`
 * 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 `.mocharc.*` and `package.json`.
 * @param {string|string[]} [argv] - Arguments to parse
 * @public
 * @alias module:lib/cli.loadOptions
 * @returns {external:parseArgs.Arguments} Parsed args from everything
 */
const loadOptions = (argv = []) => {
  let args = parse(argv);
  // short-circuit: look for a flag that would abort loading of options
  if (
    Array.from(ONE_AND_DONE_ARGS).reduce(
      (acc, arg) => acc || arg in args,
      false,
    )
  ) {
    return args;
  }

  const envConfig = parse(process.env.MOCHA_OPTIONS || "");
  const rcConfig = loadRc(args);
  const pkgConfig = loadPkgRc(args);

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

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

  // 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;
};

exports.loadRc = loadRc;
exports.loadPkgRc = loadPkgRc;
exports.loadOptions = loadOptions;
