#!/usr/bin/env node
'use strict';

/* eslint no-unused-vars: off */

/**
 * Module dependencies.
 */

const program = require('commander');
const path = require('path');
const fs = require('fs');
const minimatch = require('minimatch');
const resolve = path.resolve;
const exists = fs.existsSync;
const Mocha = require('../');
const utils = Mocha.utils;
const interfaceNames = Object.keys(Mocha.interfaces);
const join = path.join;
const cwd = process.cwd();
const getOptions = require('./options');
const mocha = new Mocha();

/**
 * Save timer references to avoid Sinon interfering (see GH-237).
 */

const Date = global.Date;
const setTimeout = global.setTimeout;
const setInterval = global.setInterval;
const clearTimeout = global.clearTimeout;
const clearInterval = global.clearInterval;

/**
 * Exits Mocha when tests + code under test has finished execution (default)
 * @param {number} code - Exit code; typically # of failures
 */
const exitLater = code => {
  process.on('exit', () => {
    process.exit(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
 */
const exit = 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();
};

/**
 * Parse list.
 */
const list = str => str.split(/ *, */);

/**
 * Parse multiple flag.
 */
const collect = (val, memo) => memo.concat(val);

/**
 * Hide the cursor.
 */
const hideCursor = () => {
  process.stdout.write('\u001b[?25l');
};

/**
 * Show the cursor.
 */
const showCursor = () => {
  process.stdout.write('\u001b[?25h');
};

/**
 * Stop play()ing.
 */
const stop = () => {
  process.stdout.write('\u001b[2K');
  clearInterval(play.timer);
};

/**
 * Play the given array of strings.
 */
const play = (arr, interval) => {
  const len = arr.length;
  interval = interval || 100;
  let i = 0;

  play.timer = setInterval(() => {
    const str = arr[i++ % len];
    process.stdout.write(`\u001b[0G${str}`);
  }, interval);
};

/**
 * Files.
 */

let files = [];

/**
 * Globals.
 */

let globals = [];

/**
 * Requires.
 */

const requires = [];

/**
 * Images.
 */

const images = {
  fail: path.join(__dirname, '..', 'assets', 'growl', 'error.png'),
  pass: path.join(__dirname, '..', 'assets', 'growl', 'ok.png')
};

// options

program
  .version(
    JSON.parse(
      fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')
    ).version
  )
  .usage('[debug] [options] [files]')
  .option(
    '-A, --async-only',
    'force all tests to take a callback (async) or return a promise'
  )
  .option('-c, --colors', 'force enabling of colors')
  .option('-C, --no-colors', 'force disabling of colors')
  .option('-G, --growl', 'enable growl notification support')
  .option('-O, --reporter-options <k=v,k2=v2,...>', 'reporter-specific options')
  .option('-R, --reporter <name>', 'specify the reporter to use', 'spec')
  .option('-S, --sort', 'sort test files')
  .option('-b, --bail', 'bail after first test failure')
  .option('-d, --debug', "enable node's debugger, synonym for node --debug")
  .option('-g, --grep <pattern>', 'only run tests matching <pattern>')
  .option('-f, --fgrep <string>', 'only run tests containing <string>')
  .option('-gc, --expose-gc', 'expose gc extension')
  .option('-i, --invert', 'inverts --grep and --fgrep matches')
  .option('-r, --require <name>', 'require the given module')
  .option('-s, --slow <ms>', '"slow" test threshold in milliseconds [75]')
  .option('-t, --timeout <ms>', 'set test-case timeout in milliseconds [2000]')
  .option(
    '-u, --ui <name>',
    `specify user-interface (${interfaceNames.join('|')})`,
    'bdd'
  )
  .option('-w, --watch', 'watch files for changes')
  .option('--check-leaks', 'check for global variable leaks')
  .option('--full-trace', 'display the full stack trace')
  .option(
    '--compilers <ext>:<module>,...',
    'use the given module(s) to compile files',
    list,
    []
  )
  .option('--debug-brk', "enable node's debugger breaking on the first line")
  .option(
    '--globals <names>',
    'allow the given comma-delimited global [names]',
    list,
    []
  )
  .option('--es_staging', 'enable all staged features')
  .option(
    '--harmony<_classes,_generators,...>',
    'all node --harmony* flags are available'
  )
  .option(
    '--preserve-symlinks',
    'Instructs the module loader to preserve symbolic links when resolving and caching modules'
  )
  .option('--icu-data-dir', 'include ICU data')
  .option(
    '--inline-diffs',
    'display actual/expected differences inline within each string'
  )
  .option('--no-diff', 'do not show a diff on failure')
  .option('--inspect', 'activate devtools in chrome')
  .option(
    '--inspect-brk',
    'activate devtools in chrome and break on the first line'
  )
  .option('--interfaces', 'display available interfaces')
  .option('--no-deprecation', 'silence deprecation warnings')
  .option(
    '--exit',
    'force shutdown of the event loop after test run: mocha will call process.exit'
  )
  .option('--no-timeouts', 'disables timeouts, given implicitly with --debug')
  .option('--no-warnings', 'silence all node process warnings')
  .option('--opts <path>', 'specify opts path', 'test/mocha.opts')
  .option('--perf-basic-prof', 'enable perf linux profiler (basic support)')
  .option('--napi-modules', 'enable experimental NAPI modules')
  .option('--prof', 'log statistical profiling information')
  .option('--log-timer-events', 'Time events including external callbacks')
  .option('--recursive', 'include sub directories')
  .option('--reporters', 'display available reporters')
  .option(
    '--retries <times>',
    'set numbers of time to retry a failed test case'
  )
  .option(
    '--throw-deprecation',
    'throw an exception anytime a deprecated function is used'
  )
  .option('--trace', 'trace function calls')
  .option('--trace-deprecation', 'show stack traces on deprecations')
  .option('--trace-warnings', 'show stack traces on node process warnings')
  .option('--use_strict', 'enforce strict mode')
  .option(
    '--watch-extensions <ext>,...',
    'additional extensions to monitor with --watch',
    list,
    ['js']
  )
  .option('--delay', 'wait for async suite definition')
  .option('--allow-uncaught', 'enable uncaught errors to propagate')
  .option('--forbid-only', 'causes test marked with only to fail the suite')
  .option(
    '--forbid-pending',
    'causes pending tests and test marked with skip to fail the suite'
  )
  .option(
    '--file <file>',
    'include a file to be ran during the suite',
    collect,
    []
  )
  .option('--exclude <file>', 'a file or glob pattern to ignore', collect, []);

program._name = 'mocha';

// init command

program
  .command('init <path>')
  .description('initialize a client-side mocha setup at <path>')
  .action(path => {
    const mkdir = require('mkdirp');
    mkdir.sync(path);
    const css = fs.readFileSync(join(__dirname, '..', 'mocha.css'));
    const js = fs.readFileSync(join(__dirname, '..', 'mocha.js'));
    const tmpl = fs.readFileSync(join(__dirname, '..', 'lib/template.html'));
    fs.writeFileSync(join(path, 'mocha.css'), css);
    fs.writeFileSync(join(path, 'mocha.js'), js);
    fs.writeFileSync(join(path, 'tests.js'), '');
    fs.writeFileSync(join(path, 'index.html'), tmpl);
    process.exit(0);
  });

// --globals

program.on('option:globals', val => {
  globals = globals.concat(list(val));
});

// --reporters

program.on('option:reporters', () => {
  console.log();
  console.log('    dot - dot matrix');
  console.log('    doc - html documentation');
  console.log('    spec - hierarchical spec list');
  console.log('    json - single json object');
  console.log('    progress - progress bar');
  console.log('    list - spec-style listing');
  console.log('    tap - test-anything-protocol');
  console.log('    landing - unicode landing strip');
  console.log('    xunit - xunit reporter');
  console.log('    min - minimal reporter (great with --watch)');
  console.log('    json-stream - newline delimited json events');
  console.log('    markdown - markdown documentation (github flavour)');
  console.log('    nyan - nyan cat!');
  console.log();
  process.exit();
});

// --interfaces

program.on('option:interfaces', () => {
  console.log('');
  interfaceNames.forEach(interfaceName => {
    console.log(`    ${interfaceName}`);
  });
  console.log('');
  process.exit();
});

// -r, --require

module.paths.push(cwd, join(cwd, 'node_modules'));

program.on('option:require', mod => {
  const abs = exists(mod) || exists(`${mod}.js`);
  if (abs) {
    mod = resolve(mod);
  }
  requires.push(mod);
});

// If not already done, load mocha.opts
if (!process.env.LOADED_MOCHA_OPTS) {
  getOptions();
}

// parse args

program.parse(process.argv);

// infinite stack traces

Error.stackTraceLimit = Infinity; // TODO: config

// reporter options

const reporterOptions = {};
if (program.reporterOptions !== undefined) {
  program.reporterOptions.split(',').forEach(opt => {
    const L = opt.split('=');
    if (L.length > 2 || L.length === 0) {
      throw new Error(`invalid reporter option '${opt}'`);
    } else if (L.length === 2) {
      reporterOptions[L[0]] = L[1];
    } else {
      reporterOptions[L[0]] = true;
    }
  });
}

// reporter

mocha.reporter(program.reporter, reporterOptions);

// --no-colors

if (!program.colors) {
  mocha.useColors(false);
}

// --colors

if (~process.argv.indexOf('--colors') || ~process.argv.indexOf('-c')) {
  mocha.useColors(true);
}

// --inline-diffs

if (program.inlineDiffs) {
  mocha.useInlineDiffs(true);
}

// --no-diff

if (process.argv.indexOf('--no-diff') !== -1) {
  mocha.hideDiff(true);
}

// --slow <ms>

if (program.slow) {
  mocha.suite.slow(program.slow);
}

// --no-timeouts

if (!program.timeouts) {
  mocha.enableTimeouts(false);
}

// --timeout

if (program.timeout) {
  mocha.suite.timeout(program.timeout);
}

// --bail

mocha.suite.bail(program.bail);

// --grep

if (program.grep) {
  mocha.grep(program.grep);
}

// --fgrep

if (program.fgrep) {
  mocha.fgrep(program.fgrep);
}

// --invert

if (program.invert) {
  mocha.invert();
}

// --check-leaks

if (program.checkLeaks) {
  mocha.checkLeaks();
}

// --stack-trace

if (program.fullTrace) {
  mocha.fullTrace();
}

// --growl

if (program.growl) {
  mocha.growl();
}

// --async-only

if (program.asyncOnly) {
  mocha.asyncOnly();
}

// --delay

if (program.delay) {
  mocha.delay();
}

// --allow-uncaught

if (program.allowUncaught) {
  mocha.allowUncaught();
}

// --globals

mocha.globals(globals);

// --retries

if (program.retries) {
  mocha.suite.retries(program.retries);
}

// --forbid-only

if (program.forbidOnly) mocha.forbidOnly();

// --forbid-pending

if (program.forbidPending) mocha.forbidPending();

// custom compiler support

if (program.compilers.length > 0) {
  require('util').deprecate(() => {},
  '"--compilers" will be removed in a future version of Mocha; see https://git.io/vdcSr for more info')();
}
const extensions = ['js'];
program.compilers.forEach(c => {
  const idx = c.indexOf(':');
  const ext = c.slice(0, idx);
  let mod = c.slice(idx + 1);

  if (mod[0] === '.') {
    mod = join(process.cwd(), mod);
  }
  require(mod);
  extensions.push(ext);
  program.watchExtensions.push(ext);
});

// requires

requires.forEach(mod => {
  require(mod);
});

// interface

mocha.ui(program.ui);

// args

const args = program.args;

// default files to test/*.{js,coffee}

if (!args.length) {
  args.push('test');
}

args.forEach(arg => {
  let newFiles;
  try {
    newFiles = utils.lookupFiles(arg, extensions, program.recursive);
  } catch (err) {
    if (err.message.indexOf('cannot resolve path') === 0) {
      console.error(
        `Warning: Could not find any test files matching pattern: ${arg}`
      );
      return;
    }

    throw err;
  }

  if (typeof newFiles !== 'undefined') {
    if (typeof newFiles === 'string') {
      newFiles = [newFiles];
    }
    newFiles = newFiles.filter(fileName =>
      program.exclude.every(pattern => !minimatch(fileName, pattern))
    );
  }

  files = files.concat(newFiles);
});

if (!files.length) {
  console.error('No test files found');
  process.exit(1);
}

// resolve
let fileArgs = program.file.map(path => resolve(path));
files = files.map(path => resolve(path));

if (program.sort) {
  files.sort();
}

// add files given through --file to be ran first
files = fileArgs.concat(files);

// --watch

let runner;
let loadAndRun;
let purge;
let rerun;

if (program.watch) {
  console.log();
  hideCursor();
  process.on('SIGINT', () => {
    showCursor();
    console.log('\n');
    process.exit(130);
  });

  const watchFiles = utils.files(cwd, ['js'].concat(program.watchExtensions));
  let runAgain = false;

  loadAndRun = () => {
    try {
      mocha.files = files;
      runAgain = false;
      runner = mocha.run(() => {
        runner = null;
        if (runAgain) {
          rerun();
        }
      });
    } catch (e) {
      console.log(e.stack);
    }
  };

  purge = () => {
    watchFiles.forEach(file => {
      delete require.cache[file];
    });
  };

  loadAndRun();

  rerun = () => {
    purge();
    stop();
    if (!program.grep) {
      mocha.grep(null);
    }
    mocha.suite = mocha.suite.clone();
    mocha.suite.ctx = new Mocha.Context();
    mocha.ui(program.ui);
    loadAndRun();
  };

  utils.watch(watchFiles, () => {
    runAgain = true;
    if (runner) {
      runner.abort();
    } else {
      rerun();
    }
  });
} else {
  // load

  mocha.files = files;
  runner = mocha.run(program.exit ? exit : exitLater);
}

process.on('SIGINT', () => {
  runner.abort();

  // This is a hack:
  // Instead of `process.exit(130)`, set runner.failures to 130 (exit code for SIGINT)
  // The amount of failures will be emitted as error code later
  runner.failures = 130;
});
