/*
 * Copyright 2011 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.cellview.client;

import com.google.gwt.cell.client.AbstractCell;
import com.google.gwt.cell.client.Cell;
import com.google.gwt.cell.client.Cell.Context;
import com.google.gwt.cell.client.TextCell;
import com.google.gwt.dom.builder.shared.TableRowBuilder;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.dom.client.NodeList;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.dom.client.TableCellElement;
import com.google.gwt.dom.client.TableRowElement;
import com.google.gwt.dom.client.TableSectionElement;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.user.cellview.client.AbstractHasData.DefaultKeyboardSelectionHandler;
import com.google.gwt.user.cellview.client.HasKeyboardPagingPolicy.KeyboardPagingPolicy;
import com.google.gwt.user.cellview.client.LoadingStateChangeEvent.LoadingState;
import com.google.gwt.user.client.ui.HasHorizontalAlignment;
import com.google.gwt.user.client.ui.HasVerticalAlignment;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.view.client.Range;

import java.util.ArrayList;
import java.util.List;

/**
 * Bases tests for subclasses of {@link AbstractCellTable}.
 * 
 * @param <T> the subclass type
 */
public abstract class AbstractCellTableTestBase<T extends AbstractCellTable<String>> extends
    AbstractHasDataTestBase {

  /**
   * A concrete column that implements a getter that always returns null.
   * 
   * @param <T> the row type
   * @param <C> the column type
   */
  private static class MockColumn<T, C> extends Column<T, C> {

    public MockColumn() {
      super(new AbstractCell<C>() {
        @Override
        public void render(Context context, C value, SafeHtmlBuilder sb) {
        }
      });
    }

    @Override
    public C getValue(T object) {
      return null;
    }
  }

  /**
   * Test that calls to addColumn results in only one redraw.
   */
  public void testAddColumnSingleRedraw() {
    final List<LoadingState> loadingStates = new ArrayList<LoadingState>();
    T table = createAbstractHasData();
    table.setPageSize(10);
    table.addLoadingStateChangeHandler(new LoadingStateChangeEvent.Handler() {
      @Override
      public void onLoadingStateChanged(LoadingStateChangeEvent event) {
        if (LoadingState.LOADED == event.getLoadingState()) {
          loadingStates.add(event.getLoadingState());
        }
      }
    });
    table.addColumn(new Column<String, String>(new TextCell()) {
      @Override
      public String getValue(String object) {
        return object + "-3";
      }
    });
    table.addColumn(new Column<String, String>(new TextCell()) {
      @Override
      public String getValue(String object) {
        return object + "-4";
      }
    });
    table.setRowData(0, createData(0, 10));
    table.getPresenter().flush();
    assertEquals(1, loadingStates.size());
  }

  /**
   * Test that an error is thrown if the builder ends the table body element
   * instead of the table row element.
   */
  public void testBuildTooManyEnds() {
    final List<Integer> builtRows = new ArrayList<Integer>();
    T table = createAbstractHasData(new TextCell());
    CellTableBuilder<String> builder = new DefaultCellTableBuilder<String>(table) {
      @Override
      public void buildRowImpl(String rowValue, int absRowIndex) {
        builtRows.add(absRowIndex);
        TableRowBuilder tr = startRow(rowValue);
        tr.endTR(); // End the tr.
        tr.end(); // Accidentally end the table section.

        // Try to start another row.
        try {
          startRow(rowValue);
          fail("Expected IllegalStateException: tbody is already ended");
        } catch (IllegalStateException e) {
          // Expected.
        }
      }
    };
    table.setTableBuilder(builder);
    table.setVisibleRange(0, 1);
    populateData(table);
    table.getPresenter().flush();

    assertEquals(1, builtRows.size());
    assertEquals(0, builtRows.get(0).intValue());
  }

  /**
   * Test that the table works if a row value is rendered into multiple rows.
   */
  public void testBuildMultipleRows() {
    T table = createAbstractHasData(new TextCell());
    CellTableBuilder<String> builder = new DefaultCellTableBuilder<String>(table) {
      @Override
      public void buildRowImpl(String rowValue, int absRowIndex) {
        super.buildRowImpl(rowValue, absRowIndex);

        // Add child rows to row five.
        if (absRowIndex == 5) {
          for (int i = 0; i < 4; i++) {
            TableRowBuilder tr = startRow(rowValue);
            tr.startTD().colSpan(2).text("child " + i).endTD();
            tr.endTR();
          }
        }
      }
    };
    table.setTableBuilder(builder);
    table.setVisibleRange(0, 10);
    populateData(table);
    table.getPresenter().flush();

    // Verify the structure.
    TableSectionElement tbody = table.getTableBodyElement();
    assertEquals(14, tbody.getChildCount());
    assertEquals("child 0", getBodyElement(table, 6, 0).getInnerText());
    assertEquals("child 1", getBodyElement(table, 7, 0).getInnerText());
    assertEquals("child 2", getBodyElement(table, 8, 0).getInnerText());
    assertEquals("child 3", getBodyElement(table, 9, 0).getInnerText());

    // Verify the row values are accessible.
    assertEquals("test 5", table.getVisibleItem(5));
    assertEquals("test 9", table.getVisibleItem(9));

    // Verify the child elements map correctly.
    assertEquals(4, table.getChildElement(4).getSectionRowIndex());
    assertEquals(5, table.getChildElement(5).getSectionRowIndex());
    assertEquals(10, table.getChildElement(6).getSectionRowIndex());
  }

  /**
   * Test that the table works if a row value is rendered into zero rows.
   */
  public void testBuildZeroRows() {
    T table = createAbstractHasData(new TextCell());
    CellTableBuilder<String> builder = new DefaultCellTableBuilder<String>(table) {
      @Override
      public void buildRowImpl(String rowValue, int absRowIndex) {
        // Skip row index 5.
        if (absRowIndex != 5) {
          super.buildRowImpl(rowValue, absRowIndex);
        }
      }
    };
    table.setTableBuilder(builder);
    table.setVisibleRange(0, 10);
    populateData(table);
    table.getPresenter().flush();

    // Verify the structure.
    TableSectionElement tbody = table.getTableBodyElement();
    assertEquals(9, tbody.getChildCount());

    // Verify the row values are accessible.
    assertEquals("test 5", table.getVisibleItem(5));
    assertEquals("test 9", table.getVisibleItem(9));

    // Verify the child elements map correctly.
    assertEquals(4, table.getChildElement(4).getSectionRowIndex());
    assertNull(table.getChildElement(5));
    assertEquals(5, table.getChildElement(6).getSectionRowIndex());
  }

  public void testCellAlignment() {
    T table = createAbstractHasData(new TextCell());
    Column<String, String> column = new Column<String, String>(new TextCell()) {
      @Override
      public String getValue(String object) {
        return object;
      }
    };
    table.addColumn(column);

    /*
     * No alignment. Some browsers (FF) return a default value when alignment is
     * not specified, others (IE/HtmlUnit) return an empty string.
     */
    table.setRowData(0, createData(0, 1));
    table.getPresenter().flush();
    TableCellElement td = getBodyElement(table, 0, 2);
    String hAlign = td.getAlign();
    String vAlign = td.getVAlign();
    assertTrue("".equals(hAlign) || "left".equals(hAlign));
    assertTrue("".equals(vAlign) || "middle".equals(vAlign));

    // Horizontal alignment.
    column.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_RIGHT);
    table.setRowData(0, createData(0, 1));
    table.getPresenter().flush();
    td = getBodyElement(table, 0, 2);
    hAlign = td.getAlign();
    vAlign = td.getVAlign();
    assertTrue("right".equals(hAlign));
    assertTrue("".equals(vAlign) || "middle".equals(vAlign));

    // Vertical alignment.
    column.setHorizontalAlignment(null);
    column.setVerticalAlignment(HasVerticalAlignment.ALIGN_BOTTOM);
    table.setRowData(0, createData(0, 1));
    table.getPresenter().flush();
    td = getBodyElement(table, 0, 2);
    hAlign = td.getAlign();
    vAlign = td.getVAlign();
    assertTrue("".equals(hAlign) || "left".equals(hAlign));
    assertTrue("bottom".equals(vAlign));

    // Horizontal and vertical alignment.
    column.setHorizontalAlignment(HasHorizontalAlignment.ALIGN_RIGHT);
    table.setRowData(0, createData(0, 1));
    table.getPresenter().flush();
    td = getBodyElement(table, 0, 2);
    hAlign = td.getAlign();
    vAlign = td.getVAlign();
    assertTrue("right".equals(hAlign));
    assertTrue("bottom".equals(vAlign));
  }

  public void testCellEvent() {
    IndexCell<String> cell = new IndexCell<String>("click");
    T table = createAbstractHasData(cell);
    
    RootPanel.get().add(table);
    table.setRowData(createData(0, 10));
    table.getPresenter().flush();

    // Trigger an event at index 5 inside the child div.
    NativeEvent event = Document.get().createClickEvent(0, 0, 0, 0, 0, false, false, false, false);
    getBodyElement(table, 5, 0).getFirstChildElement().dispatchEvent(event);
    cell.assertLastBrowserEventIndex(5);
    cell.assertLastEditingIndex(5);

    // Trigger an event at index 3 outside the child div.
    event = Document.get().createClickEvent(0, 0, 0, 0, 0, false, false, false, false);
    getBodyElement(table, 3, 0).dispatchEvent(event);
    cell.assertLastBrowserEventIndex(3);
    cell.assertLastEditingIndex(3);

    RootPanel.get().remove(table);
  }

  public void testCellStyles() {
    T table = createAbstractHasData();

    // A column that return a static cell style.
    TextColumn<String> col0 = new TextColumn<String>() {
      @Override
      public String getValue(String object) {
        return object;
      }
    };
    col0.setCellStyleNames("col0");
    table.addColumn(col0);

    // A column that returns dynamic cell styles.
    TextColumn<String> col1 = new TextColumn<String>() {
      @Override
      public String getCellStyleNames(Context context, String object) {
        return object.replace(" ", "_");
      }

      @Override
      public String getValue(String object) {
        return object;
      }
    };
    table.addColumn(col1);

    // Populate the table.
    table.setRowData(createData(0, 10));
    table.flush();

    assertTrue(getBodyElement(table, 1, 0).getClassName().contains(" col0"));
    assertFalse(getBodyElement(table, 1, 0).getClassName().contains(" test_1"));
    assertFalse(getBodyElement(table, 1, 1).getClassName().contains(" col0"));
    assertTrue(getBodyElement(table, 1, 1).getClassName().contains(" test_1"));
  }

  public void testClearColumnWidth() {
    T table = createAbstractHasData();
    assertEquals(0, table.getRealColumnCount());

    table.setColumnWidth(0, "100px");
    assertEquals(1, table.getRealColumnCount());

    table.setColumnWidth(2, "300px");
    assertEquals(3, table.getRealColumnCount());

    table.clearColumnWidth(2);
    assertEquals(1, table.getRealColumnCount());
  }

  public void testDefaultKeyboardSelectionHandlerChangePage() {
    T table = createAbstractHasData();
    DefaultKeyboardSelectionHandler<String> keyHandler =
        new DefaultKeyboardSelectionHandler<String>(table);
    table.setKeyboardSelectionHandler(keyHandler);
    HasDataPresenter<String> presenter = table.getPresenter();

    table.setRowCount(100, true);
    table.setVisibleRange(new Range(50, 10));
    populateData(table);
    presenter.flush();
    table.setKeyboardPagingPolicy(KeyboardPagingPolicy.CHANGE_PAGE);

    // keyboardPrev in middle.
    table.setKeyboardSelectedRow(1);
    presenter.flush();
    assertEquals(1, table.getKeyboardSelectedRow());
    keyHandler.prevRow();
    presenter.flush();
    assertEquals(0, table.getKeyboardSelectedRow());

    // keyboardPrev at beginning goes to previous page.
    keyHandler.prevRow();
    populateData(table);
    presenter.flush();
    assertEquals(9, table.getKeyboardSelectedRow());
    assertEquals(new Range(40, 10), table.getVisibleRange());

    // keyboardNext in middle.
    table.setKeyboardSelectedRow(8);
    presenter.flush();
    assertEquals(8, table.getKeyboardSelectedRow());
    keyHandler.nextRow();
    presenter.flush();
    assertEquals(9, table.getKeyboardSelectedRow());

    // keyboardNext at end.
    keyHandler.nextRow();
    populateData(table);
    presenter.flush();
    assertEquals(0, table.getKeyboardSelectedRow());
    assertEquals(new Range(50, 10), table.getVisibleRange());

    // keyboardPrevPage.
    table.setKeyboardSelectedRow(5);
    presenter.flush();
    assertEquals(5, table.getKeyboardSelectedRow());
    keyHandler.prevPage();
    populateData(table);
    presenter.flush();
    assertEquals(0, table.getKeyboardSelectedRow());
    assertEquals(new Range(40, 10), table.getVisibleRange());

    // keyboardNextPage.
    table.setKeyboardSelectedRow(5);
    presenter.flush();
    assertEquals(5, table.getKeyboardSelectedRow());
    keyHandler.nextPage();
    populateData(table);
    presenter.flush();
    assertEquals(0, table.getKeyboardSelectedRow());
    assertEquals(new Range(50, 10), table.getVisibleRange());

    // keyboardHome.
    keyHandler.home();
    populateData(table);
    presenter.flush();
    assertEquals(0, table.getKeyboardSelectedRow());
    assertEquals(new Range(0, 10), table.getVisibleRange());

    // keyboardPrev at first row.
    keyHandler.prevRow();
    presenter.flush();
    assertEquals(0, table.getKeyboardSelectedRow());

    // keyboardEnd.
    keyHandler.end();
    populateData(table);
    presenter.flush();
    assertEquals(9, table.getKeyboardSelectedRow());
    assertEquals(new Range(90, 10), table.getVisibleRange());

    // keyboardNext at last row.
    keyHandler.nextRow();
    presenter.flush();
  }

  public void testDefaultKeyboardSelectionHandlerCurrentPage() {
    T table = createAbstractHasData();
    DefaultKeyboardSelectionHandler<String> keyHandler =
        new DefaultKeyboardSelectionHandler<String>(table);
    table.setKeyboardSelectionHandler(keyHandler);
    HasDataPresenter<String> presenter = table.getPresenter();

    table.setRowCount(100, true);
    table.setVisibleRange(new Range(50, 10));
    populateData(table);
    presenter.flush();
    table.setKeyboardPagingPolicy(KeyboardPagingPolicy.CURRENT_PAGE);

    // keyboardPrev in middle.
    table.setKeyboardSelectedRow(1);
    presenter.flush();
    assertEquals(1, table.getKeyboardSelectedRow());
    keyHandler.prevRow();
    presenter.flush();
    assertEquals(0, table.getKeyboardSelectedRow());

    // keyboardPrev at beginning.
    keyHandler.prevRow();
    presenter.flush();
    assertEquals(0, table.getKeyboardSelectedRow());
    assertEquals(new Range(50, 10), table.getVisibleRange());

    // keyboardNext in middle.
    table.setKeyboardSelectedRow(8);
    presenter.flush();
    assertEquals(8, table.getKeyboardSelectedRow());
    keyHandler.nextRow();
    presenter.flush();
    assertEquals(9, table.getKeyboardSelectedRow());

    // keyboardNext at end.
    keyHandler.nextRow();
    presenter.flush();
    assertEquals(9, table.getKeyboardSelectedRow());
    assertEquals(new Range(50, 10), table.getVisibleRange());

    // keyboardPrevPage.
    keyHandler.prevPage(); // ignored.
    presenter.flush();
    assertEquals(9, table.getKeyboardSelectedRow());
    assertEquals(new Range(50, 10), table.getVisibleRange());

    // keyboardNextPage.
    keyHandler.nextPage(); // ignored.
    presenter.flush();
    assertEquals(9, table.getKeyboardSelectedRow());
    assertEquals(new Range(50, 10), table.getVisibleRange());

    // keyboardHome.
    keyHandler.home();
    presenter.flush();
    assertEquals(0, table.getKeyboardSelectedRow());
    assertEquals(new Range(50, 10), table.getVisibleRange());

    // keyboardEnd.
    keyHandler.end();
    presenter.flush();
    assertEquals(9, table.getKeyboardSelectedRow());
    assertEquals(new Range(50, 10), table.getVisibleRange());
  }

  public void testDefaultKeyboardSelectionHandlerIncreaseRange() {
    int pageStart = 150;
    int pageSize = 10;
    int increment = HasDataPresenter.PAGE_INCREMENT;

    T table = createAbstractHasData();
    DefaultKeyboardSelectionHandler<String> keyHandler =
        new DefaultKeyboardSelectionHandler<String>(table);
    table.setKeyboardSelectionHandler(keyHandler);
    HasDataPresenter<String> presenter = table.getPresenter();

    table.setRowCount(300, true);
    table.setVisibleRange(new Range(pageStart, pageSize));
    populateData(table);
    presenter.flush();
    table.setKeyboardPagingPolicy(KeyboardPagingPolicy.INCREASE_RANGE);

    // keyboardPrev in middle.
    table.setKeyboardSelectedRow(1);
    presenter.flush();
    assertEquals(1, table.getKeyboardSelectedRow());
    keyHandler.prevRow();
    presenter.flush();
    assertEquals(0, table.getKeyboardSelectedRow());

    // keyboardPrev at beginning.
    keyHandler.prevRow();
    populateData(table);
    presenter.flush();
    pageStart -= increment;
    pageSize += increment;
    assertEquals(increment - 1, table.getKeyboardSelectedRow());
    assertEquals(new Range(pageStart, pageSize), table.getVisibleRange());

    // keyboardNext in middle.
    table.setKeyboardSelectedRow(pageSize - 2);
    presenter.flush();
    assertEquals(pageSize - 2, table.getKeyboardSelectedRow());
    keyHandler.nextRow();
    presenter.flush();
    assertEquals(pageSize - 1, table.getKeyboardSelectedRow());

    // keyboardNext at end.
    keyHandler.nextRow();
    populateData(table);
    presenter.flush();
    pageSize += increment;
    assertEquals(pageSize - increment, table.getKeyboardSelectedRow());
    assertEquals(new Range(pageStart, pageSize), table.getVisibleRange());

    // keyboardPrevPage within range.
    table.setKeyboardSelectedRow(increment);
    presenter.flush();
    assertEquals(increment, table.getKeyboardSelectedRow());
    keyHandler.prevPage();
    presenter.flush();
    assertEquals(0, table.getKeyboardSelectedRow());
    assertEquals(new Range(pageStart, pageSize), table.getVisibleRange());

    // keyboardPrevPage outside range.
    keyHandler.prevPage();
    populateData(table);
    presenter.flush();
    assertEquals(0, table.getKeyboardSelectedRow());
    pageStart -= increment;
    pageSize += increment;
    assertEquals(new Range(pageStart, pageSize), table.getVisibleRange());

    // keyboardNextPage inside range.
    keyHandler.nextPage();
    presenter.flush();
    assertEquals(increment, table.getKeyboardSelectedRow());
    assertEquals(new Range(pageStart, pageSize), table.getVisibleRange());

    // keyboardNextPage outside range.
    table.setKeyboardSelectedRow(pageSize - 1);
    presenter.flush();
    assertEquals(pageSize - 1, table.getKeyboardSelectedRow());
    keyHandler.nextPage();
    populateData(table);
    presenter.flush();
    pageSize += increment;
    assertEquals(pageSize - 1, table.getKeyboardSelectedRow());
    assertEquals(new Range(pageStart, pageSize), table.getVisibleRange());

    // keyboardHome.
    keyHandler.home();
    populateData(table);
    presenter.flush();
    pageSize += pageStart;
    pageStart = 0;
    assertEquals(0, table.getKeyboardSelectedRow());
    assertEquals(new Range(pageStart, pageSize), table.getVisibleRange());

    // keyboardPrev at first row.
    keyHandler.prevRow();
    presenter.flush();
    assertEquals(0, table.getKeyboardSelectedRow());
    assertEquals(new Range(pageStart, pageSize), table.getVisibleRange());

    // keyboardEnd.
    keyHandler.end();
    pageSize = 300;
    populateData(table);
    presenter.flush();
    assertEquals(pageSize - 1, table.getKeyboardSelectedRow());
    assertEquals(new Range(0, pageSize), table.getVisibleRange());

    // keyboardNext at last row.
    keyHandler.nextRow();
    presenter.flush();
    assertEquals(pageSize - 1, table.getKeyboardSelectedRow());
    assertEquals(new Range(pageStart, pageSize), table.getVisibleRange());
  }

  public void testGetColumnIndex() {
    T table = createAbstractHasData();
    Column<String, String> col0 = new IdentityColumn<String>(new TextCell());
    table.addColumn(col0);
    Column<String, String> col1 = new IdentityColumn<String>(new TextCell());
    table.addColumn(col1);
    Column<String, String> col2 = new IdentityColumn<String>(new TextCell());
    table.addColumn(col2);
    assertEquals(0, table.getColumnIndex(col0));
    assertEquals(1, table.getColumnIndex(col1));
    assertEquals(2, table.getColumnIndex(col2));

    // Test a column that is not in the table.
    Column<String, String> other = new IdentityColumn<String>(new TextCell());
    assertEquals(-1, table.getColumnIndex(other));

    // Test null.
    assertEquals(-1, table.getColumnIndex(null));
  }

  public void testGetColumnOutOfBounds() {
    T table = createAbstractHasData();

    // Get column when there are no columns.
    try {
      table.getColumn(0);
      fail("Expected IndexOutOfBoundsException");
    } catch (IndexOutOfBoundsException e) {
      // Expected.
    }

    // Add some columns.
    table.addColumn(new MockColumn<String, String>());
    table.addColumn(new MockColumn<String, String>());

    // Negative index.
    try {
      table.getColumn(-1);
      fail("Expected IndexOutOfBoundsException");
    } catch (IndexOutOfBoundsException e) {
      // Expected.
    }

    // Index too high.
    try {
      table.getColumn(2);
      fail("Expected IndexOutOfBoundsException");
    } catch (IndexOutOfBoundsException e) {
      // Expected.
    }
  }

  public void testGetRowElement() {
    AbstractCellTable<String> table = createAbstractHasData(new TextCell());
    table.setRowData(0, createData(0, 10));

    // Ensure that calling getRowElement() flushes all pending changes.
    assertNotNull(table.getRowElement(9));
  }

  public void testGetSubRowElement() {
    T table = createAbstractHasData(new TextCell());
    CellTableBuilder<String> builder = new DefaultCellTableBuilder<String>(table) {
      @Override
      public void buildRowImpl(String rowValue, int absRowIndex) {
        super.buildRowImpl(rowValue, absRowIndex);

        // Add some children.
        for (int i = 0; i < 4; i++) {
          TableRowBuilder tr = startRow(rowValue);
          tr.startTD().colSpan(2).text("child " + absRowIndex + ":" + i).endTD();
          tr.endTR();
        }
      }
    };
    table.setTableBuilder(builder);
    table.setVisibleRange(0, 5);
    populateData(table);
    table.getPresenter().flush();

    // Verify the structure.
    TableSectionElement tbody = table.getTableBodyElement();
    assertEquals(25, tbody.getChildCount());

    // Test sub rows within range.
    assertEquals(0, table.getSubRowElement(0, 0).getSectionRowIndex());
    assertEquals(1, table.getSubRowElement(0, 1).getSectionRowIndex());
    assertEquals(4, table.getSubRowElement(0, 4).getSectionRowIndex());
    assertEquals(5, table.getSubRowElement(1, 0).getSectionRowIndex());
    assertEquals(8, table.getSubRowElement(1, 3).getSectionRowIndex());
    assertEquals(20, table.getSubRowElement(4, 0).getSectionRowIndex());
    assertEquals(24, table.getSubRowElement(4, 4).getSectionRowIndex());

    // Sub row does not exist within the row.
    assertNull(table.getSubRowElement(0, 5));
    assertNull(table.getSubRowElement(4, 5));

    // Row index out of bounds.
    try {
      assertNull(table.getSubRowElement(5, 0));
      fail("Expected IndexOutOfBoundsException: row index is out of bounds");
    } catch (IndexOutOfBoundsException e) {
      // Expected.
    }
  }

  public void testHeaderEvent() {
    T table = createAbstractHasData();
    IndexCell<String> cell = new IndexCell<String>("click");
    table.addColumn(new TextColumn<String>() {
      @Override
      public String getValue(String object) {
        return object;
      }
    }, new Header<String>(cell) {
      @Override
      public String getValue() {
        return "header0";
      }
    });
    RootPanel.get().add(table);
    table.setRowData(createData(0, 10));
    table.getPresenter().flush();

    // Trigger an event on the header.
    NativeEvent event = Document.get().createClickEvent(0, 0, 0, 0, 0, false, false, false, false);
    getHeaderElement(table, 0).dispatchEvent(event);
    cell.assertLastBrowserEventIndex(0);

    RootPanel.get().remove(table);
  }

  public void testSetHeaderBuilder() {
    T table = createAbstractHasData();
    HeaderBuilder<String> headerBuilder = new AbstractHeaderOrFooterBuilder<String>(table, false) {
      @Override
      protected boolean buildHeaderOrFooterImpl() {
        TableRowBuilder tr = startRow();
        tr.startTH().text("Col 0").endTH();
        tr.startTH().text("Col 1").endTH();
        tr.startTH().text("Col 2").endTH();
        tr.endTR();
        return true;
      }
    };

    // Change the header builder.
    table.setHeaderBuilder(headerBuilder);
    assertEquals(headerBuilder, table.getHeaderBuilder());
    table.getPresenter().flush();

    // Verify the new header.
    NodeList<TableRowElement> rows = table.getTableHeadElement().getRows();
    assertEquals(1, rows.getLength());
    NodeList<TableCellElement> cells = rows.getItem(0).getCells();
    assertEquals(3, cells.getLength());
    assertEquals("Col 0", cells.getItem(0).getInnerText());
    assertEquals("Col 1", cells.getItem(1).getInnerText());
    assertEquals("Col 2", cells.getItem(2).getInnerText());
  }

  public void testSetFooterBuilder() {
    T table = createAbstractHasData();
    FooterBuilder<String> footerBuilder = new AbstractHeaderOrFooterBuilder<String>(table, true) {
      @Override
      protected boolean buildHeaderOrFooterImpl() {
        TableRowBuilder tr = startRow();
        tr.startTH().text("Col 0").endTH();
        tr.startTH().text("Col 1").endTH();
        tr.startTH().text("Col 2").endTH();
        tr.endTR();
        return true;
      }
    };

    // Change the header builder.
    table.setFooterBuilder(footerBuilder);
    assertEquals(footerBuilder, table.getFooterBuilder());
    table.getPresenter().flush();

    // Verify the new header.
    NodeList<TableRowElement> rows = table.getTableFootElement().getRows();
    assertEquals(1, rows.getLength());
    NodeList<TableCellElement> cells = rows.getItem(0).getCells();
    assertEquals(3, cells.getLength());
    assertEquals("Col 0", cells.getItem(0).getInnerText());
    assertEquals("Col 1", cells.getItem(1).getInnerText());
    assertEquals("Col 2", cells.getItem(2).getInnerText());
  }

  public void testInsertColumn() {
    T table = createAbstractHasData();
    assertEquals(0, table.getColumnCount());

    // Insert first column.
    Column<String, ?> a = new MockColumn<String, String>();
    table.insertColumn(0, a);
    assertEquals(1, table.getColumnCount());
    assertEquals(a, table.getColumn(0));

    // Insert column at beginning.
    Column<String, ?> b = new MockColumn<String, String>();
    table.insertColumn(0, b);
    assertEquals(2, table.getColumnCount());
    assertEquals(b, table.getColumn(0));
    assertEquals(a, table.getColumn(1));

    // Insert column at end.
    Column<String, ?> c = new MockColumn<String, String>();
    table.insertColumn(2, c);
    assertEquals(3, table.getColumnCount());
    assertEquals(b, table.getColumn(0));
    assertEquals(a, table.getColumn(1));
    assertEquals(c, table.getColumn(2));

    // Insert column in middle.
    Column<String, ?> d = new MockColumn<String, String>();
    table.insertColumn(1, d);
    assertEquals(4, table.getColumnCount());
    assertEquals(b, table.getColumn(0));
    assertEquals(d, table.getColumn(1));
    assertEquals(a, table.getColumn(2));
    assertEquals(c, table.getColumn(3));

    // Insert column at invalid index.
    try {
      table.insertColumn(-1, d);
      fail("Expected IndexOutOfBoundsExecltion");
    } catch (IndexOutOfBoundsException e) {
      // Expected.
    }
    try {
      table.insertColumn(6, d);
      fail("Expected IndexOutOfBoundsExecltion");
    } catch (IndexOutOfBoundsException e) {
      // Expected.
    }
  }

  public void testSetAutoFooterRefreshDisabled() {
    AbstractCellTable<String> table = createAbstractHasData();
    assertFalse(table.isAutoHeaderRefreshDisabled());
    assertFalse(table.isAutoFooterRefreshDisabled());

    table.setAutoFooterRefreshDisabled(true);
    assertFalse(table.isAutoHeaderRefreshDisabled());
    assertTrue(table.isAutoFooterRefreshDisabled());

    /*
     * Inserting a column should render the headers and footers, even if auto
     * refresh is disabled.
     */
    final List<String> log = new ArrayList<String>();
    Column<String, ?> col0 = new MockColumn<String, String>();
    TextHeader header0 = new TextHeader("header0") {
      @Override
      public void render(Context context, SafeHtmlBuilder sb) {
        super.render(context, sb);
        log.add("header0 rendered");
      }
    };
    TextHeader footer0 = new TextHeader("footer0") {
      @Override
      public void render(Context context, SafeHtmlBuilder sb) {
        super.render(context, sb);
        log.add("footer0 rendered");
      }
    };
    table.addColumn(col0, header0, footer0);
    assertEquals(0, log.size()); // Headers are rendered asynchronously.
    table.getPresenter().flush(); // Force headers to render.
    assertEquals("header0 rendered", log.remove(0));
    assertEquals("footer0 rendered", log.remove(0));
    assertEquals(0, log.size());

    /*
     * Inserting another column should render the headers and footers, even if
     * auto refresh is disabled.
     */
    Column<String, ?> col1 = new MockColumn<String, String>();
    TextHeader header1 = new TextHeader("header1") {
      @Override
      public void render(Context context, SafeHtmlBuilder sb) {
        super.render(context, sb);
        log.add("header1 rendered");
      }
    };
    TextHeader footer1 = new TextHeader("footer1") {
      @Override
      public void render(Context context, SafeHtmlBuilder sb) {
        super.render(context, sb);
        log.add("footer1 rendered");
      }
    };
    table.addColumn(col1, header1, footer1);
    assertEquals(0, log.size()); // Headers are rendered asynchronously.
    table.getPresenter().flush(); // Force headers to render.
    assertEquals("header0 rendered", log.remove(0));
    assertEquals("header1 rendered", log.remove(0));
    assertEquals("footer0 rendered", log.remove(0));
    assertEquals("footer1 rendered", log.remove(0));
    assertEquals(0, log.size());

    /*
     * Removing a column should render the headers and footers, even if auto
     * refresh is disabled.
     */
    table.removeColumn(col0);
    assertEquals(0, log.size()); // Headers are rendered asynchronously.
    table.getPresenter().flush(); // Force headers to render.
    assertEquals("header1 rendered", log.remove(0));
    assertEquals("footer1 rendered", log.remove(0));
    assertEquals(0, log.size());

    /*
     * Setting data only causes footers to render if auto refresh is enabled,
     * which it is not. Header refresh is still enabled.
     */
    populateData(table);
    assertEquals(0, log.size()); // Headers are rendered asynchronously.
    table.getPresenter().flush(); // Force headers to render.
    assertEquals("header1 rendered", log.remove(0));
    assertEquals(0, log.size());

    /*
     * Sorting a column forces the headers only to refresh. The footers are not
     * refreshed.
     */
    table.getColumnSortList().push(col1);
    assertEquals("header1 rendered", log.remove(0));
    assertEquals(0, log.size());
  }

  public void testSetAutoHeaderRefreshDisabled() {
    AbstractCellTable<String> table = createAbstractHasData();
    assertFalse(table.isAutoHeaderRefreshDisabled());
    assertFalse(table.isAutoFooterRefreshDisabled());

    table.setAutoHeaderRefreshDisabled(true);
    assertTrue(table.isAutoHeaderRefreshDisabled());
    assertFalse(table.isAutoFooterRefreshDisabled());

    /*
     * Inserting a column should render the headers and footers, even if auto
     * refresh is disabled.
     */
    final List<String> log = new ArrayList<String>();
    Column<String, ?> col0 = new MockColumn<String, String>();
    TextHeader header0 = new TextHeader("header0") {
      @Override
      public void render(Context context, SafeHtmlBuilder sb) {
        super.render(context, sb);
        log.add("header0 rendered");
      }
    };
    TextHeader footer0 = new TextHeader("footer0") {
      @Override
      public void render(Context context, SafeHtmlBuilder sb) {
        super.render(context, sb);
        log.add("footer0 rendered");
      }
    };
    table.addColumn(col0, header0, footer0);
    assertEquals(0, log.size()); // Headers are rendered asynchronously.
    table.getPresenter().flush(); // Force headers to render.
    assertEquals("header0 rendered", log.remove(0));
    assertEquals("footer0 rendered", log.remove(0));
    assertEquals(0, log.size());

    /*
     * Inserting another column should render the headers and footers, even if
     * auto refresh is disabled.
     */
    Column<String, ?> col1 = new MockColumn<String, String>();
    TextHeader header1 = new TextHeader("header1") {
      @Override
      public void render(Context context, SafeHtmlBuilder sb) {
        super.render(context, sb);
        log.add("header1 rendered");
      }
    };
    TextHeader footer1 = new TextHeader("footer1") {
      @Override
      public void render(Context context, SafeHtmlBuilder sb) {
        super.render(context, sb);
        log.add("footer1 rendered");
      }
    };
    table.addColumn(col1, header1, footer1);
    assertEquals(0, log.size()); // Headers are rendered asynchronously.
    table.getPresenter().flush(); // Force headers to render.
    assertEquals("header0 rendered", log.remove(0));
    assertEquals("header1 rendered", log.remove(0));
    assertEquals("footer0 rendered", log.remove(0));
    assertEquals("footer1 rendered", log.remove(0));
    assertEquals(0, log.size());

    /*
     * Removing a column should render the headers and footers, even if auto
     * refresh is disabled.
     */
    table.removeColumn(col0);
    assertEquals(0, log.size()); // Headers are rendered asynchronously.
    table.getPresenter().flush(); // Force headers to render.
    assertEquals("header1 rendered", log.remove(0));
    assertEquals("footer1 rendered", log.remove(0));
    assertEquals(0, log.size());

    /*
     * Setting data only causes headers to render if auto refresh is enabled,
     * which it is not. Footer refresh is still enabled.
     */
    populateData(table);
    assertEquals(0, log.size()); // Headers are rendered asynchronously.
    table.getPresenter().flush(); // Force headers to render.
    assertEquals("footer1 rendered", log.remove(0));
    assertEquals(0, log.size());

    /*
     * Sorting a column forces the headers only to refresh. The footers are not
     * refreshed.
     */
    table.getColumnSortList().push(col1);
    assertEquals("header1 rendered", log.remove(0));
    assertEquals(0, log.size());
  }

  public void testSetColumnWidth() {
    AbstractCellTable<String> table = createAbstractHasData(new TextCell());
    Column<String, ?> col0 = new MockColumn<String, String>();
    Column<String, ?> col1 = new MockColumn<String, String>();
    Column<String, ?> col2 = new MockColumn<String, String>();

    // Set a column width.
    table.setColumnWidth(col0, "100px");
    table.setColumnWidth(col1, 200.0, Unit.EM);
    assertEquals("100px", table.getColumnWidth(col0));

    // Some browsers return 200.0, others 200.
    assertTrue(table.getColumnWidth(col1).contains("200"));

    // Check a column that has not been set.
    assertNull(table.getColumnWidth(col2));

    // Check a column that has been cleared.
    table.clearColumnWidth(col0);
    assertNull(table.getColumnWidth(col0));
  }

  /**
   * Test that setting column widths using columns and column indexes works
   * correctly.
   */
  public void testSetColumnWidthMixed() {
    AbstractCellTable<String> table = createAbstractHasData(new TextCell());
    Column<String, ?> col0 = new MockColumn<String, String>();
    Column<String, ?> col1 = new MockColumn<String, String>();
    Column<String, ?> col2 = new MockColumn<String, String>();

    // Column 0 set by Column.
    table.setColumnWidth(col0, "100px");

    // Column 1 set by Column and column index.
    table.setColumnWidth(col1, 200.0, Unit.EM);
    table.setColumnWidth(1, "210em");

    // Column 2 set by column index.
    table.setColumnWidth(2, "300px");

    assertEquals("100px", table.getColumnWidth(col0));
    assertEquals("300px", table.getColumnWidth(2));

    /*
     * Some browsers return 200.0, others 200. Column takes precendence over
     * column index.
     */
    assertTrue(table.getColumnWidth(col1).contains("200"));

    // Check a column that has not been set.
    assertNull(table.getColumnWidth(col2));

    // Check a column that has been cleared.
    table.clearColumnWidth(col0);
    assertNull(table.getColumnWidth(col0));
  }

  public void testSetEmptyTableWidget() {
    AbstractCellTable<String> table = createAbstractHasData(new TextCell());

    // Set a widget.
    Label l = new Label("Empty");
    table.setEmptyTableWidget(l);
    assertEquals(l, table.getEmptyTableWidget());

    // Null widget.
    table.setEmptyTableWidget(null);
    assertNull(table.getEmptyTableWidget());
  }

  public void testSetKeyboardSelectedRow() {
    AbstractCellTable<String> table = createAbstractHasData(new TextCell());
    table.setVisibleRange(0, 10);

    // Without a subrow.
    table.setKeyboardSelectedRow(5);
    assertEquals(5, table.getKeyboardSelectedRow());
    assertEquals(0, table.getKeyboardSelectedSubRow());

    // Specify a subrow.
    table.setKeyboardSelectedRow(6, 2, false);
    assertEquals(6, table.getKeyboardSelectedRow());
    assertEquals(2, table.getKeyboardSelectedSubRow());

    // Change the subrow.
    table.setKeyboardSelectedRow(6, 5, false);
    assertEquals(6, table.getKeyboardSelectedRow());
    assertEquals(5, table.getKeyboardSelectedSubRow());

    // Change the row.
    table.setKeyboardSelectedRow(7);
    assertEquals(7, table.getKeyboardSelectedRow());
    assertEquals(0, table.getKeyboardSelectedSubRow());
  }

  public void testSetLoadingIndicator() {
    AbstractCellTable<String> table = createAbstractHasData(new TextCell());

    // Set a widget.
    Label l = new Label("Loading");
    table.setLoadingIndicator(l);
    assertEquals(l, table.getLoadingIndicator());

    // Null widget.
    table.setLoadingIndicator(null);
    assertNull(table.getLoadingIndicator());
  }

  public void testSortableColumn() {
    T table = createAbstractHasData(new TextCell());
    table.getColumn(0).setSortable(true);
    table.getPresenter().flush();
    RootPanel.get().add(table);

    // Add a column sort handler.
    final List<Column<?, ?>> lastSorted = new ArrayList<Column<?, ?>>();
    table.addColumnSortHandler(new ColumnSortEvent.Handler() {
      @Override
      public void onColumnSort(ColumnSortEvent event) {
        lastSorted.clear();
        lastSorted.add(event.getColumn());
      }
    });

    // Default sort order is empty.
    ColumnSortList sortList = table.getColumnSortList();
    assertEquals(0, sortList.size());

    // Sort a column that is sortable.
    NativeEvent click = Document.get().createClickEvent(0, 0, 0, 0, 0, false, false, false, false);
    getHeaderElement(table, 0).dispatchEvent(click);
    assertEquals(1, sortList.size());
    assertEquals(table.getColumn(0), sortList.get(0).getColumn());
    assertTrue(sortList.get(0).isAscending());
    assertEquals(1, lastSorted.size());
    lastSorted.clear();

    // Sort the same column again.
    getHeaderElement(table, 0).dispatchEvent(click);
    assertEquals(1, sortList.size());
    assertEquals(table.getColumn(0), sortList.get(0).getColumn());
    assertFalse(sortList.get(0).isAscending());
    assertEquals(1, lastSorted.size());
    lastSorted.clear();
    
    // Sort the same column again, this time using the Enter key.
    NativeEvent enter = Document.get().createKeyDownEvent(false, false, false, false,
        KeyCodes.KEY_ENTER);
    getHeaderElement(table, 0).dispatchEvent(enter);
    assertEquals(1, sortList.size());
    assertEquals(table.getColumn(0), sortList.get(0).getColumn());
    assertTrue(sortList.get(0).isAscending());
    assertEquals(1, lastSorted.size());
    lastSorted.clear();

    // Sort the same column using the Enter key again.
    getHeaderElement(table, 0).dispatchEvent(enter);
    assertEquals(1, sortList.size());
    assertEquals(table.getColumn(0), sortList.get(0).getColumn());
    assertFalse(sortList.get(0).isAscending());
    assertEquals(1, lastSorted.size());
    lastSorted.clear();

    // Sort a column that is not sortable.
    getHeaderElement(table, 1).dispatchEvent(click);
    assertEquals(1, sortList.size());
    assertEquals(table.getColumn(0), sortList.get(0).getColumn());
    assertFalse(sortList.get(0).isAscending());
    assertEquals(0, lastSorted.size());

    // Cleanup.
    RootPanel.get().remove(table);
  }

  /**
   * Create an empty cell table.
   */
  protected abstract T createAbstractHasData();

  @Override
  protected final T createAbstractHasData(Cell<String> cell) {
    T table = createAbstractHasData();
    table.addColumn(new Column<String, String>(cell) {
      @Override
      public String getValue(String object) {
        return object;
      }
    }, "Column 0");
    table.addColumn(new Column<String, String>(new TextCell()) {
      @Override
      public String getValue(String object) {
        return object + "-2";
      }
    }, "Column 1");
    return table;
  }

  /**
   * Get a td element from the table body.
   * 
   * @param table the {@link CellTable}
   * @param row the row index
   * @param column the column index
   * @return the column header
   */
  protected abstract TableCellElement getBodyElement(T table, int row, int column);

  /**
   * Get the number of column headers in the table.
   * 
   * @param table the {@link CellTable}
   * @return the number of column headers
   */
  protected abstract int getHeaderCount(T table);

  /**
   * Get a column header from the table.
   * 
   * @param table the {@link CellTable}
   * @param column the column index
   * @return the column header
   */
  protected abstract TableCellElement getHeaderElement(T table, int column);
}
