(function(root){
'use strict';
// testharness doesn't know about async test queues,
// so this wrapper takes care of that

/* USAGE:
    runParallelAsyncHarness({
        // list of data to test, must be array of objects.
        // each object must contain a "name" property to describe the test
        // besides name, the object can contain whatever data you need
        tests: [
            {name: "name of test 1", custom: "data"},
            {name: "name of test 2", custom: "data"},
            // ...
        ],

        // number of tests (tests, not test-cases!) to run concurrently
        testsPerSlice: 100,

        // time in milliseconds a test-run takes
        duration: 1000,

        // test-cases to run for for the test - there must be at least one
        // each case creates its separate async_test() instance
        cases: {
            // test case named "test1"
            test1: {
                // run as a async_test.step() this callback contains your primary assertions
                start: function(testCaseKey, data, options){},
                // run as a async_test.step() this callback contains assertions to be run
                // when the test ended, immediately before teardown
                done: function(testCaseKey, data, options){}
            },
            // ...
        }

        // all callbacks are optional:

        // invoked for individual test before it starts so you can setup the environment
        // like DOM, CSS, adding event listeners and such
        setup: function(data, options){},

        // invoked after a test ended, so you can clean up the environment
        // like DOM, CSS, removing event listeners and such
        teardown: function(data, options){},

        // invoked before a batch of tests ("slice") are run concurrently
        // tests is an array of test data objects
        sliceStart: function(options, tests)

        // invoked after a batch of tests ("slice") were run concurrently
        // tests is an array of test data objects
        sliceDone: function(options, tests)

        // invoked once all tests are done
        done: function(options){}
    })
*/
root.runParallelAsyncHarness = function(options) {
    if (!options.cases) {
        throw new Error("Options don't contain test cases!");
    }

    var noop = function(){};

    // add a 100ms buffer to the test timeout, just in case
    var duration = Math.ceil(options.duration + 100);

    // names of individual tests
    var cases = Object.keys(options.cases);

    // run tests in a batch of slices
    // primarily not to overload weak devices (tablets, phones, …)
    // with too many tests running simultaneously
    var iteration = -1;
    var testPerSlice = options.testsPerSlice || 100;
    var slices = Math.ceil(options.tests.length / testPerSlice);

    // initialize all async test cases
    // Note: satisfying testharness.js needs to know all async tests before load-event
    options.tests.forEach(function(data, index) {
        data.cases = {};
        cases.forEach(function(name) {
            data.cases[name] = async_test(data.name + " / " + name, {timeout: options.timeout || 60000});
        });
    });

    function runLoop() {
        iteration++;
        if (iteration >= slices) {
            // no more slice, we're done
            (options.done || noop)(options);
            return;
        }

        // grab a slice of testss and initialize them
        var offset = iteration * testPerSlice;
        var tests = options.tests.slice(offset, offset + testPerSlice);
        tests.forEach(function(data) {
            (options.setup || noop)(data, options);

        });

        // kick off the current slice of tests
        (options.sliceStart || noop)(options, tests);

        // perform individual "start" test-case
        tests.forEach(function(data) {
            cases.forEach(function(name) {
                data.cases[name].step(function() {
                    (options.cases[name].start || noop)(data.cases[name], data, options);
                });
            });
        });

        // conclude test (possibly abort)
        setTimeout(function() {
            tests.forEach(function(data) {
                // perform individual "done" test-case
                cases.forEach(function(name) {
                    data.cases[name].step(function() {
                        (options.cases[name].done || noop)(data.cases[name], data, options);
                    });
                });
                // clean up after individual test
                (options.teardown || noop)(data, options);
                // tell harness we're done with individual test-cases
                cases.forEach(function(name) {
                    data.cases[name].done();
                });
            });

            // finish the test for current slice of tests
            (options.sliceDone || noop)(options, tests);

            // next test please, give the browser 50ms to do catch its breath
            setTimeout(runLoop, 50);
        }, duration);
    }

    // allow DOMContentLoaded before actually doing something
    setTimeout(runLoop, 100);
};

})(window);
