(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", "@dojo/core/async/Task", "./Deferred", "./Test"], factory);
    }
})(function (require, exports) {
    "use strict";
    Object.defineProperty(exports, "__esModule", { value: true });
    var Task_1 = require("@dojo/core/async/Task");
    var Deferred_1 = require("./Deferred");
    var Test_1 = require("./Test");
    var Suite = (function () {
        function Suite(options) {
            var _this = this;
            /**
             * If true, the suite will publish its start topic after the before callback has finished,
             * and will publish its end topic before the after callback has finished.
             */
            this.publishAfterSetup = false;
            Object.keys(options).filter(function (key) {
                return key !== 'tests';
            }).forEach(function (key) {
                _this[key] = options[key];
            });
            this.tests = [];
            if (options.tests) {
                options.tests.forEach(function (suiteOrTest) { return _this.add(suiteOrTest); });
            }
            if (!this.name && this.parent) {
                throw new Error('A non-root Suite must have a name');
            }
        }
        Object.defineProperty(Suite.prototype, "bail", {
            /**
             * A flag used to indicate whether a test run shoudl stop after a failed test.
             */
            get: function () {
                return this._bail || (this.parent && this.parent.bail);
            },
            set: function (value) {
                this._bail = value;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(Suite.prototype, "executor", {
            /**
             * The executor used to run this Suite.
             */
            get: function () {
                // Prefer the parent's executor
                return (this.parent && this.parent.executor) || this._executor;
            },
            set: function (value) {
                if (this._executor) {
                    var error = new Error('An executor may only be set once per suite');
                    error.name = 'AlreadyAssigned';
                    throw error;
                }
                this._executor = value;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(Suite.prototype, "grep", {
            /**
             * A regular expression used to filter, by test ID, which tests are run.
             */
            get: function () {
                return this._grep || (this.parent && this.parent.grep) || /.*/;
            },
            set: function (value) {
                this._grep = value;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(Suite.prototype, "id", {
            /**
             * The unique identifier of the suite, assuming all combinations of suite + test are unique.
             */
            get: function () {
                var name = [];
                var suite = this;
                do {
                    suite.name != null && name.unshift(suite.name);
                } while ((suite = suite.parent));
                return name.join(' - ');
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(Suite.prototype, "parentId", {
            /**
             * The unique identifier of the suite's parent.
             */
            get: function () {
                var parent = this.parent;
                if (parent) {
                    return parent.id;
                }
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(Suite.prototype, "remote", {
            /**
             * The WebDriver interface for driving a remote environment. This value is only guaranteed to exist from the
             * before/beforeEach/afterEach/after and test methods, since environments are not instantiated until they are
             * actually ready to be tested against.
             */
            get: function () {
                return (this.parent && this.parent.remote) ? this.parent.remote : this._remote;
            },
            set: function (value) {
                if (this._remote) {
                    throw new Error('AlreadyAssigned: remote may only be set once per suite');
                }
                this._remote = value;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(Suite.prototype, "sessionId", {
            /**
             * The sessionId of the environment in which the suite executed.
             */
            get: function () {
                if (this.parent) {
                    return this.parent.sessionId;
                }
                if (this._sessionId) {
                    return this._sessionId;
                }
                if (this.remote) {
                    return this.remote.session.sessionId;
                }
                return '';
            },
            set: function (value) {
                this._sessionId = value;
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(Suite.prototype, "numTests", {
            /**
             * The total number of tests in this suite and any sub-suites. To get only the number of tests for this suite,
             * look at `this.tests.length`.
             */
            get: function () {
                return this.tests.reduce(function (numTests, suiteOrTest) {
                    if (isSuite(suiteOrTest)) {
                        return numTests + suiteOrTest.numTests;
                    }
                    return numTests + 1;
                }, 0);
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(Suite.prototype, "numFailedTests", {
            /**
             * The total number of tests in this test suite and any sub-suites that have failed.
             */
            get: function () {
                return this.tests.reduce(function (numFailedTests, suiteOrTest) {
                    if (isSuite(suiteOrTest)) {
                        return numFailedTests + suiteOrTest.numFailedTests;
                    }
                    else if (!(suiteOrTest.hasPassed || suiteOrTest.skipped) || suiteOrTest.error) {
                        return numFailedTests + 1;
                    }
                    return numFailedTests;
                }, 0);
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(Suite.prototype, "numSkippedTests", {
            /**
             * The total number of tests in this test suite and any sub-suites that were skipped.
             */
            get: function () {
                return this.tests.reduce(function (numSkippedTests, suiteOrTest) {
                    if (isSuite(suiteOrTest)) {
                        return numSkippedTests + suiteOrTest.numSkippedTests;
                    }
                    else if (suiteOrTest.skipped) {
                        return numSkippedTests + 1;
                    }
                    return numSkippedTests;
                }, 0);
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(Suite.prototype, "hasParent", {
            /**
             * Whether or not this suite has a parent (for parity with serialized Suites).
             */
            get: function () {
                return Boolean(this.parent);
            },
            enumerable: true,
            configurable: true
        });
        Object.defineProperty(Suite.prototype, "timeout", {
            get: function () {
                if (this._timeout != null) {
                    return this._timeout;
                }
                if (this.parent) {
                    return this.parent.timeout;
                }
                return 30000;
            },
            set: function (value) {
                this._timeout = value;
            },
            enumerable: true,
            configurable: true
        });
        /**
         * Add a test or suite to this suite.
         */
        Suite.prototype.add = function (suiteOrTest) {
            if (!Test_1.isTest(suiteOrTest) && !isSuite(suiteOrTest)) {
                throw new Error('Tried to add invalid suite or test');
            }
            if (suiteOrTest.parent != null && suiteOrTest.parent !== this) {
                throw new Error('This Suite or Test already belongs to another parent');
            }
            this.tests.forEach(function (existingSuiteOrTest) {
                if (existingSuiteOrTest.name === suiteOrTest.name) {
                    throw new Error("A suite or test named \"" + suiteOrTest.name + "\" has already been added");
                }
            });
            suiteOrTest.parent = this;
            this.tests.push(suiteOrTest);
            if (Test_1.isTest(suiteOrTest)) {
                this.executor.emit('testAdd', suiteOrTest);
            }
            else {
                this.executor.emit('suiteAdd', suiteOrTest);
            }
        };
        /**
         * Runs test suite in order:
         *
         * * before
         * * <for each test>
         *   * beforeEach
         *   * test
         *   * afterEach
         * * after
         *
         * If before, beforeEach, afterEach, or after throw, the suite itself will be marked as failed
         * and no further tests in the suite will be executed.
         */
        Suite.prototype.run = function () {
            var _this = this;
            var startTime;
            // Run when the suite starts
            var start = function () {
                return _this.executor.emit('suiteStart', _this).then(function () {
                    startTime = Date.now();
                });
            };
            // Run when the suite has ended
            var end = function () {
                _this.timeElapsed = Date.now() - startTime;
                return _this.executor.emit('suiteEnd', _this);
            };
            // Run the before and after suite lifecycle methods
            var runLifecycleMethod = function (suite, name, test) {
                var result;
                return new Task_1.default(function (resolve, reject) {
                    var dfd;
                    var timeout;
                    // Provide a new Suite#async method for each call of a lifecycle method since there's no concept of
                    // a Suite-wide async deferred as there is for Tests.
                    suite.async = function (_timeout) {
                        timeout = _timeout;
                        var _dfd = new Deferred_1.default();
                        dfd = _dfd;
                        suite.async = function () {
                            return _dfd;
                        };
                        return _dfd;
                    };
                    var suiteFunc = suite[name];
                    // Call the lifecycle function. The suite.async method above may be called within this function
                    // call. If `test` is defined (i.e., this is beforeEach or afterEach), pass it first, followed by
                    // the suite. If `test` is not defined, just pass the suite. This ordering is maintain backwards
                    // compatibility with previous versions of Intern.
                    result = suiteFunc && (test ? suiteFunc.call(suite, test, suite) : suiteFunc.call(suite, suite));
                    // If dfd is set, it means the async method was called
                    if (dfd) {
                        // Assign to a const so TS knows it's defined
                        var _dfd_1 = dfd;
                        // If a timeout was set, async was called, so we should use the dfd created by the call to
                        // manage the timeout.
                        if (timeout) {
                            var timer_1 = setTimeout(function () {
                                var error = new Error("Timeout reached on " + suite.id + "#" + name);
                                error.name = 'TimeoutError';
                                _dfd_1.reject(error);
                            }, timeout);
                            _dfd_1.promise.catch(function (_error) { }).then(function () { return timer_1 && clearTimeout(timer_1); });
                        }
                        // If the return value looks like a promise, resolve the dfd if the return value resolves
                        if (Task_1.isThenable(result)) {
                            result.then(function () { return _dfd_1.resolve(); }, function (error) { return _dfd_1.reject(error); });
                        }
                        // Use the dfd.promise as the final result
                        result = dfd.promise;
                    }
                    if (Task_1.isThenable(result)) {
                        result.then(function () { return resolve(); }, reject);
                    }
                    else {
                        resolve();
                    }
                }, function () {
                    if (Task_1.isTask(result)) {
                        result.cancel();
                    }
                })
                    .finally(function () {
                    // Remove the async method since it should only be available within a lifecycle function call
                    suite.async = undefined;
                })
                    .catch(function (error) {
                    if (error.name !== 'Skipped') {
                        if (!_this.error) {
                            _this.executor.log('Suite errored with non-skip error', error);
                            _this.error = error;
                        }
                        throw error;
                    }
                });
            };
            // Convenience method to run 'before' suite lifecycle method
            var before = function () {
                return runLifecycleMethod(_this, 'before');
            };
            // Convenience method to run the 'after' suite lifecycle method
            var after = function () {
                return runLifecycleMethod(_this, 'after');
            };
            // Reset some state in case someone tries to re-run the same suite
            // TODO: Cancel any previous outstanding suite run
            // TODO: Test
            this.error = undefined;
            this.timeElapsed = 0;
            var task;
            var runTask;
            try {
                task = this.publishAfterSetup ? before().then(start) : start().then(before);
            }
            catch (error) {
                return Task_1.default.reject(error);
            }
            // The task that manages running this suite's tests
            return task.then(function () {
                // Run the beforeEach or afterEach methods for a given test in the proper order based on the current nested
                // Suite structure
                var runTestLifecycle = function (name, test) {
                    var methodQueue = [];
                    var suite = _this;
                    do {
                        if (name === 'beforeEach') {
                            // beforeEach executes in order parent -> child;
                            methodQueue.push(suite);
                        }
                        else {
                            // afterEach executes in order child -> parent
                            methodQueue.unshift(suite);
                        }
                    } while ((suite = suite.parent));
                    var currentMethod;
                    return new Task_1.default(function (resolve, reject) {
                        var firstError;
                        var handleError = function (error) {
                            if (name === 'afterEach') {
                                firstError = firstError || error;
                                next();
                            }
                            else {
                                reject(error);
                            }
                        };
                        var next = function () {
                            var suite = methodQueue.pop();
                            if (!suite) {
                                firstError ? reject(firstError) : resolve();
                                return;
                            }
                            currentMethod = runLifecycleMethod(suite, name, test).then(next, handleError);
                        };
                        next();
                    }, function () {
                        methodQueue.splice(0, methodQueue.length);
                        if (currentMethod) {
                            currentMethod.cancel();
                        }
                    });
                };
                var i = 0;
                var tests = _this.tests;
                var current;
                // Run each of the tests in this suite
                runTask = new Task_1.default(function (resolve, reject) {
                    var firstError;
                    var testTask;
                    var next = function () {
                        var test = tests[i++];
                        // The task is over when there are no more tests to run
                        if (!test) {
                            firstError ? reject(firstError) : resolve();
                            return;
                        }
                        var handleError = function (error) {
                            // An error may be associated with a deeper test already, in which case we do not
                            // want to reassociate it with a more generic parent
                            if (error && error.relatedTest == null) {
                                error.relatedTest = test;
                            }
                        };
                        var runTest = function () {
                            // Errors raised when running child tests should be reported but should not cause
                            // this suite’s run to reject, since this suite itself has not failed.
                            var result = test.run().catch(function (error) { handleError(error); });
                            testTask = new Task_1.default(function (resolve) { result.then(resolve); }, function () { result.cancel(); });
                            return testTask;
                        };
                        // If the suite will be skipped, mark the current test as skipped. This will skip both
                        // individual tests and nested suites.
                        if (_this.skipped != null) {
                            test.skipped = _this.skipped;
                        }
                        // test is a suite
                        if (isSuite(test)) {
                            current = runTest();
                        }
                        else {
                            if (!_this.grep.test(test.id)) {
                                test.skipped = 'grep';
                            }
                            if (test.skipped != null) {
                                current = _this.executor.emit('testEnd', test);
                            }
                            else {
                                current = runTestLifecycle('beforeEach', test)
                                    .then(runTest)
                                    .finally(function () {
                                    if (testTask && testTask.state === 1 /* Pending */) {
                                        testTask.cancel();
                                    }
                                    testTask = undefined;
                                    return runTestLifecycle('afterEach', test);
                                })
                                    .catch(function (error) {
                                    firstError = firstError || error;
                                    return handleError(error);
                                });
                            }
                        }
                        current.then(function () {
                            var skipRestOfSuite = function () {
                                _this.skipped = _this.skipped != null ? _this.skipped : BAIL_REASON;
                            };
                            // If the test was a suite and the suite was skipped due to bailing, skip the rest of this
                            // suite
                            if (isSuite(test) && test.skipped === BAIL_REASON) {
                                skipRestOfSuite();
                            }
                            else if (test.error && _this.bail) {
                                skipRestOfSuite();
                            }
                            next();
                        });
                    };
                    next();
                }, function () {
                    // Ensure no more tests will run
                    i = Infinity;
                    if (current) {
                        current.cancel();
                    }
                });
                return runTask;
            })
                .finally(function () {
                if (runTask && runTask.state === 1 /* Pending */) {
                    runTask.cancel();
                }
            })
                .finally(function () { return _this.publishAfterSetup ? end() : after(); })
                .finally(function () { return _this.publishAfterSetup ? after() : end(); });
        };
        /**
         * Skips this suite.
         *
         * @param {String} message
         * If provided, will be stored in this suite's `skipped` property.
         */
        Suite.prototype.skip = function (message) {
            if (message === void 0) { message = 'suite skipped'; }
            this.skipped = message;
            // Use the SKIP constant from Test so that calling Suite#skip from a test won't fail the test.
            throw Test_1.SKIP;
        };
        Suite.prototype.toJSON = function () {
            var _this = this;
            var json = {
                hasParent: Boolean(this.parent),
                tests: this.tests.map(function (test) { return test.toJSON(); })
            };
            var properties = [
                'name',
                'id',
                'parentId',
                'sessionId',
                'timeElapsed',
                'numTests',
                'numFailedTests',
                'numSkippedTests',
                'skipped'
            ];
            properties.forEach(function (key) {
                var value = _this[key];
                if (typeof value !== 'undefined') {
                    json[key] = value;
                }
            });
            if (this.error) {
                json.error = {
                    name: this.error.name,
                    message: this.error.message,
                    stack: this.error.stack
                };
                if (this.error.relatedTest && this.error.relatedTest !== this) {
                    // relatedTest can be the Suite itself in the case of nested suites (a nested Suite's error is
                    // caught by a parent Suite, which assigns the nested Suite as the relatedTest, resulting in
                    // nestedSuite.relatedTest === nestedSuite); in that case, don't serialize it
                    json.error.relatedTest = this.error.relatedTest.toJSON();
                }
            }
            return json;
        };
        return Suite;
    }());
    exports.default = Suite;
    function isSuite(value) {
        return Array.isArray(value.tests) && typeof value.hasParent === 'boolean';
    }
    exports.isSuite = isSuite;
    // BAIL_REASON needs to be a string so that Intern can tell when a remote has bailed during unit tests so that it can
    // skip functional tests.
    var BAIL_REASON = 'bailed';
});
//# sourceMappingURL=Suite.js.map