// Copyright 2012 Google Inc. 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.

package com.google.collide.client.util;

import collide.client.util.Elements;

import com.google.collide.client.util.HoverController.HoverListener;
import com.google.collide.client.util.HoverController.UnhoverListener;
import com.google.gwt.junit.client.GWTTestCase;
import com.google.gwt.user.client.Timer;

import elemental.dom.Document;
import elemental.dom.Element;
import elemental.events.MouseEvent;

/**
 * Tests for {@link HoverController}.
 *
 */
public class HoverControllerTest extends GWTTestCase {

  private static class MockHoverListener implements HoverListener, UnhoverListener {

    private int hoverCalled;
    private int unhoverCalled;

    public void assertHoverCount(int expected) {
      assertEquals(expected, hoverCalled);
    }

    public void assertUnhoverCount(int expected) {
      assertEquals(expected, unhoverCalled);
    }

    @Override
    public void onHover() {
      hoverCalled++;
    }

    @Override
    public void onUnhover() {
      unhoverCalled++;
    }
  }

  @Override
  public String getModuleName() {
    return "com.google.collide.client.util.UtilTestModule";
  }

  /**
   * Tests a basic hover/unhover sequence.
   */
  public void testBasicHoverSequence() {
    final MockHoverListener listener = new MockHoverListener();
    final Element[] elems = createAndAttachElements(3);
    HoverController controller = new HoverController();
    controller.setHoverListener(listener);
    controller.setUnhoverListener(listener);
    controller.setUnhoverDelay(25);
    controller.addPartner(elems[0]);
    controller.addPartner(elems[1]);
    controller.addPartner(elems[2]);
    listener.assertHoverCount(0);
    listener.assertUnhoverCount(0);

    // Mouseover an element. onHover() called synchronously.
    mouseover(elems[1]);
    listener.assertHoverCount(1);
    listener.assertUnhoverCount(0);

    // Mouseout an element. onUnhover() is not called because we are still in
    // the unhover delay.
    mouseout(elems[2]);
    listener.assertHoverCount(1);
    listener.assertUnhoverCount(0);

    // Wait for the unhover delay.
    delayTestFinish(1000);
    new Timer() {
      @Override
      public void run() {
        listener.assertHoverCount(1);
        listener.assertUnhoverCount(1);

        // Cleanup.
        detachElements(elems);
        finishTest();
      }
    }.schedule(100);
  }

  /**
   * Tests that once we start hovering, subsequent mouseover events do not call
   * {@link MockHoverListener#onHover()}.
   */
  public void testMouseoverWhileHovering() {
    final MockHoverListener listener = new MockHoverListener();
    final Element[] elems = createAndAttachElements(3);
    HoverController controller = new HoverController();
    controller.setHoverListener(listener);
    controller.setUnhoverListener(listener);
    controller.setUnhoverDelay(25);
    controller.addPartner(elems[0]);
    controller.addPartner(elems[1]);
    controller.addPartner(elems[2]);
    listener.assertHoverCount(0);
    listener.assertUnhoverCount(0);

    // Mouseover an element. onHover() is called synchronously.
    mouseover(elems[1]);
    listener.assertHoverCount(1);
    listener.assertUnhoverCount(0);

    // Mouseover another element.
    mouseover(elems[2]);
    listener.assertHoverCount(1);
    listener.assertUnhoverCount(0);

    // Mouseover the same element.
    mouseover(elems[2]);
    listener.assertHoverCount(1);
    listener.assertUnhoverCount(0);

    // Cleanup.
    detachElements(elems);
  }

  /**
   * Tests that if the user mouses over an element after mousing out of an
   * element, but before the unhover delay fires, then
   * {@link UnhoverListener#onUnhover()} is not called.
   */
  public void testMouseoverWithinUnhoverDelay() {
    final MockHoverListener listener = new MockHoverListener();
    final Element[] elems = createAndAttachElements(3);
    HoverController controller = new HoverController();
    controller.setHoverListener(listener);
    controller.setUnhoverListener(listener);
    controller.setUnhoverDelay(25);
    controller.addPartner(elems[0]);
    controller.addPartner(elems[1]);
    controller.addPartner(elems[2]);
    listener.assertHoverCount(0);
    listener.assertUnhoverCount(0);

    // mouseover an element. onHover() called synchronously.
    mouseover(elems[1]);
    listener.assertHoverCount(1);
    listener.assertUnhoverCount(0);

    // mouseout an element. onUnhover() is not called because we are still in
    // the unhover delay.
    mouseout(elems[2]);
    listener.assertHoverCount(1);
    listener.assertUnhoverCount(0);

    // mouseover within the delay. onUnhover() is cancelled.
    mouseover(elems[2]);
    listener.assertHoverCount(1);
    listener.assertUnhoverCount(0);

    // Verify unhover is never called.
    delayTestFinish(1000);
    new Timer() {
      @Override
      public void run() {
        listener.assertHoverCount(1);
        listener.assertUnhoverCount(0);

        // Cleanup.
        detachElements(elems);
        finishTest();
      }
    }.schedule(100);
  }

  /**
   * Tests that nothing breaks if no listeners are set.
   */
  public void testNoListeners() {
    final Element[] elems = createAndAttachElements(3);
    HoverController controller = new HoverController();
    controller.setUnhoverDelay(0);
    controller.addPartner(elems[0]);
    controller.addPartner(elems[1]);
    controller.addPartner(elems[2]);

    // mouseover an element.
    mouseover(elems[1]);

    // mouseout an element.
    mouseout(elems[2]);

    // Cleanup.
    detachElements(elems);
  }

  /**
   * Tests that setting the unhover delay to a negative value prevents
   * {@link UnhoverListener#onUnhover()} from being called.
   */
  public void testSetUnhoverDelayNevagtive() {
    final MockHoverListener listener = new MockHoverListener();
    final Element[] elems = createAndAttachElements(3);
    HoverController controller = new HoverController();
    controller.setHoverListener(listener);
    controller.setUnhoverListener(listener);
    controller.setUnhoverDelay(-1);
    controller.addPartner(elems[0]);
    controller.addPartner(elems[1]);
    controller.addPartner(elems[2]);
    listener.assertHoverCount(0);
    listener.assertUnhoverCount(0);

    // mouseover an element. onHover() called synchronously.
    mouseover(elems[1]);
    listener.assertHoverCount(1);
    listener.assertUnhoverCount(0);

    // mouseout an element. onUnhover() not called.
    mouseout(elems[2]);
    listener.assertHoverCount(1);
    listener.assertUnhoverCount(0);

    // Verify unhover is never called.
    delayTestFinish(1000);
    new Timer() {
      @Override
      public void run() {
        listener.assertHoverCount(1);
        listener.assertUnhoverCount(0);

        // Cleanup.
        detachElements(elems);
        finishTest();
      }
    }.schedule(100);
  }

  /**
   * Tests that setting the unhover delay to zero forces
   * {@link UnhoverListener#onUnhover()} to be called synchronously.
   */
  public void testSetUnhoverDelayZero() {
    final MockHoverListener listener = new MockHoverListener();
    final Element[] elems = createAndAttachElements(3);
    HoverController controller = new HoverController();
    controller.setHoverListener(listener);
    controller.setUnhoverListener(listener);
    controller.setUnhoverDelay(0);
    controller.addPartner(elems[0]);
    controller.addPartner(elems[1]);
    controller.addPartner(elems[2]);
    listener.assertHoverCount(0);
    listener.assertUnhoverCount(0);

    // mouseover an element. onHover() called synchronously.
    mouseover(elems[1]);
    listener.assertHoverCount(1);
    listener.assertUnhoverCount(0);

    // mouseout an element. onUnhover() called synchronously.
    mouseout(elems[2]);
    listener.assertHoverCount(1);
    listener.assertUnhoverCount(1);

    // Cleanup.
    detachElements(elems);
  }

  private Element[] createAndAttachElements(int count) {
    Document doc = Elements.getDocument();
    Element[] elems = new Element[count];
    for (int i = 0; i < count; i++) {
      Element elem = doc.createDivElement();
      doc.getBody().appendChild(elem);
      elems[i] = elem;
    }
    return elems;
  }

  private void mouseevent(Element target, String type) {
    MouseEvent evt = (MouseEvent) Elements.getDocument().createEvent(Document.Events.MOUSE);
    evt.initMouseEvent(type, true, true, null, 0, 0, 0, 0, 0, false, false, false, false,
        MouseEvent.Button.PRIMARY, null);
    target.dispatchEvent(evt);
  }


  private void mouseout(Element target) {
    mouseevent(target, "mouseout");
  }

  private void mouseover(Element target) {
    mouseevent(target, "mouseover");
  }


  private void detachElements(Element[] elems) {
    for (Element elem : elems) {
      elem.removeFromParent();
    }
  }
}
