// Copyright 2012 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview goog.events.EventTarget tester.
 * @author chrishenry@google.com (Chris Henry)
 */

goog.provide('goog.events.eventTargetTester');
goog.setTestOnly('goog.events.eventTargetTester');
goog.provide('goog.events.eventTargetTester.KeyType');
goog.setTestOnly('goog.events.eventTargetTester.KeyType');
goog.provide('goog.events.eventTargetTester.UnlistenReturnType');
goog.setTestOnly('goog.events.eventTargetTester.UnlistenReturnType');

goog.require('goog.array');
goog.require('goog.events');
goog.require('goog.events.Event');
goog.require('goog.events.EventTarget');
goog.require('goog.testing.asserts');
goog.require('goog.testing.recordFunction');


/**
 * Setup step for the test functions. This needs to be called from the
 * test setUp.
 * @param {function():!goog.events.Listenable} listenableFactoryFn Function
 *     that will return a new Listenable instance each time it is called.
 * @param {Function} listenFn Function that, given the same signature
 *     as goog.events.listen, will add listener to the given event
 *     target.
 * @param {Function} unlistenFn Function that, given the same
 *     signature as goog.events.unlisten, will remove listener from
 *     the given event target.
 * @param {Function} unlistenByKeyFn Function that, given 2
 *     parameters: src and key, will remove the corresponding
 *     listener.
 * @param {Function} listenOnceFn Function that, given the same
 *     signature as goog.events.listenOnce, will add a one-time
 *     listener to the given event target.
 * @param {Function} dispatchEventFn Function that, given the same
 *     signature as goog.events.dispatchEvent, will dispatch the event
 *     on the given event target.
 * @param {Function} removeAllFn Function that, given the same
 *     signature as goog.events.removeAll, will remove all listeners
 *     according to the contract of goog.events.removeAll.
 * @param {Function} getListenersFn Function that, given the same
 *     signature as goog.events.getListeners, will retrieve listeners.
 * @param {Function} getListenerFn Function that, given the same
 *     signature as goog.events.getListener, will retrieve the
 *     listener object.
 * @param {Function} hasListenerFn Function that, given the same
 *     signature as goog.events.hasListener, will determine whether
 *     listeners exist.
 * @param {goog.events.eventTargetTester.KeyType} listenKeyType The
 *     key type returned by listen call.
 * @param {goog.events.eventTargetTester.UnlistenReturnType}
 *     unlistenFnReturnType
 *     Whether we should check return value from
 *     unlisten call. If unlisten does not return a value, this should
 *     be set to false.
 * @param {boolean} objectListenerSupported Whether listener of type
 *     Object is supported.
 */
goog.events.eventTargetTester.setUp = function(
    listenableFactoryFn,
    listenFn, unlistenFn, unlistenByKeyFn, listenOnceFn,
    dispatchEventFn, removeAllFn,
    getListenersFn, getListenerFn, hasListenerFn,
    listenKeyType, unlistenFnReturnType, objectListenerSupported) {
  listenableFactory = listenableFactoryFn;
  listen = listenFn;
  unlisten = unlistenFn;
  unlistenByKey = unlistenByKeyFn;
  listenOnce = listenOnceFn;
  dispatchEvent = dispatchEventFn;
  removeAll = removeAllFn;
  getListeners = getListenersFn;
  getListener = getListenerFn;
  hasListener = hasListenerFn;
  keyType = listenKeyType;
  unlistenReturnType = unlistenFnReturnType;
  objectTypeListenerSupported = objectListenerSupported;

  listeners = [];
  for (var i = 0; i < goog.events.eventTargetTester.MAX_; i++) {
    listeners[i] = createListener();
  }

  eventTargets = [];
  for (i = 0; i < goog.events.eventTargetTester.MAX_; i++) {
    eventTargets[i] = listenableFactory();
  }
};


/**
 * Teardown step for the test functions. This needs to be called from
 * test teardown.
 */
goog.events.eventTargetTester.tearDown = function() {
  for (var i = 0; i < goog.events.eventTargetTester.MAX_; i++) {
    goog.dispose(eventTargets[i]);
  }
};


/**
 * The type of key returned by key-returning functions (listen).
 * @enum {number}
 */
goog.events.eventTargetTester.KeyType = {
  /**
   * Returns number for key.
   */
  NUMBER: 0,

  /**
   * Returns undefined (no return value).
   */
  UNDEFINED: 1
};


/**
 * The type of unlisten function's return value.
 */
goog.events.eventTargetTester.UnlistenReturnType = {
  /**
   * Returns boolean indicating whether unlisten is successful.
   */
  BOOLEAN: 0,

  /**
   * Returns undefind (no return value).
   */
  UNDEFINED: 1
};


/**
 * Expando property used on "listener" function to determine if a
 * listener has already been checked. This is what allows us to
 * implement assertNoOtherListenerIsCalled.
 * @type {string}
 */
goog.events.eventTargetTester.ALREADY_CHECKED_PROP = '__alreadyChecked';


/**
 * Expando property used on "listener" function to record the number
 * of times it has been called the last time assertListenerIsCalled is
 * done. This allows us to verify that it has not been called more
 * times in assertNoOtherListenerIsCalled.
 */
goog.events.eventTargetTester.NUM_CALLED_PROP = '__numCalled';


/**
 * The maximum number of initialized event targets (in eventTargets
 * array) and listeners (in listeners array).
 * @type {number}
 * @private
 */
goog.events.eventTargetTester.MAX_ = 10;


/**
 * Contains test event types.
 * @enum {string}
 */
var EventType = {
  A: goog.events.getUniqueId('a'),
  B: goog.events.getUniqueId('b'),
  C: goog.events.getUniqueId('c')
};


var listenableFactory, listen, unlisten, unlistenByKey, listenOnce;
var dispatchEvent, removeAll, getListeners, getListener, hasListener;
var keyType, unlistenReturnType, objectTypeListenerSupported;
var eventTargets, listeners;



/**
 * Custom event object for testing.
 * @constructor
 * @extends {goog.events.Event}
 * @final
 */
var TestEvent = function() {
  TestEvent.base(this, 'constructor', EventType.A);
};
goog.inherits(TestEvent, goog.events.Event);


/**
 * Creates a listener that executes the given function (optional).
 * @param {!Function=} opt_listenerFn The optional function to execute.
 * @return {!Function} The listener function.
 */
function createListener(opt_listenerFn) {
  return goog.testing.recordFunction(opt_listenerFn);
}


/**
 * Asserts that the given listener is called numCount number of times.
 * @param {!Function} listener The listener to check.
 * @param {number} numCount The number of times. See also the times()
 *     function below.
 */
function assertListenerIsCalled(listener, numCount) {
  assertEquals('Listeners is not called the correct number of times.',
               numCount, listener.getCallCount());
  listener[goog.events.eventTargetTester.ALREADY_CHECKED_PROP] = true;
  listener[goog.events.eventTargetTester.NUM_CALLED_PROP] = numCount;
}


/**
 * Asserts that no other listeners, other than those verified via
 * assertListenerIsCalled, have been called since the last
 * resetListeners().
 */
function assertNoOtherListenerIsCalled() {
  goog.array.forEach(listeners, function(l, index) {
    if (!l[goog.events.eventTargetTester.ALREADY_CHECKED_PROP]) {
      assertEquals(
          'Listeners ' + index + ' is unexpectedly called.',
          0, l.getCallCount());
    } else {
      assertEquals(
          'Listeners ' + index + ' is unexpectedly called.',
          l[goog.events.eventTargetTester.NUM_CALLED_PROP], l.getCallCount());
    }
  });
}


/**
 * Resets all listeners call count to 0.
 */
function resetListeners() {
  goog.array.forEach(listeners, function(l) {
    l.reset();
    l[goog.events.eventTargetTester.ALREADY_CHECKED_PROP] = false;
  });
}


/**
 * The number of times a listener should have been executed. This
 * exists to make assertListenerIsCalled more readable.  This is used
 * like so: assertListenerIsCalled(listener, times(2));
 * @param {number} n The number of times a listener should have been
 *     executed.
 * @return {number} The number n.
 */
function times(n) {
  return n;
}


function testNoListener() {
  dispatchEvent(eventTargets[0], EventType.A);
  assertNoOtherListenerIsCalled();
}


function testOneListener() {
  listen(eventTargets[0], EventType.A, listeners[0]);
  dispatchEvent(eventTargets[0], EventType.A);
  assertListenerIsCalled(listeners[0], times(1));
  assertNoOtherListenerIsCalled();

  resetListeners();

  dispatchEvent(eventTargets[0], EventType.B);
  dispatchEvent(eventTargets[0], EventType.C);
  assertNoOtherListenerIsCalled();
}


function testTwoListenersOfSameType() {
  var key1 = listen(eventTargets[0], EventType.A, listeners[0]);
  var key2 = listen(eventTargets[0], EventType.A, listeners[1]);

  if (keyType == goog.events.eventTargetTester.KeyType.NUMBER) {
    assertNotEquals(key1, key2);
  } else {
    assertUndefined(key1);
    assertUndefined(key2);
  }

  dispatchEvent(eventTargets[0], EventType.A);
  assertListenerIsCalled(listeners[0], times(1));
  assertListenerIsCalled(listeners[1], times(1));
  assertNoOtherListenerIsCalled();
}


function testInstallingSameListeners() {
  var key1 = listen(eventTargets[0], EventType.A, listeners[0]);
  var key2 = listen(eventTargets[0], EventType.A, listeners[0]);
  var key3 = listen(eventTargets[0], EventType.B, listeners[0]);

  if (keyType == goog.events.eventTargetTester.KeyType.NUMBER) {
    assertEquals(key1, key2);
    assertNotEquals(key1, key3);
  } else {
    assertUndefined(key1);
    assertUndefined(key2);
    assertUndefined(key3);
  }

  dispatchEvent(eventTargets[0], EventType.A);
  assertListenerIsCalled(listeners[0], times(1));

  dispatchEvent(eventTargets[0], EventType.B);
  assertListenerIsCalled(listeners[0], times(2));

  assertNoOtherListenerIsCalled();
}


function testScope() {
  listeners[0] = createListener(function(e) {
    assertEquals('Wrong scope with undefined scope', eventTargets[0], this);
  });
  listeners[1] = createListener(function(e) {
    assertEquals('Wrong scope with null scope', eventTargets[0], this);
  });
  var scope = {};
  listeners[2] = createListener(function(e) {
    assertEquals('Wrong scope with specific scope object', scope, this);
  });
  listen(eventTargets[0], EventType.A, listeners[0]);
  listen(eventTargets[0], EventType.A, listeners[1], false, null);
  listen(eventTargets[0], EventType.A, listeners[2], false, scope);

  dispatchEvent(eventTargets[0], EventType.A);
  assertListenerIsCalled(listeners[0], times(1));
  assertListenerIsCalled(listeners[1], times(1));
  assertListenerIsCalled(listeners[2], times(1));
}


function testDispatchEventDoesNotThrowWithDisposedEventTarget() {
  goog.dispose(eventTargets[0]);
  assertTrue(dispatchEvent(eventTargets[0], EventType.A));
}


function testDispatchEventWithObjectLiteral() {
  listen(eventTargets[0], EventType.A, listeners[0]);

  assertTrue(dispatchEvent(eventTargets[0], {type: EventType.A}));
  assertListenerIsCalled(listeners[0], times(1));
  assertNoOtherListenerIsCalled();
}


function testDispatchEventWithCustomEventObject() {
  listen(eventTargets[0], EventType.A, listeners[0]);

  var e = new TestEvent();
  assertTrue(dispatchEvent(eventTargets[0], e));
  assertListenerIsCalled(listeners[0], times(1));
  assertNoOtherListenerIsCalled();

  var actualEvent = listeners[0].getLastCall().getArgument(0);

  assertEquals(e, actualEvent);
  assertEquals(eventTargets[0], actualEvent.target);
}


function testDisposingEventTargetRemovesListeners() {
  if (!(listenableFactory() instanceof goog.events.EventTarget)) {
    return;
  }
  listen(eventTargets[0], EventType.A, listeners[0]);
  goog.dispose(eventTargets[0]);
  dispatchEvent(eventTargets[0], EventType.A);

  assertNoOtherListenerIsCalled();
}


/**
 * Unlisten/unlistenByKey should still work after disposal. There are
 * many circumstances when this is actually necessary. For example, a
 * user may have listened to an event target and stored the key
 * (e.g. in a goog.events.EventHandler) and only unlisten after the
 * target has been disposed.
 */
function testUnlistenWorksAfterDisposal() {
  var key = listen(eventTargets[0], EventType.A, listeners[0]);
  goog.dispose(eventTargets[0]);
  unlisten(eventTargets[0], EventType.A, listeners[1]);
  if (unlistenByKey) {
    unlistenByKey(eventTargets[0], key);
  }
}


function testRemovingListener() {
  var ret1 = unlisten(eventTargets[0], EventType.A, listeners[0]);
  listen(eventTargets[0], EventType.A, listeners[0]);
  var ret2 = unlisten(eventTargets[0], EventType.A, listeners[1]);
  var ret3 = unlisten(eventTargets[0], EventType.B, listeners[0]);
  var ret4 = unlisten(eventTargets[1], EventType.A, listeners[0]);

  dispatchEvent(eventTargets[0], EventType.A);
  assertListenerIsCalled(listeners[0], times(1));

  var ret5 = unlisten(eventTargets[0], EventType.A, listeners[0]);
  var ret6 = unlisten(eventTargets[0], EventType.A, listeners[0]);

  dispatchEvent(eventTargets[0], EventType.A);
  assertListenerIsCalled(listeners[0], times(1));

  assertNoOtherListenerIsCalled();

  if (unlistenReturnType ==
      goog.events.eventTargetTester.UnlistenReturnType.BOOLEAN) {
    assertFalse(ret1);
    assertFalse(ret2);
    assertFalse(ret3);
    assertFalse(ret4);
    assertTrue(ret5);
    assertFalse(ret6);
  } else {
    assertUndefined(ret1);
    assertUndefined(ret2);
    assertUndefined(ret3);
    assertUndefined(ret4);
    assertUndefined(ret5);
    assertUndefined(ret6);
  }
}


function testCapture() {
  eventTargets[0].setParentEventTarget(eventTargets[1]);
  eventTargets[1].setParentEventTarget(eventTargets[2]);

  eventTargets[9].setParentEventTarget(eventTargets[0]);

  var ordering = 0;
  listeners[0] = createListener(
      function(e) {
        assertEquals(eventTargets[2], e.currentTarget);
        assertEquals(eventTargets[0], e.target);
        assertEquals('First capture listener is not called first', 0, ordering);
        ordering++;
      });
  listeners[1] = createListener(
      function(e) {
        assertEquals(eventTargets[1], e.currentTarget);
        assertEquals(eventTargets[0], e.target);
        assertEquals('2nd capture listener is not called 2nd', 1, ordering);
        ordering++;
      });
  listeners[2] = createListener(
      function(e) {
        assertEquals(eventTargets[0], e.currentTarget);
        assertEquals(eventTargets[0], e.target);
        assertEquals('3rd capture listener is not called 3rd', 2, ordering);
        ordering++;
      });

  listen(eventTargets[2], EventType.A, listeners[0], true);
  listen(eventTargets[1], EventType.A, listeners[1], true);
  listen(eventTargets[0], EventType.A, listeners[2], true);

  // These should not be called.
  listen(eventTargets[3], EventType.A, listeners[3], true);

  listen(eventTargets[0], EventType.B, listeners[4], true);
  listen(eventTargets[0], EventType.C, listeners[5], true);
  listen(eventTargets[1], EventType.B, listeners[6], true);
  listen(eventTargets[1], EventType.C, listeners[7], true);
  listen(eventTargets[2], EventType.B, listeners[8], true);
  listen(eventTargets[2], EventType.C, listeners[9], true);

  dispatchEvent(eventTargets[0], EventType.A);
  assertListenerIsCalled(listeners[0], times(1));
  assertListenerIsCalled(listeners[1], times(1));
  assertListenerIsCalled(listeners[2], times(1));
  assertNoOtherListenerIsCalled();
}


function testBubble() {
  eventTargets[0].setParentEventTarget(eventTargets[1]);
  eventTargets[1].setParentEventTarget(eventTargets[2]);

  eventTargets[9].setParentEventTarget(eventTargets[0]);

  var ordering = 0;
  listeners[0] = createListener(
      function(e) {
        assertEquals(eventTargets[0], e.currentTarget);
        assertEquals(eventTargets[0], e.target);
        assertEquals('First bubble listener is not called first', 0, ordering);
        ordering++;
      });
  listeners[1] = createListener(
      function(e) {
        assertEquals(eventTargets[1], e.currentTarget);
        assertEquals(eventTargets[0], e.target);
        assertEquals('2nd bubble listener is not called 2nd', 1, ordering);
        ordering++;
      });
  listeners[2] = createListener(
      function(e) {
        assertEquals(eventTargets[2], e.currentTarget);
        assertEquals(eventTargets[0], e.target);
        assertEquals('3rd bubble listener is not called 3rd', 2, ordering);
        ordering++;
      });

  listen(eventTargets[0], EventType.A, listeners[0]);
  listen(eventTargets[1], EventType.A, listeners[1]);
  listen(eventTargets[2], EventType.A, listeners[2]);

  // These should not be called.
  listen(eventTargets[3], EventType.A, listeners[3]);

  listen(eventTargets[0], EventType.B, listeners[4]);
  listen(eventTargets[0], EventType.C, listeners[5]);
  listen(eventTargets[1], EventType.B, listeners[6]);
  listen(eventTargets[1], EventType.C, listeners[7]);
  listen(eventTargets[2], EventType.B, listeners[8]);
  listen(eventTargets[2], EventType.C, listeners[9]);

  dispatchEvent(eventTargets[0], EventType.A);
  assertListenerIsCalled(listeners[0], times(1));
  assertListenerIsCalled(listeners[1], times(1));
  assertListenerIsCalled(listeners[2], times(1));
  assertNoOtherListenerIsCalled();
}


function testCaptureAndBubble() {
  eventTargets[0].setParentEventTarget(eventTargets[1]);
  eventTargets[1].setParentEventTarget(eventTargets[2]);

  listen(eventTargets[0], EventType.A, listeners[0], true);
  listen(eventTargets[1], EventType.A, listeners[1], true);
  listen(eventTargets[2], EventType.A, listeners[2], true);

  listen(eventTargets[0], EventType.A, listeners[3]);
  listen(eventTargets[1], EventType.A, listeners[4]);
  listen(eventTargets[2], EventType.A, listeners[5]);

  dispatchEvent(eventTargets[0], EventType.A);
  assertListenerIsCalled(listeners[0], times(1));
  assertListenerIsCalled(listeners[1], times(1));
  assertListenerIsCalled(listeners[2], times(1));
  assertListenerIsCalled(listeners[3], times(1));
  assertListenerIsCalled(listeners[4], times(1));
  assertListenerIsCalled(listeners[5], times(1));
  assertNoOtherListenerIsCalled();
}


function testPreventDefaultByReturningFalse() {
  listeners[0] = createListener(function(e) { return false; });
  listeners[1] = createListener(function(e) { return true; });
  listen(eventTargets[0], EventType.A, listeners[0]);
  listen(eventTargets[0], EventType.A, listeners[1]);

  var result = dispatchEvent(eventTargets[0], EventType.A);
  assertFalse(result);
}


function testPreventDefault() {
  listeners[0] = createListener(function(e) { e.preventDefault(); });
  listeners[1] = createListener(function(e) { return true; });
  listen(eventTargets[0], EventType.A, listeners[0]);
  listen(eventTargets[0], EventType.A, listeners[1]);

  var result = dispatchEvent(eventTargets[0], EventType.A);
  assertFalse(result);
}


function testPreventDefaultAtCapture() {
  listeners[0] = createListener(function(e) { e.preventDefault(); });
  listeners[1] = createListener(function(e) { return true; });
  listen(eventTargets[0], EventType.A, listeners[0], true);
  listen(eventTargets[0], EventType.A, listeners[1], true);

  var result = dispatchEvent(eventTargets[0], EventType.A);
  assertFalse(result);
}


function testStopPropagation() {
  eventTargets[0].setParentEventTarget(eventTargets[1]);
  eventTargets[1].setParentEventTarget(eventTargets[2]);

  listeners[0] = createListener(function(e) { e.stopPropagation(); });
  listen(eventTargets[0], EventType.A, listeners[0]);
  listen(eventTargets[0], EventType.A, listeners[1]);
  listen(eventTargets[1], EventType.A, listeners[2]);
  listen(eventTargets[2], EventType.A, listeners[3]);

  dispatchEvent(eventTargets[0], EventType.A);

  assertListenerIsCalled(listeners[0], times(1));
  assertListenerIsCalled(listeners[1], times(1));
  assertNoOtherListenerIsCalled();
}


function testStopPropagation2() {
  eventTargets[0].setParentEventTarget(eventTargets[1]);
  eventTargets[1].setParentEventTarget(eventTargets[2]);

  listeners[1] = createListener(function(e) { e.stopPropagation(); });
  listen(eventTargets[0], EventType.A, listeners[0]);
  listen(eventTargets[0], EventType.A, listeners[1]);
  listen(eventTargets[1], EventType.A, listeners[2]);
  listen(eventTargets[2], EventType.A, listeners[3]);

  dispatchEvent(eventTargets[0], EventType.A);

  assertListenerIsCalled(listeners[0], times(1));
  assertListenerIsCalled(listeners[1], times(1));
  assertNoOtherListenerIsCalled();
}


function testStopPropagation3() {
  eventTargets[0].setParentEventTarget(eventTargets[1]);
  eventTargets[1].setParentEventTarget(eventTargets[2]);

  listeners[2] = createListener(function(e) { e.stopPropagation(); });
  listen(eventTargets[0], EventType.A, listeners[0]);
  listen(eventTargets[0], EventType.A, listeners[1]);
  listen(eventTargets[1], EventType.A, listeners[2]);
  listen(eventTargets[2], EventType.A, listeners[3]);

  dispatchEvent(eventTargets[0], EventType.A);

  assertListenerIsCalled(listeners[0], times(1));
  assertListenerIsCalled(listeners[1], times(1));
  assertListenerIsCalled(listeners[2], times(1));
  assertNoOtherListenerIsCalled();
}


function testStopPropagationAtCapture() {
  eventTargets[0].setParentEventTarget(eventTargets[1]);
  eventTargets[1].setParentEventTarget(eventTargets[2]);

  listeners[0] = createListener(function(e) { e.stopPropagation(); });
  listen(eventTargets[2], EventType.A, listeners[0], true);
  listen(eventTargets[1], EventType.A, listeners[1], true);
  listen(eventTargets[0], EventType.A, listeners[2], true);
  listen(eventTargets[0], EventType.A, listeners[3]);
  listen(eventTargets[1], EventType.A, listeners[4]);
  listen(eventTargets[2], EventType.A, listeners[5]);

  dispatchEvent(eventTargets[0], EventType.A);

  assertListenerIsCalled(listeners[0], times(1));
  assertNoOtherListenerIsCalled();
}


function testHandleEvent() {
  if (!objectTypeListenerSupported) {
    return;
  }

  var obj = {};
  obj.handleEvent = goog.testing.recordFunction();

  listen(eventTargets[0], EventType.A, obj);
  dispatchEvent(eventTargets[0], EventType.A);

  assertEquals(1, obj.handleEvent.getCallCount());
}


function testListenOnce() {
  if (!listenOnce) {
    return;
  }

  listenOnce(eventTargets[0], EventType.A, listeners[0], true);
  listenOnce(eventTargets[0], EventType.A, listeners[1]);
  listenOnce(eventTargets[0], EventType.B, listeners[2]);

  dispatchEvent(eventTargets[0], EventType.A);

  assertListenerIsCalled(listeners[0], times(1));
  assertListenerIsCalled(listeners[1], times(1));
  assertListenerIsCalled(listeners[2], times(0));
  assertNoOtherListenerIsCalled();
  resetListeners();

  dispatchEvent(eventTargets[0], EventType.A);

  assertListenerIsCalled(listeners[0], times(0));
  assertListenerIsCalled(listeners[1], times(0));
  assertListenerIsCalled(listeners[2], times(0));

  dispatchEvent(eventTargets[0], EventType.B);
  assertListenerIsCalled(listeners[2], times(1));
  assertNoOtherListenerIsCalled();
}


function testUnlistenInListen() {
  listeners[1] = createListener(
      function(e) {
        unlisten(eventTargets[0], EventType.A, listeners[1]);
        unlisten(eventTargets[0], EventType.A, listeners[2]);
      });
  listen(eventTargets[0], EventType.A, listeners[0]);
  listen(eventTargets[0], EventType.A, listeners[1]);
  listen(eventTargets[0], EventType.A, listeners[2]);
  listen(eventTargets[0], EventType.A, listeners[3]);

  dispatchEvent(eventTargets[0], EventType.A);

  assertListenerIsCalled(listeners[0], times(1));
  assertListenerIsCalled(listeners[1], times(1));
  assertListenerIsCalled(listeners[2], times(0));
  assertListenerIsCalled(listeners[3], times(1));
  assertNoOtherListenerIsCalled();
  resetListeners();

  dispatchEvent(eventTargets[0], EventType.A);
  assertListenerIsCalled(listeners[0], times(1));
  assertListenerIsCalled(listeners[1], times(0));
  assertListenerIsCalled(listeners[2], times(0));
  assertListenerIsCalled(listeners[3], times(1));
  assertNoOtherListenerIsCalled();
}


function testUnlistenByKeyInListen() {
  if (!unlistenByKey) {
    return;
  }

  var key1, key2;
  listeners[1] = createListener(
      function(e) {
        unlistenByKey(eventTargets[0], key1);
        unlistenByKey(eventTargets[0], key2);
      });
  listen(eventTargets[0], EventType.A, listeners[0]);
  key1 = listen(eventTargets[0], EventType.A, listeners[1]);
  key2 = listen(eventTargets[0], EventType.A, listeners[2]);
  listen(eventTargets[0], EventType.A, listeners[3]);

  dispatchEvent(eventTargets[0], EventType.A);

  assertListenerIsCalled(listeners[0], times(1));
  assertListenerIsCalled(listeners[1], times(1));
  assertListenerIsCalled(listeners[2], times(0));
  assertListenerIsCalled(listeners[3], times(1));
  assertNoOtherListenerIsCalled();
  resetListeners();

  dispatchEvent(eventTargets[0], EventType.A);
  assertListenerIsCalled(listeners[0], times(1));
  assertListenerIsCalled(listeners[1], times(0));
  assertListenerIsCalled(listeners[2], times(0));
  assertListenerIsCalled(listeners[3], times(1));
  assertNoOtherListenerIsCalled();
}


function testSetParentEventTarget() {
  assertNull(eventTargets[0].getParentEventTarget());

  eventTargets[0].setParentEventTarget(eventTargets[1]);
  assertEquals(eventTargets[1], eventTargets[0].getParentEventTarget());
  assertNull(eventTargets[1].getParentEventTarget());

  eventTargets[0].setParentEventTarget(null);
  assertNull(eventTargets[0].getParentEventTarget());
}


function testListenOnceAfterListenDoesNotChangeExistingListener() {
  if (!listenOnce) {
    return;
  }

  listen(eventTargets[0], EventType.A, listeners[0]);
  listenOnce(eventTargets[0], EventType.A, listeners[0]);

  dispatchEvent(eventTargets[0], EventType.A);
  dispatchEvent(eventTargets[0], EventType.A);
  dispatchEvent(eventTargets[0], EventType.A);

  assertListenerIsCalled(listeners[0], times(3));
  assertNoOtherListenerIsCalled();
}


function testListenOnceAfterListenOnceDoesNotChangeExistingListener() {
  if (!listenOnce) {
    return;
  }

  listenOnce(eventTargets[0], EventType.A, listeners[0]);
  listenOnce(eventTargets[0], EventType.A, listeners[0]);

  dispatchEvent(eventTargets[0], EventType.A);
  dispatchEvent(eventTargets[0], EventType.A);
  dispatchEvent(eventTargets[0], EventType.A);

  assertListenerIsCalled(listeners[0], times(1));
  assertNoOtherListenerIsCalled();
}


function testListenAfterListenOnceRemoveOnceness() {
  if (!listenOnce) {
    return;
  }

  listenOnce(eventTargets[0], EventType.A, listeners[0]);
  listen(eventTargets[0], EventType.A, listeners[0]);

  dispatchEvent(eventTargets[0], EventType.A);
  dispatchEvent(eventTargets[0], EventType.A);
  dispatchEvent(eventTargets[0], EventType.A);

  assertListenerIsCalled(listeners[0], times(3));
  assertNoOtherListenerIsCalled();
}


function testUnlistenAfterListenOnce() {
  if (!listenOnce) {
    return;
  }

  listenOnce(eventTargets[0], EventType.A, listeners[0]);
  unlisten(eventTargets[0], EventType.A, listeners[0]);
  dispatchEvent(eventTargets[0], EventType.A);

  listen(eventTargets[0], EventType.A, listeners[0]);
  listenOnce(eventTargets[0], EventType.A, listeners[0]);
  unlisten(eventTargets[0], EventType.A, listeners[0]);
  dispatchEvent(eventTargets[0], EventType.A);

  listenOnce(eventTargets[0], EventType.A, listeners[0]);
  listen(eventTargets[0], EventType.A, listeners[0]);
  unlisten(eventTargets[0], EventType.A, listeners[0]);
  dispatchEvent(eventTargets[0], EventType.A);

  listenOnce(eventTargets[0], EventType.A, listeners[0]);
  listenOnce(eventTargets[0], EventType.A, listeners[0]);
  unlisten(eventTargets[0], EventType.A, listeners[0]);
  dispatchEvent(eventTargets[0], EventType.A);

  assertNoOtherListenerIsCalled();
}


function testRemoveAllWithType() {
  if (!removeAll) {
    return;
  }

  listen(eventTargets[0], EventType.A, listeners[0], true);
  listen(eventTargets[0], EventType.A, listeners[1]);
  listen(eventTargets[0], EventType.C, listeners[2], true);
  listen(eventTargets[0], EventType.C, listeners[3]);
  listen(eventTargets[0], EventType.B, listeners[4], true);
  listen(eventTargets[0], EventType.B, listeners[5], true);
  listen(eventTargets[0], EventType.B, listeners[6]);
  listen(eventTargets[0], EventType.B, listeners[7]);

  assertEquals(4, removeAll(eventTargets[0], EventType.B));

  dispatchEvent(eventTargets[0], EventType.A);
  dispatchEvent(eventTargets[0], EventType.B);
  dispatchEvent(eventTargets[0], EventType.C);

  assertListenerIsCalled(listeners[0], times(1));
  assertListenerIsCalled(listeners[1], times(1));
  assertListenerIsCalled(listeners[2], times(1));
  assertListenerIsCalled(listeners[3], times(1));
  assertNoOtherListenerIsCalled();
}


function testRemoveAll() {
  if (!removeAll) {
    return;
  }

  listen(eventTargets[0], EventType.A, listeners[0], true);
  listen(eventTargets[0], EventType.A, listeners[1]);
  listen(eventTargets[0], EventType.C, listeners[2], true);
  listen(eventTargets[0], EventType.C, listeners[3]);
  listen(eventTargets[0], EventType.B, listeners[4], true);
  listen(eventTargets[0], EventType.B, listeners[5], true);
  listen(eventTargets[0], EventType.B, listeners[6]);
  listen(eventTargets[0], EventType.B, listeners[7]);

  assertEquals(8, removeAll(eventTargets[0]));

  dispatchEvent(eventTargets[0], EventType.A);
  dispatchEvent(eventTargets[0], EventType.B);
  dispatchEvent(eventTargets[0], EventType.C);

  assertNoOtherListenerIsCalled();
}


function testRemoveAllCallsMarkAsRemoved() {
  if (!removeAll) {
    return;
  }

  var key0 = listen(eventTargets[0], EventType.A, listeners[0]);
  var key1 = listen(eventTargets[1], EventType.A, listeners[1]);

  assertNotNullNorUndefined(key0.listener);
  assertFalse(key0.removed);
  assertNotNullNorUndefined(key1.listener);
  assertFalse(key1.removed);

  assertEquals(1, removeAll(eventTargets[0]));
  assertNull(key0.listener);
  assertTrue(key0.removed);
  assertNotNullNorUndefined(key1.listener);
  assertFalse(key1.removed);

  assertEquals(1, removeAll(eventTargets[1]));
  assertNull(key1.listener);
  assertTrue(key1.removed);
}


function testGetListeners() {
  if (!getListeners) {
    return;
  }

  listen(eventTargets[0], EventType.A, listeners[0], true);
  listen(eventTargets[0], EventType.A, listeners[1], true);
  listen(eventTargets[0], EventType.A, listeners[2]);
  listen(eventTargets[0], EventType.A, listeners[3]);

  var l = getListeners(eventTargets[0], EventType.A, true);
  assertEquals(2, l.length);
  assertEquals(listeners[0], l[0].listener);
  assertEquals(listeners[1], l[1].listener);

  l = getListeners(eventTargets[0], EventType.A, false);
  assertEquals(2, l.length);
  assertEquals(listeners[2], l[0].listener);
  assertEquals(listeners[3], l[1].listener);

  l = getListeners(eventTargets[0], EventType.B, true);
  assertEquals(0, l.length);
}


function testGetListener() {
  if (!getListener) {
    return;
  }

  listen(eventTargets[0], EventType.A, listeners[0], true);

  assertNotNull(getListener(eventTargets[0], EventType.A, listeners[0], true));
  assertNull(
      getListener(eventTargets[0], EventType.A, listeners[0], true, {}));
  assertNull(getListener(eventTargets[1], EventType.A, listeners[0], true));
  assertNull(getListener(eventTargets[0], EventType.B, listeners[0], true));
  assertNull(getListener(eventTargets[0], EventType.A, listeners[1], true));
}


function testHasListener() {
  if (!hasListener) {
    return;
  }

  assertFalse(hasListener(eventTargets[0]));

  listen(eventTargets[0], EventType.A, listeners[0], true);

  assertTrue(hasListener(eventTargets[0]));
  assertTrue(hasListener(eventTargets[0], EventType.A));
  assertTrue(hasListener(eventTargets[0], EventType.A, true));
  assertTrue(hasListener(eventTargets[0], undefined, true));
  assertFalse(hasListener(eventTargets[0], EventType.A, false));
  assertFalse(hasListener(eventTargets[0], undefined, false));
  assertFalse(hasListener(eventTargets[0], EventType.B));
  assertFalse(hasListener(eventTargets[0], EventType.B, true));
  assertFalse(hasListener(eventTargets[1]));
}


function testFiringEventBeforeDisposeInternalWorks() {
  /**
   * @extends {goog.events.EventTarget}
   * @constructor
   * @final
   */
  var MockTarget = function() {
    MockTarget.base(this, 'constructor');
  };
  goog.inherits(MockTarget, goog.events.EventTarget);

  MockTarget.prototype.disposeInternal = function() {
    dispatchEvent(this, EventType.A);
    MockTarget.base(this, 'disposeInternal');
  };

  var t = new MockTarget();
  try {
    listen(t, EventType.A, listeners[0]);
    t.dispose();
    assertListenerIsCalled(listeners[0], times(1));
  } catch (e) {
    goog.dispose(t);
  }
}


function testLoopDetection() {
  var target = listenableFactory();
  target.setParentEventTarget(target);

  try {
    target.dispatchEvent('string');
    fail('expected error');
  } catch (e) {
    assertContains('infinite', e.message);
  }
}
