// Copyright 2010 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.ui.HoverCardTest');
goog.setTestOnly('goog.ui.HoverCardTest');

goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.math.Coordinate');
goog.require('goog.style');
goog.require('goog.testing.MockClock');
goog.require('goog.testing.events');
goog.require('goog.testing.events.Event');
goog.require('goog.testing.jsunit');
goog.require('goog.ui.HoverCard');

var timer = new goog.testing.MockClock();
var card;

// Variables for mocks
var triggeredElement;
var cancelledElement;
var showDelay;
var shownCard;
var hideDelay;

// spans
var john;
var jane;
var james;
var bill;
var child;

// Inactive
var elsewhere;
var offAnchor;

function setUpPage() {
  john = goog.dom.getElement('john');
  jane = goog.dom.getElement('jane');
  james = goog.dom.getElement('james');
  bill = goog.dom.getElement('bill');
  child = goog.dom.getElement('child');
}

function setUp() {
  timer.install();
  triggeredElement = null;
  cancelledElement = null;
  showDelay = null;
  shownCard = null;
  hideDelay = null;
  elsewhere = goog.dom.getElement('notpopup');
  offAnchor = new goog.math.Coordinate(1, 1);
}

function initCard(opt_isAnchor, opt_checkChildren, opt_maxSearchSteps) {
  var isAnchor = opt_isAnchor || {SPAN: 'email'};
  card = new goog.ui.HoverCard(isAnchor, opt_checkChildren);
  card.setText('Test hovercard');

  if (opt_maxSearchSteps != null) {
    card.setMaxSearchSteps(opt_maxSearchSteps);
  }

  goog.events.listen(card, goog.ui.HoverCard.EventType.TRIGGER, onTrigger);
  goog.events.listen(card, goog.ui.HoverCard.EventType.CANCEL_TRIGGER,
                     onCancel);
  goog.events.listen(card, goog.ui.HoverCard.EventType.BEFORE_SHOW,
                     onBeforeShow);

  // This gets around the problem where AdvancedToolTip thinks it's
  // receiving a ghost event because cursor position hasn't moved off of
  // (0, 0).
  card.cursorPosition = new goog.math.Coordinate(1, 1);
}

// Event handlers
function onTrigger(event) {
  triggeredElement = event.anchor;
  if (showDelay) {
    card.setShowDelayMs(showDelay);
  }
  return true;
}

function onCancel(event) {
  cancelledElement = event.anchor;
}

function onBeforeShow() {
  shownCard = card.getAnchorElement();
  if (hideDelay) {
    card.setHideDelayMs(hideDelay);
  }
  return true;
}

function tearDown() {
  card.dispose();
  timer.uninstall();
}


/**
 * Verify that hovercard displays and goes away under normal circumstances.
 */
function testTrigger() {
  initCard();

  // Mouse over correct element fires trigger
  showDelay = 500;
  goog.testing.events.fireMouseOverEvent(john, elsewhere);
  assertEquals('Hovercard should have triggered', john,
               triggeredElement);

  // Show card after delay
  timer.tick(showDelay - 1);
  assertNull('Card should not have shown', shownCard);
  assertFalse(card.isVisible());
  hideDelay = 5000;
  timer.tick(1);
  assertEquals('Card should have shown', john, shownCard);
  assertTrue(card.isVisible());

  // Mouse out leads to hide delay
  goog.testing.events.fireMouseOutEvent(john, elsewhere);
  goog.testing.events.fireMouseMoveEvent(document, offAnchor);
  timer.tick(hideDelay - 1);
  assertTrue('Card should still be visible', card.isVisible());
  timer.tick(10);
  assertFalse('Card should be hidden', card.isVisible());
}


/**
 * Verify that CANCEL_TRIGGER event occurs when mouse goes out of
 * triggering element before hovercard is shown.
 */
function testOnCancel() {
  initCard();

  showDelay = 500;
  goog.testing.events.fireMouseOverEvent(john, elsewhere);
  timer.tick(showDelay - 1);
  goog.testing.events.fireMouseOutEvent(john, elsewhere);
  goog.testing.events.fireMouseMoveEvent(document, offAnchor);
  timer.tick(10);
  assertFalse('Card should be hidden', card.isVisible());
  assertEquals('Should have cancelled trigger', john, cancelledElement);
}


/**
 * Verify that mousing over non-triggering elements don't interfere.
 */
function testMouseOverNonTrigger() {
  initCard();

  // Mouse over correct element fires trigger
  showDelay = 500;
  goog.testing.events.fireMouseOverEvent(john, elsewhere);
  timer.tick(showDelay);

  // Mouse over and out other element does nothing
  triggeredElement = null;
  goog.testing.events.fireMouseOverEvent(jane, elsewhere);
  timer.tick(showDelay + 1);
  assertNull(triggeredElement);
}


/**
 * Verify that a mouse over event with no target will not break
 * hover card.
 */
function testMouseOverNoTarget() {
  initCard();
  card.handleTriggerMouseOver_(new goog.testing.events.Event());
}


/**
 * Verify that mousing over a second trigger before the first one shows
 * will correctly cancel the first and show the second.
 */
function testMultipleTriggers() {
  initCard();

  // Test second trigger when first one still pending
  showDelay = 500;
  hideDelay = 1000;
  goog.testing.events.fireMouseOverEvent(john, elsewhere);
  timer.tick(250);
  goog.testing.events.fireMouseOutEvent(john, james);
  goog.testing.events.fireMouseOverEvent(james, john);
  // First trigger should cancel because it isn't showing yet
  assertEquals('Should cancel first trigger', john, cancelledElement);
  timer.tick(300);
  assertFalse(card.isVisible());
  timer.tick(250);
  assertEquals('Should show second card', james, shownCard);
  assertTrue(card.isVisible());

  goog.testing.events.fireMouseOutEvent(james, john);
  goog.testing.events.fireMouseOverEvent(john, james);
  assertEquals('Should still show second card', james,
               card.getAnchorElement());
  assertTrue(card.isVisible());

  shownCard = null;
  timer.tick(501);
  assertEquals('Should show first card again', john, shownCard);
  assertTrue(card.isVisible());

  // Test that cancelling while another is showing gives correct cancel
  // information
  cancelledElement = null;
  goog.testing.events.fireMouseOutEvent(john, james);
  goog.testing.events.fireMouseOverEvent(james, john);
  goog.testing.events.fireMouseOutEvent(james, elsewhere);
  assertEquals('Should cancel second card', james, cancelledElement);
}


/**
 * Verify manual triggering.
 */
function testManualTrigger() {
  initCard();

  // Doesn't normally trigger for div tag
  showDelay = 500;
  goog.testing.events.fireMouseOverEvent(bill, elsewhere);
  timer.tick(showDelay);
  assertFalse(card.isVisible());

  // Manually trigger element
  card.triggerForElement(bill);
  hideDelay = 600;
  timer.tick(showDelay);
  assertTrue(card.isVisible());
  goog.testing.events.fireMouseOutEvent(bill, elsewhere);
  goog.testing.events.fireMouseMoveEvent(document, offAnchor);
  timer.tick(hideDelay);
  assertFalse(card.isVisible());
}


/**
 * Verify creating with isAnchor function.
 */
function testIsAnchor() {
  // Initialize card so only bill triggers it.
  initCard(function(element) {
    return element == bill;
  });

  showDelay = 500;
  goog.testing.events.fireMouseOverEvent(bill, elsewhere);
  timer.tick(showDelay);
  assertTrue('Should trigger card', card.isVisible());

  hideDelay = 300;
  goog.testing.events.fireMouseOutEvent(bill, elsewhere);
  goog.testing.events.fireMouseMoveEvent(document, offAnchor);
  timer.tick(hideDelay);
  assertFalse(card.isVisible());

  goog.testing.events.fireMouseOverEvent(john, elsewhere);
  timer.tick(showDelay);
  assertFalse('Should not trigger card', card.isVisible());
}


/**
 * Verify mouse over child of anchor triggers hovercard.
 */
function testAnchorWithChildren() {
  initCard();

  showDelay = 500;
  goog.testing.events.fireMouseOverEvent(james, elsewhere);
  timer.tick(250);

  // Moving from an anchor to a child of that anchor shouldn't cancel
  // or retrigger.
  var childBounds = goog.style.getBounds(child);
  var inChild = new goog.math.Coordinate(childBounds.left + 1,
                                         childBounds.top + 1);
  goog.testing.events.fireMouseOutEvent(james, child);
  goog.testing.events.fireMouseMoveEvent(child, inChild);
  assertNull("Shouldn't cancel trigger", cancelledElement);
  triggeredElement = null;
  goog.testing.events.fireMouseOverEvent(child, james);
  assertNull("Shouldn't retrigger card", triggeredElement);
  timer.tick(250);
  assertTrue('Card should show with original delay', card.isVisible());

  hideDelay = 300;
  goog.testing.events.fireMouseOutEvent(child, elsewhere);
  goog.testing.events.fireMouseMoveEvent(child, offAnchor);
  timer.tick(hideDelay);
  assertFalse(card.isVisible());

  goog.testing.events.fireMouseOverEvent(child, elsewhere);
  timer.tick(showDelay);
  assertTrue('Mouse over child should trigger card', card.isVisible());
}

function testNoTriggerWithMaxSearchSteps() {
  initCard(undefined, true, 0);

  showDelay = 500;
  goog.testing.events.fireMouseOverEvent(child, elsewhere);
  timer.tick(showDelay);
  assertFalse('Should not trigger card', card.isVisible());
}

function testTriggerWithMaxSearchSteps() {
  initCard(undefined, true, 2);

  showDelay = 500;
  goog.testing.events.fireMouseOverEvent(child, elsewhere);
  timer.tick(showDelay);
  assertTrue('Should trigger card', card.isVisible());
}

function testPositionAfterSecondTriggerWithMaxSearchSteps() {
  initCard(undefined, true, 2);

  showDelay = 500;
  goog.testing.events.fireMouseOverEvent(john, elsewhere);
  timer.tick(showDelay);
  assertTrue('Should trigger card', card.isVisible());
  assertEquals('Card cursor x coordinate should be 1',
      card.position_.coordinate.x, 1);
  card.cursorPosition = new goog.math.Coordinate(2, 2);
  goog.testing.events.fireMouseOverEvent(child, elsewhere);
  timer.tick(showDelay);
  assertTrue('Should trigger card', card.isVisible());
  assertEquals('Card cursor x coordinate should be 2',
      card.position_.coordinate.x, 2);
}
