/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

goog.module('goog.fx.animTest');
goog.setTestOnly();

const Animation = goog.require('goog.fx.Animation');
const AnimationDelay = goog.require('goog.async.AnimationDelay');
const Delay = goog.require('goog.async.Delay');
const MockClock = goog.require('goog.testing.MockClock');
const PropertyReplacer = goog.require('goog.testing.PropertyReplacer');
const events = goog.require('goog.events');
const functions = goog.require('goog.functions');
const fxAnim = goog.require('goog.fx.anim');
const googObject = goog.require('goog.object');
const recordFunction = goog.require('goog.testing.recordFunction');
const testSuite = goog.require('goog.testing.testSuite');
const userAgent = goog.require('goog.userAgent');

let clock;
let replacer;

/**
 * @param {!Function} delayType The constructor for Delay or AnimationDelay.
 *     The methods will be mocked out.
 * @suppress {checkTypes,visibility} suppression added to enable type checking
 */
function registerAndUnregisterAnimationWithMocks(delayType) {
  let timerCount = 0;

  replacer.set(delayType.prototype, 'start', () => {
    timerCount++;
  });
  replacer.set(delayType.prototype, 'stop', () => {
    timerCount--;
  });
  replacer.set(delayType.prototype, 'isActive', () => timerCount > 0);

  const forbiddenDelayType =
      delayType == AnimationDelay ? Delay : AnimationDelay;
  replacer.set(forbiddenDelayType.prototype, 'start', functions.error());
  replacer.set(forbiddenDelayType.prototype, 'stop', functions.error());
  replacer.set(forbiddenDelayType.prototype, 'isActive', functions.error());

  const anim = new Animation([0], [1], 1000);
  const anim2 = new Animation([0], [1], 1000);

  fxAnim.registerAnimation(anim);

  assertTrue(
      'Should contain the animation',
      googObject.containsValue(fxAnim.activeAnimations_, anim));
  assertEquals('Should have called start once', 1, timerCount);

  fxAnim.registerAnimation(anim2);

  assertEquals('Should not have called start again', 1, timerCount);

  // Add anim again.
  fxAnim.registerAnimation(anim);
  assertTrue(
      'Should contain the animation',
      googObject.containsValue(fxAnim.activeAnimations_, anim));
  assertEquals('Should not have called start again', 1, timerCount);

  fxAnim.unregisterAnimation(anim);
  assertFalse(
      'Should not contain the animation',
      googObject.containsValue(fxAnim.activeAnimations_, anim));
  assertEquals('clearTimeout should not have been called', 1, timerCount);

  fxAnim.unregisterAnimation(anim2);
  assertEquals('There should be no remaining timers', 0, timerCount);

  // Make sure we don't trigger setTimeout or setInterval.
  clock.tick(1000);
  fxAnim.cycleAnimations_(Date.now());

  assertEquals('There should be no remaining timers', 0, timerCount);

  anim.dispose();
  anim2.dispose();
}

testSuite({
  setUpPage() {
    clock = new MockClock(true);
  },

  tearDownPage() {
    clock.dispose();
  },

  setUp() {
    replacer = new PropertyReplacer();
  },

  tearDown() {
    replacer.reset();
    fxAnim.tearDown();
  },

  testDelayWithMocks() {
    fxAnim.setAnimationWindow(null);
    registerAndUnregisterAnimationWithMocks(Delay);
  },

  testAnimationDelayWithMocks() {
    fxAnim.setAnimationWindow(window);
    registerAndUnregisterAnimationWithMocks(AnimationDelay);
  },

  /** @suppress {visibility} suppression added to enable type checking */
  testRegisterAndUnregisterAnimationWithRequestAnimationFrameGecko() {
    // Only FF4 onwards support requestAnimationFrame.
    if (!userAgent.GECKO || userAgent.isVersionOrHigher('17')) {
      return;
    }

    fxAnim.setAnimationWindow(window);

    const anim = new Animation([0], [1], 1000);
    const anim2 = new Animation([0], [1], 1000);

    fxAnim.registerAnimation(anim);

    assertTrue(
        'Should contain the animation',
        googObject.containsValue(fxAnim.activeAnimations_, anim));

    assertEquals(
        'Should have listen to MozBeforePaint once', 1,
        events.getListeners(window, 'MozBeforePaint', false).length);

    fxAnim.registerAnimation(anim2);

    assertEquals(
        'Should not add more listener for MozBeforePaint', 1,
        events.getListeners(window, 'MozBeforePaint', false).length);

    // Add anim again.
    fxAnim.registerAnimation(anim);
    assertTrue(
        'Should contain the animation',
        googObject.containsValue(fxAnim.activeAnimations_, anim));
    assertEquals(
        'Should not add more listener for MozBeforePaint', 1,
        events.getListeners(window, 'MozBeforePaint', false).length);

    fxAnim.unregisterAnimation(anim);
    assertFalse(
        'Should not contain the animation',
        googObject.containsValue(fxAnim.activeAnimations_, anim));
    assertEquals(
        'Should not clear listener for MozBeforePaint yet', 1,
        events.getListeners(window, 'MozBeforePaint', false).length);

    fxAnim.unregisterAnimation(anim2);
    assertEquals(
        'There should be no more listener for MozBeforePaint', 0,
        events.getListeners(window, 'MozBeforePaint', false).length);

    anim.dispose();
    anim2.dispose();

    fxAnim.setAnimationWindow(null);
  },

  /** @suppress {visibility} suppression added to enable type checking */
  testRegisterUnregisterAnimation() {
    const anim = new Animation([0], [1], 1000);

    fxAnim.registerAnimation(anim);

    assertTrue(
        'There should be an active timer',
        fxAnim.animationDelay_ && fxAnim.animationDelay_.isActive());
    assertEquals(
        'There should be an active animations', 1,
        googObject.getCount(fxAnim.activeAnimations_));

    fxAnim.unregisterAnimation(anim);

    assertTrue(
        'There should be no active animations',
        googObject.isEmpty(fxAnim.activeAnimations_));
    assertFalse(
        'There should be no active timer',
        fxAnim.animationDelay_ && fxAnim.animationDelay_.isActive());

    anim.dispose();
  },

  /** @suppress {missingProperties} suppression added to enable type checking */
  testCycleWithMockClock() {
    fxAnim.setAnimationWindow(null);
    const anim = new Animation([0], [1], 1000);
    anim.onAnimationFrame = recordFunction();

    fxAnim.registerAnimation(anim);
    clock.tick(fxAnim.TIMEOUT);

    assertEquals(1, anim.onAnimationFrame.getCallCount());
  },

  /** @suppress {missingProperties} suppression added to enable type checking */
  testCycleWithMockClockAndAnimationWindow() {
    fxAnim.setAnimationWindow(window);
    const anim = new Animation([0], [1], 1000);
    anim.onAnimationFrame = recordFunction();

    fxAnim.registerAnimation(anim);
    clock.tick(fxAnim.TIMEOUT);

    assertEquals(1, anim.onAnimationFrame.getCallCount());
  },
});
