var fs = require('fs');

var tapOut = require('tap-out');
var through = require('through2');
var duplexer = require('duplexer');
var format = require('chalk');
var prettyMs = require('pretty-ms');
var _ = require('lodash');
var repeat = require('repeat-string');
var symbols = require('figures');

var lTrimList = require('./lib/utils/l-trim-list');

module.exports = function (spec) {

  spec = spec || {};

  var OUTPUT_PADDING = spec.padding || '  ';

  var output = through();
  var parser = tapOut();
  var stream = duplexer(parser, output);
  var startTime = new Date().getTime();

  output.push('\n');

  parser.on('test', function (test) {

    output.push('\n' + pad(format.underline(test.name)) + '\n\n');
  });

  // Passing assertions
  parser.on('pass', function (assertion) {

    if (/# SKIP/.test(assertion.name)) {
      var name = assertion.name.replace(' # SKIP', '')
      name = format.cyan('- ' + name);

      output.push(pad('  ' + name + '\n'));
    }
    else {
      var glyph = format.green(symbols.tick);
      var name = format.dim(assertion.name);

      output.push(pad('  ' + glyph + ' ' + name + '\n'));
    }

  });

  // Failing assertions
  parser.on('fail', function (assertion) {

    var glyph = symbols.cross;
    var title =  glyph + ' ' + assertion.name;
    var raw = format.cyan(prettifyRawError(assertion.error.raw));
    var divider = _.fill(
      new Array((title).length + 1),
      '-'
    ).join('');

    output.push('\n' + pad('  ' + format.red(title) + '\n'));
    output.push(pad('  ' + format.red(divider) + '\n'));
    output.push(raw);

    stream.failed = true;
  });

  parser.on('comment', function (comment) {

    output.push(pad('  ' + format.yellow(comment.raw)) + '\n');
  });

  // All done
  parser.on('output', function (results) {

    output.push('\n\n');

    // Most likely a failure upstream
    if (results.plans.length < 1) {
      process.exit(1);
    }

    if (results.fail.length > 0) {
      output.push(formatErrors(results));
      output.push('\n');
    }

    output.push(formatTotals(results));
    output.push('\n\n\n');

    // Exit if no tests run. This is a result of 1 of 2 things:
    //  1. No tests and asserts were written
    //  2. There was some error before the TAP got to the parser
    if (results.tests.length === 0 &&
        results.asserts.length === 0) {
      process.exit(1);
    }
  });

  // Utils

  function prettifyRawError (rawError) {

    return rawError.split('\n').map(function (line) {

      return pad(line);
    }).join('\n') + '\n\n';
  }

  function formatErrors (results) {

    var failCount = results.fail.length;
    var past = (failCount === 1) ? 'was' : 'were';
    var plural = (failCount === 1) ? 'failure' : 'failures';

    var out = '\n' + pad(format.red.bold('Failed Tests:') + ' There ' + past + ' ' + format.red.bold(failCount) + ' ' + plural + '\n');
    out += formatFailedAssertions(results);

    return out;
  }

  function formatTotals (results) {

    if (results.tests.length === 0 &&
        results.asserts.length === 0) {
      return pad(format.red(symbols.cross + ' No tests found'));
    }

    return _.filter([
      pad('total:     ' + results.asserts.length),
      pad(format.green('passing:   ' + results.pass.length)),
      results.fail.length > 0 ? pad(format.red('failing:   ' + results.fail.length)) : undefined,
      pad('duration:  ' + prettyMs(new Date().getTime() - startTime))
    ], _.identity).join('\n');
  }

  function formatFailedAssertions (results) {

    var out = '';

    var groupedAssertions = _.groupBy(results.fail, function (assertion) {
      return assertion.test;
    });

    _.each(groupedAssertions, function (assertions, testNumber) {

      // Wrie failed assertion's test name
      var test = _.find(results.tests, {number: parseInt(testNumber)});
      out += '\n' + pad('  ' + test.name + '\n\n');

      // Write failed assertion
      _.each(assertions, function (assertion) {

        out += pad('    ' + format.red(symbols.cross) + ' ' + format.red(assertion.name)) + '\n';
      });

      out += '\n';
    });

    return out;
  }

  function pad (str) {

    return OUTPUT_PADDING + str;
  }

  return stream;
};
