/*
 * Copyright 2009 Google Inc.
 * 
 * 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.gwt.user.client.ui;

import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.junit.DoNotRunWith;
import com.google.gwt.junit.Platform;
import com.google.gwt.junit.client.GWTTestCase;
import com.google.gwt.user.client.History;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Window;

/**
 * Tests for {@link PopupPanel}.
 */
public class PopupTest extends GWTTestCase {

  /**
   * The Widget adder used to set the widget in a {@link PopupPanel}.
   */
  private static class Adder implements HasWidgetsTester.WidgetAdder {
    @Override
    public void addChild(HasWidgets container, Widget child) {
      ((PopupPanel) container).setWidget(child);
    }
  }

  /**
   * Expose otherwise private or protected methods.
   */
  private static class TestablePopupPanel extends PopupPanel {
    private int onLoadCount;

    public void assertOnLoadCount(int expected) {
      assertEquals(expected, onLoadCount);
    }

    @Override
    public com.google.gwt.user.client.Element getContainerElement() {
      return super.getContainerElement();
    }

    @Override
    public void onLoad() {
      super.onLoad();
      onLoadCount++;
    }
  }

  @Override
  public String getModuleName() {
    return "com.google.gwt.user.User";
  }

  /**
   * Test the basic accessors.
   */
  public void testAccessors() {
    PopupPanel popup = createPopupPanel();

    // Animation enabled
    assertFalse(popup.isAnimationEnabled());
    popup.setAnimationEnabled(true);
    assertTrue(popup.isAnimationEnabled());

    // Modal
    popup.setModal(true);
    assertTrue(popup.isModal());
    popup.setModal(false);
    assertFalse(popup.isModal());

    // AutoHide enabled
    popup.setAutoHideEnabled(true);
    assertTrue(popup.isAutoHideEnabled());
    popup.setAutoHideEnabled(false);
    assertFalse(popup.isAutoHideEnabled());

    // PreviewAllNativeEvents enabled
    popup.setPreviewingAllNativeEvents(true);
    assertTrue(popup.isPreviewingAllNativeEvents());
    popup.setPreviewingAllNativeEvents(false);
    assertFalse(popup.isPreviewingAllNativeEvents());

    // setVisible
    assertTrue(popup.isVisible());
    popup.setVisible(false);
    assertFalse(popup.isVisible());
    popup.setVisible(true);
    assertTrue(popup.isVisible());

    // isShowing
    assertFalse(popup.isShowing());
    popup.show();
    assertTrue(popup.isShowing());
    popup.hide();
    assertFalse(popup.isShowing());
  }

  public void testAttachDetachOrder() {
    HasWidgetsTester.testAll(createPopupPanel(), new Adder(), false);
  }

  public void testAutoHidePartner() {
    final PopupPanel popup = new PopupPanel();

    // Add a partner
    DivElement partner0 = Document.get().createDivElement();
    popup.addAutoHidePartner(partner0);
    popup.addAutoHidePartner(Document.get().createDivElement());

    // Remove a partner
    popup.removeAutoHidePartner(partner0);
  }

  public void testAutoHideOnHistoryEvent() {
    PopupPanel popup = createPopupPanel();
    popup.show();
    assertTrue(popup.isShowing());

    // When autoHideOnHistoryEvent is disabled, the popup remains visible.
    History.newItem("popupToken0");
    assertTrue(popup.isShowing());

    // When autoHideOnHistoryEvent is enabled, the popup is hidden.
    popup.setAutoHideOnHistoryEventsEnabled(true);
    History.newItem("popupToken1");
    assertFalse(popup.isShowing());
  }

  /**
   * Tests that a large PopupPanel is not positioned off the top or left edges
   * of the browser window, making part of the panel unreachable.
   *
   * <p>This test is broken in HtmlUnit 2.55.0, see
   * https://github.com/HtmlUnit/htmlunit/issues/382.</p>
   */
  @DoNotRunWith(Platform.HtmlUnitLayout)
  public void testCenterLargePopup() {
    PopupPanel popup = new PopupPanel();
    popup.setHeight("4096px");
    popup.setWidth("4096px");
    popup.setWidget(new Label("foo"));
    popup.center();
    assertEquals(0, popup.getAbsoluteTop());
    assertEquals(0, popup.getAbsoluteLeft());
  }

  /**
   * Issue 2463: If a {@link PopupPanel} contains a dependent {@link PopupPanel}
   * that is hidden or shown in the onDetach or onAttach method, we could run
   * into conflicts with the animations. The {@link MenuBar} exhibits this
   * behavior because, when we detach a {@link MenuBar} from the page, it closes
   * all of its sub menus, each located in a different {@link PopupPanel}.
   */
  public void testDependantPopupPanel() {
    // Create the dependent popup
    final PopupPanel dependantPopup = createPopupPanel();
    dependantPopup.setAnimationEnabled(true);

    // Create the primary popup
    final PopupPanel primaryPopup = new PopupPanel(false, false) {
      @Override
      protected void onAttach() {
        dependantPopup.show();
        super.onAttach();
      }

      @Override
      protected void onDetach() {
        dependantPopup.hide();
        super.onDetach();
      }
    };
    primaryPopup.setAnimationEnabled(true);

    testDependantPopupPanel(primaryPopup);
  }

  public void testGlassPanelDisabled() {
    // Verify that the glass is disabled by default
    PopupPanel popup = createPopupPanel();
    assertFalse(popup.isGlassEnabled());
    assertNull(popup.getGlassElement());

    // Verify the glass panel is never created
    popup.show();
    assertNull(popup.getGlassElement());
    popup.hide();
  }

  public void testGlassDisabledWhileShowing() {
    // Show the popup and glass panel
    PopupPanel popup = createPopupPanel();
    popup.setGlassEnabled(true);
    Element glass = popup.getGlassElement();
    popup.show();

    // Disable the glass panel and hide the popup
    popup.setGlassEnabled(false);
    assertTrue(isAttached(glass));
    popup.hide();
    assertFalse(isAttached(glass));

    // Show the popup and verify that glass is no longer used
    popup.show();
    assertFalse(isAttached(glass));
    popup.hide();
  }

  public void testGlassEnabled() {
    // Verify that the glass is disabled by default
    PopupPanel popup = createPopupPanel();
    assertFalse(popup.isGlassEnabled());
    assertNull(popup.getGlassElement());

    // Enable the glass panel and verify it is created
    popup.setGlassEnabled(true);
    Element glass = popup.getGlassElement();
    assertNotNull(glass);
    assertFalse(isAttached(glass));

    // Show the popup and verify the glass panel is added
    popup.show();
    assertTrue(isAttached(glass));

    // Hide the popup and verify the glass panel is removed
    popup.hide();
    assertFalse(isAttached(glass));
  }

  public void testGlassEnabledWhileShowing() {
    // Verify that the glass is disabled by default
    PopupPanel popup = createPopupPanel();
    assertFalse(popup.isGlassEnabled());
    assertNull(popup.getGlassElement());

    // Show the popup and enable the glass panel
    popup.show();
    popup.setGlassEnabled(true);
    Element glass = popup.getGlassElement();
    assertNotNull(glass);
    assertFalse(isAttached(glass));

    // Hide the popup and verify the glass panel is removed
    popup.hide();
    assertFalse(isAttached(glass));

    // Show the popup and verify the glas is now used
    popup.show();
    assertTrue(isAttached(glass));
    popup.hide();
  }

  /**
   * Test the hiding a popup while it is showing will not result in an illegal
   * state.
   */
  public void testHideWhileShowing() {
    PopupPanel popup = createPopupPanel();

    // Start showing the popup.
    popup.setAnimationEnabled(true);
    popup.show();
    assertTrue(popup.isShowing());

    // Hide the popup while its showing.
    popup.hide();
    assertFalse(popup.isShowing());
  }

  /**
   * Test that the onLoad method is only called once when showing the popup.
   */
  public void testOnLoad() {
    TestablePopupPanel popup = new TestablePopupPanel();

    // show() without animation
    {
      popup.setAnimationEnabled(false);
      popup.show();
      popup.assertOnLoadCount(1);
      popup.hide();
    }

    // show() with animation
    {
      popup.setAnimationEnabled(true);
      popup.show();
      popup.assertOnLoadCount(2);
      popup.hide();
    }

    // center() without animation
    {
      popup.setAnimationEnabled(false);
      popup.center();
      popup.assertOnLoadCount(3);
      popup.hide();
    }

    // center() with animation
    {
      popup.setAnimationEnabled(true);
      popup.center();
      popup.assertOnLoadCount(4);
      popup.hide();
    }
  }

  /**
   * This test is broken in HtmlUnit 2.55.0, see
   * https://github.com/HtmlUnit/htmlunit/issues/382.
   */
  @DoNotRunWith(Platform.HtmlUnitLayout)
  public void testPopup() {
    // Get rid of window margins so we can test absolute position.
    Window.setMargin("0px");

    PopupPanel popup = createPopupPanel();
    popup.setAnimationEnabled(false);
    Label lbl = new Label("foo");

    // Make sure that setting the popup's size & position works _before_
    // setting its widget.
    popup.setSize("384px", "128px");
    popup.setPopupPosition(128, 64);
    popup.setWidget(lbl);
    popup.show();

    // DecoratorPanel adds width and height because it wraps the content in a
    // 3x3 table.
    assertTrue("Expected >= 384, got " + popup.getOffsetWidth(),
        popup.getOffsetWidth() >= 384);
    assertTrue("Expected >= 128, got " + popup.getOffsetHeight(),
        popup.getOffsetHeight() >= 128);
    assertEquals(128, popup.getPopupLeft());
    assertEquals(64, popup.getPopupTop());

    // Make sure that the popup returns to the correct position
    // after hiding and showing it.
    popup.hide();
    popup.show();
    assertEquals(128, popup.getPopupLeft());
    assertEquals(64, popup.getPopupTop());

    // Make sure that setting the popup's size & position works _after_
    // setting its widget (and that clearing its size properly resizes it to
    // its widget's size).
    popup.setSize("", "");
    popup.setPopupPosition(16, 16);

    // DecoratorPanel adds width and height because it wraps the content in a
    // 3x3 table.
    assertTrue(popup.getOffsetWidth() >= lbl.getOffsetWidth());
    assertTrue(popup.getOffsetWidth() >= lbl.getOffsetHeight());
    assertEquals(16, popup.getAbsoluteLeft());
    assertEquals(16, popup.getAbsoluteTop());

    // Ensure that hiding the popup fires the appropriate events.
    delayTestFinish(1000);
    popup.addCloseHandler(new CloseHandler<PopupPanel>() {
      @Override
      public void onClose(CloseEvent<PopupPanel> event) {
        finishTest();
      }
    });
    popup.hide();
  }

  /**
   * Issue 4720: Glass is not removed when removeFromParent is called.
   */
  public void testRemoveFromParent() {
    PopupPanel popup = createPopupPanel();
    popup.setGlassEnabled(true);
    assertNull(popup.getGlassElement().getParentElement());

    popup.show();
    assertNotNull(popup.getGlassElement().getParentElement());

    popup.removeFromParent();
    assertNull(popup.getGlassElement().getParentElement());
  }

  public void testSeparateContainers() {
    TestablePopupPanel p1 = new TestablePopupPanel();
    TestablePopupPanel p2 = new TestablePopupPanel();
    assertTrue(p1.getContainerElement() != null);
    assertTrue(p2.getContainerElement() != null);
    assertFalse(p1.getContainerElement() == p2.getContainerElement());
  }

  /**
   * Issue 2481: Try to set the contents of the popup while the popup is
   * attached. When we hide the popup, this should not leave the popup in an
   * invalid attach state.
   */
  public void testSetWidgetWhileAttached() {
    PopupPanel popup = createPopupPanel();
    popup.show();
    popup.setWidget(new Label("test"));
    popup.hide();
  }

  public void testSetVisibleWithGlass() {
    PopupPanel popup = createPopupPanel();
    popup.setGlassEnabled(true);
    popup.show();

    Element glass = popup.getGlassElement();
    assertTrue(popup.isVisible());
    assertFalse("hidden".equalsIgnoreCase(glass.getStyle().getVisibility()));

    popup.setVisible(false);
    assertFalse(popup.isVisible());
    assertTrue("hidden".equalsIgnoreCase(glass.getStyle().getVisibility()));

    popup.setVisible(true);
    assertTrue(popup.isVisible());
    assertFalse("hidden".equalsIgnoreCase(glass.getStyle().getVisibility()));

    popup.hide();
  }

  /**
   * Test that showing a popup while it is attached does not put it in an
   * invalid state.
   */
  public void testShowWhileAttached() {
    PopupPanel popup = createPopupPanel();
    RootPanel.get().add(popup);
    popup.show();
    assertTrue(popup.isAttached());
    assertTrue(popup.isShowing());

    popup.hide();
    assertFalse(popup.isAttached());
    assertFalse(popup.isShowing());
  }

  /**
   * Test that showing a popup while it is hiding will not result in an illegal
   * state.
   */
  public void testShowWhileHiding() {
    PopupPanel popup = createPopupPanel();

    // Show the popup
    popup.setAnimationEnabled(false);
    popup.show();
    assertTrue(popup.isShowing());

    // Start hiding the popup
    popup.setAnimationEnabled(true);
    popup.hide();
    assertFalse(popup.isShowing());

    // Show the popup while its hiding
    popup.show();
    assertTrue(popup.isShowing());
  }

  /**
   * Create a new PopupPanel.
   */
  protected PopupPanel createPopupPanel() {
    return new PopupPanel();
  }

  @Override
  protected void gwtTearDown() {
    RootPanel.get().clear();
  }

  /**
   * @see #testDependantPopupPanel()
   */
  protected void testDependantPopupPanel(final PopupPanel primaryPopup) {
    // Show the popup
    primaryPopup.show();

    // Hide the popup
    new Timer() {
      @Override
      public void run() {
        primaryPopup.hide();
      }
    }.schedule(1000);

    delayTestFinish(5000);
    // Give time for any errors to occur
    new Timer() {
      @Override
      public void run() {
        finishTest();
      }
    }.schedule(2000);
  }

  private boolean isAttached(Element elem) {
    return Document.get().getBody().isOrHasChild(elem);
  }
}
