/* eslint no-use-before-define:0, no-console:0, class-methods-use-this:0 */
'use strict';

// Import

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

var _get = function get(object, property, receiver) { if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { return get(parent, property, receiver); } } else if ("value" in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } };

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

var EventEmitterGrouped = require('event-emitter-grouped');

var _require = require('taskgroup'),
    Task = _require.Task,
    TaskGroup = _require.TaskGroup;

// =================================
// Generic

function _setConfig() {
	var _config = this.config,
	    before = _config.before,
	    after = _config.after;

	if (before) {
		delete this.config.before;
		this.on('before', before);
	}
	if (after) {
		delete this.config.after;
		this.on('after', after);
	}
	return this;
}

function _run(next) {
	var _this = this;

	for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
		args[_key - 1] = arguments[_key];
	}

	if (!this.started) {
		this.emitSerial('before', function (err) {
			if (err) _this.emit('error', err);
			next.apply(_this, args);
		});
	} else {
		next.apply(this, args);
	}
	return this;
}

function _finish(next) {
	var _this2 = this;

	for (var _len2 = arguments.length, args = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
		args[_key2 - 1] = arguments[_key2];
	}

	if (!this.exited) {
		this.emitSerial('after', function (err) {
			if (err) _this2.emit('error', err);
			next.apply(_this2, args);
		});
	} else {
		next.apply(this, args);
	}
	return this;
}

// =================================
// Test

var Test = function (_Task) {
	_inherits(Test, _Task);

	function Test() {
		_classCallCheck(this, Test);

		return _possibleConstructorReturn(this, (Test.__proto__ || Object.getPrototypeOf(Test)).apply(this, arguments));
	}

	_createClass(Test, [{
		key: 'setConfig',
		value: function setConfig() {
			var _get2;

			for (var _len3 = arguments.length, args = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
				args[_key3] = arguments[_key3];
			}

			(_get2 = _get(Test.prototype.__proto__ || Object.getPrototypeOf(Test.prototype), 'setConfig', this)).call.apply(_get2, [this].concat(args));
			return _setConfig.call(this);
		}
	}, {
		key: 'run',
		value: function run() {
			for (var _len4 = arguments.length, args = Array(_len4), _key4 = 0; _key4 < _len4; _key4++) {
				args[_key4] = arguments[_key4];
			}

			return _run.call(this, _get(Test.prototype.__proto__ || Object.getPrototypeOf(Test.prototype), 'run', this), args);
		}
	}, {
		key: 'finish',
		value: function finish() {
			for (var _len5 = arguments.length, args = Array(_len5), _key5 = 0; _key5 < _len5; _key5++) {
				args[_key5] = arguments[_key5];
			}

			return _finish.call(this, _get(Test.prototype.__proto__ || Object.getPrototypeOf(Test.prototype), 'finish', this), args);
		}
	}], [{
		key: 'create',
		value: function create() {
			for (var _len6 = arguments.length, args = Array(_len6), _key6 = 0; _key6 < _len6; _key6++) {
				args[_key6] = arguments[_key6];
			}

			return new (Function.prototype.bind.apply(this, [null].concat(args)))();
		}
	}, {
		key: 'isTest',
		value: function isTest(test) {
			return test instanceof Test;
		}
	}]);

	return Test;
}(Task);

// =================================
// Suite

var Suite = function (_TaskGroup) {
	_inherits(Suite, _TaskGroup);

	_createClass(Suite, [{
		key: 'setConfig',
		value: function setConfig() {
			var _get3;

			for (var _len7 = arguments.length, args = Array(_len7), _key7 = 0; _key7 < _len7; _key7++) {
				args[_key7] = arguments[_key7];
			}

			(_get3 = _get(Suite.prototype.__proto__ || Object.getPrototypeOf(Suite.prototype), 'setConfig', this)).call.apply(_get3, [this].concat(args));
			return _setConfig.call(this);
		}
	}, {
		key: 'run',
		value: function run() {
			for (var _len8 = arguments.length, args = Array(_len8), _key8 = 0; _key8 < _len8; _key8++) {
				args[_key8] = arguments[_key8];
			}

			return _run.call.apply(_run, [this, _get(Suite.prototype.__proto__ || Object.getPrototypeOf(Suite.prototype), 'run', this)].concat(args));
		}
	}, {
		key: 'finish',
		value: function finish() {
			for (var _len9 = arguments.length, args = Array(_len9), _key9 = 0; _key9 < _len9; _key9++) {
				args[_key9] = arguments[_key9];
			}

			return _finish.call.apply(_finish, [this, _get(Suite.prototype.__proto__ || Object.getPrototypeOf(Suite.prototype), 'finish', this)].concat(args));
		}
	}, {
		key: 'suite',
		value: function suite() {
			for (var _len10 = arguments.length, args = Array(_len10), _key10 = 0; _key10 < _len10; _key10++) {
				args[_key10] = arguments[_key10];
			}

			var suite = new (Function.prototype.bind.apply(Suite, [null].concat(args)))();
			return this.addTaskGroup(suite);
		}
	}, {
		key: 'describe',
		value: function describe() {
			return this.suite.apply(this, arguments);
		}
	}, {
		key: 'test',
		value: function test() {
			for (var _len11 = arguments.length, args = Array(_len11), _key11 = 0; _key11 < _len11; _key11++) {
				args[_key11] = arguments[_key11];
			}

			var test = new (Function.prototype.bind.apply(Test, [null].concat(args)))();
			return this.addTask(test);
		}
	}, {
		key: 'it',
		value: function it() {
			return this.test.apply(this, arguments);
		}
	}], [{
		key: 'create',
		value: function create() {
			for (var _len12 = arguments.length, args = Array(_len12), _key12 = 0; _key12 < _len12; _key12++) {
				args[_key12] = arguments[_key12];
			}

			return new (Function.prototype.bind.apply(this, [null].concat(args)))();
		}
	}, {
		key: 'isSuite',
		value: function isSuite(suite) {
			return suite instanceof Suite;
		}
	}]);

	function Suite() {
		var _ref;

		var _ret;

		_classCallCheck(this, Suite);

		for (var _len13 = arguments.length, args = Array(_len13), _key13 = 0; _key13 < _len13; _key13++) {
			args[_key13] = arguments[_key13];
		}

		var _this4 = _possibleConstructorReturn(this, (_ref = Suite.__proto__ || Object.getPrototypeOf(Suite)).call.apply(_ref, [this].concat(args)));

		var me = _this4;

		// Shallow Listeners
		_this4.on('item.add', function (item) {
			if (Test.isTest(item)) {
				item.on('running', function () {
					me.testRunCallback(item);
				});
				item.done(function (err) {
					me.testCompleteCallback(item, err);
				});
				item.on('before', function (complete) {
					me.emitSerial('test.before', this, complete);
				});
				item.on('after', function (complete) {
					me.emitSerial('test.after', this, complete);
				});
			} else if (Suite.isSuite(item)) {
				item.on('running', function () {
					me.suiteRunCallback(item);
				});
				item.done(function (err) {
					me.suiteCompleteCallback(item, err);
				});
				item.on('before', function (complete) {
					me.emitSerial('suite.before', this, complete);
				});
				item.on('after', function (complete) {
					me.emitSerial('suite.after', this, complete);
				});
			}

			// Add nested listener
			// nestedListener(item)
		});

		// Chain
		return _ret = _this4, _possibleConstructorReturn(_this4, _ret);
	}

	_createClass(Suite, [{
		key: 'addMethod',
		value: function addMethod(method) {
			var config = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};

			if (config.reporting == null) config.reporting = false;
			if (config.name == null) config.name = 'suite initializer for ' + this.name;
			if (config.args == null) config.args = [this.suite.bind(this), this.test.bind(this)];
			return _get(Suite.prototype.__proto__ || Object.getPrototypeOf(Suite.prototype), 'addMethod', this).call(this, method, config);
		}

		// =================================
		// Callbacks

	}, {
		key: 'suiteRunCallback',
		value: function suiteRunCallback(suite) {
			var report = suite.config.reporting !== false;

			if (report) {
				++joePrivate.totalSuites;
				joe.report('startSuite', suite);
			}
		}
	}, {
		key: 'suiteCompleteCallback',
		value: function suiteCompleteCallback(suite, err) {
			var report = suite.config.reporting !== false;

			if (err) {
				joePrivate.addErrorLog({ suite: suite, err: err });
				if (report) {
					++joePrivate.totalFailedSuites;
				}
			} else if (report) {
				++joePrivate.totalPassedSuites;
			}

			if (err || report) {
				joe.report('finishSuite', suite, err);
			}
		}
	}, {
		key: 'testRunCallback',
		value: function testRunCallback(test) {
			var report = test.config.reporting !== false;

			if (report) {
				++joePrivate.totalTests;
				joe.report('startTest', test);
			}
		}
	}, {
		key: 'testCompleteCallback',
		value: function testCompleteCallback(test, err) {
			var report = test.config.reporting !== false;

			if (err) {
				joePrivate.addErrorLog({ test: test, err: err });
				if (report) {
					++joePrivate.totalFailedTests;
				}
			} else if (report) {
				++joePrivate.totalPassedTests;
			}

			if (err || report) {
				joe.report('finishTest', test, err);
			}
		}
	}]);

	return Suite;
}(TaskGroup);

// =================================
// Event Emitter Grouped

// Add event emitter grouped to our classes


Object.getOwnPropertyNames(EventEmitterGrouped.prototype).forEach(function (key) {
	Test.prototype[key] = Suite.prototype[key] = EventEmitterGrouped.prototype[key];
});

// =================================
// Private Interface

// Creare out private interface for Joe
// The reason we have a public and private interface for joe is that we do not want tests being able to modify the test results
// As such, the private interface contains properties that must be mutable by the public interface, but not mutable by the bad tests
var joePrivate = {

	// Global Suite
	// We use a global suite to contain all of the Suite suites and joe.test tests
	globalSuite: null,

	// Get Global Suite
	// We have a getter for the global suite to create it when it is actually needed
	getGlobalSuite: function getGlobalSuite() {
		// If it doesn't exist, then create it and name it joe
		if (joePrivate.globalSuite == null) {
			joePrivate.globalSuite = new Suite({
				reporting: false,
				name: 'global joe suite'
			}).run();
		}

		// Return the global suite
		return joePrivate.globalSuite;
	},


	// Error Logs
	// We log all the errors that have occured with their suite and test
	// so the reporters can access them
	errorLogs: [], // [{err, suite, test, name}]

	// Add Error Log
	// Logs an error into the errors array, however only if we haven't already logged it
	// log = {err,suite,test,name}
	addErrorLog: function addErrorLog(errorLog) {
		var lastLog = joePrivate.errorLogs[joePrivate.errorLogs.length - 1];
		if (errorLog.err === (lastLog && lastLog.err)) {
			// ignore
		} else {
			joePrivate.errorLogs.push(errorLog);
		}
		return joePrivate;
	},


	// Exited?
	// Whether or not joe has already exited, either via error or via finishing everything it is meant to be doing
	// We store this flag, as we do not want to exit multiple times if we have multiple errors or exit signals
	exited: false,

	// Reports
	// This is a listing of all the reporters we will be using
	// Reporters are what output the results of our tests/suites to the user (Joe just runs them)
	reporters: [],

	// Totals
	// Here are a bunch of totals we use to calculate our progress
	// They are mostly used by reporters, however we do use them to figure out if joe was successful or not
	totalSuites: 0,
	totalPassedSuites: 0,
	totalFailedSuites: 0,
	totalTests: 0,
	totalPassedTests: 0,
	totalFailedTests: 0,

	// Get Reporters
	// If no reporters have been set, attempt to load reporters
	// Reporters will be loaded in order of descending preference
	// the `--joe-reporter=value` command line arguments
	// the `JOE_REPORTER` environment variable
	getReporters: function getReporters() {
		// Check if have no reporters
		if (joePrivate.reporters.length === 0) {
			// Prepare
			var reporters = [];

			// Cycle through our CLI arguments
			// looking for --joe-reporter=REPORTER
			if (process && process.argv) {
				var args = process.argv;
				for (var i = 0; i < args.length; ++i) {
					var arg = args[i];
					var _reporter = arg.replace(/^--joe-reporter=/, '');
					if (_reporter === arg) continue;
					reporters.push(_reporter);
				}
			}

			// If the CLI arguments returned no reporters
			// then attempt the environment variable
			if (reporters.length === 0 && process && process.env && process.env.JOE_REPORTER) {
				reporters.push(process.env.JOE_REPORTER);
			}

			// Attempt to load each reporter
			reporters.forEach(function (nameOrPath) {
				// Prepare
				var Reporter = null;

				// Attempt joe-reporter-nameOrPath first
				// as require('console') will return the console
				try {
					Reporter = require('joe-reporter-' + nameOrPath);
				} catch (nameError) {
					try {
						Reporter = require(nameOrPath);
					} catch (pathError) {
						throw new Error('joe could not find the reporter: ' + reporter);
					}
				}

				// Instantiate the reporter
				var reporter = new Reporter();

				// Add the reporter
				joe.addReporter(reporter);
			});
		}

		// Return our reporters
		return joePrivate.reporters;
	}
};

// =================================
// Public Interface

// Create the interface for Joe
var joe = {
	// Get Totals
	// Fetches all the different types of totals we have collected
	// and determines the incomplete suites and tasks
	// as well as whether or not everything has succeeded correctly (no incomplete, no failures, no errors)
	getTotals: function getTotals() {
		// Fetch
		var totalSuites = joePrivate.totalSuites,
		    totalPassedSuites = joePrivate.totalPassedSuites,
		    totalFailedSuites = joePrivate.totalFailedSuites,
		    totalTests = joePrivate.totalTests,
		    totalPassedTests = joePrivate.totalPassedTests,
		    totalFailedTests = joePrivate.totalFailedTests,
		    errorLogs = joePrivate.errorLogs;

		// Calculate

		var totalIncompleteSuites = totalSuites - totalPassedSuites - totalFailedSuites;
		var totalIncompleteTests = totalTests - totalPassedTests - totalFailedTests;
		var totalErrors = errorLogs.length;
		var success = totalIncompleteSuites === 0 && totalFailedSuites === 0 && totalIncompleteTests === 0 && totalFailedTests === 0 && totalErrors === 0;

		// Return
		var result = {
			totalSuites: totalSuites,
			totalPassedSuites: totalPassedSuites,
			totalFailedSuites: totalFailedSuites,
			totalIncompleteSuites: totalIncompleteSuites,
			totalTests: totalTests,
			totalPassedTests: totalPassedTests,
			totalFailedTests: totalFailedTests,
			totalIncompleteTests: totalIncompleteTests,
			totalErrors: totalErrors,
			success: success
		};

		return result;
	},


	// Get Errors
	// Returns a cloned array of all the error logs
	getErrorLogs: function getErrorLogs() {
		return joePrivate.errorLogs.slice();
	},


	// Has Errors
	// Returns false if there were no incomplete, no failures and no errors
	hasErrors: function hasErrors() {
		return joe.getTotals().success === false;
	},


	// Has Exited
	// Returns true if we have exited already
	// we do not want to exit multiple times
	hasExited: function hasExited() {
		return joePrivate.exited === true;
	},


	// Has Reportes
	// Do we have any reporters yet?
	hasReporters: function hasReporters() {
		return joePrivate.reporters !== 0;
	},


	// Add Reporter
	// Add a reporter to the list of reporters we will be using
	addReporter: function addReporter(reporter) {
		// Add joe to the reporter
		reporter.joe = joe;

		// Add the reporter to the list of reporters we have
		joePrivate.reporters.push(reporter);

		// Chain
		return joe;
	},


	// Set Reporter
	// Clear all the other reporters we may be using, and just use this one
	setReporter: function setReporter(reporterInstance) {
		joePrivate.reporters = [];
		if (reporterInstance) {
			joe.addReporter(reporterInstance);
		}

		// Chain
		return joe;
	},


	// Report
	// Report and event to our reporters
	report: function report(event) {
		// Fetch the reporters
		var reporters = joePrivate.getReporters();

		// Check we have reporters

		for (var _len14 = arguments.length, args = Array(_len14 > 1 ? _len14 - 1 : 0), _key14 = 1; _key14 < _len14; _key14++) {
			args[_key14 - 1] = arguments[_key14];
		}

		if (reporters.length === 0) {
			var _console;

			(_console = console).error.apply(_console, ['joe has no reporters to log:', event].concat(args));
			joe.exit(1);
			return joe;
		}

		// For each reporter that we have
		// Trigger the event handler if it exists for it
		for (var i = 0; i < reporters.length; ++i) {
			var _reporter2 = reporters[i];
			if (_reporter2[event]) _reporter2[event].apply(_reporter2, args);
		}

		// Chain
		return joe;
	},


	// Exit
	// Exit our process with the specifeid exitCode
	// If no exitCode is set, then we determine it through the hasErrors call
	exit: function exit() {
		var exitCode = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
		var reason = arguments[1];

		// Check
		if (joe.hasExited()) return;
		joePrivate.exited = true;

		// Determine exit code
		if (!exitCode) {
			exitCode = joe.hasErrors() ? 1 : 0;
		}

		// Stop running more tests if we have begun
		var suite = joePrivate.getGlobalSuite();
		if (suite) suite.destroy();

		// Report our exit
		joe.report('exit', exitCode, reason);

		// Kill our process with the correct exit code
		if (process && process.exit) {
			process.exit(exitCode);
		}

		// Chain
		return joe;
	}
};

// =================================
// Interface

// Create our public interface for creating suites and tests
joe.describe = joe.suite = function suite() {
	var _joePrivate$getGlobal;

	return (_joePrivate$getGlobal = joePrivate.getGlobalSuite()).suite.apply(_joePrivate$getGlobal, arguments);
};
joe.it = joe.test = function test() {
	var _joePrivate$getGlobal2;

	return (_joePrivate$getGlobal2 = joePrivate.getGlobalSuite()).test.apply(_joePrivate$getGlobal2, arguments);
};

// Freeze our public interface from changes
if (Object.freeze) {
	Object.freeze(joe);
}

// =================================
// Events

// On node systems, wait until the process exits
// such that errors that occur outside of the tests can be caught before joe shuts down
if (process) {
	process.on('beforeExit', function () {
		joe.exit(0, 'beforeExit');
	});

	process.on('exit', function () {
		joe.exit(0, 'exit');
	});

	// Have last, as this way it won't silence errors that may have occured earlier
	process.on('uncaughtException', function (err) {
		if (!err) err = new Error('uncaughtException was emitted without an error');
		joePrivate.addErrorLog({ err: err, name: 'uncaughtException' });
		joe.exit(1, 'uncaughtException');
	});
}

// On browser systems, wait until the tests have finished
else {
		joePrivate.getGlobalSuite().on('destroyed', function () {
			joe.exit(0, 'destroyed');
		});
	}

// =================================
// Export

module.exports = joe;