'use strict';

describe('$$animateAsyncRun', function() {
  it('should fire the callback only when one or more RAFs have passed',
    inject(function($$animateAsyncRun, $$rAF) {

    var trigger = $$animateAsyncRun();
    var called = false;
    trigger(function() {
      called = true;
    });

    expect(called).toBe(false);
    $$rAF.flush();
    expect(called).toBe(true);
  }));

  it('should immediately fire the callback if a RAF has passed since construction',
    inject(function($$animateAsyncRun, $$rAF) {

    var trigger = $$animateAsyncRun();
    $$rAF.flush();

    var called = false;
    trigger(function() {
      called = true;
    });
    expect(called).toBe(true);
  }));
});

describe('$$AnimateRunner', function() {
  they('should trigger the host $prop function',
    ['end', 'cancel', 'pause', 'resume'], function(method) {

    inject(function($$AnimateRunner) {
      var host = {};
      var spy = host[method] = jasmine.createSpy();
      var runner = new $$AnimateRunner(host);
      runner[method]();
      expect(spy).toHaveBeenCalled();
    });
  });

  they('should trigger the inner runner\'s host $prop function',
    ['end', 'cancel', 'pause', 'resume'], function(method) {

    inject(function($$AnimateRunner) {
      var host = {};
      var spy = host[method] = jasmine.createSpy();
      var runner1 = new $$AnimateRunner();
      var runner2 = new $$AnimateRunner(host);
      runner1.setHost(runner2);
      runner1[method]();
      expect(spy).toHaveBeenCalled();
    });
  });

  it('should resolve the done function only if one RAF has passed',
    inject(function($$AnimateRunner, $$rAF) {

    var runner = new $$AnimateRunner();
    var spy = jasmine.createSpy();
    runner.done(spy);
    runner.complete(true);
    expect(spy).not.toHaveBeenCalled();
    $$rAF.flush();
    expect(spy).toHaveBeenCalled();
  }));

  it('should resolve with the status provided in the completion function',
    inject(function($$AnimateRunner, $$rAF) {

    var runner = new $$AnimateRunner();
    var capturedValue;
    runner.done(function(val) {
      capturedValue = val;
    });
    runner.complete('special value');
    $$rAF.flush();
    expect(capturedValue).toBe('special value');
  }));

  they('should immediately resolve each combined runner in a bottom-up order when $prop is called',
    ['end', 'cancel'], function(method) {

    inject(function($$AnimateRunner) {
      var runner1 = new $$AnimateRunner();
      var runner2 = new $$AnimateRunner();
      runner1.setHost(runner2);

      var status1, status2, signature = '';
      runner1.done(function(status) {
        signature += '1';
        status1 = status;
      });

      runner2.done(function(status) {
        signature += '2';
        status2 = status;
      });

      runner1[method]();

      var expectedStatus = method === 'end';
      expect(status1).toBe(expectedStatus);
      expect(status2).toBe(expectedStatus);
      expect(signature).toBe('21');
    });
  });

  they('should resolve/reject using a newly created promise when .then() is used upon $prop',
    ['end', 'cancel'], function(method) {

    inject(function($$AnimateRunner, $rootScope) {
      var runner1 = new $$AnimateRunner();
      var runner2 = new $$AnimateRunner();
      runner1.setHost(runner2);

      var status1;
      runner1.then(
        function() { status1 = 'pass'; },
        function() { status1 = 'fail'; });

      var status2;
      runner2.then(
        function() { status2 = 'pass'; },
        function() { status2 = 'fail'; });

      runner1[method]();

      var expectedStatus = method === 'end' ? 'pass' : 'fail';

      expect(status1).toBeUndefined();
      expect(status2).toBeUndefined();

      $rootScope.$digest();
      expect(status1).toBe(expectedStatus);
      expect(status2).toBe(expectedStatus);
    });
  });

  it('should expose/create the contained promise when getPromise() is called',
    inject(function($$AnimateRunner, $rootScope) {

    var runner = new $$AnimateRunner();
    expect(isPromiseLike(runner.getPromise())).toBeTruthy();
  }));

  it('should expose the `catch` promise function to handle the rejected state',
    inject(function($$AnimateRunner, $rootScope) {

    var runner = new $$AnimateRunner();
    var animationFailed = false;
    runner.catch(function() {
      animationFailed = true;
    });
    runner.cancel();
    $rootScope.$digest();
    expect(animationFailed).toBe(true);
  }));

  it('should use timeouts to trigger async operations when the document is hidden', function() {
    var hidden = true;

    module(function($provide) {

      $provide.value('$$isDocumentHidden', function() {
        return hidden;
      });
    });

    inject(function($$AnimateRunner, $rootScope, $$rAF, $timeout) {
      var spy = jasmine.createSpy();
      var runner = new $$AnimateRunner();
      runner.done(spy);
      runner.complete(true);
      expect(spy).not.toHaveBeenCalled();
      $$rAF.flush();
      expect(spy).not.toHaveBeenCalled();
      $timeout.flush();
      expect(spy).toHaveBeenCalled();

      hidden = false;

      spy = jasmine.createSpy();
      runner = new $$AnimateRunner();
      runner.done(spy);
      runner.complete(true);
      expect(spy).not.toHaveBeenCalled();
      $$rAF.flush();
      expect(spy).toHaveBeenCalled();
      expect(function() {
        $timeout.flush();
      }).toThrow();
    });
  });

  they('should expose the `finally` promise function to handle the final state when $prop',
    { 'rejected': 'cancel', 'resolved': 'end' }, function(method) {
    inject(function($$AnimateRunner, $rootScope) {
        var runner = new $$AnimateRunner();
        var animationComplete = false;
        runner.finally(function() {
          animationComplete = true;
        }).catch(noop);
        runner[method]();
        $rootScope.$digest();
        expect(animationComplete).toBe(true);
    });
  });

  describe('.all()', function() {
    it('should resolve when all runners have naturally resolved',
      inject(function($$rAF, $$AnimateRunner) {

      var runner1 = new $$AnimateRunner();
      var runner2 = new $$AnimateRunner();
      var runner3 = new $$AnimateRunner();

      var status;
      $$AnimateRunner.all([runner1, runner2, runner3], function(response) {
        status = response;
      });

      runner1.complete(true);
      runner2.complete(true);
      runner3.complete(true);

      expect(status).toBeUndefined();

      $$rAF.flush();

      expect(status).toBe(true);
    }));

    they('should immediately resolve if and when all runners have been $prop',
      { ended: 'end', cancelled: 'cancel' }, function(method) {

      inject(function($$AnimateRunner) {
        var runner1 = new $$AnimateRunner();
        var runner2 = new $$AnimateRunner();
        var runner3 = new $$AnimateRunner();

        var expectedStatus = method === 'end';

        var status;
        $$AnimateRunner.all([runner1, runner2, runner3], function(response) {
          status = response;
        });

        runner1[method]();
        runner2[method]();
        runner3[method]();

        expect(status).toBe(expectedStatus);
      });
    });

    it('should return a status of `false` if one or more runners was cancelled',
      inject(function($$AnimateRunner) {

      var runner1 = new $$AnimateRunner();
      var runner2 = new $$AnimateRunner();
      var runner3 = new $$AnimateRunner();

      var status;
      $$AnimateRunner.all([runner1, runner2, runner3], function(response) {
        status = response;
      });

      runner1.end();
      runner2.end();
      runner3.cancel();

      expect(status).toBe(false);
    }));
  });

  describe('.chain()', function() {
    it('should evaluate an array of functions in a chain',
      inject(function($$rAF, $$AnimateRunner) {

      var runner1 = new $$AnimateRunner();
      var runner2 = new $$AnimateRunner();
      var runner3 = new $$AnimateRunner();

      var log = [];

      var items = [];
      items.push(function(fn) {
        runner1.done(function() {
          log.push(1);
          fn();
        });
      });

      items.push(function(fn) {
        runner2.done(function() {
          log.push(2);
          fn();
        });
      });

      items.push(function(fn) {
        runner3.done(function() {
          log.push(3);
          fn();
        });
      });

      var status;
      $$AnimateRunner.chain(items, function(response) {
        status = response;
      });

      $$rAF.flush();

      runner2.complete(true);
      expect(log).toEqual([]);
      expect(status).toBeUndefined();

      runner1.complete(true);
      expect(log).toEqual([1,2]);
      expect(status).toBeUndefined();

      runner3.complete(true);
      expect(log).toEqual([1,2,3]);
      expect(status).toBe(true);
    }));

    it('should break the chain when a function evaluates to false',
      inject(function($$rAF, $$AnimateRunner) {

      var runner1 = new $$AnimateRunner();
      var runner2 = new $$AnimateRunner();
      var runner3 = new $$AnimateRunner();
      var runner4 = new $$AnimateRunner();
      var runner5 = new $$AnimateRunner();
      var runner6 = new $$AnimateRunner();

      var log = [];

      var items = [];
      items.push(function(fn) { log.push(1); runner1.done(fn); });
      items.push(function(fn) { log.push(2); runner2.done(fn); });
      items.push(function(fn) { log.push(3); runner3.done(fn); });
      items.push(function(fn) { log.push(4); runner4.done(fn); });
      items.push(function(fn) { log.push(5); runner5.done(fn); });
      items.push(function(fn) { log.push(6); runner6.done(fn); });

      var status;
      $$AnimateRunner.chain(items, function(response) {
        status = response;
      });

      runner1.complete('');
      runner2.complete(null);
      runner3.complete(undefined);
      runner4.complete(0);
      runner5.complete(false);

      runner6.complete(true);

      $$rAF.flush();

      expect(log).toEqual([1,2,3,4,5]);
      expect(status).toBe(false);
    }));
  });
});
