#!/usr/bin/env node

/**
 * Module dependencies.
 */

var program = require('commander')
  , sprintf = require('util').format
  , path = require('path')
  , fs = require('fs')
  , glob = require('glob')
  , resolve = path.resolve
  , exists = fs.existsSync || path.existsSync
  , Mocha = require('../')
  , utils = Mocha.utils
  , interfaces = Mocha.interfaces
  , join = path.join
  , basename = path.basename
  , cwd = process.cwd()
  , mocha = new Mocha;

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

var Date = global.Date
  , setTimeout = global.setTimeout
  , setInterval = global.setInterval
  , clearTimeout = global.clearTimeout
  , clearInterval = global.clearInterval;

/**
 * Files.
 */

var files = [];

/**
 * Globals.
 */

var globals = [];

/**
 * Requires.
 */

var requires = [];

/**
 * Images.
 */

var images = {
    fail: __dirname + '/../images/error.png'
  , pass: __dirname + '/../images/ok.png'
};

// options

program
  .version(JSON.parse(fs.readFileSync(__dirname + '/../package.json', 'utf8')).version)
  .usage('[debug] [options] [files]')
  .option('-A, --async-only', "force all tests to take a callback (async)")
  .option('-c, --colors', 'force enabling of colors')
  .option('-C, --no-colors', 'force disabling of colors')
  .option('-G, --growl', 'enable growl notification support')
  .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('-gc', '--expose-gc', 'expose gc extension')
  .option('-i, --invert', 'inverts --grep 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 (bdd|tdd|exports)', 'bdd')
  .option('-w, --watch', 'watch files for changes')
  .option('--check-leaks', 'check for global variable leaks')
  .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('--harmony', 'enable all harmony features (except typeof)')
  .option('--harmony-collections', 'enable harmony collections (sets, maps, and weak maps)')
  .option('--harmony-generators', 'enable harmony generators')
  .option('--harmony-proxies', 'enable harmony proxies')
  .option('--inline-diffs', 'display actual/expected differences inline within each string')
  .option('--interfaces', 'display available interfaces')
  .option('--no-deprecation', 'silence deprecation warnings')
  .option('--no-exit', 'require a clean shutdown of the event loop: mocha will not call process.exit')
  .option('--no-timeouts', 'disables timeouts, given implicitly with --debug')
  .option('--opts <path>', 'specify opts path', 'test/mocha.opts')
  .option('--prof', 'log statistical profiling information')
  .option('--recursive', 'include sub directories')
  .option('--reporters', 'display available reporters')
  .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('--watch-extensions <ext>,...', 'additional extensions to monitor with --watch', list, [])

program.name = 'mocha';

// init command

program
  .command('init <path>')
  .description('initialize a client-side mocha setup at <path>')
  .action(function(path){
    var mkdir = require('mkdirp');
    mkdir.sync(path);
    var css = fs.readFileSync(join(__dirname, '..', 'mocha.css'));
    var js = fs.readFileSync(join(__dirname, '..', 'mocha.js'));
    var 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('globals', function(val){
  globals = globals.concat(list(val));
});

// --reporters

program.on('reporters', function(){
  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('    html-cov - HTML test coverage');
  console.log('    json-cov - JSON test coverage');
  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('interfaces', function(){
  console.log('');
  console.log('    bdd');
  console.log('    tdd');
  console.log('    qunit');
  console.log('    exports');
  console.log('');
  process.exit();
});

// -r, --require

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

program.on('require', function(mod){
  var abs = exists(mod) || exists(mod + '.js');
  if (abs) mod = resolve(mod);
  requires.push(mod);
});

// --opts

var optsPath = process.argv.indexOf('--opts') !== -1
    ? process.argv[process.argv.indexOf('--opts') + 1]
    : 'test/mocha.opts';

try {
  var opts = fs.readFileSync(optsPath, 'utf8')
    .trim()
    .split(/\s+/);

  process.argv = process.argv
    .slice(0, 2)
    .concat(opts.concat(process.argv.slice(2)));
} catch (err) {
  // ignore
}

// parse args

program.parse(process.argv);

// infinite stack traces

Error.stackTraceLimit = Infinity; // TODO: config

// reporter

mocha.reporter(program.reporter);

// interface

mocha.ui(program.ui);

// load reporter

try {
  Reporter = require('../lib/reporters/' + program.reporter);
} catch (err) {
  try {
    Reporter = require(program.reporter);
  } catch (err) {
    throw new Error('reporter "' + program.reporter + '" does not exist');
  }
}

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

// --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(new RegExp(program.grep));

// --invert

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

// --check-leaks

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

// --growl

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

// --async-only

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

// --globals

mocha.globals(globals);

// custom compiler support

var extensions = ['js'];
program.compilers.forEach(function(c) {
  var compiler = c.split(':')
    , ext = compiler[0]
    , mod = compiler[1];

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

var re = new RegExp('\\.(' + extensions.join('|') + ')$');

// requires

requires.forEach(function(mod) {
  require(mod);
});

// files

var files = []
  , args = program.args;

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

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

args.forEach(function(arg){
  files = files.concat(lookupFiles(arg, program.recursive));
});

// resolve

files = files.map(function(path){
  return resolve(path);
});

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

// --watch

var runner;
if (program.watch) {
  console.log();
  hideCursor();
  process.on('SIGINT', function(){
    showCursor();
    console.log('\n');
    process.exit();
  });


  var watchFiles = utils.files(cwd, [ 'js', 'coffee', 'litcoffee', 'coffee.md' ].concat(program.watchExtensions));
  var runAgain = false;

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

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

  loadAndRun();

  function rerun() {
    purge();
    stop()
    mocha.suite = mocha.suite.clone();
    mocha.suite.ctx = new Mocha.Context;
    mocha.ui(program.ui);
    loadAndRun();
  }

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

  return;
}

// load

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

function exitLater(code) {
  process.on('exit', function() { process.exit(code) })
}

process.on('SIGINT', function() { runner.abort(); })

// enable growl notifications

function growl(runner, reporter) {
  var notify = require('growl');

  runner.on('end', function(){
    var stats = reporter.stats;
    if (stats.failures) {
      var msg = stats.failures + ' of ' + runner.total + ' tests failed';
      notify(msg, { name: 'mocha', title: 'Failed', image: images.fail });
    } else {
      notify(stats.passes + ' tests passed in ' + stats.duration + 'ms', {
          name: 'mocha'
        , title: 'Passed'
        , image: images.pass
      });
    }
  });
}

/**
 * Parse list.
 */

function list(str) {
  return str.split(/ *, */);
}

/**
 * Hide the cursor.
 */

function hideCursor(){
  process.stdout.write('\u001b[?25l');
};

/**
 * Show the cursor.
 */

function showCursor(){
  process.stdout.write('\u001b[?25h');
};

/**
 * Stop play()ing.
 */

function stop() {
  process.stdout.write('\u001b[2K');
  clearInterval(play.timer);
}

/**
 * Lookup file names at the given `path`.
 */

function lookupFiles(path, recursive) {
  var files = [];

  if (!exists(path)) {
    if (exists(path + '.js')) {
      path += '.js'
    } else {
      files = glob.sync(path);
      if (!files.length) throw new Error("cannot resolve path (or pattern) '" + path + "'");
      return files;
    }
  }

  var stat = fs.statSync(path);
  if (stat.isFile()) return path;

  fs.readdirSync(path).forEach(function(file){
    file = join(path, file);
    var stat = fs.statSync(file);
    if (stat.isDirectory()) {
      if (recursive) files = files.concat(lookupFiles(file, recursive));
      return
    }
    if (!stat.isFile() || !re.test(file) || basename(file)[0] == '.') return;
    files.push(file);
  });

  return files;
}

/**
 * Play the given array of strings.
 */

function play(arr, interval) {
  var len = arr.length
    , interval = interval || 100
    , i = 0;

  play.timer = setInterval(function(){
    var str = arr[i++ % len];
    process.stdout.write('\u001b[0G' + str);
  }, interval);
}
