/* eslint-env mocha */
'use strict';

var Reporter = require('../index');

var mochaVersion = process.env.MOCHA_VERSION || '';
var Mocha = require('mocha' + mochaVersion);
var Runner = Mocha.Runner;
var Suite = Mocha.Suite;
var Test = Mocha.Test;

var fs = require('fs');
var path = require('path');
var rimraf = require('rimraf');

var chai = require('chai');
var expect = chai.expect;
var FakeTimer = require('@sinonjs/fake-timers');
var xmllint = require('xmllint');
var chaiXML = require('chai-xml');
var mockXml = require('./mock-results');
var mockJunitSuites = require('./mock-junit-suites');
var testConsole = require('test-console');

var debug = require('debug')('mocha-junit-reporter:tests');

chai.use(chaiXML);

describe('mocha-junit-reporter', function() {
  var filePath;
  var MOCHA_FILE;
  var stdout;

  function mockStdout () {
    stdout = testConsole.stdout.inspect();
    return stdout;
  }

  function createTest(name, options, fn) {
    if (typeof options === 'function') {
      fn = options;
      options = null;
    }
    options = options || {};

    // null fn means no callback which mocha treats as pending test.
    // undefined fn means caller wants a default fn.
    if (fn === undefined) {
      fn = function () {};
    }

    var test = new Test(name, fn);

    var duration = options.duration;
    if (duration != null) {
      // mock duration so we have consistent output
      Object.defineProperty(test, 'duration', {
        set: function() {
          // do nothing
        },
        get: function() {
          return duration;
        }
      });
    }

    return test;
  }

  function runTests(reporter, options, callback) {
    if (!callback) {
      callback = options;
      options = null;
    }
    options = options || {};
    options.invalidChar = options.invalidChar || '';
    options.title = options.title || 'Foo Bar';

    var runner = reporter.runner;
    var rootSuite = runner.suite;

    var suite1 = Suite.create(rootSuite, options.title);
    suite1.addTest(createTest('can weez the juice', {
      duration: 101
    }));

    suite1.addTest(createTest('can narfle the garthog', {duration: 2002}, function(done) {
      var err = new Error(options.invalidChar + 'expected garthog to be dead' + options.invalidChar);
      err.stack = 'this is where the stack would be';
      done(err);
    }));

    suite1.addTest(createTest('can behave like a flandip', {duration: 30003}, function(done) {
      var err = new Error('expected baz to be masher, a hustler, an uninvited grasper of cone');
      err.name = 'BazError';
      err.stack = 'stack';
      done(err);
    }));

    var suite2 = Suite.create(rootSuite, 'Another suite!');
    suite2.addTest(createTest('works', {duration: 400004}));

    if (options.includePending) {
      var pendingSuite = Suite.create(rootSuite, 'Pending suite!');
      pendingSuite.addTest(createTest('pending', null, null));
    }

    var _onSuiteEnd = reporter._onSuiteEnd.bind(reporter);

    reporter._onSuiteEnd = function(suite) {
      if (suite === rootSuite) {
        // root suite took no time to execute
        reporter._Date.clock.tick(0);
      } else if (suite === suite1) {
        // suite1 took an arbitrary amount of time that includes time to run each test + setup and teardown
        reporter._Date.clock.tick(100001);
      } else if (suite === suite2) {
        reporter._Date.clock.tick(400005);
      }

      return _onSuiteEnd(suite);
    };

    runRunner(runner, callback);
  }

  function assertXmlEquals(actual, expected) {
    expect(actual).xml.to.be.valid();
    expect(actual).xml.to.equal(expected);
  }

  function verifyMochaFile(runner, path, options) {
    var now = (new Date()).toISOString();
    debug('verify', now);
    var output = fs.readFileSync(path, 'utf-8');
    assertXmlEquals(output, mockXml(runner.stats, options));
    debug('done', now);
  }

  function removeTestPath(callback) {
    rimraf(__dirname + '/output', function(err) {
      if (err) {
        return callback(err);
      }

      // tests that exercise defaults will write to $CWD/test-results.xml
      rimraf(__dirname + '/../test-results.xml', callback);
    });
  }

  function createRunner() {
    // mocha always has a root suite
    var rootSuite = new Suite('', 'root', true);

    // We don't want Mocha to emit timeout errors.
    // If we want to simulate errors, we'll emit them ourselves.
    rootSuite.timeout(0);

    return new Runner(rootSuite);
  }

  function createReporter(options) {
    options = options || {};
    filePath = path.join(path.dirname(__dirname), options.mochaFile || '');

    var mocha = new Mocha({
      reporter: Reporter,
      allowUncaught: true
    });

    return new mocha._reporter(createRunner(), {
      reporterOptions: options,
      Date: FakeTimer.createClock(0).Date
    });
  }

  function runRunner(runner, callback) {
    runner.run(function(failureCount) {
      if (runner.dispose) {
        // Ensure uncaught exception handlers are cleared before we execute test assertions.
        // Otherwise, this runner would intercept uncaught exceptions that were already handled by the mocha instance
        // running our tests.
        runner.dispose();
      }

      callback(failureCount);
    });
  }

  function getFileNameWithHash(path) {
    var filenames = fs.readdirSync(path);
    var expected = /(^results\.)([a-f0-9]{32})(\.xml)$/i;

    for (var i = 0; i < filenames.length; i++) {
      if (expected.test(filenames[i])) {
        return filenames[i];
      }
    }
  }

  before(function(done) {
    // cache this
    MOCHA_FILE = process.env.MOCHA_FILE;

    removeTestPath(done);
  });

  after(function() {
    // reset this
    process.env.MOCHA_FILE = MOCHA_FILE;
  });

  beforeEach(function() {
    filePath = undefined;
    delete process.env.MOCHA_FILE;
    delete process.env.PROPERTIES;
  });

  afterEach(function(done) {
    debug('after');
    if (stdout) {
      stdout.restore();
    }

    removeTestPath(done);
  });

  it('can produce a JUnit XML report', function(done) {
    var reporter = createReporter({mochaFile: 'test/output/mocha.xml'});
    runTests(reporter, function() {
      verifyMochaFile(reporter.runner, filePath);
      done();
    });
  });

  it('can handle getXml being called twice', function() {
    var reporter = createReporter({mochaFile: 'test/output/mocha.xml'});
    var testsuites = mockJunitSuites.withStringTimes();
    reporter.getXml(testsuites);
  });

  it('respects `process.env.MOCHA_FILE`', function(done) {
    process.env.MOCHA_FILE = 'test/output/results.xml';
    var reporter = createReporter();
    runTests(reporter, function() {
      verifyMochaFile(reporter.runner, process.env.MOCHA_FILE);
      done();
    });
  });

  it('respects `process.env.PROPERTIES`', function(done) {
    process.env.PROPERTIES = 'CUSTOM_PROPERTY:ABC~123';
    var reporter = createReporter({mochaFile: 'test/output/properties.xml'});
    runTests(reporter, function() {
      verifyMochaFile(reporter.runner, filePath, {
        properties: [
          {
            name: 'CUSTOM_PROPERTY',
            value: 'ABC~123'
          }
        ]
      });
      done();
    });
  });

  it('respects `--reporter-options mochaFile=`', function(done) {
    var reporter = createReporter({mochaFile: 'test/output/results.xml'});
    runTests(reporter, function() {
      verifyMochaFile(reporter.runner, filePath);
      done();
    });
  });

  it('respects `[hash]` pattern in test results report filename', function(done) {
    var dir = 'test/output/';
    var path = dir + 'results.[hash].xml';
    var reporter = createReporter({mochaFile: path});
    runTests(reporter, function() {
      verifyMochaFile(reporter.runner, dir + getFileNameWithHash(dir));
      done();
    });
  });


  it("respects `[testsuitesTitle]` pattern in test results report filename", function (done) {
    var dir = "test/output/";
    var path = dir + "results.[testsuitesTitle].xml";
    var reporter = createReporter({ mochaFile: path });
    runTests(reporter, function () {
      verifyMochaFile(
        reporter.runner,
        dir + "results." + reporter._options.testsuitesTitle + ".xml"
      );
      done();
    });
  });

  it("respects `[rootSuiteTitle]` pattern in test results report filename", function (done) {
    var dir = "test/output/";
    var path = dir + "results.[rootSuiteTitle].xml";
    var reporter = createReporter({ mochaFile: path });
    runTests(reporter, function () {
      verifyMochaFile(
        reporter.runner,
        dir +
          "results." +
          reporter._testsuites[0].testsuite[0]._attr.name +
          ".xml"
      );
      done();
    });
  });

  it("respects `[suiteFilename]` pattern in test results report filename", function (done) {
    var dir = "test/output/";
    var path = dir + "results.[suiteFilename].xml";
    var reporter = createReporter({ mochaFile: path });
    runTests(reporter, function () {
      verifyMochaFile(
        reporter.runner,
        dir +
          "results." +
          reporter._testsuites[0].testsuite[0]._attr.file +
          ".xml"
      );
      done();
    });
  });

  it("respects `[suiteName]` pattern in test results report filename", function (done) {
    var dir = "test/output/";
    var path = dir + "results.[suiteName].xml";
    var reporter = createReporter({ mochaFile: path });
    runTests(reporter, function () {
      verifyMochaFile(
        reporter.runner,
        dir +
          "results." +
          reporter._testsuites[1].testsuite[0]._attr.name +
          ".xml"
      );
      done();
    });
  });

  it('will create intermediate directories', function(done) {
    var reporter = createReporter({mochaFile: 'test/output/foo/mocha.xml'});
    runTests(reporter, function() {
      verifyMochaFile(reporter.runner, filePath);
      done();
    });
  });

  it('creates valid XML report for invalid message', function(done) {
    var reporter = createReporter({mochaFile: 'test/output/mocha.xml'});
    runTests(reporter, {invalidChar: '\u001b'}, function() {
      assertXmlEquals(reporter._xml, mockXml(reporter.runner.stats));
      done();
    });
  });

  it('creates valid XML report even if title contains ANSI character sequences', function(done) {
    var reporter = createReporter({mochaFile: 'test/output/mocha.xml'});
    runTests(reporter, {title: '