'use strict';

describe('$interval', function() {
  /* global $IntervalProvider: false */

  beforeEach(module(function($provide) {
    var repeatFns = [],
        nextRepeatId = 0,
        now = 0,
        $window;

    $window = {
      setInterval: function(fn, delay, count) {
        repeatFns.push({
          nextTime:(now + delay),
          delay: delay,
          fn: fn,
          id: nextRepeatId
        });
        repeatFns.sort(function(a, b) { return a.nextTime - b.nextTime;});

        return nextRepeatId++;
      },

      clearInterval: function(id) {
        var fnIndex;

        angular.forEach(repeatFns, function(fn, index) {
          if (fn.id === id) fnIndex = index;
        });

        if (isDefined(fnIndex)) {
          repeatFns.splice(fnIndex, 1);
          return true;
        }

        return false;
      },

      flush: function(millis) {
        now += millis;
        while (repeatFns.length && repeatFns[0].nextTime <= now) {
          var task = repeatFns[0];
          task.fn();
          task.nextTime += task.delay;
          repeatFns.sort(function(a, b) { return a.nextTime - b.nextTime;});
        }
        return millis;
      }
    };

    $provide.provider('$interval', $IntervalProvider);
    $provide.value('$window', $window);
  }));

  it('should run tasks repeatedly', inject(function($interval, $window) {
    var counter = 0;
    $interval(function() { counter++; }, 1000);

    expect(counter).toBe(0);

    $window.flush(1000);
    expect(counter).toBe(1);

    $window.flush(1000);

    expect(counter).toBe(2);
  }));

  it('should call $apply after each task is executed',
      inject(function($interval, $rootScope, $window) {
    var applySpy = spyOn($rootScope, '$apply').and.callThrough();

    $interval(noop, 1000);
    expect(applySpy).not.toHaveBeenCalled();

    $window.flush(1000);
    expect(applySpy).toHaveBeenCalledOnce();

    applySpy.calls.reset();

    $interval(noop, 1000);
    $interval(noop, 1000);
    $window.flush(1000);
    expect(applySpy).toHaveBeenCalledTimes(3);
  }));


  it('should NOT call $apply if invokeApply is set to false',
      inject(function($interval, $rootScope, $window) {
    var applySpy = spyOn($rootScope, '$apply').and.callThrough();

    $interval(noop, 1000, 0, false);
    expect(applySpy).not.toHaveBeenCalled();

    $window.flush(2000);
    expect(applySpy).not.toHaveBeenCalled();
  }));


  it('should NOT call $evalAsync or $digest if invokeApply is set to false',
      inject(function($interval, $rootScope, $window, $timeout) {
    var evalAsyncSpy = spyOn($rootScope, '$evalAsync').and.callThrough();
    var digestSpy = spyOn($rootScope, '$digest').and.callThrough();
    var notifySpy = jasmine.createSpy('notify');

    $interval(notifySpy, 1000, 1, false);

    $window.flush(2000);
    $timeout.flush(); // flush $browser.defer() timeout

    expect(notifySpy).toHaveBeenCalledOnce();
    expect(evalAsyncSpy).not.toHaveBeenCalled();
    expect(digestSpy).not.toHaveBeenCalled();
  }));


  it('should not depend on `notify` to trigger the callback call', function() {
    module(function($provide) {
      $provide.decorator('$q', function($delegate) {
        function replacement() {}
        replacement.defer = function() {
          var result = $delegate.defer();
          result.notify = noop;
          return result;
        };
        return replacement;
      });
    });

    inject(function($interval, $window) {
      var counter = 0;
      $interval(function() { counter++; }, 1000);

      expect(counter).toBe(0);

      $window.flush(1000);
      expect(counter).toBe(1);

      $window.flush(1000);

      expect(counter).toBe(2);
    });
  });


  it('should allow you to specify the delay time', inject(function($interval, $window) {
    var counter = 0;
    $interval(function() { counter++; }, 123);

    expect(counter).toBe(0);

    $window.flush(122);
    expect(counter).toBe(0);

    $window.flush(1);
    expect(counter).toBe(1);
  }));


  it('should allow you to specify a number of iterations', inject(function($interval, $window) {
    var counter = 0;
    $interval(function() {counter++;}, 1000, 2);

    $window.flush(1000);
    expect(counter).toBe(1);
    $window.flush(1000);
    expect(counter).toBe(2);
    $window.flush(1000);
    expect(counter).toBe(2);
  }));


  it('should allow you to specify a number of arguments', inject(function($interval, $window) {
    var task1 = jasmine.createSpy('task1'),
        task2 = jasmine.createSpy('task2'),
        task3 = jasmine.createSpy('task3');
    $interval(task1, 1000, 2, true, 'Task1');
    $interval(task2, 1000, 2, true, 'Task2');
    $interval(task3, 1000, 2, true, 'I', 'am', 'a', 'Task3', 'spy');

    $window.flush(1000);
    expect(task1).toHaveBeenCalledWith('Task1');
    expect(task2).toHaveBeenCalledWith('Task2');
    expect(task3).toHaveBeenCalledWith('I', 'am', 'a', 'Task3', 'spy');

    task1.calls.reset();
    task2.calls.reset();
    task3.calls.reset();

    $window.flush(1000);
    expect(task1).toHaveBeenCalledWith('Task1');
    expect(task2).toHaveBeenCalledWith('Task2');
    expect(task3).toHaveBeenCalledWith('I', 'am', 'a', 'Task3', 'spy');

  }));


  it('should return a promise which will be updated with the count on each iteration',
      inject(function($interval, $window) {
    var log = [],
        promise = $interval(function() { log.push('tick'); }, 1000);

    promise.then(function(value) { log.push('promise success: ' + value); },
                 function(err) { log.push('promise error: ' + err); },
                 function(note) { log.push('promise update: ' + note); });
    expect(log).toEqual([]);

    $window.flush(1000);
    expect(log).toEqual(['tick', 'promise update: 0']);

    $window.flush(1000);
    expect(log).toEqual(['tick', 'promise update: 0', 'tick', 'promise update: 1']);
  }));


  it('should return a promise which will be resolved after the specified number of iterations',
      inject(function($interval, $window) {
    var log = [],
        promise = $interval(function() { log.push('tick'); }, 1000, 2);

    promise.then(function(value) { log.push('promise success: ' + value); },
                 function(err) { log.push('promise error: ' + err); },
                 function(note) { log.push('promise update: ' + note); });
    expect(log).toEqual([]);

    $window.flush(1000);
    expect(log).toEqual(['tick', 'promise update: 0']);
    $window.flush(1000);

    expect(log).toEqual([
      'tick', 'promise update: 0', 'tick', 'promise update: 1', 'promise success: 2'
    ]);

  }));


  describe('exception handling', function() {
    beforeEach(module(function($exceptionHandlerProvider) {
      $exceptionHandlerProvider.mode('log');
    }));


    it('should delegate exception to the $exceptionHandler service', inject(
        function($interval, $exceptionHandler, $window) {
      $interval(function() { throw 'Test Error'; }, 1000);
      expect($exceptionHandler.errors).toEqual([]);

      $window.flush(1000);
      expect($exceptionHandler.errors).toEqual(['Test Error']);

      $window.flush(1000);
      expect($exceptionHandler.errors).toEqual(['Test Error', 'Test Error']);
    }));


    it('should call $apply even if an exception is thrown in callback', inject(
        function($interval, $rootScope, $window) {
      var applySpy = spyOn($rootScope, '$apply').and.callThrough();

      $interval(function() { throw 'Test Error'; }, 1000);
      expect(applySpy).not.toHaveBeenCalled();

      $window.flush(1000);
      expect(applySpy).toHaveBeenCalled();
    }));


    it('should still update the interval promise when an exception is thrown',
        inject(function($interval, $window) {
      var log = [],
          promise = $interval(function() { throw 'Some Error'; }, 1000);

      promise.then(function(value) { log.push('promise success: ' + value); },
                 function(err) { log.push('promise error: ' + err); },
                 function(note) { log.push('promise update: ' + note); });
      $window.flush(1000);

      expect(log).toEqual(['promise update: 0']);
    }));
  });


  describe('cancel', function() {
    it('should cancel tasks', inject(function($interval, $window) {
      var task1 = jasmine.createSpy('task1', 1000),
          task2 = jasmine.createSpy('task2', 1000),
          task3 = jasmine.createSpy('task3', 1000),
          promise1, promise3;

      promise1 = $interval(task1, 200);
      $interval(task2, 1000);
      promise3 = $interval(task3, 333);

      $interval.cancel(promise3);
      $interval.cancel(promise1);
      $window.flush(1000);

      expect(task1).not.toHaveBeenCalled();
      expect(task2).toHaveBeenCalledOnce();
      expect(task3).not.toHaveBeenCalled();
    }));


    it('should cancel the promise', inject(function($interval, $rootScope, $window) {
      var promise = $interval(noop, 1000),
          log = [];
      promise.then(function(value) { log.push('promise success: ' + value); },
                 function(err) { log.push('promise error: ' + err); },
                 function(note) { log.push('promise update: ' + note); });
      expect(log).toEqual([]);

      $window.flush(1000);
      $interval.cancel(promise);
      $window.flush(1000);
      $rootScope.$apply(); // For resolving the promise -
                           // necessary since q uses $rootScope.evalAsync.

      expect(log).toEqual(['promise update: 0', 'promise error: canceled']);
    }));


    it('should return true if a task was successfully canceled',
        inject(function($interval, $window) {
      var task1 = jasmine.createSpy('task1'),
          task2 = jasmine.createSpy('task2'),
          promise1, promise2;

      promise1 = $interval(task1, 1000, 1);
      $window.flush(1000);
      promise2 = $interval(task2, 1000, 1);

      expect($interval.cancel(promise1)).toBe(false);
      expect($interval.cancel(promise2)).toBe(true);
    }));


    it('should not throw an error when given an undefined promise', inject(function($interval) {
      expect($interval.cancel()).toBe(false);
    }));


    it('should throw an error when given a non-$interval promise', inject(function($interval) {
      var promise = $interval(noop).then(noop);
      expect(function() { $interval.cancel(promise); }).toThrowMinErr('$interval', 'badprom');
    }));


    it('should not trigger digest when cancelled', inject(function($interval, $rootScope, $browser) {
      var watchSpy = jasmine.createSpy('watchSpy');
      $rootScope.$watch(watchSpy);

      var t = $interval();
      $interval.cancel(t);
      expect(function() {$browser.defer.flush();}).toThrowError('No deferred tasks to be flushed');
      expect(watchSpy).not.toHaveBeenCalled();
    }));
  });

  describe('$window delegation', function() {
    it('should use $window.setInterval instead of the global function', inject(function($interval, $window) {
      var setIntervalSpy = spyOn($window, 'setInterval');

      $interval(noop, 1000);
      expect(setIntervalSpy).toHaveBeenCalled();
    }));

    it('should use $window.clearInterval instead of the global function', inject(function($interval, $window) {
      var clearIntervalSpy = spyOn($window, 'clearInterval');

      $interval(noop, 1000, 1);
      $window.flush(1000);
      expect(clearIntervalSpy).toHaveBeenCalled();

      clearIntervalSpy.calls.reset();
      $interval.cancel($interval(noop, 1000));
      expect(clearIntervalSpy).toHaveBeenCalled();
    }));
  });
});
