// Copyright 2006 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.

goog.provide('goog.eventsTest');
goog.setTestOnly('goog.eventsTest');

goog.require('goog.asserts.AssertionError');
goog.require('goog.debug.EntryPointMonitor');
goog.require('goog.debug.ErrorHandler');
goog.require('goog.debug.entryPointRegistry');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.events');
goog.require('goog.events.BrowserFeature');
goog.require('goog.events.CaptureSimulationMode');
goog.require('goog.events.Event');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.events.Listener');
goog.require('goog.functions');
goog.require('goog.testing.PropertyReplacer');
goog.require('goog.testing.jsunit');
goog.require('goog.testing.recordFunction');

var originalHandleBrowserEvent = goog.events.handleBrowserEvent_;
var propertyReplacer;
var et1, et2, et3;

function setUp() {
  et1 = new goog.events.EventTarget();
  et2 = new goog.events.EventTarget();
  et3 = new goog.events.EventTarget();
  propertyReplacer = new goog.testing.PropertyReplacer();
}

function tearDown() {
  goog.events.CAPTURE_SIMULATION_MODE = goog.events.CaptureSimulationMode.ON;
  goog.events.handleBrowserEvent_ = originalHandleBrowserEvent;
  goog.disposeAll(et1, et2, et3);
  goog.events.removeAll(document.body);
  propertyReplacer.reset();
}

function testProtectBrowserEventEntryPoint() {
  var errorHandlerFn = goog.testing.recordFunction();
  var errorHandler = new goog.debug.ErrorHandler(errorHandlerFn);

  goog.events.protectBrowserEventEntryPoint(errorHandler);

  var browserEventHandler =
      goog.testing.recordFunction(goog.events.handleBrowserEvent_);
  goog.events.handleBrowserEvent_ = function() {
    try {
      browserEventHandler.apply(this, arguments);
    } catch (e) {
      // Ignored.
    }
  };

  var err = Error('test');
  var body = document.body;
  goog.events.listen(
      body, goog.events.EventType.CLICK, function() { throw err; });

  dispatchClick(body);

  assertEquals(
      'Error handler callback should be called.', 1,
      errorHandlerFn.getCallCount());
  assertEquals(err, errorHandlerFn.getLastCall().getArgument(0));

  assertEquals(1, browserEventHandler.getCallCount());
  var err2 = browserEventHandler.getLastCall().getError();
  assertNotNull(err2);
  assertTrue(err2 instanceof goog.debug.ErrorHandler.ProtectedFunctionError);
}

function testSelfRemove() {
  var callback = function() {
    // This listener removes itself during event dispatching, so it
    // is marked as 'removed' but not actually removed until after event
    // dispatching ends.
    goog.events.removeAll(et1, 'click');

    // Test that goog.events.getListener ignores events marked as 'removed'.
    assertNull(goog.events.getListener(et1, 'click', callback));
  };
  var key = goog.events.listen(et1, 'click', callback);
  goog.events.dispatchEvent(et1, 'click');
}

function testHasListener() {
  var div = goog.dom.createElement(goog.dom.TagName.DIV);
  assertFalse(goog.events.hasListener(div));

  var key = goog.events.listen(div, 'click', function() {});
  assertTrue(goog.events.hasListener(div));
  assertTrue(goog.events.hasListener(div, 'click'));
  assertTrue(goog.events.hasListener(div, 'click', false));
  assertTrue(goog.events.hasListener(div, undefined, false));

  assertFalse(goog.events.hasListener(div, 'click', true));
  assertFalse(goog.events.hasListener(div, undefined, true));
  assertFalse(goog.events.hasListener(div, 'mouseup'));

  // Test that hasListener returns false when all listeners are removed.
  goog.events.unlistenByKey(key);
  assertFalse(goog.events.hasListener(div));
}

function testHasListenerWithEventTarget() {
  assertFalse(goog.events.hasListener(et1));

  function callback(){};
  goog.events.listen(et1, 'test', callback, true);
  assertTrue(goog.events.hasListener(et1));
  assertTrue(goog.events.hasListener(et1, 'test'));
  assertTrue(goog.events.hasListener(et1, 'test', true));
  assertTrue(goog.events.hasListener(et1, undefined, true));

  assertFalse(goog.events.hasListener(et1, 'click'));
  assertFalse(goog.events.hasListener(et1, 'test', false));

  goog.events.unlisten(et1, 'test', callback, true);
  assertFalse(goog.events.hasListener(et1));
}

function testHasListenerWithMultipleTargets() {
  function callback(){};

  goog.events.listen(et1, 'test1', callback, true);
  goog.events.listen(et2, 'test2', callback, true);

  assertTrue(goog.events.hasListener(et1));
  assertTrue(goog.events.hasListener(et2));
  assertTrue(goog.events.hasListener(et1, 'test1'));
  assertTrue(goog.events.hasListener(et2, 'test2'));

  assertFalse(goog.events.hasListener(et1, 'et2'));
  assertFalse(goog.events.hasListener(et2, 'et1'));

  goog.events.removeAll(et1);
  goog.events.removeAll(et2);
}

function testBubbleSingle() {
  et1.setParentEventTarget(et2);
  et2.setParentEventTarget(et3);

  var count = 0;
  function callback() { count++; }

  goog.events.listen(et3, 'test', callback, false);

  et1.dispatchEvent('test');

  assertEquals(1, count);

  goog.events.removeAll(et1);
  goog.events.removeAll(et2);
  goog.events.removeAll(et3);
}

function testCaptureSingle() {
  et1.setParentEventTarget(et2);
  et2.setParentEventTarget(et3);

  var count = 0;
  function callback() { count++; }

  goog.events.listen(et3, 'test', callback, true);

  et1.dispatchEvent('test');

  assertEquals(1, count);

  goog.events.removeAll(et1);
  goog.events.removeAll(et2);
  goog.events.removeAll(et3);
}

function testCaptureAndBubble() {
  et1.setParentEventTarget(et2);
  et2.setParentEventTarget(et3);

  var count = 0;
  function callbackCapture1() {
    count++;
    assertEquals(3, count);
  }
  function callbackBubble1() {
    count++;
    assertEquals(4, count);
  }

  function callbackCapture2() {
    count++;
    assertEquals(2, count);
  }
  function callbackBubble2() {
    count++;
    assertEquals(5, count);
  }

  function callbackCapture3() {
    count++;
    assertEquals(1, count);
  }
  function callbackBubble3() {
    count++;
    assertEquals(6, count);
  }

  goog.events.listen(et1, 'test', callbackCapture1, true);
  goog.events.listen(et1, 'test', callbackBubble1, false);
  goog.events.listen(et2, 'test', callbackCapture2, true);
  goog.events.listen(et2, 'test', callbackBubble2, false);
  goog.events.listen(et3, 'test', callbackCapture3, true);
  goog.events.listen(et3, 'test', callbackBubble3, false);

  et1.dispatchEvent('test');

  assertEquals(6, count);

  goog.events.removeAll(et1);
  goog.events.removeAll(et2);
  goog.events.removeAll(et3);

  // Try again with the new API:
  count = 0;

  goog.events.listen(et1, 'test', callbackCapture1, {capture: true});
  goog.events.listen(et1, 'test', callbackBubble1, {capture: false});
  goog.events.listen(et2, 'test', callbackCapture2, {capture: true});
  goog.events.listen(et2, 'test', callbackBubble2, {capture: false});
  goog.events.listen(et3, 'test', callbackCapture3, {capture: true});
  goog.events.listen(et3, 'test', callbackBubble3, {capture: false});

  et1.dispatchEvent('test');

  assertEquals(6, count);

  goog.events.removeAll(et1);
  goog.events.removeAll(et2);
  goog.events.removeAll(et3);


  // Try again with the new API and without capture simulation:
  if (!goog.events.BrowserFeature.HAS_W3C_EVENT_SUPPORT) return;
  goog.events.CAPTURE_SIMULATION_MODE =
      goog.events.CaptureSimulationMode.OFF_AND_FAIL;
  count = 0;

  goog.events.listen(et1, 'test', callbackCapture1, {capture: true});
  goog.events.listen(et1, 'test', callbackBubble1, {capture: false});
  goog.events.listen(et2, 'test', callbackCapture2, {capture: true});
  goog.events.listen(et2, 'test', callbackBubble2, {capture: false});
  goog.events.listen(et3, 'test', callbackCapture3, {capture: true});
  goog.events.listen(et3, 'test', callbackBubble3, {capture: false});

  et1.dispatchEvent('test');

  assertEquals(6, count);

  goog.events.removeAll(et1);
  goog.events.removeAll(et2);
  goog.events.removeAll(et3);
}

function testCapturingRemovesBubblingListener() {
  var bubbleCount = 0;
  function callbackBubble() { bubbleCount++; }

  var captureCount = 0;
  function callbackCapture() {
    captureCount++;
    goog.events.removeAll(et1);
  }

  goog.events.listen(et1, 'test', callbackCapture, true);
  goog.events.listen(et1, 'test', callbackBubble, false);

  et1.dispatchEvent('test');
  assertEquals(1, captureCount);
  assertEquals(0, bubbleCount);
}

function dispatchClick(target) {
  if (target.click) {
    target.click();
  } else {
    var e = document.createEvent('MouseEvents');
    e.initMouseEvent(
        'click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false,
        0, null);
    target.dispatchEvent(e);
  }
}

function testHandleBrowserEventBubblingListener() {
  var count = 0;
  var body = document.body;
  goog.events.listen(body, 'click', function() { count++; });
  dispatchClick(body);
  assertEquals(1, count);
}

function testHandleBrowserEventCapturingListener() {
  var count = 0;
  var body = document.body;
  goog.events.listen(body, 'click', function() { count++; }, true);
  dispatchClick(body);
  assertEquals(1, count);
}

function testHandleBrowserEventCapturingAndBubblingListener() {
  var count = 1;
  var body = document.body;
  goog.events.listen(body, 'click', function() { count += 3; }, true);
  goog.events.listen(body, 'click', function() { count *= 5; }, false);
  dispatchClick(body);
  assertEquals(20, count);
}

function testHandleBrowserEventCapturingRemovesBubblingListener() {
  var body = document.body;

  var bubbleCount = 0;
  function callbackBubble() { bubbleCount++; }

  var captureCount = 0;
  function callbackCapture() {
    captureCount++;
    goog.events.removeAll(body);
  }

  goog.events.listen(body, 'click', callbackCapture, true);
  goog.events.listen(body, 'click', callbackBubble, false);

  dispatchClick(body);
  assertEquals(1, captureCount);
  assertEquals(0, bubbleCount);
}

function testHandleEventPropagationOnParentElement() {
  var count = 1;
  goog.events.listen(
      document.documentElement, 'click', function() { count += 3; }, true);
  goog.events.listen(
      document.documentElement, 'click', function() { count *= 5; }, false);
  dispatchClick(document.body);
  assertEquals(20, count);
}

function testEntryPointRegistry() {
  var monitor = new goog.debug.EntryPointMonitor();
  var replacement = function() {};
  monitor.wrap =
      goog.testing.recordFunction(goog.functions.constant(replacement));

  goog.debug.entryPointRegistry.monitorAll(monitor);
  assertTrue(monitor.wrap.getCallCount() >= 1);
  assertEquals(replacement, goog.events.handleBrowserEvent_);
}

// Fixes bug http://b/6434926
function testListenOnceHandlerDispatchCausingInfiniteLoop() {
  var handleFoo =
      goog.testing.recordFunction(function() { et1.dispatchEvent('foo'); });

  goog.events.listenOnce(et1, 'foo', handleFoo);

  et1.dispatchEvent('foo');

  assertEquals(
      'Handler should be called only once.', 1, handleFoo.getCallCount());
}

function testCreationStack() {
  if (!new Error().stack) return;
  propertyReplacer.replace(goog.events.Listener, 'ENABLE_MONITORING', true);

  var div = goog.dom.createElement(goog.dom.TagName.DIV);
  var key =
      goog.events.listen(div, goog.events.EventType.CLICK, goog.nullFunction);
  var listenerStack = key.creationStack;

  // Check that the name of this test function occurs in the stack trace.
  assertContains('testCreationStack', listenerStack);
  goog.events.unlistenByKey(key);
}

function testListenOnceAfterListenDoesNotChangeExistingListener() {
  var listener = goog.testing.recordFunction();
  goog.events.listen(document.body, 'click', listener);
  goog.events.listenOnce(document.body, 'click', listener);

  dispatchClick(document.body);
  dispatchClick(document.body);
  dispatchClick(document.body);

  assertEquals(3, listener.getCallCount());
}

function testListenOnceAfterListenOnceDoesNotChangeExistingListener() {
  var listener = goog.testing.recordFunction();
  goog.events.listenOnce(document.body, 'click', listener);
  goog.events.listenOnce(document.body, 'click', listener);

  dispatchClick(document.body);
  dispatchClick(document.body);
  dispatchClick(document.body);

  assertEquals(1, listener.getCallCount());
}

function testListenAfterListenOnceRemoveOnceness() {
  var listener = goog.testing.recordFunction();
  goog.events.listenOnce(document.body, 'click', listener);
  goog.events.listen(document.body, 'click', listener);

  dispatchClick(document.body);
  dispatchClick(document.body);
  dispatchClick(document.body);

  assertEquals(3, listener.getCallCount());
}

function testUnlistenAfterListenOnce() {
  var listener = goog.testing.recordFunction();

  goog.events.listenOnce(document.body, 'click', listener);
  goog.events.unlisten(document.body, 'click', listener);
  dispatchClick(document.body);

  goog.events.listenOnce(document.body, 'click', listener);
  goog.events.listen(document.body, 'click', listener);
  goog.events.unlisten(document.body, 'click', listener);
  dispatchClick(document.body);

  goog.events.listen(document.body, 'click', listener);
  goog.events.listenOnce(document.body, 'click', listener);
  goog.events.unlisten(document.body, 'click', listener);
  dispatchClick(document.body);

  goog.events.listenOnce(document.body, 'click', listener);
  goog.events.listenOnce(document.body, 'click', listener);
  goog.events.unlisten(document.body, 'click', listener);
  dispatchClick(document.body);

  assertEquals(0, listener.getCallCount());
}

function testEventBubblingWithReentrantDispatch_bubbling() {
  runEventPropagationWithReentrantDispatch(false);
}

function testEventBubblingWithReentrantDispatch_capture() {
  runEventPropagationWithReentrantDispatch(true);
}

function runEventPropagationWithReentrantDispatch(useCapture) {
  var eventType = 'test-event-type';

  var child = et1;
  var parent = et2;
  child.setParentEventTarget(parent);

  var firstTarget = useCapture ? parent : child;
  var secondTarget = useCapture ? child : parent;

  var firstListener = function(evt) {
    if (evt.isFirstEvent) {
      // Fires another event of the same type the first time it is invoked.
      child.dispatchEvent(new goog.events.Event(eventType));
    }
  };
  goog.events.listen(firstTarget, eventType, firstListener, useCapture);

  var secondListener = goog.testing.recordFunction();
  goog.events.listen(secondTarget, eventType, secondListener, useCapture);

  // Fire the first event.
  var firstEvent = new goog.events.Event(eventType);
  firstEvent.isFirstEvent = true;
  child.dispatchEvent(firstEvent);

  assertEquals(2, secondListener.getCallCount());
}

function testEventPropagationWhenListenerRemoved_bubbling() {
  runEventPropagationWhenListenerRemoved(false);
}

function testEventPropagationWhenListenerRemoved_capture() {
  runEventPropagationWhenListenerRemoved(true);
}

function runEventPropagationWhenListenerRemoved(useCapture) {
  var eventType = 'test-event-type';

  var child = et1;
  var parent = et2;
  child.setParentEventTarget(parent);

  var firstTarget = useCapture ? parent : child;
  var secondTarget = useCapture ? child : parent;

  var firstListener = goog.testing.recordFunction();
  var secondListener = goog.testing.recordFunction();
  goog.events.listenOnce(firstTarget, eventType, firstListener, useCapture);
  goog.events.listen(secondTarget, eventType, secondListener, useCapture);

  child.dispatchEvent(new goog.events.Event(eventType));

  assertEquals(1, secondListener.getCallCount());
}

function testEventPropagationWhenListenerAdded_bubbling() {
  runEventPropagationWhenListenerAdded(false);
}

function testEventPropagationWhenListenerAdded_capture() {
  runEventPropagationWhenListenerAdded(true);
}

function runEventPropagationWhenListenerAdded(useCapture) {
  var eventType = 'test-event-type';

  var child = et1;
  var parent = et2;
  child.setParentEventTarget(parent);

  var firstTarget = useCapture ? parent : child;
  var secondTarget = useCapture ? child : parent;

  var firstListener = function() {
    goog.events.listen(secondTarget, eventType, secondListener, useCapture);
  };
  var secondListener = goog.testing.recordFunction();
  goog.events.listen(firstTarget, eventType, firstListener, useCapture);

  child.dispatchEvent(new goog.events.Event(eventType));

  assertEquals(1, secondListener.getCallCount());
}

function testEventPropagationWhenListenerAddedAndRemoved_bubbling() {
  runEventPropagationWhenListenerAddedAndRemoved(false);
}

function testEventPropagationWhenListenerAddedAndRemoved_capture() {
  runEventPropagationWhenListenerAddedAndRemoved(true);
}

function runEventPropagationWhenListenerAddedAndRemoved(useCapture) {
  var eventType = 'test-event-type';

  var child = et1;
  var parent = et2;
  child.setParentEventTarget(parent);

  var firstTarget = useCapture ? parent : child;
  var secondTarget = useCapture ? child : parent;

  var firstListener = function() {
    goog.events.listen(secondTarget, eventType, secondListener, useCapture);
  };
  var secondListener = goog.testing.recordFunction();
  goog.events.listenOnce(firstTarget, eventType, firstListener, useCapture);

  child.dispatchEvent(new goog.events.Event(eventType));

  assertEquals(1, secondListener.getCallCount());
}

function testAssertWhenUsedWithUninitializedCustomEventTarget() {
  var SubClass = function() { /* does not call superclass ctor */ };
  goog.inherits(SubClass, goog.events.EventTarget);

  var instance = new SubClass();

  var e;
  e = assertThrows(function() {
    goog.events.listen(instance, 'test1', function() {});
  });
  assertTrue(e instanceof goog.asserts.AssertionError);
  e = assertThrows(function() {
    goog.events.dispatchEvent(instance, 'test1');
  });
  assertTrue(e instanceof goog.asserts.AssertionError);
  e = assertThrows(function() { instance.dispatchEvent('test1'); });
  assertTrue(e instanceof goog.asserts.AssertionError);
}

function testAssertWhenDispatchEventIsUsedWithNonCustomEventTarget() {
  var obj = {};
  e = assertThrows(function() { goog.events.dispatchEvent(obj, 'test1'); });
  assertTrue(e instanceof goog.asserts.AssertionError);
}


function testPropagationStoppedDuringCapture() {
  var captureHandler =
      goog.testing.recordFunction(function(e) { e.stopPropagation(); });
  var bubbleHandler = goog.testing.recordFunction();

  var body = document.body;
  var div = goog.dom.createElement(goog.dom.TagName.DIV);
  body.appendChild(div);
  try {
    goog.events.listen(body, 'click', captureHandler, true);
    goog.events.listen(div, 'click', bubbleHandler, false);
    goog.events.listen(body, 'click', bubbleHandler, false);

    dispatchClick(div);
    assertEquals(1, captureHandler.getCallCount());
    assertEquals(0, bubbleHandler.getCallCount());

    goog.events.unlisten(body, 'click', captureHandler, true);

    dispatchClick(div);
    assertEquals(2, bubbleHandler.getCallCount());
  } finally {
    goog.dom.removeNode(div);
    goog.events.removeAll(body);
    goog.events.removeAll(div);
  }
}

function testPropagationStoppedDuringBubble() {
  var captureHandler = goog.testing.recordFunction();
  var bubbleHandler1 =
      goog.testing.recordFunction(function(e) { e.stopPropagation(); });
  var bubbleHandler2 = goog.testing.recordFunction();

  var body = document.body;
  var div = goog.dom.createElement(goog.dom.TagName.DIV);
  body.appendChild(div);
  try {
    goog.events.listen(body, 'click', captureHandler, true);
    goog.events.listen(div, 'click', bubbleHandler1, false);
    goog.events.listen(body, 'click', bubbleHandler2, false);

    dispatchClick(div);
    assertEquals(1, captureHandler.getCallCount());
    assertEquals(1, bubbleHandler1.getCallCount());
    assertEquals(0, bubbleHandler2.getCallCount());
  } finally {
    goog.dom.removeNode(div);
    goog.events.removeAll(body);
    goog.events.removeAll(div);
  }
}

function testAddingCaptureListenerDuringBubbleShouldNotFireTheListener() {
  var body = document.body;
  var div = goog.dom.createElement(goog.dom.TagName.DIV);
  body.appendChild(div);

  var captureHandler1 = goog.testing.recordFunction();
  var captureHandler2 = goog.testing.recordFunction();
  var bubbleHandler = goog.testing.recordFunction(function(e) {
    goog.events.listen(body, 'click', captureHandler1, true);
    goog.events.listen(div, 'click', captureHandler2, true);
  });

  try {
    goog.events.listen(div, 'click', bubbleHandler, false);

    dispatchClick(div);

    // These verify that the capture handlers registered in the bubble
    // handler is not invoked in the same event propagation phase.
    assertEquals(0, captureHandler1.getCallCount());
    assertEquals(0, captureHandler2.getCallCount());
    assertEquals(1, bubbleHandler.getCallCount());
  } finally {
    goog.dom.removeNode(div);
    goog.events.removeAll(body);
    goog.events.removeAll(div);
  }
}

function testRemovingCaptureListenerDuringBubbleWouldNotFireListenerTwice() {
  var body = document.body;
  var div = goog.dom.createElement(goog.dom.TagName.DIV);
  body.appendChild(div);

  var captureHandler = goog.testing.recordFunction();
  var bubbleHandler1 = goog.testing.recordFunction(function(e) {
    goog.events.unlisten(body, 'click', captureHandler, true);
  });
  var bubbleHandler2 = goog.testing.recordFunction();

  try {
    goog.events.listen(body, 'click', captureHandler, true);
    goog.events.listen(div, 'click', bubbleHandler1, false);
    goog.events.listen(body, 'click', bubbleHandler2, false);

    dispatchClick(div);
    assertEquals(1, captureHandler.getCallCount());

    // Verify that neither of these handlers are called more than once.
    assertEquals(1, bubbleHandler1.getCallCount());
    assertEquals(1, bubbleHandler2.getCallCount());
  } finally {
    goog.dom.removeNode(div);
    goog.events.removeAll(body);
    goog.events.removeAll(div);
  }
}

function testCaptureSimulationModeOffAndFail() {
  goog.events.CAPTURE_SIMULATION_MODE =
      goog.events.CaptureSimulationMode.OFF_AND_FAIL;
  var captureHandler = goog.testing.recordFunction();

  if (!goog.events.BrowserFeature.HAS_W3C_EVENT_SUPPORT) {
    var err = assertThrows(function() {
      goog.events.listen(document.body, 'click', captureHandler, true);
    });
    assertTrue(err instanceof goog.asserts.AssertionError);

    // Sanity tests.
    dispatchClick(document.body);
    assertEquals(0, captureHandler.getCallCount());
  } else {
    goog.events.listen(document.body, 'click', captureHandler, true);
    dispatchClick(document.body);
    assertEquals(1, captureHandler.getCallCount());
  }
}

function testCaptureSimulationModeOffAndSilent() {
  goog.events.CAPTURE_SIMULATION_MODE =
      goog.events.CaptureSimulationMode.OFF_AND_SILENT;
  var captureHandler = goog.testing.recordFunction();

  goog.events.listen(document.body, 'click', captureHandler, true);
  if (!goog.events.BrowserFeature.HAS_W3C_EVENT_SUPPORT) {
    dispatchClick(document.body);
    assertEquals(0, captureHandler.getCallCount());
  } else {
    dispatchClick(document.body);
    assertEquals(1, captureHandler.getCallCount());
  }
}