define([
    'jquery',
    'bitbucket/internal/util/promise'
], function(
    $,
    promiseUtil
) {

    'use strict';

    var DELAY_INTERVAL = 500;

    module('util-promise', {
        setup: function() {
            var testContext = this;
            this.clock = sinon.useFakeTimers();
            this.delayed = promiseUtil.delay(function() {
                // Allow testContext.promiseFactory to be replaced with another function at runtime
                return testContext.promiseFactory.apply(testContext, arguments);
            }, DELAY_INTERVAL)();
            this.delayedDone = sinon.spy();
            this.delayed.done(this.delayedDone);
        },
        teardown: function() {
            this.clock.restore();
        },
        promiseFactory: function() {
            return $.Deferred().resolve().promise();
        }
    });

    test("delay", function() {
        this.clock.tick(DELAY_INTERVAL - 1);
        strictEqual(this.delayedDone.callCount, 0, "Promise is not resolved yet");
        this.clock.tick(1);
        strictEqual(this.delayedDone.callCount, 1, "Promise is resolved after elapsed interval");
    });

    test("delay then abort before delay interval elapsed", function() {
        this.clock.tick(DELAY_INTERVAL - 1);
        this.delayed.abort();
        this.clock.tick(1);
        strictEqual(this.delayedDone.callCount, 0, "Promise is not resolved after elapsed interval once aborted");
    });

    test("delay then abort after delay interval elapsed", function() {
        var abort = sinon.spy();
        var promiseFactory = function () {
            return $.Deferred().promise({
                abort: abort
            });
        };
        this.delayed = promiseUtil.delay(promiseFactory, DELAY_INTERVAL)();
        this.clock.tick(DELAY_INTERVAL);
        this.delayed.abort();

        strictEqual(abort.callCount, 1, "Promise is aborted");
    });

    test("delay then reset", function() {
        this.clock.tick(DELAY_INTERVAL - 1);
        strictEqual(this.delayedDone.callCount, 0, "Promise is not resolved yet");
        // First reset
        this.delayed.reset();
        this.clock.tick(DELAY_INTERVAL - 1);
        strictEqual(this.delayedDone.callCount, 0, "Promise is not resolved yet due to reset");
        // Second reset
        this.delayed.reset();
        this.clock.tick(DELAY_INTERVAL - 1);
        strictEqual(this.delayedDone.callCount, 0, "Promise is not resolved yet due to reset");
        this.clock.tick(1);
        strictEqual(this.delayedDone.callCount, 1, "Promise is resolved after elapsed interval after last reset");
    });

    test("reset does nothing after abort", function() {
        this.delayed.abort();
        this.delayed.reset();
        this.clock.tick(DELAY_INTERVAL);
        strictEqual(this.delayedDone.callCount, 0, "Promise is not resolved");
    });

    test("reset does nothing after interval expires", function() {
        // Expire interval
        this.clock.tick(DELAY_INTERVAL);
        // Set up spies
        this.delayedDone.reset();
        sinon.spy(this, 'promiseFactory');
        // Reset delayed then expire interval again
        this.delayed.reset();
        this.clock.tick(DELAY_INTERVAL);
        strictEqual(this.delayedDone.callCount, 0, "Promise is not resolved again");
        strictEqual(this.promiseFactory.callCount, 0, "Promise factory is not invoked again");
    });

    function isAbortable(abortable) {
        ok(abortable, 'Abortable exists.');
        equal(typeof abortable.then, 'function', 'has then function');
        equal(typeof abortable.thenAbortable, 'function', 'has thenAbortable function');
        equal(typeof abortable.abort, 'function', 'has abort function');
    }

    test("thenAbortable - preempted", function() {
        var deferred = $.Deferred();
        deferred.abort = sinon.spy();
        var abortable = promiseUtil.thenAbortable(deferred);
        isAbortable(abortable);

        var secondaryDeferred = $.Deferred();
        secondaryDeferred.abort = sinon.spy();

        var unabortableFaliure = sinon.spy(function() { return secondaryDeferred; });
        var unabortable = abortable.then(null, unabortableFaliure);

        var abortedFailure = sinon.spy(function() { return secondaryDeferred; });
        var secondAbortable = abortable.thenAbortable(null, abortedFailure);

        abortable.abort();
        equal(deferred.abort.callCount, 1, 'abort call is propagated down');

        var expectedThis = {};
        var expectedArg = 'testValue';
        deferred.rejectWith(expectedThis, [ expectedArg ]);
        equal(unabortableFaliure.callCount, 1, 'callback for aborted `then` should fire');
        equal(abortedFailure.callCount, 0, 'callback for aborted `thenAbortable` should not fire');

        equal(unabortable.state(), 'pending', 'Unabortable state is not affected by abort call');
        equal(secondAbortable.state(), 'rejected', 'Abortable immediately rejects');

        secondAbortable.fail(function() {
            equal(this, expectedThis, 'callback called with expected `this`');
            equal(arguments.length, 1, 'callback called with expected arg length');
            equal(arguments[0], expectedArg, 'callback called with expected args');
        });
        isAbortable(secondAbortable);

        secondAbortable.abort();
        equal(deferred.abort.callCount, 1, 'abort call is ignored, not propagated');
        equal(unabortableFaliure.callCount, 1, 'abort call is ignored');
        equal(abortedFailure.callCount, 0, 'abort call is ignored');
    });

    test("thenAbortable - abort after callback", function() {
        var deferred = $.Deferred();
        deferred.abort = sinon.spy();
        var abortable = promiseUtil.thenAbortable(deferred);
        isAbortable(abortable);

        var secondaryDeferred = $.Deferred();
        secondaryDeferred.abort = sinon.spy();

        var expectedThis = {};
        var expectedArg = 'testValue';

        var resolveHandler = sinon.spy(function() {
            equal(this, expectedThis, 'callback called with expected `this`');
            equal(arguments.length, 1, 'callback called with expected arg length');
            equal(arguments[0], expectedArg, 'callback called with expected args');

            return secondaryDeferred;
        });

        var secondaryAbortable = abortable.thenAbortable(resolveHandler);

        deferred.resolveWith(expectedThis, [ expectedArg ]);
        abortable.abort();

        equal(resolveHandler.callCount, 1, 'resolveHandler called');
        equal(secondaryAbortable.state(), 'pending', 'abortable not rejected due to abort');
        equal(secondaryDeferred.abort.callCount, 1, 'abort was propagated to secondary deferred');

        secondaryDeferred.resolve();
        equal(secondaryAbortable.state(), 'resolved', 'Resolving the secondary deferred resolves the secondary abortable');
    });

    test('whenAbortable', function() {
        var resolved = $.Deferred().resolve(1);
        var rejected = $.Deferred().reject(2);
        var pending = $.Deferred();

        var combined;

        combined = promiseUtil.whenAbortable(resolved, rejected, pending);
        isAbortable(combined);
        equal(combined.state(), 'rejected', 'Any rejected rejects the combined promise');
        combined.fail(function(arg) {
            equal(arg, 2, 'Reject handler gets correct argument');
        });

        combined = promiseUtil.whenAbortable(resolved, pending);
        isAbortable(combined);
        equal(combined.state(), 'pending', 'Pending without rejection leaves the combined promise pending');


        combined = promiseUtil.whenAbortable(resolved, resolved);
        isAbortable(combined);
        equal(combined.state(), 'resolved', 'All resolved resolves the combined promise');
        combined.done(function(argA, argB) {
            deepEqual(argA, 1, 'Resolve handler gets args from each promise');
            deepEqual(argB, 1, 'Resolve handler gets args from each promise');
        });
    });

    test('settle', function() {
        var immediateResolve = $.Deferred().resolve('a');
        var immediateReject = $.Deferred().reject('c');
        var delayedReject = $.Deferred();
        var settlePromise = promiseUtil.settle(immediateResolve, delayedReject, immediateReject);
        equal(settlePromise.state(), 'pending', 'Not resolved/rejected until all resolved or rejected');

        var expectedThis = {};
        delayedReject.rejectWith(expectedThis, ['b']); // first rejected this value is used
        equal(settlePromise.state(), 'rejected', 'Rejected if any are rejected');
        settlePromise.fail(function() {
            equal(this, expectedThis, 'Takes the `this` of the first rejected promise.');
            deepEqual([].slice.call(arguments), [['a'], ['b'], ['c']], 'Args are in order');
        });
    });
});