/* global qFactory: false */
'use strict';

/**
  http://wiki.commonjs.org/wiki/Promises
  http://www.slideshare.net/domenicdenicola/callbacks-promises-and-coroutines-oh-my-the-evolution-of-asynchronicity-in-javascript

  Q:  https://github.com/kriskowal/q
      https://github.com/kriskowal/q/blob/master/design/README.js
      https://github.com/kriskowal/uncommonjs/blob/master/promises/specification.md
      http://jsconf.eu/2010/speaker/commonjs_i_promise_by_kris_kow.html
        - good walkthrough of the Q api's and design, jump to 15:30

  twisted: http://twistedmatrix.com/documents/11.0.0/api/twisted.internet.defer.Deferred.html
  dojo: https://github.com/dojo/dojo/blob/master/_base/Deferred.js
        http://dojotoolkit.org/api/1.6/dojo/Deferred
        http://dojotoolkit.org/documentation/tutorials/1.6/promises/
  when.js: https://github.com/briancavalier/when.js
  DART: http://www.dartlang.org/docs/api/Promise.html#Promise::Promise
        http://code.google.com/p/dart/source/browse/trunk/dart/corelib/src/promise.dart
        http://codereview.chromium.org/8271014/patch/11003/12005
        https://chromereviews.googleplex.com/3365018/
  WinJS: http://msdn.microsoft.com/en-us/library/windows/apps/br211867.aspx

  http://download.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/Future.html
  http://en.wikipedia.org/wiki/Futures_and_promises
  http://wiki.ecmascript.org/doku.php?id=strawman:deferred_functions
  http://wiki.ecmascript.org/doku.php?id=strawman:async_functions


  http://jsperf.com/throw-vs-return
*/

describe('q', function() {
  var q, q_no_error, defer, deferred, promise, log, exceptionHandlerCalls;

  // The following private functions are used to help with logging for testing invocation of the
  // promise callbacks.
  function _argToString(arg) {
    return (typeof arg === 'object' && !(isError(arg))) ? toJson(arg) : '' + arg;
  }

  function _argumentsToString(args) {
    return sliceArgs(args).map(_argToString).join(',');
  }

  // Help log invocation of success(), finally(), progress() and error()
  function _logInvocation(funcName, args, returnVal, throwReturnVal) {
    var logPrefix = funcName + '(' +  _argumentsToString(args) + ')';
    if (throwReturnVal) {
      log.push(logPrefix + '->throw(' +  _argToString(returnVal) + ')');
      throw returnVal;
    } else {
      if (isUndefined(returnVal)) {
        log.push(logPrefix);
      } else {
        log.push(logPrefix + '->' +  _argToString(returnVal));
      }
      return returnVal;
    }
  }

  /**
   * Creates a callback that logs its invocation in `log`.
   *
   * @param {(number|string)} name Suffix for 'success' name. e.g. success(1) => success1
   * @param {*=} returnVal Value that the callback should return. If unspecified, the passed in
   *     value is returned.
   * @param {boolean=} throwReturnVal If true, the `returnVal` will be thrown rather than returned.
   */
  function success(name, returnVal, throwReturnVal) {
    var returnValDefined = (arguments.length >= 2);
    name = 'success' + (name || '');
    return function() {
      return _logInvocation(name, arguments, (returnValDefined ? returnVal : arguments[0]), throwReturnVal);
    };
  }

  /**
   * Creates a callback that logs its invocation in `log`.
   *
   * @param {(number|string)} name Suffix for 'finally' name. e.g. finally(1) => finally
   * @param {*=} returnVal Value that the callback should return. If unspecified, the passed in
   *     value is returned.
   * @param {boolean=} throwReturnVal If true, the `returnVal` will be thrown rather than returned.
   */
  function fin(name, returnVal, throwReturnVal) {
    var returnValDefined = (arguments.length >= 2);
    name = 'finally' + (name || '');
    return function() {
      return _logInvocation(name, arguments, (returnValDefined ? returnVal : arguments[0]), throwReturnVal);
    };
  }

  /**
   * Creates a callback that logs its invocation in `log`.
   *
   * @param {(number|string)} name Suffix for 'progress' name. e.g. progress(1) => progress
   * @param {*=} returnVal Value that the callback should return. If unspecified, the passed in
   *     value is returned.
   * @param {boolean=} throwReturnVal If true, the `returnVal` will be thrown rather than returned.
   */
  function progress(name, returnVal, throwReturnVal) {
    var returnValDefined = (arguments.length >= 2);
    name = 'progress' + (name || '');
    return function() {
      return _logInvocation(name, arguments, (returnValDefined ? returnVal : arguments[0]), throwReturnVal);
    };
  }

  /**
   * Creates a callback that logs its invocation in `log`.
   *
   * @param {(number|string)} name Suffix for 'error' name. e.g. error(1) => error
   * @param {*=} returnVal Value that the callback should return. If unspecified, the passed in
   *     value is first passed to q.reject() and the result is returned.
   * @param {boolean=} throwReturnVal If true, the `returnVal` will be thrown rather than returned.
   */
  function error(name, returnVal, throwReturnVal) {
    var returnValDefined = (arguments.length >= 2);
    name = 'error' + (name || '');
    return function() {
      if (!returnValDefined) {
        _logInvocation(name, arguments, 'reject(' + _argToString(arguments[0]) + ')', throwReturnVal);
        return q.reject(arguments[0]);
      } else {
        return _logInvocation(name, arguments, returnVal, throwReturnVal);
      }
    };
  }

  /** helper for synchronous resolution of deferred */
  function syncResolve(deferred, result) {
    deferred.resolve(result);
    mockNextTick.flush();
  }


  /** helper for synchronous rejection of deferred */
  function syncReject(deferred, reason) {
    deferred.reject(reason);
    mockNextTick.flush();
  }


  /** helper for synchronous notification of deferred */
  function syncNotify(deferred, progress) {
    deferred.notify(progress);
    mockNextTick.flush();
  }


  /** converts the `log` to a '; '-separated string */
  function logStr() {
    return log.join('; ');
  }


  var mockNextTick = {
    nextTick: function(task) {
      mockNextTick.queue.push(task);
    },
    queue: [],
    logExceptions: true,
    flush: function() {
      if (!mockNextTick.queue.length) throw new Error('Nothing to be flushed!');
      while (mockNextTick.queue.length) {
        var queue = mockNextTick.queue;
        mockNextTick.queue = [];
        forEach(queue, function(task) {
          try {
            task();
          } catch (e) {
            if (mockNextTick.logExceptions) {
              dump('exception in mockNextTick:', e, e.name, e.message, task);
            }
          }
        });
      }
    }
  };


  function exceptionHandler(exception, reason) {
    if (typeof reason === 'undefined') {
      exceptionHandlerCalls.push({ reason: exception });
    } else {
      exceptionHandlerCalls.push({ reason: reason, exception: exception });
    }
  }

  beforeEach(function() {
    q = qFactory(mockNextTick.nextTick, exceptionHandler, true);
    q_no_error = qFactory(mockNextTick.nextTick, exceptionHandler, false);
    defer = q.defer;
    deferred =  defer();
    promise = deferred.promise;
    log = [];
    exceptionHandlerCalls = [];
    mockNextTick.queue = [];
  });


  afterEach(function() {
    expect(mockNextTick.queue.length).toBe(0);
  });


  describe('$Q', function() {
    var resolve, reject, resolve2, reject2;
    var createPromise = function() {
      return q(function(resolveFn, rejectFn) {
        if (resolve === null) {
          resolve = resolveFn;
          reject = rejectFn;
        } else if (resolve2 === null) {
          resolve2 = resolveFn;
          reject2 = rejectFn;
        }
      });
    };

    afterEach(function() {
      resolve = reject = resolve2 = reject2 = null;
    });

    it('should return a Promise', function() {
      var promise = q(noop);
      expect(typeof promise.then).toBe('function');
      expect(typeof promise.catch).toBe('function');
      expect(typeof promise.finally).toBe('function');
    });

    it('should support the instanceof operator', function() {
      // eslint-disable-next-line new-cap
      var promise = new q(noop);
      expect(promise instanceof q).toBe(true);
      promise = q(noop);
      expect(promise instanceof q).toBe(true);
    });


    describe('resolve', function() {
      it('should fulfill the promise and execute all success callbacks in the registration order',
          function() {
        var promise = createPromise();
        promise.then(success(1), error());
        promise.then(success(2), error());
        expect(logStr()).toBe('');

        resolve('foo');
        mockNextTick.flush();
        expect(logStr()).toBe('success1(foo)->foo; success2(foo)->foo');
      });


      it('should do nothing if a promise was previously resolved', function() {
        var promise = createPromise();
        promise.then(success(), error());
        expect(logStr()).toBe('');

        resolve('foo');
        mockNextTick.flush();
        expect(logStr()).toBe('success(foo)->foo');

        log = [];
        resolve('bar');
        reject('baz');
        expect(mockNextTick.queue.length).toBe(0);
        expect(logStr()).toBe('');
      });


      it('should do nothing if a promise was previously rejected', function() {
        var promise = createPromise();
        promise.then(success(), error());
        expect(logStr()).toBe('');

        reject('foo');
        mockNextTick.flush();
        expect(logStr()).toBe('error(foo)->reject(foo)');

        log = [];
        resolve('bar');
        reject('baz');
        expect(mockNextTick.queue.length).toBe(0);
        expect(logStr()).toBe('');
      });


      it('should allow deferred resolution with a new promise', function() {
        var promise = createPromise();

        promise.then(success(), error());

        resolve(createPromise());
        expect(logStr()).toBe('');

        resolve2('foo');
        mockNextTick.flush();
        expect(logStr()).toBe('success(foo)->foo');
      });


      it('should call the callback in the next turn', function() {
        var promise = createPromise();
        promise.then(success());
        expect(logStr()).toBe('');

        resolve('foo');
        expect(logStr()).toBe('');

        mockNextTick.flush();
        expect(logStr()).toBe('success(foo)->foo');
      });


      it('should not break if a callbacks registers another callback', function() {
        var promise = createPromise();
        promise.then(function() {
          log.push('outer');
          promise.then(function() {
            log.push('inner');
          });
        });

        resolve('foo');
        expect(logStr()).toBe('');

        mockNextTick.flush();
        expect(logStr()).toBe('outer; inner');
      });


      it('should not break if a callbacks tries to resolve the deferred again', function() {
        var promise = createPromise();
        promise.then(function(val) {
          log.push('then1(' + val + ')->resolve(bar)');
          deferred.resolve('bar'); // nop
        });

        promise.then(success(2));

        resolve('foo');
        expect(logStr()).toBe('');

        mockNextTick.flush();
        expect(logStr()).toBe('then1(foo)->resolve(bar); success2(foo)->foo');
      });
    });


    describe('reject', function() {
      it('should reject the promise and execute all error callbacks in the registration order',
          function() {
        var promise = createPromise();
        promise.then(success(), error(1));
        promise.then(success(), error(2));
        expect(logStr()).toBe('');

        reject('foo');
        mockNextTick.flush();
        expect(logStr()).toBe('error1(foo)->reject(foo); error2(foo)->reject(foo)');
      });


      it('should do nothing if a promise was previously resolved', function() {
        var promise = createPromise();
        promise.then(success(1), error(1));
        expect(logStr()).toBe('');

        resolve('foo');
        mockNextTick.flush();
        expect(logStr()).toBe('success1(foo)->foo');

        log = [];
        reject('bar');
        resolve('baz');
        expect(mockNextTick.queue.length).toBe(0);
        expect(logStr()).toBe('');

        promise.then(success(2), error(2));
        expect(logStr()).toBe('');
        mockNextTick.flush();
        expect(logStr()).toBe('success2(foo)->foo');
      });


      it('should do nothing if a promise was previously rejected', function() {
        var promise = createPromise();
        promise.then(success(1), error(1));
        expect(logStr()).toBe('');

        reject('foo');
        mockNextTick.flush();
        expect(logStr()).toBe('error1(foo)->reject(foo)');

        log = [];
        reject('bar');
        resolve('baz');
        expect(mockNextTick.queue.length).toBe(0);
        expect(logStr()).toBe('');

        promise.then(success(2), error(2));
        expect(logStr()).toBe('');
        mockNextTick.flush();
        expect(logStr()).toBe('error2(foo)->reject(foo)');
      });


      it('should not defer rejection with a new promise', function() {
        var promise = createPromise();
        promise.then(success(), error());

        reject(createPromise());
        mockNextTick.flush();
        expect(logStr()).toBe('error({})->reject({})');
      });


      it('should call the error callback in the next turn', function() {
        var promise = createPromise();
        promise.then(success(), error());
        expect(logStr()).toBe('');

        reject('foo');
        expect(logStr()).toBe('');

        mockNextTick.flush();
        expect(logStr()).toBe('error(foo)->reject(foo)');
      });


      it('should support non-bound execution', function() {
        var promise = createPromise();
        promise.then(success(), error());
        reject('detached');
        mockNextTick.flush();
        expect(logStr()).toBe('error(detached)->reject(detached)');
      });
    });


    describe('promise', function() {
      describe('then', function() {
        it('should allow registration of a success callback without an errback or progressBack ' +
          'and resolve', function() {
          var promise = createPromise();
          promise.then(success());
          resolve('foo');
          mockNextTick.flush();
          expect(logStr()).toBe('success(foo)->foo');
        });


        it('should allow registration of a success callback without an errback and reject',
            function() {
          var promise = createPromise();
          promise.then(success());
          reject('foo');
          mockNextTick.flush();
          expect(logStr()).toBe('');
        });


        it('should allow registration of an errback without a success or progress callback and ' +
          ' reject', function() {
          var promise = createPromise();
          promise.then(null, error());
          reject('oops!');
          mockNextTick.flush();
          expect(logStr()).toBe('error(oops!)->reject(oops!)');
        });


        it('should allow registration of an errback without a success callback and resolve',
            function() {
          var promise = createPromise();
          promise.then(null, error());
          resolve('done');
          mockNextTick.flush();
          expect(logStr()).toBe('');
        });


        it('should allow registration of a progressBack without a success callback and resolve',
            function() {
          var promise = createPromise();
          promise.then(null, null, progress());
          resolve('done');
          mockNextTick.flush();
          expect(logStr()).toBe('');
        });


        it('should allow registration of a progressBack without a error callback and reject',
            function() {
          var promise = createPromise();
          promise.then(null, null, progress());
          reject('oops!');
          mockNextTick.flush();
          expect(logStr()).toBe('');
        });


        it('should resolve all callbacks with the original value', function() {
          var promise = createPromise();
          promise.then(success('A', 'aVal'), error(), progress());
          promise.then(success('B', 'bErr', true), error(), progress());
          promise.then(success('C', q.reject('cReason')), error(), progress());
          promise.then(success('D', q.reject('dReason'), true), error(), progress());
          promise.then(success('E', 'eVal'), error(), progress());

          expect(logStr()).toBe('');
          resolve('yup');
          mockNextTick.flush();
          expect(log).toEqual(['successA(yup)->aVal',
                               'successB(yup)->throw(bErr)',
                               'successC(yup)->{}',
                               'successD(yup)->throw({})',
                               'successE(yup)->eVal']);
        });


        it('should reject all callbacks with the original reason', function() {
          var promise = createPromise();
          promise.then(success(), error('A', 'aVal'), progress());
          promise.then(success(), error('B', 'bEr', true), progress());
          promise.then(success(), error('C', q.reject('cReason')), progress());
          promise.then(success(), error('D', 'dVal'), progress());

          expect(logStr()).toBe('');
          reject('noo!');
          mockNextTick.flush();
          expect(logStr()).toBe('errorA(noo!)->aVal; errorB(noo!)->throw(bEr); errorC(noo!)->{}; errorD(noo!)->dVal');
        });


        it('should propagate resolution and rejection between dependent promises', function() {
          var promise = createPromise();
          promise.then(success(1, 'x'),       error('1')).
                  then(success(2, 'y', true), error('2')).
                  then(success(3),            error(3, 'z', true)).
                  then(success(4),            error(4, 'done')).
                  then(success(5),            error(5));

          expect(logStr()).toBe('');
          resolve('sweet!');
          mockNextTick.flush();
          expect(log).toEqual(['success1(sweet!)->x',
                               'success2(x)->throw(y)',
                               'error3(y)->throw(z)',
                               'error4(z)->done',
                               'success5(done)->done']);
        });


        it('should reject a derived promise if an exception is thrown while resolving its parent',
            function() {
          var promise = createPromise();
          promise.then(success(1, 'oops', true), error(1)).
                  then(success(2),               error(2));
          resolve('done!');
          mockNextTick.flush();
          expect(logStr()).toBe('success1(done!)->throw(oops); error2(oops)->reject(oops)');
        });


        it('should reject a derived promise if an exception is thrown while rejecting its parent',
            function() {
          var promise = createPromise();
          promise.then(null,       error(1, 'oops', true)).
                  then(success(2), error(2));
          reject('timeout');
          mockNextTick.flush();
          expect(logStr()).toBe('error1(timeout)->throw(oops); error2(oops)->reject(oops)');
        });


        it('should call success callback in the next turn even if promise is already resolved',
            function() {
          var promise = createPromise();
          resolve('done!');

          promise.then(success());
          expect(logStr()).toBe('');

          mockNextTick.flush();
          expect(log).toEqual(['success(done!)->done!']);
        });


        it('should call error callback in the next turn even if promise is already rejected',
            function() {
          var promise = createPromise();
          reject('oops!');

          promise.then(null, error());
          expect(logStr()).toBe('');

          mockNextTick.flush();
          expect(log).toEqual(['error(oops!)->reject(oops!)']);
        });

        it('should forward success resolution when success callbacks are not functions', function() {
          var promise = createPromise();
          resolve('yay!');

          promise.then(1).
                  then(null).
                  then({}).
                  then('gah!').
                  then([]).
                  then(success());

          expect(logStr()).toBe('');

          mockNextTick.flush();
          expect(log).toEqual(['success(yay!)->yay!']);
        });

        it('should forward error resolution when error callbacks are not functions', function() {
          var promise = createPromise();
          reject('oops!');

          promise.then(null, 1).
                  then(null, null).
                  then(null, {}).
                  then(null, 'gah!').
                  then(null, []).
                  then(null, error());

          expect(logStr()).toBe('');

          mockNextTick.flush();
          expect(log).toEqual(['error(oops!)->reject(oops!)']);
        });
      });


      describe('finally', function() {
        it('should not take an argument',
            function() {
          var promise = createPromise();
          promise['finally'](fin(1));
          resolve('foo');
          mockNextTick.flush();
          expect(logStr()).toBe('finally1()');
        });

        describe('when the promise is fulfilled', function() {
          it('should call the callback',
              function() {
            var promise = createPromise();
            promise.then(success(1))['finally'](fin(1));
            resolve('foo');
            mockNextTick.flush();
            expect(logStr()).toBe('success1(foo)->foo; finally1()');
          });

          it('should fulfill with the original value',
              function() {
            var promise = createPromise();
            promise['finally'](fin('B', 'b'), error('B')).
                    then(success('BB', 'bb'), error('BB'));
            resolve('RESOLVED_VAL');
            mockNextTick.flush();
            expect(log).toEqual(['finallyB()->b',
                                 'successBB(RESOLVED_VAL)->bb']);
          });


          it('should fulfill with the original value (larger test)',
              function() {
            var promise = createPromise();
            promise.then(success('A', 'a'), error('A'));
            promise['finally'](fin('B', 'b'), error('B')).
                    then(success('BB', 'bb'), error('BB'));
            promise.then(success('C', 'c'), error('C'))['finally'](fin('CC', 'IGNORED'))
                   .then(success('CCC', 'cc'), error('CCC'))
                   .then(success('CCCC', 'ccc'), error('CCCC'));
            resolve('RESOLVED_VAL');
            mockNextTick.flush();

            expect(log).toEqual(['successA(RESOLVED_VAL)->a',
                                 'finallyB()->b',
                                 'successC(RESOLVED_VAL)->c',
                                 'finallyCC()->IGNORED',
                                 'successBB(RESOLVED_VAL)->bb',
                                 'successCCC(c)->cc',
                                 'successCCCC(cc)->ccc']);
          });

          describe('when the callback returns a promise', function() {
            describe('that is fulfilled', function() {
              it('should fulfill with the original reason after that promise resolves',
                function() {
                var promise = createPromise();
                var promise2 = createPromise();
                resolve2('bar');

                promise['finally'](fin(1, promise))
                       .then(success(2));

                resolve('foo');
                mockNextTick.flush();

                expect(logStr()).toBe('finally1()->{}; success2(foo)->foo');
              });
            });

            describe('that is rejected', function() {
              it('should reject with this new rejection reason',
                  function() {
                var promise = createPromise();
                var promise2 = createPromise();
                reject2('bar');
                promise['finally'](fin(1, promise2))
                       .then(success(2), error(1));
                resolve('foo');
                mockNextTick.flush();
                expect(logStr()).toBe('finally1()->{}; error1(bar)->reject(bar)');
              });
            });

          });

          describe('when the callback throws an exception', function() {
            it('should reject with this new exception', function() {
              var promise = createPromise();
              promise['finally'](fin(1, 'exception', true))
                     .then(success(1), error(2));
              resolve('foo');
              mockNextTick.flush();
              expect(logStr()).toBe('finally1()->throw(exception); error2(exception)->reject(exception)');
            });
          });

        });


        describe('when the promise is rejected', function() {
          it('should call the callback', function() {
            var promise = createPromise();
            promise['finally'](fin(1))
                   .then(success(2), error(1));
            reject('foo');
            mockNextTick.flush();
            expect(logStr()).toBe('finally1(); error1(foo)->reject(foo)');
          });

          it('should reject with the original reason', function() {
            var promise = createPromise();
            promise['finally'](fin(1), 'hello')
                   .then(success(2), error(2));
            reject('original');
            mockNextTick.flush();
            expect(logStr()).toBe('finally1(); error2(original)->reject(original)');
          });

          describe('when the callback returns a promise', function() {
            describe('that is fulfilled', function() {
              it('should reject with the original reason after that promise resolves', function() {
                var promise = createPromise();
                var promise2 = createPromise();
                resolve2('bar');
                promise['finally'](fin(1, promise2))
                       .then(success(2), error(2));
                reject('original');
                mockNextTick.flush();
                expect(logStr()).toBe('finally1()->{}; error2(original)->reject(original)');
              });
            });

            describe('that is rejected', function() {
              it('should reject with the new reason', function() {
                var promise = createPromise();
                var promise2 = createPromise();
                reject2('bar');
                promise['finally'](fin(1, promise2))
                       .then(success(2), error(1));
                resolve('foo');
                mockNextTick.flush();
                expect(logStr()).toBe('finally1()->{}; error1(bar)->reject(bar)');
              });
            });
          });

          describe('when the callback throws an exception', function() {
            it('should reject with this new exception', function() {
              var promise = createPromise();
              promise['finally'](fin(1, 'exception', true))
                     .then(success(1), error(2));
              resolve('foo');
              mockNextTick.flush();
              expect(logStr()).toBe('finally1()->throw(exception); error2(exception)->reject(exception)');
            });
          });
        });
      });

      describe('catch', function() {
        it('should be a shorthand for defining promise error handlers', function() {
          var promise = createPromise();
          promise['catch'](error(1)).then(null, error(2));
          reject('foo');
          mockNextTick.flush();
          expect(logStr()).toBe('error1(foo)->reject(foo); error2(foo)->reject(foo)');
        });
      });
    });
  });


  describe('defer', function() {
    it('should create a new deferred', function() {
      expect(deferred.promise).toBeDefined();
      expect(deferred.resolve).toBeDefined();
      expect(deferred.reject).toBeDefined();
    });


    describe('resolve', function() {
      it('should fulfill the promise and execute all success callbacks in the registration order',
          function() {
        promise.then(success(1), error());
        promise.then(success(2), error());
        expect(logStr()).toBe('');

        deferred.resolve('foo');
        mockNextTick.flush();
        expect(logStr()).toBe('success1(foo)->foo; success2(foo)->foo');
      });


      it('should complain if promise fulfilled with itself', function() {
        var resolveSpy = jasmine.createSpy('resolve');
        var rejectSpy = jasmine.createSpy('reject');
        promise.then(resolveSpy, rejectSpy);
        deferred.resolve(deferred.promise);
        mockNextTick.flush();

        expect(resolveSpy).not.toHaveBeenCalled();
        expect(rejectSpy).toHaveBeenCalled();
        expect(rejectSpy.calls.argsFor(0)[0]).toEqualMinErr('$q', 'qcycle',
            'Expected promise to be resolved with value other than itself');
      });


      it('should do nothing if a promise was previously resolved', function() {
        promise.then(success(), error());
        expect(logStr()).toBe('');

        deferred.resolve('foo');
        mockNextTick.flush();
        expect(logStr()).toBe('success(foo)->foo');

        log = [];
        deferred.resolve('bar');
        deferred.reject('baz');
        expect(mockNextTick.queue.length).toBe(0);
        expect(logStr()).toBe('');
      });


      it('should do nothing if a promise was previously rejected', function() {
        promise.then(success(), error());
        expect(logStr()).toBe('');

        deferred.reject('foo');
        mockNextTick.flush();
        expect(logStr()).toBe('error(foo)->reject(foo)');

        log = [];
        deferred.resolve('bar');
        deferred.reject('baz');
        expect(mockNextTick.queue.length).toBe(0);
        expect(logStr()).toBe('');
      });


      it('should allow deferred resolution with a new promise', function() {
        var deferred2 = defer();
        promise.then(success(), error());

        deferred.resolve(deferred2.promise);
        expect(logStr()).toBe('');

        deferred2.resolve('foo');
        mockNextTick.flush();
        expect(logStr()).toBe('success(foo)->foo');
      });


      it('should call the callback in the next turn', function() {
        promise.then(success());
        expect(logStr()).toBe('');

        deferred.resolve('foo');
        expect(logStr()).toBe('');

        mockNextTick.flush();
        expect(logStr()).toBe('success(foo)->foo');
      });


      it('should support non-bound execution', function() {
        var resolver = deferred.resolve;
        promise.then(success(), error());
        resolver('detached');
        mockNextTick.flush();
        expect(logStr()).toBe('success(detached)->detached');
      });


      it('should not break if a callbacks registers another callback', function() {
        promise.then(function() {
          log.push('outer');
          promise.then(function() {
            log.push('inner');
          });
        });

        deferred.resolve('foo');
        expect(logStr()).toBe('');

        mockNextTick.flush();
        expect(logStr()).toBe('outer; inner');
      });


      it('should not break if a callbacks tries to resolve the deferred again', function() {
        promise.then(function(val) {
          log.push('then1(' + val + ')->resolve(bar)');
          deferred.resolve('bar'); // nop
        });

        promise.then(success(2));

        deferred.resolve('foo');
        expect(logStr()).toBe('');

        mockNextTick.flush();
        expect(logStr()).toBe('then1(foo)->resolve(bar); success2(foo)->foo');
      });
    });


    describe('reject', function() {
      it('should reject the promise and execute all error callbacks in the registration order',
          function() {
        promise.then(success(), error(1));
        promise.then(success(), error(2));
        expect(logStr()).toBe('');

        deferred.reject('foo');
        mockNextTick.flush();
        expect(logStr()).toBe('error1(foo)->reject(foo); error2(foo)->reject(foo)');
      });


      it('should do nothing if a promise was previously resolved', function() {
        promise.then(success(1), error(1));
        expect(logStr()).toBe('');

        deferred.resolve('foo');
        mockNextTick.flush();
        expect(logStr()).toBe('success1(foo)->foo');

        log = [];
        deferred.reject('bar');
        deferred.resolve('baz');
        expect(mockNextTick.queue.length).toBe(0);
        expect(logStr()).toBe('');

        promise.then(success(2), error(2));
        expect(logStr()).toBe('');
        mockNextTick.flush();
        expect(logStr()).toBe('success2(foo)->foo');
      });


      it('should do nothing if a promise was previously rejected', function() {
        promise.then(success(1), error(1));
        expect(logStr()).toBe('');

        deferred.reject('foo');
        mockNextTick.flush();
        expect(logStr()).toBe('error1(foo)->reject(foo)');

        log = [];
        deferred.reject('bar');
        deferred.resolve('baz');
        expect(mockNextTick.queue.length).toBe(0);
        expect(logStr()).toBe('');

        promise.then(success(2), error(2));
        expect(logStr()).toBe('');
        mockNextTick.flush();
        expect(logStr()).toBe('error2(foo)->reject(foo)');
      });


      it('should not defer rejection with a new promise', function() {
        var deferred2 = defer();
        promise.then(success(), error());

        deferred.reject(deferred2.promise);
        mockNextTick.flush();
        expect(logStr()).toBe('error({})->reject({})');
      });


      it('should call the error callback in the next turn', function() {
        promise.then(success(), error());
        expect(logStr()).toBe('');

        deferred.reject('foo');
        expect(logStr()).toBe('');

        mockNextTick.flush();
        expect(logStr()).toBe('error(foo)->reject(foo)');
      });


      it('should support non-bound execution', function() {
        var rejector = deferred.reject;
        promise.then(success(), error());
        rejector('detached');
        mockNextTick.flush();
        expect(logStr()).toBe('error(detached)->reject(detached)');
      });
    });


    describe('notify', function() {
      it('should execute all progress callbacks in the registration order',
          function() {
        promise.then(success(1), error(1), progress(1));
        promise.then(success(2), error(2), progress(2));
        expect(logStr()).toBe('');

        deferred.notify('foo');
        mockNextTick.flush();
        expect(logStr()).toBe('progress1(foo)->foo; progress2(foo)->foo');
      });


      it('should do nothing if a promise was previously resolved', function() {
        promise.then(success(1), error(1), progress(1));
        expect(logStr()).toBe('');

        deferred.resolve('foo');
        mockNextTick.flush();
        expect(logStr()).toBe('success1(foo)->foo');

        log = [];
        deferred.notify('bar');
        expect(mockNextTick.queue.length).toBe(0);
        expect(logStr()).toBe('');
      });


      it('should do nothing if a promise was previously rejected', function() {
        promise.then(success(1), error(1), progress(1));
        expect(logStr()).toBe('');

        deferred.reject('foo');
        mockNextTick.flush();
        expect(logStr()).toBe('error1(foo)->reject(foo)');

        log = [];
        deferred.reject('bar');
        deferred.resolve('baz');
        deferred.notify('qux');
        expect(mockNextTick.queue.length).toBe(0);
        expect(logStr()).toBe('');

        promise.then(success(2), error(2), progress(2));
        expect(logStr()).toBe('');
        mockNextTick.flush();
        expect(logStr()).toBe('error2(foo)->reject(foo)');
      });


      it('should not apply any special treatment to promises passed to notify', function() {
        var deferred2 = defer();
        promise.then(success(), error(), progress());

        deferred.notify(deferred2.promise);
        mockNextTick.flush();
        expect(logStr()).toBe('progress({})->{}');
      });


      it('should call the progress callbacks in the next turn', function() {
        promise.then(success(), error(), progress(1));
        promise.then(success(), error(), progress(2));
        expect(logStr()).toBe('');

        deferred.notify('foo');
        expect(logStr()).toBe('');

        mockNextTick.flush();
        expect(logStr()).toBe('progress1(foo)->foo; progress2(foo)->foo');
      });


      it('should ignore notifications sent out in the same turn before listener registration',
          function() {
        deferred.notify('foo');
        promise.then(success(), error(), progress(1));
        expect(logStr()).toBe('');
        expect(mockNextTick.queue).toEqual([]);
      });


      it('should support non-bound execution', function() {
        var notify = deferred.notify;
        promise.then(success(), error(), progress());
        notify('detached');
        mockNextTick.flush();
        expect(logStr()).toBe('progress(detached)->detached');
      });


      it('should not save and re-emit progress notifications between ticks', function() {
        promise.then(success(1), error(1), progress(1));
        deferred.notify('foo');
        deferred.notify('bar');
        mockNextTick.flush();
        expect(logStr()).toBe('progress1(foo)->foo; progress1(bar)->bar');

        log = [];
        promise.then(success(2), error(2), progress(2));
        deferred.notify('baz');
        mockNextTick.flush();
        expect(logStr()).toBe('progress1(baz)->baz; progress2(baz)->baz');
      });

    });


    describe('promise', function() {
      it('should have a then method', function() {
        expect(typeof promise.then).toBe('function');
      });

      it('should have a catch method', function() {
        expect(typeof promise['catch']).toBe('function');
      });

      it('should have a finally method', function() {
        expect(typeof promise['finally']).toBe('function');
      });


      describe('then', function() {
        it('should allow registration of a success callback without an errback or progressBack ' +
          'and resolve', function() {
          promise.then(success());
          syncResolve(deferred, 'foo');
          expect(logStr()).toBe('success(foo)->foo');
        });


        it('should allow registration of a success callback without an errback and reject',
            function() {
          promise.then(success());
          syncReject(deferred, 'foo');
          expect(logStr()).toBe('');
        });


        it('should allow registration of a success callback without a progressBack and notify',
            function() {
          promise.then(success());
          syncNotify(deferred, 'doing');
          expect(logStr()).toBe('');
        });


        it('should allow registration of an errback without a success or progress callback and ' +
          ' reject', function() {
          promise.then(null, error());
          syncReject(deferred, 'oops!');
          expect(logStr()).toBe('error(oops!)->reject(oops!)');
        });


        it('should allow registration of an errback without a success callback and resolve',
            function() {
          promise.then(null, error());
          syncResolve(deferred, 'done');
          expect(logStr()).toBe('');
        });


        it('should allow registration of an errback without a progress callback and notify',
            function() {
          promise.then(null, error());
          syncNotify(deferred, 'doing');
          expect(logStr()).toBe('');
        });


        it('should allow registration of a progressBack without a success or error callback and ' +
          'notify', function() {
          promise.then(null, null, progress());
          syncNotify(deferred, 'doing');
          expect(logStr()).toBe('progress(doing)->doing');
        });


        it('should allow registration of a progressBack without a success callback and resolve',
            function() {
          promise.then(null, null, progress());
          syncResolve(deferred, 'done');
          expect(logStr()).toBe('');
        });


        it('should allow registration of a progressBack without a error callback and reject',
            function() {
          promise.then(null, null, progress());
          syncReject(deferred, 'oops!');
          expect(logStr()).toBe('');
        });


        it('should resolve all callbacks with the original value', function() {
          promise.then(success('A', 'aVal'), error(), progress());
          promise.then(success('B', 'bErr', true), error(), progress());
          promise.then(success('C', q.reject('cReason')), error(), progress());
          promise.then(success('D', q.reject('dReason'), true), error(), progress());
          promise.then(success('E', 'eVal'), error(), progress());

          expect(logStr()).toBe('');
          syncResolve(deferred, 'yup');
          expect(log).toEqual(['successA(yup)->aVal',
                               'successB(yup)->throw(bErr)',
                               'successC(yup)->{}',
                               'successD(yup)->throw({})',
                               'successE(yup)->eVal']);
        });


        it('should reject all callbacks with the original reason', function() {
          promise.then(success(), error('A', 'aVal'), progress());
          promise.then(success(), error('B', 'bEr', true), progress());
          promise.then(success(), error('C', q.reject('cReason')), progress());
          promise.then(success(), error('D', 'dVal'), progress());

          expect(logStr()).toBe('');
          syncReject(deferred, 'noo!');
          expect(logStr()).toBe('errorA(noo!)->aVal; errorB(noo!)->throw(bEr); errorC(noo!)->{}; errorD(noo!)->dVal');
        });


        it('should notify all callbacks with the original value', function() {
          promise.then(success(), error(), progress('A', 'aVal'));
          promise.then(success(), error(), progress('B', 'bErr', true));
          promise.then(success(), error(), progress('C', q.reject('cReason')));
          promise.then(success(), error(), progress('C_reject', q.reject('cRejectReason'), true));
          promise.then(success(), error(), progress('Z', 'the end!'));

          expect(logStr()).toBe('');
          syncNotify(deferred, 'yup');
          expect(log).toEqual(['progressA(yup)->aVal',
                               'progressB(yup)->throw(bErr)',
                               'progressC(yup)->{}',
                               'progressC_reject(yup)->throw({})',
                               'progressZ(yup)->the end!']);
        });


        it('should propagate resolution and rejection between dependent promises', function() {
          promise.then(success(1, 'x'),       error('1')).
                  then(success(2, 'y', true), error('2')).
                  then(success(3),            error(3, 'z', true)).
                  then(success(4),            error(4, 'done')).
                  then(success(5),            error(5));

          expect(logStr()).toBe('');
          syncResolve(deferred, 'sweet!');
          expect(log).toEqual(['success1(sweet!)->x',
                               'success2(x)->throw(y)',
                               'error3(y)->throw(z)',
                               'error4(z)->done',
                               'success5(done)->done']);
        });


        it('should propagate notification between dependent promises', function() {
          promise.then(success(), error(), progress(1, 'a')).
                  then(success(), error(), progress(2, 'b')).
                  then(success(), error(), progress(3, 'c')).
                  then(success(), error(), progress(4)).
                  then(success(), error(), progress(5));

          expect(logStr()).toBe('');
          syncNotify(deferred, 'wait');
          expect(log).toEqual(['progress1(wait)->a',
                               'progress2(a)->b',
                               'progress3(b)->c',
                               'progress4(c)->c',
                               'progress5(c)->c']);
        });


        it('should reject a derived promise if an exception is thrown while resolving its parent',
            function() {
          promise.then(success(1, 'oops', true), error(1)).
                  then(success(2),               error(2));
          syncResolve(deferred, 'done!');
          expect(logStr()).toBe('success1(done!)->throw(oops); error2(oops)->reject(oops)');
        });


        it('should reject a derived promise if an exception is thrown while rejecting its parent',
            function() {
          promise.then(null,       error(1, 'oops', true)).
                  then(success(2), error(2));
          syncReject(deferred, 'timeout');
          expect(logStr()).toBe('error1(timeout)->throw(oops); error2(oops)->reject(oops)');
        });


        it('should stop notification propagation in case of error', function() {
          promise.then(success(), error(), progress(1)).
                  then(success(), error(), progress(2, 'ops!', true)).
                  then(success(), error(), progress(3));

          expect(logStr()).toBe('');
          syncNotify(deferred, 'wait');
          expect(log).toEqual(['progress1(wait)->wait',
                               'progress2(wait)->throw(ops!)']);
        });


        it('should call success callback in the next turn even if promise is already resolved',
            function() {
          deferred.resolve('done!');

          promise.then(success());
          expect(logStr()).toBe('');

          mockNextTick.flush();
          expect(log).toEqual(['success(done!)->done!']);
        });


        it('should call error callback in the next turn even if promise is already rejected',
            function() {
          deferred.reject('oops!');

          promise.then(null, error());
          expect(logStr()).toBe('');

          mockNextTick.flush();
          expect(log).toEqual(['error(oops!)->reject(oops!)']);
        });

        it('should forward success resolution when success callbacks are not functions', function() {
          deferred.resolve('yay!');

          promise.then(1).
                  then(null).
                  then({}).
                  then('gah!').
                  then([]).
                  then(success());

          expect(logStr()).toBe('');

          mockNextTick.flush();
          expect(log).toEqual(['success(yay!)->yay!']);
        });

        it('should forward error resolution when error callbacks are not functions', function() {
          deferred.reject('oops!');

          promise.then(null, 1).
                  then(null, null).
                  then(null, {}).
                  then(null, 'gah!').
                  then(null, []).
                  then(null, error());

          expect(logStr()).toBe('');

          mockNextTick.flush();
          expect(log).toEqual(['error(oops!)->reject(oops!)']);
        });
      });


      describe('finally', function() {

        it('should not take an argument',
            function() {
          promise['finally'](fin(1));
          syncResolve(deferred, 'foo');
          expect(logStr()).toBe('finally1()');
        });

        describe('when the promise is fulfilled', function() {

          it('should call the callback',
              function() {
            promise.then(success(1))['finally'](fin(1));
            syncResolve(deferred, 'foo');
            expect(logStr()).toBe('success1(foo)->foo; finally1()');
          });

          it('should fulfill with the original value',
              function() {
            promise['finally'](fin('B', 'b'), error('B')).
                    then(success('BB', 'bb'), error('BB'));
            syncResolve(deferred, 'RESOLVED_VAL');
            expect(log).toEqual(['finallyB()->b',
                                 'successBB(RESOLVED_VAL)->bb']);
          });


          it('should fulfill with the original value (larger test)',
              function() {
            promise.then(success('A', 'a'), error('A'));
            promise['finally'](fin('B', 'b'), error('B')).
                    then(success('BB', 'bb'), error('BB'));
            promise.then(success('C', 'c'), error('C'))['finally'](fin('CC', 'IGNORED'))
                   .then(success('CCC', 'cc'), error('CCC'))
                   .then(success('CCCC', 'ccc'), error('CCCC'));
            syncResolve(deferred, 'RESOLVED_VAL');

            expect(log).toEqual(['successA(RESOLVED_VAL)->a',
                                 'finallyB()->b',
                                 'successC(RESOLVED_VAL)->c',
                                 'finallyCC()->IGNORED',
                                 'successBB(RESOLVED_VAL)->bb',
                                 'successCCC(c)->cc',
                                 'successCCCC(cc)->ccc']);
          });

          describe('when the callback returns a promise', function() {

            describe('that is fulfilled', function() {
              it('should fulfill with the original reason after that promise resolves',
                function() {

                var returnedDef = defer();
                returnedDef.resolve('bar');

                promise['finally'](fin(1, returnedDef.promise))
                       .then(success(2));

                syncResolve(deferred, 'foo');

                expect(logStr()).toBe('finally1()->{}; success2(foo)->foo');
              });
            });

            describe('that is rejected', function() {
              it('should reject with this new rejection reason',
                function() {
                var returnedDef = defer();
                returnedDef.reject('bar');
                promise['finally'](fin(1, returnedDef.promise))
                       .then(success(2), error(1));
                syncResolve(deferred, 'foo');
                expect(logStr()).toBe('finally1()->{}; error1(bar)->reject(bar)');
              });
            });

          });

          describe('when the callback throws an exception', function() {
            it('should reject with this new exception', function() {
              promise['finally'](fin(1, 'exception', true))
                     .then(success(1), error(2));
              syncResolve(deferred, 'foo');
              expect(logStr()).toBe('finally1()->throw(exception); error2(exception)->reject(exception)');
            });
          });

        });


        describe('when the promise is rejected', function() {

          it('should call the callback', function() {
            promise['finally'](fin(1))
                   .then(success(2), error(1));
            syncReject(deferred, 'foo');
            expect(logStr()).toBe('finally1(); error1(foo)->reject(foo)');
          });

          it('should reject with the original reason', function() {
            promise['finally'](fin(1), 'hello')
                   .then(success(2), error(2));
            syncReject(deferred, 'original');
            expect(logStr()).toBe('finally1(); error2(original)->reject(original)');
          });

          describe('when the callback returns a promise', function() {

            describe('that is fulfilled', function() {

              it('should reject with the original reason after that promise resolves', function() {
                var returnedDef = defer();
                returnedDef.resolve('bar');
                promise['finally'](fin(1, returnedDef.promise))
                       .then(success(2), error(2));
                syncReject(deferred, 'original');
                expect(logStr()).toBe('finally1()->{}; error2(original)->reject(original)');
              });

            });

            describe('that is rejected', function() {

              it('should reject with the new reason', function() {
                var returnedDef = defer();
                returnedDef.reject('bar');
                promise['finally'](fin(1, returnedDef.promise))
                       .then(success(2), error(1));
                syncResolve(deferred, 'foo');
                expect(logStr()).toBe('finally1()->{}; error1(bar)->reject(bar)');
              });

            });

          });

          describe('when the callback throws an exception', function() {

            it('should reject with this new exception', function() {
              promise['finally'](fin(1, 'exception', true))
                     .then(success(1), error(2));
              syncResolve(deferred, 'foo');
              expect(logStr()).toBe('finally1()->throw(exception); error2(exception)->reject(exception)');
            });

          });

        });
      });

      describe('catch', function() {
        it('should be a shorthand for defining promise error handlers', function() {
          promise['catch'](error(1)).then(null, error(2));
          syncReject(deferred, 'foo');
          expect(logStr()).toBe('error1(foo)->reject(foo); error2(foo)->reject(foo)');
        });
      });
    });
  });


  describe('reject', function() {
    it('should package a string into a rejected promise', function() {
      var rejectedPromise = q.reject('not gonna happen');
      promise.then(success(), error());
      syncResolve(deferred, rejectedPromise);
      expect(log).toEqual(['error(not gonna happen)->reject(not gonna happen)']);
    });


    it('should package an exception into a rejected promise', function() {
      var rejectedPromise = q.reject(new Error('not gonna happen'));
      promise.then(success(), error());
      syncResolve(deferred, rejectedPromise);
      expect(log).toEqual(['error(Error: not gonna happen)->reject(Error: not gonna happen)']);
    });


    it('should return a promise that forwards callbacks if the callbacks are missing', function() {
      var rejectedPromise = q.reject('rejected');
      promise.then(success(), error());
      syncResolve(deferred, rejectedPromise.then());
      expect(log).toEqual(['error(rejected)->reject(rejected)']);
    });


    it('should catch exceptions thrown in errback and forward them to derived promises', function() {
      var rejectedPromise = q.reject('rejected');
      rejectedPromise.then(null, error('Broken', 'catch me!', true)).
                      then(null, error('Affected'));
      mockNextTick.flush();
      expect(log).toEqual(['errorBroken(rejected)->throw(catch me!)', 'errorAffected(catch me!)->reject(catch me!)']);
    });


    it('should have functions `finally` and `catch`', function() {
      var rejectedPromise = q.reject('rejected');
      expect(rejectedPromise['finally']).not.toBeUndefined();
      expect(rejectedPromise['catch']).not.toBeUndefined();
      rejectedPromise.catch(noop);
      mockNextTick.flush();
    });
  });


  describe('when', function() {
    describe('resolution', function() {
      it('should call the success callback in the next turn when the value is a non-promise',
          function() {
        q.when('hello', success(), error());
        expect(logStr()).toBe('');
        mockNextTick.flush();
        expect(logStr()).toBe('success(hello)->hello');
      });


      it('should call the success callback in the next turn when the value is a resolved promise',
          function() {
        deferred.resolve('hello');
        q.when(deferred.promise, success(), error());
        expect(logStr()).toBe('');
        mockNextTick.flush();
        expect(logStr()).toBe('success(hello)->hello');
      });


      it('should call the errback in the next turn when the value is a rejected promise', function() {
        deferred.reject('nope');
        q.when(deferred.promise, success(), error());
        expect(logStr()).toBe('');
        mockNextTick.flush();
        expect(logStr()).toBe('error(nope)->reject(nope)');
      });


      it('should call the success callback after the original promise is resolved',
          function() {
        q.when(deferred.promise, success(), error());
        expect(logStr()).toBe('');
        expect(logStr()).toBe('');
        syncResolve(deferred, 'hello');
        expect(logStr()).toBe('success(hello)->hello');
      });


      it('should call the errback after the original promise is rejected',
          function() {
        q.when(deferred.promise, success(), error());
        expect(logStr()).toBe('');
        expect(logStr()).toBe('');
        syncReject(deferred, 'nope');
        expect(logStr()).toBe('error(nope)->reject(nope)');
      });
    });


    describe('notification', function() {
      it('should call the progressBack when the value is a promise and gets notified',
          function() {
        q.when(deferred.promise, success(), error(), progress());
        expect(logStr()).toBe('');
        syncNotify(deferred, 'notification');
        expect(logStr()).toBe('progress(notification)->notification');
      });
    });


    describe('resolve', function() {
      it('should be an alias of the "when" function', function() {
        expect(q.resolve).toBeDefined();
        expect(q.resolve).toEqual(q.when);
      });
    });


    describe('optional callbacks', function() {
      it('should not require success callback and propagate resolution', function() {
        q.when('hi', null, error()).then(success(2), error());
        expect(logStr()).toBe('');
        mockNextTick.flush();
        expect(logStr()).toBe('success2(hi)->hi');
      });


      it('should not require success callback and propagate rejection', function() {
        q.when(q.reject('sorry'), null, error(1)).then(success(), error(2));
        expect(logStr()).toBe('');
        mockNextTick.flush();
        expect(logStr()).toBe('error1(sorry)->reject(sorry); error2(sorry)->reject(sorry)');
      });


      it('should not require errback and propagate resolution', function() {
        q.when('hi', success(1, 'hello')).then(success(2), error());
        expect(logStr()).toBe('');
        mockNextTick.flush();
        expect(logStr()).toBe('success1(hi)->hello; success2(hello)->hello');
      });


      it('should not require errback and propagate rejection', function() {
        q.when(q.reject('sorry'), success()).then(success(2), error(2));
        expect(logStr()).toBe('');
        mockNextTick.flush();
        expect(logStr()).toBe('error2(sorry)->reject(sorry)');
      });


      it('should not require progressBack and propagate notification', function() {
        q.when(deferred.promise).
          then(success(), error(), progress());
        expect(logStr()).toBe('');
        syncNotify(deferred, 'notification');
        expect(logStr()).toBe('progress(notification)->notification');
      });
    });


    describe('returned promise', function() {
      it('should return a promise that can be resolved with a value returned from the success ' +
          'callback', function() {
        q.when('hello', success(1, 'hi'), error()).then(success(2), error());
        mockNextTick.flush();
        expect(logStr()).toBe('success1(hello)->hi; success2(hi)->hi');
      });


      it('should return a promise that can be rejected with a rejected promise returned from the ' +
          'success callback', function() {
        q.when('hello', success(1, q.reject('sorry')), error()).then(success(), error(2));
        mockNextTick.flush();
        expect(logStr()).toBe('success1(hello)->{}; error2(sorry)->reject(sorry)');
      });


      it('should return a promise that can be resolved with a value returned from the errback',
          function() {
        q.when(q.reject('sorry'), success(), error(1, 'hi')).then(success(2), error());
        mockNextTick.flush();
        expect(logStr()).toBe('error1(sorry)->hi; success2(hi)->hi');
      });


      it('should return a promise that can be rejected with a rejected promise returned from the ' +
          'errback', function() {
        q.when(q.reject('sorry'), success(), error(1, q.reject('sigh'))).then(success(), error(2));
        mockNextTick.flush();
        expect(logStr()).toBe('error1(sorry)->{}; error2(sigh)->reject(sigh)');
      });


      it('should return a promise that can be resolved with a promise returned from the success ' +
          'callback', function() {
        var deferred2 = defer();
        q.when('hi', success(1, deferred2.promise), error()).then(success(2), error());
        mockNextTick.flush();
        expect(logStr()).toBe('success1(hi)->{}');
        syncResolve(deferred2, 'finally!');
        expect(logStr()).toBe('success1(hi)->{}; success2(finally!)->finally!');
      });


      it('should return a promise that can be resolved with promise returned from the errback ' +
          'callback', function() {
        var deferred2 = defer();
        q.when(q.reject('sorry'), success(), error(1, deferred2.promise)).then(success(2), error());
        mockNextTick.flush();
        expect(logStr()).toBe('error1(sorry)->{}');
        syncResolve(deferred2, 'finally!');
        expect(logStr()).toBe('error1(sorry)->{}; success2(finally!)->finally!');
      });
    });


    describe('security', function() {
      it('should call success callback only once even if the original promise gets fulfilled ' +
          'multiple times', function() {
        var evilPromise = {
          then: function(success, error, progress) {
            evilPromise.success = success;
            evilPromise.error = error;
            evilPromise.progress = progress;
          }
        };

        q.when(evilPromise, success(), error());
        expect(logStr()).toBe('');
        evilPromise.success('done');
        mockNextTick.flush(); // TODO(i) wrong queue, evil promise would be resolved outside of the
                              //   scope.$apply lifecycle and in that case we should have some kind
                              //   of fallback queue for calling our callbacks from. Otherwise the
                              //   application will get stuck until something triggers next $apply.
        expect(logStr()).toBe('success(done)->done');

        evilPromise.success('evil is me');
        evilPromise.error('burn burn');
        expect(logStr()).toBe('success(done)->done');
      });


      it('should call errback only once even if the original promise gets fulfilled multiple ' +
          'times', function() {
        var evilPromise = {
          then: function(success, error, progress) {
            evilPromise.success = success;
            evilPromise.error = error;
            evilPromise.progress = progress;
          }
        };

        q.when(evilPromise, success(), error());
        expect(logStr()).toBe('');
        evilPromise.error('failed');
        mockNextTick.flush();
        expect(logStr()).toBe('error(failed)->reject(failed)');

        evilPromise.error('muhaha');
        evilPromise.success('take this');
        expect(logStr()).toBe('error(failed)->reject(failed)');
      });


      it('should not call progressBack after promise gets fulfilled, even if original promise ' +
          'gets notified multiple times', function() {
        var evilPromise = {
          then: function(success, error, progress) {
            evilPromise.success = success;
            evilPromise.error = error;
            evilPromise.progress = progress;
          }
        };

        q.when(evilPromise, success(), error(), progress());
        expect(logStr()).toBe('');
        evilPromise.progress('notification');
        evilPromise.success('ok');
        mockNextTick.flush();
        expect(logStr()).toBe('progress(notification)->notification; success(ok)->ok');

        evilPromise.progress('muhaha');
        expect(logStr()).toBe('progress(notification)->notification; success(ok)->ok');
      });
    });
  });


  describe('all (array)', function() {
    it('should resolve all or nothing', function() {
      var result;
      q.all([]).then(function(r) { result = r; });
      mockNextTick.flush();
      expect(result).toEqual([]);
    });


    it('should take an array of promises and return a promise for an array of results', function() {
      var deferred1 = defer(),
          deferred2 = defer();

      q.all([promise, deferred1.promise, deferred2.promise]).then(success(), error());
      expect(logStr()).toBe('');
      syncResolve(deferred, 'hi');
      expect(logStr()).toBe('');
      syncResolve(deferred2, 'cau');
      expect(logStr()).toBe('');
      syncResolve(deferred1, 'hola');
      expect(logStr()).toBe('success(["hi","hola","cau"])->["hi","hola","cau"]');
    });


    it('should reject the derived promise if at least one of the promises in the array is rejected',
        function() {
      var deferred1 = defer(),
          deferred2 = defer();

      q.all([promise, deferred1.promise, deferred2.promise]).then(success(), error());
      expect(logStr()).toBe('');
      syncResolve(deferred2, 'cau');
      expect(logStr()).toBe('');
      syncReject(deferred1, 'oops');
      expect(logStr()).toBe('error(oops)->reject(oops)');
    });


    it('should not forward notifications from individual promises to the combined promise',
        function() {
      var deferred1 = defer(),
          deferred2 = defer();

      q.all([promise, deferred1.promise, deferred2.promise]).then(success(), error(), progress());
      expect(logStr()).toBe('');
      deferred.notify('x');
      deferred2.notify('y');
      expect(logStr()).toBe('');
      mockNextTick.flush();
      expect(logStr()).toBe('');
    });


    it('should ignore multiple resolutions of an (evil) array promise', function() {
      var evilPromise = {
        then: function(success, error) {
          evilPromise.success = success;
          evilPromise.error = error;
        }
      };

      q.all([promise, evilPromise]).then(success(), error());
      expect(logStr()).toBe('');

      evilPromise.success('first');
      evilPromise.success('muhaha');
      evilPromise.error('arghhh');
      expect(logStr()).toBe('');

      syncResolve(deferred, 'done');
      expect(logStr()).toBe('success(["done","first"])->["done","first"]');
    });
  });

  describe('all (hash)', function() {
    it('should resolve all or nothing', function() {
      var result;
      q.all({}).then(function(r) { result = r; });
      mockNextTick.flush();
      expect(result).toEqual({});
    });


    it('should take a hash of promises and return a promise for a hash of results', function() {
      var deferred1 = defer(),
          deferred2 = defer();

      q.all({en: promise, fr: deferred1.promise, es: deferred2.promise}).then(success(), error());
      expect(logStr()).toBe('');
      syncResolve(deferred, 'hi');
      expect(logStr()).toBe('');
      syncResolve(deferred2, 'hola');
      expect(logStr()).toBe('');
      syncResolve(deferred1, 'salut');
      expect(logStr()).toBe('success({"en":"hi","es":"hola","fr":"salut"})->{"en":"hi","es":"hola","fr":"salut"}');
    });


    it('should reject the derived promise if at least one of the promises in the hash is rejected',
        function() {
      var deferred1 = defer(),
          deferred2 = defer();

      q.all({en: promise, fr: deferred1.promise, es: deferred2.promise}).then(success(), error());
      expect(logStr()).toBe('');
      syncResolve(deferred2, 'hola');
      expect(logStr()).toBe('');
      syncReject(deferred1, 'oops');
      expect(logStr()).toBe('error(oops)->reject(oops)');
    });


    it('should ignore multiple resolutions of an (evil) hash promise', function() {
      var evilPromise = {
        then: function(success, error) {
          evilPromise.success = success;
          evilPromise.error = error;
        }
      };

      q.all({good: promise, evil: evilPromise}).then(success(), error());
      expect(logStr()).toBe('');

      evilPromise.success('first');
      evilPromise.success('muhaha');
      evilPromise.error('arghhh');
      expect(logStr()).toBe('');

      syncResolve(deferred, 'done');
      expect(logStr()).toBe('success({"evil":"first","good":"done"})->{"evil":"first","good":"done"}');
    });

    it('should handle correctly situation when given the same promise several times', function() {
      q.all({first: promise, second: promise, third: promise}).then(success(), error());
      expect(logStr()).toBe('');

      syncResolve(deferred, 'done');
      expect(logStr()).toBe('success({"first":"done","second":"done","third":"done"})->{"first":"done","second":"done","third":"done"}');
    });
  });

  describe('race (array)', function() {
    it('should do nothing if given an empty array', function() {
      q.race([]).then(success(), error());
      expect(mockNextTick.queue.length).toBe(0);
      expect(logStr()).toBe('');
    });

    it('should resolve as soon as the first promise is settled by resolution', function() {
      var deferred1 = defer(),
          deferred2 = defer();

      q.race([promise, deferred1.promise, deferred2.promise]).then(success(), error());
      expect(logStr()).toBe('');
      syncResolve(deferred1, 'hi');
      expect(logStr()).toBe('success(hi)->hi');
      syncResolve(deferred2, 'cau');
      expect(logStr()).toBe('success(hi)->hi');
      syncReject(deferred, 'hola');
      expect(logStr()).toBe('success(hi)->hi');
    });

    it('should reject as soon as the first promise is settled by rejection', function() {
      var deferred1 = defer(),
          deferred2 = defer();

      q.race([promise, deferred1.promise, deferred2.promise]).then(success(), error());
      expect(logStr()).toBe('');
      syncReject(deferred1, 'hi');
      expect(logStr()).toBe('error(hi)->reject(hi)');
      syncResolve(deferred2, 'cau');
      expect(logStr()).toBe('error(hi)->reject(hi)');
      syncReject(deferred, 'hola');
      expect(logStr()).toBe('error(hi)->reject(hi)');
    });
  });

  describe('race (hash)', function() {
    it('should do nothing if given an empty object', function() {
      q.race({}).then(success(), error());
      expect(mockNextTick.queue.length).toBe(0);
      expect(logStr()).toBe('');
    });

    it('should resolve as soon as the first promise is settled by resolution', function() {
      var deferred1 = defer(),
          deferred2 = defer();

      q.race({a: promise, b: deferred1.promise, c: deferred2.promise}).then(success(), error());
      expect(logStr()).toBe('');
      syncResolve(deferred1, 'hi');
      expect(logStr()).toBe('success(hi)->hi');
      syncResolve(deferred2, 'cau');
      expect(logStr()).toBe('success(hi)->hi');
      syncReject(deferred, 'hola');
      expect(logStr()).toBe('success(hi)->hi');
    });

    it('should reject as soon as the first promise is settled by rejection', function() {
      var deferred1 = defer(),
          deferred2 = defer();

      q.race({a: promise, b: deferred1.promise, c: deferred2.promise}).then(success(), error());
      expect(logStr()).toBe('');
      syncReject(deferred1, 'hi');
      expect(logStr()).toBe('error(hi)->reject(hi)');
      syncResolve(deferred2, 'cau');
      expect(logStr()).toBe('error(hi)->reject(hi)');
      syncReject(deferred, 'hola');
      expect(logStr()).toBe('error(hi)->reject(hi)');
    });
  });

  describe('exception logging', function() {
    var mockExceptionLogger = {
      log: [],
      logger: function(e) {
        mockExceptionLogger.log.push(e);
      }
    };


    beforeEach(function() {
      q = qFactory(mockNextTick.nextTick, mockExceptionLogger.logger);
      defer = q.defer;
      deferred =  defer();
      promise = deferred.promise;
      log = [];
      mockExceptionLogger.log = [];
    });


    describe('in then', function() {
      it('should NOT log exceptions thrown in a success callback but reject the derived promise',
          function() {
        var success1 = success(1, 'oops', true);
        promise.then(success1).then(success(2), error(2)).catch(noop);
        syncResolve(deferred, 'done');
        expect(logStr()).toBe('success1(done)->throw(oops); error2(oops)->reject(oops)');
        expect(mockExceptionLogger.log).toEqual([]);
      });


      it('should NOT log exceptions when a success callback returns rejected promise', function() {
        promise.then(success(1, q.reject('rejected'))).then(success(2), error(2)).catch(noop);
        syncResolve(deferred, 'done');
        expect(logStr()).toBe('success1(done)->{}; error2(rejected)->reject(rejected)');
        expect(mockExceptionLogger.log).toEqual([]);
      });


      it('should NOT log exceptions thrown in an errback but reject the derived promise',
          function() {
        var error1 = error(1, 'oops', true);
        promise.then(null, error1).then(success(2), error(2)).catch(noop);
        syncReject(deferred, 'nope');
        expect(logStr()).toBe('error1(nope)->throw(oops); error2(oops)->reject(oops)');
        expect(mockExceptionLogger.log).toEqual([]);
      });


      it('should NOT log exceptions when an errback returns a rejected promise', function() {
        promise.then(null, error(1, q.reject('rejected'))).then(success(2), error(2)).catch(noop);
        syncReject(deferred, 'nope');
        expect(logStr()).toBe('error1(nope)->{}; error2(rejected)->reject(rejected)');
        expect(mockExceptionLogger.log).toEqual([]);
      });


      it('should log exceptions thrown in a progressBack and stop propagation, but should NOT reject ' +
        'the promise', function() {
          promise.then(success(), error(), progress(1, 'failed', true)).then(null, error(1), progress(2)).catch(noop);
          syncNotify(deferred, '10%');
          expect(logStr()).toBe('progress1(10%)->throw(failed)');
          expect(mockExceptionLogger.log).toEqual(['failed']);
          log = [];
          syncResolve(deferred, 'ok');
          expect(logStr()).toBe('success(ok)->ok');
        }
      );
    });


    describe('in when', function() {
      it('should NOT log exceptions thrown in a success callback but reject the derived promise',
          function() {
        var success1 = success(1, 'oops', true);
        q.when('hi', success1, error()).then(success(), error(2)).catch(noop);
        mockNextTick.flush();
        expect(logStr()).toBe('success1(hi)->throw(oops); error2(oops)->reject(oops)');
        expect(mockExceptionLogger.log).toEqual([]);
      });


      it('should NOT log exceptions when a success callback returns rejected promise', function() {
        q.when('hi', success(1, q.reject('rejected'))).then(success(2), error(2)).catch(noop);
        mockNextTick.flush();
        expect(logStr()).toBe('success1(hi)->{}; error2(rejected)->reject(rejected)');
        expect(mockExceptionLogger.log).toEqual([]);
      });


      it('should NOT log exceptions thrown in a errback but reject the derived promise', function() {
        var error1 = error(1, 'oops', true);
        q.when(q.reject('sorry'), success(), error1).then(success(), error(2)).catch(noop);
        mockNextTick.flush();
        expect(logStr()).toBe('error1(sorry)->throw(oops); error2(oops)->reject(oops)');
        expect(mockExceptionLogger.log).toEqual([]);
      });


      it('should NOT log exceptions when an errback returns a rejected promise', function() {
        q.when(q.reject('sorry'), success(), error(1, q.reject('rejected'))).
          then(success(2), error(2)).catch(noop);
        mockNextTick.flush();
        expect(logStr()).toBe('error1(sorry)->{}; error2(rejected)->reject(rejected)');
        expect(mockExceptionLogger.log).toEqual([]);
      });
    });
  });


  describe('when exceptionHandler is called', function() {
    function CustomError() { }
    CustomError.prototype = Object.create(Error.prototype);

    var errorEg = new Error('Fail');
    var errorStr = toDebugString(errorEg);

    var customError = new CustomError('Custom');
    var customErrorStr = toDebugString(customError);

    var nonErrorObj = { isATest: 'this is' };
    var nonErrorObjStr = toDebugString(nonErrorObj);

    var fixtures = [
      {
        type: 'Error object',
        value: errorEg,
        expected: {
          exception: errorEg,
          reason: 'Possibly unhandled rejection: ' + errorStr
        }
      },
      {
        type: 'custom Error object',
        value: customError,
        expected: {
          exception: customError,
          reason: 'Possibly unhandled rejection: ' + customErrorStr
        }
      },
      {
        type: 'non-Error object',
        value: nonErrorObj,
        expected: {
          reason: 'Possibly unhandled rejection: ' + nonErrorObjStr
        }
      },
      {
        type: 'string primitive',
        value: 'foo',
        expected: {
          reason: 'Possibly unhandled rejection: foo'
        }
      }
    ];
    forEach(fixtures, function(fixture) {
      var type = fixture.type;
      var value = fixture.value;
      var expected = fixture.expected;

      describe('with ' + type, function() {

        it('should log an unhandled rejected promise', function() {
          var defer = q.defer();
          defer.reject(value);
          mockNextTick.flush();
          expect(exceptionHandlerCalls).toEqual([expected]);
        });


        it('should not log an unhandled rejected promise if disabled', function() {
          var defer = q_no_error.defer();
          defer.reject(value);
          expect(exceptionHandlerCalls).toEqual([]);
        });


        it('should log a handled rejected promise on a promise without rejection callbacks', function() {
          var defer = q.defer();
          defer.promise.then(noop);
          defer.reject(value);
          mockNextTick.flush();
          expect(exceptionHandlerCalls).toEqual([expected]);
        });


        it('should not log a handled rejected promise', function() {
          var defer = q.defer();
          defer.promise.catch(noop);
          defer.reject(value);
          mockNextTick.flush();
          expect(exceptionHandlerCalls).toEqual([]);
        });


        it('should not log a handled rejected promise that is handled in a future tick', function() {
          var defer = q.defer();
          defer.promise.catch(noop);
          defer.resolve(q.reject(value));
          mockNextTick.flush();
          expect(exceptionHandlerCalls).toEqual([]);
        });
      });
    });
  });
});
