(function (factory) {
    if (typeof module === "object" && typeof module.exports === "object") {
        var v = factory(require, exports);
        if (v !== undefined) module.exports = v;
    }
    else if (typeof define === "function" && define.amd) {
        define(["require", "exports", "../Suite", "@dojo/core/lang", "@dojo/core/async/Task", "../common/ErrorFormatter", "../common/util", "../interfaces/object", "../interfaces/tdd", "../interfaces/bdd", "../interfaces/benchmark", "@dojo/shim/Promise", "chai", "@dojo/shim/global"], factory);
    }
})(function (require, exports) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    var Suite_1 = require("../Suite");
    var lang_1 = require("@dojo/core/lang");
    var Task_1 = require("@dojo/core/async/Task");
    var ErrorFormatter_1 = require("../common/ErrorFormatter");
    var util_1 = require("../common/util");
    var object_1 = require("../interfaces/object");
    var tdd_1 = require("../interfaces/tdd");
    var bdd_1 = require("../interfaces/bdd");
    var benchmark_1 = require("../interfaces/benchmark");
    var Promise_1 = require("@dojo/shim/Promise");
    var chai = require("chai");
    var global_1 = require("@dojo/shim/global");
    var console = global_1.default.console;
    /**
     * This is the default executor class.
     */
    var Executor = (function () {
        function Executor(options) {
            var _this = this;
            this._hasSuiteErrors = false;
            this._hasTestErrors = false;
            this._config = {
                bail: false,
                baseline: false,
                benchmark: false,
                browser: {
                    plugins: [],
                    reporters: [],
                    require: [],
                    suites: []
                },
                coverageVariable: '__coverage__',
                debug: false,
                defaultTimeout: 30000,
                excludeInstrumentation: /(?:node_modules|browser|tests)\//,
                filterErrorStack: false,
                grep: new RegExp(''),
                loader: { script: 'default' },
                name: 'intern',
                node: {
                    plugins: [],
                    reporters: [],
                    require: [],
                    suites: []
                },
                plugins: [],
                reporters: [],
                require: [],
                sessionId: '',
                suites: []
            };
            if (options) {
                this.configure(options);
            }
            this._listeners = {};
            this._reporters = [];
            this._plugins = {};
            this._loadingPlugins = [];
            this.registerInterface('object', object_1.getInterface(this));
            this.registerInterface('tdd', tdd_1.getInterface(this));
            this.registerInterface('bdd', bdd_1.getInterface(this));
            this.registerInterface('benchmark', benchmark_1.getInterface(this));
            this.registerPlugin('chai', function () { return chai; });
            this._rootSuite = new Suite_1.default({ executor: this });
            // This is the first suiteEnd listener. When the root unit test suite ends, it will emit a coverage message
            // before any other suiteEnd listeners are called.
            this.on('suiteEnd', function (suite) {
                if (!suite.hasParent && !suite.sessionId) {
                    return _this._emitCoverage('unit tests');
                }
            });
        }
        Object.defineProperty(Executor.prototype, "config", {
            /**
             * The resolved configuration for this executor.
             */
            get: function () {
                return this._config;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(Executor.prototype, "suites", {
            /**
             * The root suites managed by this executor
             */
            get: function () {
                return [this._rootSuite];
            },
            enumerable: true,
            configurable: true
        });
        /**
         * Format an error, normalizing the stack trace and resolving source map references
         */
        Executor.prototype.formatError = function (error, options) {
            if (!this._errorFormatter) {
                this._errorFormatter = new ErrorFormatter_1.default(this);
            }
            return this._errorFormatter.format(error, options);
        };
        /**
         * Add a suite to the set of suites that will be run when `run` is called. The suited is added by calling a factory
         * function. The use of a factory allows for distinct instances of a given suite to be create when an executor has
         * multiple root suites.
         * parent suites.
         *
         * @param factory A function that will create a Suite object and add it to a given parent suite
         */
        Executor.prototype.addSuite = function (factory) {
            factory(this._rootSuite);
        };
        /**
         * Update this executor's configuration.
         */
        Executor.prototype.configure = function (options) {
            var _this = this;
            options = options || {};
            Object.keys(options).forEach(function (key) {
                var _a = _this._evalProperty(key), name = _a.name, addToExisting = _a.addToExisting;
                _this._processOption(name, options[key], addToExisting);
            });
        };
        Executor.prototype.emit = function (eventName, data) {
            if (eventName === 'suiteEnd' && data.error) {
                this._hasSuiteErrors = true;
            }
            else if (eventName === 'testEnd' && data.error) {
                this._hasTestErrors = true;
            }
            var notifications = [];
            (this._listeners[eventName] || []).forEach(function (listener) {
                notifications.push(Task_1.default.resolve(listener(data)));
            });
            var starListeners = this._listeners['*'] || [];
            if (starListeners.length > 0) {
                var starEvent_1 = { name: eventName, data: data };
                starListeners.forEach(function (listener) {
                    notifications.push(Task_1.default.resolve(listener(starEvent_1)));
                });
            }
            if (notifications.length === 0) {
                // Report errors and warnings when no listeners are registered
                if (eventName === 'error') {
                    console.error(this.formatError(data));
                }
                else if (eventName === 'warning') {
                    console.warn("WARNING: " + data);
                }
                return Task_1.default.resolve();
            }
            return Task_1.default.all(notifications);
        };
        Executor.prototype.getInterface = function (name) {
            return this.getPlugin("interface." + name);
        };
        Executor.prototype.getPlugin = function (type, name) {
            var pluginName = typeof name === 'undefined' ? type : type + "." + name;
            if (!(pluginName in this._plugins)) {
                throw new Error("A plugin named \"" + pluginName + "\" has not been registered");
            }
            return this._plugins[pluginName];
        };
        /**
         * Convenience method for emitting log events
         */
        Executor.prototype.log = function () {
            var args = [];
            for (var _i = 0; _i < arguments.length; _i++) {
                args[_i] = arguments[_i];
            }
            if (this.config.debug) {
                var message = args.map(function (arg) {
                    var type = typeof arg;
                    if (type === 'string') {
                        return arg;
                    }
                    if (type === 'function' || arg instanceof RegExp) {
                        return arg.toString();
                    }
                    if (arg instanceof Error) {
                        arg = { name: arg.name, message: arg.message, stack: arg.stack };
                    }
                    try {
                        return JSON.stringify(arg);
                    }
                    catch (error) {
                        return arg.toString();
                    }
                }).join(' ');
                return this.emit('log', message);
            }
            else {
                return Task_1.default.resolve();
            }
        };
        Executor.prototype.on = function (eventName, listener) {
            var _eventName;
            if (typeof listener === 'undefined') {
                listener = eventName;
                _eventName = '*';
            }
            else {
                _eventName = eventName;
            }
            var listeners = this._listeners[_eventName];
            if (!listeners) {
                listeners = this._listeners[_eventName] = [];
            }
            if (listeners.indexOf(listener) === -1) {
                listeners.push(listener);
            }
            var handle = {
                destroy: function () {
                    this.destroy = function () { };
                    util_1.pullFromArray(listeners, listener);
                }
            };
            return handle;
        };
        /**
         * Register an interface plugin
         */
        Executor.prototype.registerInterface = function (name, iface) {
            this.registerPlugin("interface." + name, function () { return iface; });
        };
        /**
         * Set the loader script that will be used to load plugins and suites.
         * will handle the loading of test suites.
         */
        Executor.prototype.registerLoader = function (init) {
            this._loaderInit = Promise_1.default.resolve(init(this._loaderConfig));
        };
        Executor.prototype.registerPlugin = function (type, name, init) {
            var pluginName = typeof init === 'undefined' ? type : type + "." + name;
            var pluginInit = typeof init === 'undefined' ? name : init;
            var options = this._loadingPluginOptions;
            var result = options ? pluginInit(options) : pluginInit();
            if (Task_1.isThenable(result)) {
                // If the result is thenable, push it on the loading queue
                this._loadingPlugins.push({
                    name: pluginName,
                    init: new Task_1.default(function (resolve, reject) { return result.then(function (value) { return resolve(value); }, reject); }, function () { Task_1.isTask(result) && result.cancel(); })
                });
            }
            else {
                // If the result is not thenable, immediately add it to the plugins list
                this._assignPlugin(pluginName, result);
            }
        };
        /**
         * Register a reporter plugin
         */
        Executor.prototype.registerReporter = function (name, Ctor) {
            this.registerPlugin('reporter', name, function () { return Ctor; });
        };
        /**
         * Run tests. This method sets up the environment for test execution, runs the tests, and runs any finalization code
         * afterwards. Subclasses should override `_beforeRun`, `_runTests`, and `_afterRun` to alter how tests are run.
         */
        Executor.prototype.run = function () {
            var _this = this;
            // Only allow the executor to be started once
            if (!this._runTask) {
                var runError_1;
                try {
                    this._runTask = this._resolveConfig();
                    if (this.config.showConfig) {
                        this._runTask = this._runTask
                            .then(function () {
                            // Emit the config as JSON deeply sorted by key
                            var sort = function (value) {
                                if (Array.isArray(value)) {
                                    value = value.map(sort).sort();
                                }
                                else if (typeof value === 'object') {
                                    var newObj_1 = {};
                                    Object.keys(value).sort().forEach(function (key) {
                                        newObj_1[key] = sort(value[key]);
                                    });
                                    value = newObj_1;
                                }
                                return value;
                            };
                            console.log(JSON.stringify(sort(_this.config), null, '    '));
                        })
                            .catch(function (error) {
                            // Display resolution errors because reporters haven't been installed yet
                            console.error(_this.formatError(error));
                            throw error;
                        });
                    }
                    else {
                        var currentTask_1;
                        this._runTask = this._runTask
                            .then(function () { return _this._loadRequires(); })
                            .then(function () { return _this._loadLoader(); })
                            .then(function () { return _this._loadPlugins(); })
                            .then(function () { return _this._loadSuites(); })
                            .then(function () { return _this._beforeRun(); })
                            .then(function (skipTests) {
                            if (skipTests) {
                                return;
                            }
                            // Keep track of distinct tasks to allow them to be cancelled
                            var outerTask;
                            var testingTask;
                            currentTask_1 = new Task_1.default(function (resolve, reject) {
                                outerTask = _this.emit('beforeRun')
                                    .then(function () {
                                    return _this.emit('runStart')
                                        .then(function () { return testingTask = _this._runTests(); })
                                        .catch(function (error) {
                                        runError_1 = error;
                                        return _this.emit('error', error);
                                    })
                                        .finally(function () { return _this.emit('runEnd'); });
                                })
                                    .finally(function () { return _this.emit('afterRun'); })
                                    .then(resolve, reject);
                            }, function () {
                                if (testingTask && testingTask.state === 1 /* Pending */) {
                                    testingTask.cancel();
                                }
                                else if (outerTask && outerTask.state === 1 /* Pending */) {
                                    outerTask.cancel();
                                }
                            });
                            return currentTask_1;
                        })
                            .finally(function () { return _this._afterRun(); })
                            .finally(function () {
                            if (currentTask_1 && currentTask_1.state === 1 /* Pending */) {
                                currentTask_1.cancel();
                            }
                        })
                            .catch(function (error) {
                            return _this.emit('error', error).finally(function () {
                                // A runError has priority over any cleanup errors, so rethrow one if it exists.
                                throw runError_1 || error;
                            });
                        })
                            .then(function () {
                            // If we didn't have any cleanup errors but a runError was caught, throw it to reject the
                            // run task
                            if (runError_1) {
                                throw runError_1;
                            }
                            // If there were no run errors but any suites had errors, throw an error to reject the run
                            // task.
                            if (_this._hasSuiteErrors) {
                                throw new Error('One or more suite errors occurred during testing');
                            }
                            // If there were no run errors but any tests failed, throw an error to reject the run task.
                            if (_this._hasTestErrors) {
                                throw new Error('One or more tests failed');
                            }
                        });
                    }
                }
                catch (error) {
                    this._runTask = this.emit('error', error).then(function () {
                        return Task_1.default.reject(error);
                    });
                }
            }
            return this._runTask;
        };
        /**
         * Code to execute after the main test run has finished to shut down the test system.
         */
        Executor.prototype._afterRun = function () {
            return Task_1.default.resolve();
        };
        /**
         * Add a resolved plugin to the internal plugins list
         */
        Executor.prototype._assignPlugin = function (name, plugin) {
            if (name.indexOf('reporter.') === 0 && typeof plugin !== 'function') {
                throw new Error('A reporter plugin must be a constructor');
            }
            this._plugins[name] = plugin;
        };
        /**
         * Code to execute before the main test run has started to set up the test system. This is where Executors can do
         * any last-minute configuration before the testing process begins.
         *
         * This method returns a Task that resolves to a boolean. A value of true indicates that Intern should skip running
         * tests and exit normally.
         */
        Executor.prototype._beforeRun = function () {
            var _this = this;
            var config = this.config;
            var envReporters = config[this.environment].reporters;
            // Take reporters from the base config that aren't also specified in an environment config
            var baseReporters = config.reporters.filter(function (reporter) {
                return !envReporters.some(function (envReporter) { return envReporter.name === reporter.name; });
            });
            baseReporters.concat(envReporters).forEach(function (reporter) {
                var ReporterClass = _this.getPlugin('reporter', reporter.name);
                if (!ReporterClass) {
                    throw new Error("A reporter named " + reporter.name + " hasn't been registered");
                }
                if (typeof ReporterClass !== 'function') {
                    throw new Error("The reporter " + reporter.name + " isn't a valid constructor");
                }
                _this._reporters.push(new ReporterClass(_this, reporter.options));
            });
            this._rootSuite.bail = config.bail;
            this._rootSuite.grep = config.grep;
            this._rootSuite.name = config.name;
            this._rootSuite.sessionId = config.sessionId;
            this._rootSuite.timeout = config.defaultTimeout;
            return Task_1.default.resolve(false);
        };
        Executor.prototype._emitCoverage = function (source) {
            var coverage = global_1.default[this.config.coverageVariable];
            if (coverage) {
                return this.emit('coverage', { coverage: coverage, source: source, sessionId: this.config.sessionId });
            }
        };
        /**
         * Evaluate a config property key
         */
        Executor.prototype._evalProperty = function (key) {
            var addToExisting = key[key.length - 1] === '+';
            var name = addToExisting ? key.slice(0, key.length - 1) : key;
            return { name: name, addToExisting: addToExisting };
        };
        /**
         * Load a loader
         */
        Executor.prototype._loadLoader = function () {
            var _this = this;
            // If registerLoader was already called, just wait for that loader to initialize
            if (this._loaderInit) {
                return this._loaderInit.then(function (loader) {
                    _this._loader = loader;
                });
            }
            else {
                var config = this.config;
                var loader = config[this.environment].loader || config.loader;
                var script_1 = loader.script;
                switch (script_1) {
                    case 'default':
                    case 'dojo':
                    case 'dojo2':
                    case 'systemjs':
                        script_1 = config.internPath + "loaders/" + script_1 + ".js";
                }
                this._loaderConfig = loader.options || {};
                return this.loadScript(script_1).then(function () {
                    if (!_this._loaderInit) {
                        throw new Error("Loader script " + script_1 + " did not register a loader callback");
                    }
                    return _this._loaderInit;
                }).then(function (loader) {
                    _this._loader = loader;
                });
            }
        };
        /**
         * Load plugins
         */
        Executor.prototype._loadPlugins = function () {
            var _this = this;
            var plugins = this.config.plugins.concat(this.config[this.environment].plugins);
            this._autoLoadingPlugins = true;
            return plugins
                .reduce(function (previous, plugin) {
                return previous.then(function () {
                    _this._loadingPluginOptions = plugin.options;
                    return _this._loader([plugin.script]).then(function () { _this._loadingPluginOptions = undefined; });
                });
            }, Task_1.default.resolve())
                .finally(function () {
                _this._autoLoadingPlugins = false;
            })
                .then(function () {
                // Wait for all plugin registrations, both configured ones and any that were manually registered, to
                // resolve
                return Task_1.default.all(_this._loadingPlugins.map(function (entry) { return entry.init; }))
                    .then(function (plugins) {
                    plugins.forEach(function (plugin, index) {
                        var name = _this._loadingPlugins[index].name;
                        _this._assignPlugin(name, plugin);
                    });
                });
            });
        };
        /**
         * Load scripts in the `require` list. These will be loaded sequentially in order using a platform-specific loading
         * mechanism (script injection or Node's require).
         */
        Executor.prototype._loadRequires = function () {
            var _this = this;
            var requires = this.config.require.concat(this.config[this.environment].require);
            requires.reduce(function (previous, script) {
                return previous.then(function () { return _this.loadScript(script); });
            }, Task_1.default.resolve());
        };
        /**
         * Load suites
         */
        Executor.prototype._loadSuites = function () {
            var _this = this;
            var suites = this.config.suites.concat(this.config[this.environment].suites);
            return Task_1.default.resolve(this._loader(suites))
                .then(function () { _this.log('Loaded suites:', suites); });
        };
        /**
         * Process an arbitrary config value. Subclasses can override this method to pre-process arguments or handle them
         * instead of allowing Executor to.
         */
        Executor.prototype._processOption = function (name, value, addToExisting) {
            var _this = this;
            switch (name) {
                case 'loader':
                    this._setOption(name, util_1.parseValue(name, value, 'object', 'script'));
                    break;
                case 'bail':
                case 'baseline':
                case 'benchmark':
                case 'debug':
                case 'filterErrorStack':
                case 'showConfig':
                    this._setOption(name, util_1.parseValue(name, value, 'boolean'));
                    break;
                case 'basePath':
                case 'coverageVariable':
                case 'description':
                case 'internPath':
                case 'name':
                case 'sessionId':
                    this._setOption(name, util_1.parseValue(name, value, 'string'));
                    break;
                case 'defaultTimeout':
                    this._setOption(name, util_1.parseValue(name, value, 'number'));
                    break;
                case 'excludeInstrumentation':
                    if (value === true) {
                        this._setOption(name, value);
                    }
                    else if (typeof value === 'string' || value instanceof RegExp) {
                        this._setOption(name, util_1.parseValue(name, value, 'regexp'));
                    }
                    else {
                        throw new Error("Invalid value \"" + value + "\" for " + name + "; must be (string | RegExp | true)");
                    }
                    break;
                case 'grep':
                    this._setOption(name, util_1.parseValue(name, value, 'regexp'));
                    break;
                case 'reporters':
                    this._setOption(name, util_1.parseValue(name, value, 'object[]', 'name'), addToExisting);
                    break;
                case 'plugins':
                    this._setOption(name, util_1.parseValue(name, value, 'object[]', 'script'), addToExisting);
                    break;
                case 'require':
                case 'suites':
                    this._setOption(name, util_1.parseValue(name, value, 'string[]'), addToExisting);
                    break;
                case 'node':
                case 'browser':
                    var envConfig_1 = this.config[name];
                    var envName_1 = name;
                    value = util_1.parseValue(name, value, 'object');
                    if (value) {
                        Object.keys(value).forEach(function (key) {
                            var resource = value[key];
                            var _a = _this._evalProperty(key), name = _a.name, addToExisting = _a.addToExisting;
                            switch (name) {
                                case 'loader':
                                    resource = util_1.parseValue(name, resource, 'object', 'script');
                                    _this._setOption(name, resource, false, envConfig_1);
                                    break;
                                case 'plugins':
                                    resource = util_1.parseValue('plugins', resource, 'object[]', 'script');
                                    _this._setOption(name, resource, addToExisting, envConfig_1);
                                    break;
                                case 'reporters':
                                    resource = util_1.parseValue('reporters', resource, 'object[]', 'name');
                                    _this._setOption(name, resource, addToExisting, envConfig_1);
                                    break;
                                case 'require':
                                case 'suites':
                                    resource = util_1.parseValue('suites', resource, 'string[]');
                                    _this._setOption(name, resource, addToExisting, envConfig_1);
                                    break;
                                default:
                                    throw new Error("Invalid property " + key + " in " + envName_1 + " config");
                            }
                        });
                    }
                    break;
                default:
                    console.warn("Config has unknown option \"" + name + "\"");
                    this._setOption(name, value);
            }
        };
        /**
         * Set an option value.
         */
        Executor.prototype._setOption = function (name, value, addToExisting, config) {
            if (addToExisting === void 0) { addToExisting = false; }
            config = config || this.config;
            // addToExisting
            if (addToExisting) {
                var currentValue = config[name];
                currentValue.push.apply(currentValue, value);
            }
            else {
                config[name] = value;
            }
        };
        /**
         * Resolve the config object.
         */
        Executor.prototype._resolveConfig = function () {
            var config = this.config;
            if (config.internPath != null) {
                config.internPath = util_1.normalizePathEnding(config.internPath);
            }
            else {
                config.internPath = '';
            }
            if (config.benchmark) {
                config.benchmarkConfig = lang_1.deepMixin({
                    mode: config.baseline ? 'baseline' : 'test',
                    id: 'Benchmark',
                    filename: 'baseline.json',
                    thresholds: {
                        warn: { rme: 3, mean: 5 },
                        fail: { rme: 6, mean: 10 }
                    },
                    verbosity: 0
                }, config.benchmarkConfig || {});
            }
            return Task_1.default.resolve();
        };
        /**
         * Runs each of the root suites, limited to a certain number of suites at the same time by `maxConcurrency`.
         */
        Executor.prototype._runTests = function () {
            return this._rootSuite.run();
        };
        return Executor;
    }());
    exports.default = Executor;
});
//# sourceMappingURL=Executor.js.map