/*
 * Copyright 2007 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.user.client.DOM;

import junit.framework.Assert;

import java.util.Iterator;
import java.util.Set;

/**
 * All test cases for widgets that implement HasWidgets should derive from this
 * test case, and make sure to run all of its test templates.
 */
public abstract class HasWidgetsTester {

  /**
   * Used in test templates to allow the child class to specify how a widget
   * will be added to its container. This is necessary because
   * {@link HasWidgets#add(Widget)} is allowed to throw
   * {@link UnsupportedOperationException}.
   */
  interface WidgetAdder {

    /**
     * Adds the specified child to a container.
     */
    void addChild(HasWidgets container, Widget child);
  }

  /**
   * Default implementation used by containers for which
   * {@link HasWidgets#add(Widget)} will not throw an exception.
   */
  static class DefaultWidgetAdder implements WidgetAdder {
    @Override
    public void addChild(HasWidgets container, Widget child) {
      container.add(child);
    }
  }

  private static class TestWidget extends Widget {
    TestWidget() {
      setElement(DOM.createDiv());
    }

    @Override
    protected void onLoad() {
      // During onLoad, isAttached must be true, and the element be a descendant
      // of the body element.
      Assert.assertTrue(isAttached());
      Assert.assertTrue(RootPanel.getBodyElement().isOrHasChild(getElement()));
    }

    @Override
    protected void onUnload() {
      // During onUnload, everything must *still* be attached.
      Assert.assertTrue(isAttached());
      Assert.assertTrue(RootPanel.getBodyElement().isOrHasChild(getElement()));
    }
  }

  /**
   * Runs all tests for {@link HasWidgets}. It is recommended that tests call
   * this method or {@link #testAll(HasWidgets, WidgetAdder} so that future
   * tests are automatically included.
   * 
   * @param container
   */
  static void testAll(HasWidgets container) {
    testAll(container, new DefaultWidgetAdder());
  }

  /**
   * Runs all tests for {@link HasWidgets}. It is recommended that tests call
   * this method or {@link #testAll(HasWidgets, WidgetAdder)} so that future
   * tests are automatically included.
   * 
   * @param container
   * @param adder
   */
  static void testAll(HasWidgets container, WidgetAdder adder) {
    testAll(container, adder, false);
  }

  /**
   * Runs all tests for {@link HasWidgets}. It is recommended that tests call
   * this method or {@link #testAll(HasWidgets, WidgetAdder)} so that future
   * tests are automatically included.
   * 
   * @param container the container widget to test
   * @param adder the method of adding children
   * @param supportsMultipleWidgets true if container supports multiple children
   */
  static void testAll(HasWidgets container, WidgetAdder adder,
      boolean supportsMultipleWidgets) {
    testAttachDetachOrder(container, adder);
    testRemovalOfNonExistantChild(container);
    testDoAttachChildrenWithError(container, adder, supportsMultipleWidgets);
    testDoDetachChildrenWithError(container, adder, supportsMultipleWidgets);
  }

  /**
   * Tests attach and detach order, assuming that the container's
   * {@link HasWidgets#add(Widget)} method does not throw
   * {@link UnsupportedOperationException}.
   * 
   * @param test
   * @param container
   * @see #testAttachDetachOrder(TestCase, HasWidgets,
   *      com.google.gwt.user.client.ui.HasWidgetsTester.WidgetAdder)
   */
  static void testAttachDetachOrder(HasWidgets container) {
    testAttachDetachOrder(container, new DefaultWidgetAdder());
  }

  /**
   * Ensures that children are attached and detached in the proper order. This
   * must result in the child's onLoad() method being called just *after* its
   * element is attached to the DOM, and its onUnload method being called just
   * *before* its element is detached from the DOM.
   */
  static void testAttachDetachOrder(HasWidgets container, WidgetAdder adder) {
    resetContainer(container);

    // Make sure the container's attached.
    Assert.assertTrue(container instanceof Widget);
    RootPanel.get().add((Widget) container);

    // Adding and removing the test widget will cause it to test onLoad and
    // onUnload order.
    TestWidget widget = new TestWidget();
    adder.addChild(container, widget);
    Assert.assertTrue(widget.isAttached());
    Assert.assertTrue(RootPanel.getBodyElement().isOrHasChild(widget.getElement()));
    container.remove(widget);

    // After removal, the widget should be detached.
    Assert.assertFalse(widget.isAttached());
    Assert.assertFalse(RootPanel.getBodyElement().isOrHasChild(widget.getElement()));
  }

  /**
   * Ensures that the physical and logical state of children are consistent even
   * if one of the children throws an error in onLoad.
   * 
   * @param container the container
   * @param adder the method of adding children
   * @param supportMultipleWidgets true if container supports multiple widgets
   */
  static void testDoAttachChildrenWithError(HasWidgets container,
      WidgetAdder adder, boolean supportMultipleWidgets) {
    resetContainer(container);

    // Create a widget that will throw an exception onLoad.
    BadWidget badWidget = new BadWidget();
    badWidget.setFailOnLoad(true);

    // Add some children to a panel.
    if (supportMultipleWidgets) {
      adder.addChild(container, new Label("test0"));
      adder.addChild(container, new Label("test1"));
      adder.addChild(container, badWidget);
      adder.addChild(container, new Label("test2"));
      adder.addChild(container, new Label("test3"));
    } else {
      adder.addChild(container, badWidget);
    }
    Assert.assertFalse(badWidget.isAttached());

    // Attach the widget.
    try {
      RootPanel.get().add((Widget) container);
    } catch (AttachDetachException e) {
      // Expected.
      Set<Throwable> causes = e.getCauses();
      Assert.assertEquals(1, causes.size());
      Throwable[] throwables = causes.toArray(new Throwable[1]);
      // Composites that use internal panels for layout (eg. TabPanel) will
      // throws the AttachDetachException from the inner panel instead of an
      // IllegalArgumentException from the bad widget
      Assert.assertTrue(throwables[0] instanceof IllegalArgumentException
          || throwables[0] instanceof AttachDetachException);
    }
    Iterator<Widget> children = container.iterator();
    while (children.hasNext()) {
      Widget w = children.next();
      Assert.assertTrue(w.isAttached());
      assertContainerIsOrHasChild(container, w);
    }
    Assert.assertEquals(RootPanel.get(), ((Widget) container).getParent());

    // Detach the panel.
    RootPanel.get().remove((Widget) container);
    Assert.assertFalse(badWidget.isAttached());
  }

  /**
   * Ensures that the physical and logical state of children are consistent even
   * if one of the children throws an error in onUnload.
   * 
   * @param container the container
   * @param adder the method of adding children
   * @param supportMultipleWidgets true if container supports multiple widgets
   */
  static void testDoDetachChildrenWithError(HasWidgets container,
      WidgetAdder adder, boolean supportMultipleWidgets) {
    resetContainer(container);

    // Create a widget that will throw an exception onUnload.
    BadWidget badWidget = new BadWidget();
    badWidget.setFailOnUnload(true);

    // Add some children to a panel.
    if (supportMultipleWidgets) {
      adder.addChild(container, new Label("test0"));
      adder.addChild(container, new Label("test1"));
      adder.addChild(container, badWidget);
      adder.addChild(container, new Label("test2"));
      adder.addChild(container, new Label("test3"));
    } else {
      adder.addChild(container, badWidget);
    }
    Assert.assertFalse(badWidget.isAttached());

    // Attach the widget.
    RootPanel.get().add((Widget) container);
    Assert.assertTrue(badWidget.isAttached());

    try {
      RootPanel.get().remove((Widget) container);
    } catch (AttachDetachException e) {
      // Expected.
      Set<Throwable> causes = e.getCauses();
      Assert.assertEquals(1, causes.size());
      Throwable[] throwables = causes.toArray(new Throwable[1]);
      // Composites that use internal panels for layout (eg. TabPanel) will
      // throws the AttachDetachException from the inner panel instead of an
      // IllegalArgumentException from the bad widget
      Assert.assertTrue(throwables[0] instanceof IllegalArgumentException
          || throwables[0] instanceof AttachDetachException);
    }
    Iterator<Widget> children = container.iterator();
    while (children.hasNext()) {
      Widget w = children.next();
      Assert.assertFalse(w.isAttached());
      assertContainerIsOrHasChild(container, w);
    }
    Assert.assertNull(((Widget) container).getParent());
  }

  /**
   * Tests to ensure that {@link HasWidgets#remove(Widget)} is resilient to
   * being called with a widget that is not present as a child in the container.
   * 
   * @param container
   */
  static void testRemovalOfNonExistantChild(HasWidgets container) {
    resetContainer(container);
    TestWidget widget = new TestWidget();
    container.remove(widget);
  }

  /**
   * Assert that the container is a parent of the child. Some Panels are not the
   * direct parent of their children, so we walk up the chain looking for a
   * parent.
   * 
   * @param container
   * @param child
   */
  private static void assertContainerIsOrHasChild(HasWidgets container,
      Widget child) {
    boolean containerIsOrHasChild = false;
    Widget parent = child.getParent();
    while (parent != null && !containerIsOrHasChild) {
      if (parent == container) {
        containerIsOrHasChild = true;
      }
      parent = parent.getParent();
    }
    Assert.assertTrue(containerIsOrHasChild);
  }

  /**
   * Reset the container between tests.
   * 
   * @param container the container
   */
  private static void resetContainer(HasWidgets container) {
    container.clear();
    if (((Widget) container).isAttached()) {
      RootPanel.get().remove((Widget) container);
    }
  }
}
