/*
 * Copyright 2010 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.cell.client;

import com.google.gwt.cell.client.Cell.Context;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.junit.client.GWTTestCase;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.EventListener;

import java.util.Set;

/**
 * Base class for testing {@link Cell}.
 *
 * @param <T> the cell type
 */
public abstract class CellTestBase<T> extends GWTTestCase {

  /**
   * A mock cell used for testing.
   *
   * @param <T> the cell type
   */
  static class MockCell<T> extends AbstractCell<T> {

    private final boolean isSelectable;
    private Context lastContext;
    private T lastEventValue;
    private Element lastParentElement;
    private final T updateValue;

    public MockCell(boolean isSelectable, T updateValue,
        String... consumedEvents) {
      super(consumedEvents);
      this.isSelectable = isSelectable;
      this.updateValue = updateValue;
    }

    public void assertLastEventValue(T expected) {
      assertEquals(expected, lastEventValue);
    }

    public void assertLastParentElement(Element expected) {
      assertEquals(expected, lastParentElement);
    }
    
    @Override
    public boolean dependsOnSelection() {
      return isSelectable;
    }

    public Context getLastContext() {
      return lastContext;
    }

    @Override
    public boolean handlesSelection() {
      return isSelectable;
    }

    @Override
    public void onBrowserEvent(Context context, Element parent, T value,
        NativeEvent event, ValueUpdater<T> valueUpdater) {
      lastContext = context;
      lastParentElement = parent;
      lastEventValue = value;
      if (valueUpdater != null) {
        valueUpdater.update(updateValue);
      }
    }

    @Override
    public void render(Context context, T value, SafeHtmlBuilder sb) {
      lastContext = context;
      if (value != null) {
        sb.appendEscaped(String.valueOf(value));
      }
    }
  }

  /**
   * A mock {@link ValueUpdater} used for testing.
   */
  class MockValueUpdater implements ValueUpdater<T> {

    private T lastValue;

    public void assertLastValue(T expected) {
      assertEquals(expected, lastValue);
      lastValue = null;
    }

    @Override
    public void update(T value) {
      assertNull(lastValue);
      assertNotNull(value);
      this.lastValue = value;
    }
  }

  /**
   * The default row value key used for all tests.
   */
  protected static final Object DEFAULT_KEY = new Object();

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

  public void testDependsOnSelection() {
    assertEquals(dependsOnSelection(), createCell().dependsOnSelection());
  }

  public void testGetConsumedEvents() {
    Set<String> consumedEvents = createCell().getConsumedEvents();
    String[] expected = getConsumedEvents();
    if (consumedEvents == null && expected == null) {
      return;
    }
    assertEquals(expected.length, consumedEvents.size());
    for (String typeName : expected) {
      assertTrue(consumedEvents.contains(typeName));
    }
  }

  public void testHandlesSelection() {
    // None of the provided cells handle selection.
    assertFalse(createCell().handlesSelection());
  }

  /**
   * Test rendering the cell with a valid value and no view data.
   */
  public void testRender() {
    Cell<T> cell = createCell();
    T value = createCellValue();
    SafeHtmlBuilder sb = new SafeHtmlBuilder();
    Context context = new Context(0, 0, null);
    cell.render(context, value, sb);
    assertEquals(getExpectedInnerHtml(), sb.toSafeHtml().asString());
  }

  /**
   * Test rendering the cell with a negative index is handled.
   */
  public void testRenderNegativeIndex() {
    Cell<T> cell = createCell();
    T value = createCellValue();
    SafeHtmlBuilder sb = new SafeHtmlBuilder();
    Context context = new Context(-1, -1, null);
    cell.render(context, value, sb);
    assertEquals(getExpectedInnerHtml(), sb.toSafeHtml().asString());
  }

  /**
   * Test rendering the cell with a null value and no view data.
   */
  public void testRenderNull() {
    Cell<T> cell = createCell();
    SafeHtmlBuilder sb = new SafeHtmlBuilder();
    Context context = new Context(0, 0, null);
    cell.render(context, null, sb);
    assertEquals(getExpectedInnerHtmlNull(), sb.toSafeHtml().asString());
  }

  /**
   * Create a new cell to test.
   *
   * @return the new cell
   */
  protected abstract Cell<T> createCell();

  /**
   * Create a value to test.
   *
   * @return the cell value
   */
  protected abstract T createCellValue();

  /**
   * Does the cell type depend on selection? Default to false.
   *
   * @return true expected value of dependsOnSelection
   */
  protected abstract boolean dependsOnSelection();

  /**
   * Get the expected events that the cell should consume.
   *
   * @return the consumed events.
   */
  protected abstract String[] getConsumedEvents();

  /**
   * Get the expected inner HTML value of the rendered cell.
   *
   * @return the expected string
   */
  protected abstract String getExpectedInnerHtml();

  /**
   * Get the expected inner HTML value of the rendered cell when null is passed
   * as the cell value.
   *
   * @return the expected string
   */
  protected abstract String getExpectedInnerHtmlNull();

  /**
   * Test
   * {@link Cell#onBrowserEvent(Element, Object, Object, NativeEvent, ValueUpdater)}
   * with the specified conditions.
   * 
   * @param startHtml the innerHTML of the cell before the test starts
   * @param event the event to fire
   * @param value the cell value
   * @param expectedValue the expected value passed to the value updater, or
   *          null if none expected
   * @return the parent element
   */
  protected Element testOnBrowserEvent(String startHtml, NativeEvent event, final T value,
      T expectedValue) {
    return testOnBrowserEvent(createCell(), startHtml, event, value, expectedValue, true);
  }

  /**
   * Test
   * {@link Cell#onBrowserEvent(Element, Object, Object, NativeEvent, ValueUpdater)}
   * with the specified conditions.
   * 
   * @param cell the cell to use
   * @param startHtml the innerHTML of the cell before the test starts
   * @param event the event to fire
   * @param value the cell value
   * @param expectedValue the expected value passed to the value updater, or
   *          null if none expected
   * @param dispatchToFirstChild true to dispatch to the first child of the
   *          rendered parent element, if one is available
   * @return the parent element
   */
  protected Element testOnBrowserEvent(final Cell<T> cell, String startHtml, NativeEvent event,
      final T value, T expectedValue, boolean dispatchToFirstChild) {
    // Setup the parent element.
    final Element parent = Document.get().createDivElement();
    parent.setInnerHTML(startHtml);
    Document.get().getBody().appendChild(parent);

    // If the element has a child, use it as the event target.
    Element target = parent;
    if (dispatchToFirstChild) {
      Element child = parent.getFirstChildElement();
      target = (child == null) ? parent : child;
    }

    // Pass the event to the cell.
    final MockValueUpdater valueUpdater = new MockValueUpdater();
    Event.setEventListener(parent, new EventListener() {
      @Override
      public void onBrowserEvent(Event event) {
        try {
          DOM.setEventListener(parent, null);
          Context context = new Context(0, 0, DEFAULT_KEY);
          cell.onBrowserEvent(context, parent, value, event, valueUpdater);
          parent.removeFromParent();
        } catch (Exception e) {
          // We are in an event loop, so events may not propagate out to JUnit.
          fail("An exception occured while handling the event: " + e.getMessage());
        }
      }
    });
    Event.sinkEvents(target, Event.getTypeInt(event.getType()));
    target.dispatchEvent(event);
    assertNull(DOM.getEventListener(parent));

    // Check the expected value and view data.
    valueUpdater.assertLastValue(expectedValue);
    return parent;
  }
}
