/*
 * 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.user.cellview.client;
import com.google.gwt.cell.client.AbstractCell;
import com.google.gwt.cell.client.TextCell;
import com.google.gwt.event.dom.client.KeyCodes;
import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.view.client.ListDataProvider;
import com.google.gwt.view.client.TreeViewModel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
 * Tests for {@link CellTree}.
 */
public class CellTreeTest extends AbstractCellTreeTestBase {
  public CellTreeTest() {
    super(false);
  }
  public void testRefreshEmptyNode() {
    // An empty data provider.
    final ListDataProvider<String> provider = new ListDataProvider<String>();
    TreeViewModel model = new TreeViewModel() {
      @Override
      public NodeInfo<?> getNodeInfo(Object value) {
        TextCell cell = new TextCell();
        return new DefaultNodeInfo<String>(provider, cell);
      }
      @Override
      public boolean isLeaf(Object value) {
        return false;
      }
    };
    // Create the tree.
    CellTree tree = createAbstractCellTree(model, null);
    tree.rootNode.listView.presenter.flush();
    // Refresh the empty list.
    provider.refresh();
    provider.flush();
    tree.rootNode.listView.presenter.flush();
  }
  /**
   * Test that CellTree handles rendering the same content, but with a different
   * underlying value.
   */
  public void testRenderSameContent() {
    final AbstractCell<Integer> intCell = new AbstractCell<Integer>() {
      @Override
      public void render(Context context, Integer value, SafeHtmlBuilder sb) {
        sb.append(value % 10); // Render the units digit only.
      }
    };
    // Create a data provider for the root node.
    final ListDataProvider<Integer> root = new ListDataProvider<Integer>();
    for (int i = 0; i < 9; i++) {
      root.getList().add(i);
    }
    TreeViewModel model = new TreeViewModel() {
      @Override
      public NodeInfo<?> getNodeInfo(Object value) {
        if (value == null) {
          // Return the root node.
          return new DefaultNodeInfo<Integer>(root, intCell);
        } else {
          // Return a child node.
          return new DefaultNodeInfo<String>(new ListDataProvider<String>(), new TextCell());
        }
      }
      @Override
      public boolean isLeaf(Object value) {
        return false;
      }
    };
    CellTree tree = createAbstractCellTree(model, null);
    RootPanel.get().add(tree);
    tree.rootNode.listView.presenter.flush();
    // Open the first child.
    TreeNode rootNode = tree.getRootTreeNode();
    assertEquals(1, rootNode.getChildValue(1));
    TreeNode child1 = rootNode.setChildOpen(1, true);
    assertFalse(child1.isDestroyed());
    assertTrue(rootNode.isChildOpen(1));
    // Replace all values in the list.
    List<Integer> oldData = root.getList();
    List<Integer> newData = new ArrayList<Integer>();
    for (int l : oldData) {
      newData.add(l + 100); // renders the same as the current value.
    }
    root.setList(newData);
    root.flush();
    tree.rootNode.listView.presenter.flush();
    // Child1 is closed and destroyed.
    assertFalse(rootNode.isChildOpen(1));
    assertTrue(child1.isDestroyed());
    RootPanel.get().remove(tree);
  }
  /**
   * Test that replacing a subset of children updates both the TreeNode value
   * and the underlying DOM correctly.
   */
  public void testReplaceChildren() {
    CellTree cellTree = (CellTree) tree;
    TreeNode root = cellTree.getRootTreeNode();
    // Open a couple of child nodes.
    TreeNode a = root.setChildOpen(0, true);
    TreeNode b = root.setChildOpen(1, true);
    assertEquals("a", a.getValue());
    assertEquals("ab", a.getChildValue(1));
    assertEquals("b", b.getValue());
    assertEquals("bc", b.getChildValue(2));
    // Replace "b" with a "new" value.
    model.getRootDataProvider().getList().set(1, "new");
    model.getRootDataProvider().flush();
    assertFalse(a.isDestroyed());
    assertTrue(b.isDestroyed());
    TreeNode newNode = root.setChildOpen(1, true);
    assertEquals("a", a.getValue());
    assertEquals("ab", a.getChildValue(1));
    assertEquals("new", newNode.getValue());
    assertEquals("newc", newNode.getChildValue(2));
    // Check the underlying DOM values.
    CellTreeNodeView<?> aImpl = cellTree.rootNode.getChildNode(0);
    CellTreeNodeView<?> newNodeImpl = cellTree.rootNode.getChildNode(1);
    assertEquals("a", aImpl.getCellParent().getInnerText());
    assertEquals(10, aImpl.ensureChildContainer().getChildCount());
    assertEquals("new", newNodeImpl.getCellParent().getInnerText());
  }
  public void testAriaSelectedAndExpanded() {
    CellTree cellTree = (CellTree) tree;
    TreeNode root = cellTree.getRootTreeNode();
    TreeNode newNode = root.setChildOpen(1, true);
    cellTree.rootNode.getChildNode(1).setSelected(true);
    model.getRootDataProvider().refresh();
    model.getRootDataProvider().flush();
    root.setChildOpen(1, true);
    CellTreeNodeView<?> newNodeImpl = cellTree.rootNode.getChildNode(1);
    assertEquals("true", newNodeImpl.getElement().getAttribute("aria-selected"));
    // Check aria-expanded on open
    assertEquals("true", newNodeImpl.getElement().getAttribute("aria-expanded"));
    // Check aria-expanded on close
    root.setChildOpen(1, false);
    newNodeImpl = cellTree.rootNode.getChildNode(1);
    assertEquals("false", newNodeImpl.getElement().getAttribute("aria-expanded"));
    cellTree.rootNode.getChildNode(1).setSelected(false);
    model.getRootDataProvider().refresh();
    model.getRootDataProvider().flush();
    root.setChildOpen(1, true);
    newNodeImpl = cellTree.rootNode.getChildNode(1);
    assertEquals("false", newNodeImpl.getElement().getAttribute("aria-selected"));
  }
  public void testSetDefaultNodeSize() {
    CellTree cellTree = (CellTree) tree;
    TreeNode root = cellTree.getRootTreeNode();
    assertEquals(10, root.getChildCount());
    TreeNode b = root.setChildOpen(1, true);
    assertEquals(10, b.getChildCount());
    // Change the default size.
    cellTree.setDefaultNodeSize(5);
    assertEquals(5, cellTree.getDefaultNodeSize());
    assertEquals(10, b.getChildCount());
    TreeNode d = root.setChildOpen(3, true);
    assertEquals(5, d.getChildCount());
  }
  public void testAriaAttributes() {
    CellTree cellTree = (CellTree) tree;
    TreeNode rootNode = cellTree.getRootTreeNode();
    // Open a child node.
    TreeNode aNode = rootNode.setChildOpen(0, true);
    // Check role="tree"
    assertEquals("tree", cellTree.rootNode.getElement().getAttribute("role"));
    // Check 1st level
    CellTreeNodeView<?> aView = cellTree.rootNode.getChildNode(0);
    CellTreeNodeView<?> bView = cellTree.rootNode.getChildNode(1);
    // Check treeitem role, level, posinset, and setsize
    assertEquals("treeitem", aView.getElement().getAttribute("role"));
    assertEquals("1", aView.getElement().getAttribute("aria-level"));
    assertEquals("1", aView.getElement().getAttribute("aria-posinset"));
    assertEquals("10", aView.getElement().getAttribute("aria-setsize"));
    assertEquals("treeitem", bView.getElement().getAttribute("role"));
    assertEquals("1", bView.getElement().getAttribute("aria-level"));
    assertEquals("2", bView.getElement().getAttribute("aria-posinset"));
    assertEquals("10", bView.getElement().getAttribute("aria-setsize"));
    // Check 2nd level
    assertTrue(aNode.getChildCount() != 0);
    assertEquals(aNode, aView.getTreeNode());
    assertTrue(aView.getChildCount() != 0);
    CellTreeNodeView<?> aViewChild = aView.getChildNode(0);
    // Check treeitem role, level, posinset, and setsize
    assertEquals("treeitem", aViewChild.getElement().getAttribute("role"));
    assertEquals("2", aViewChild.getElement().getAttribute("aria-level"));
    assertEquals("1", aViewChild.getElement().getAttribute("aria-posinset"));
    assertEquals("10", aViewChild.getElement().getAttribute("aria-setsize"));
    // Check aria-expanded
    assertEquals("true", aView.getElement().getAttribute("aria-expanded"));
    assertEquals("false", aViewChild.getElement().getAttribute("aria-expanded"));
    while (!aNode.isChildLeaf(0)) {
      aNode = aNode.setChildOpen(0, true);
      aView = aView.getChildNode(0);
    }
    assertEquals(aNode, aView.getTreeNode());
    assertEquals("", aView.getChildNode(0).getElement().getAttribute("aria-expanded"));
    // Change default size and check aria-setsize and aria-posinset
    cellTree.setDefaultNodeSize(5);
    TreeNode cNode = rootNode.setChildOpen(3, true);
    CellTreeNodeView<?> cView = cellTree.rootNode.getChildNode(3);
    assertEquals(5, cNode.getChildCount());
    CellTreeNodeView<?> cViewChildFirst = cView.getChildNode(0);
    assertEquals("1", cViewChildFirst.getElement().getAttribute("aria-posinset"));
    assertEquals("5", cViewChildFirst.getElement().getAttribute("aria-setsize"));
    CellTreeNodeView<?> cViewChildLast = cView.getChildNode(cView.getChildCount() - 1);
    assertEquals("5", cViewChildLast.getElement().getAttribute("aria-posinset"));
    assertEquals("5", cViewChildLast.getElement().getAttribute("aria-setsize"));
  }
  public void testExplicitKeyboardSelection() {
    CellTree cellTree = (CellTree) tree;
    TreeNode root = cellTree.getRootTreeNode();
    final String cellTreeKeyboardSelectedItemStyleName =
        cellTree.getStyle().cellTreeKeyboardSelectedItem();
    // Navigate through tree to level 4 nodes.
    TreeNode l1Node = root.setChildOpen(0, true); // a
    CellTreeNodeView<?> l1View = cellTree.rootNode.getChildNode(0);
    TreeNode l2Node = l1Node.setChildOpen(0, true); // aa
    CellTreeNodeView<?> l2View = l1View.getChildNode(0);
    TreeNode l3Node = l2Node.setChildOpen(0, true); // aaa
    CellTreeNodeView<?> l3View = l2View.getChildNode(0);
    ((CellTreeNodeView.TreeNodeImpl) l3Node).flush();
    CellTreeNodeView<?> l4View = l3View.getChildNode(0);
    // l4Node is leaf, cannot get a reference via setChildOpen except via non-public nodeView
    TreeNode l4Node = l4View.getTreeNode(); // aaaa
    // Set keyboard selected node to a subtree node, l3Node = first child of l2Node
    cellTree.setKeyboardSelectedTreeNode(l2Node, 0, true);
    assertEquals(l3View, cellTree.getKeyboardSelectedNode());
    assertEquals(l3Node, cellTree.getKeyboardSelectedTreeNode());
    assertTrue(CellTreeNodeView.getSelectionElement(l3View.getElement()).getClassName()
        .indexOf(cellTreeKeyboardSelectedItemStyleName) >= 0);
    // Set keyboard selected node to a leaf node.
    cellTree.setKeyboardSelectedTreeNode(l3Node, 0, true);
    assertEquals(l4View, cellTree.getKeyboardSelectedNode());
    assertEquals(l4Node, cellTree.getKeyboardSelectedTreeNode());
    assertTrue(CellTreeNodeView.getSelectionElement(l4View.getElement()).getClassName()
        .indexOf(cellTreeKeyboardSelectedItemStyleName) >= 0);
    l1Node.setChildOpen(0, false); // close l2node
    ((CellTreeNodeView.TreeNodeImpl) l1Node).flush();
    // Try to select a leaf node in closed subtree
    try {
      cellTree.setKeyboardSelectedTreeNode(l3Node, 0, true);
      fail("should have thrown");
    } catch (IllegalStateException e) {
      assertEquals(e.getMessage(), "TreeNode no longer exists.");
    }
    // Try to select a subtree node in closed subtree
    try {
      cellTree.setKeyboardSelectedTreeNode(l2Node, 0, true);
      fail("should have thrown");
    } catch (IllegalStateException e) {
      assertEquals(e.getMessage(), "TreeNode no longer exists.");
    }
    // Still ok to select closed subtree node
    cellTree.setKeyboardSelectedTreeNode(l1Node, 0, true);
    // Create another tree of same structure
    CellTree anotherTree = createAbstractCellTree(model, root.getValue());
    l1Node = anotherTree.getRootTreeNode().setChildOpen(0, true); // a
    ((CellTreeNodeView.TreeNodeImpl) l1Node).flush();
    // Now l1Node refers to a subtree node in anotherTree
    // Select in the same cell tree is ok
    anotherTree.setKeyboardSelectedTreeNode(l1Node, 0, true);
    // Select in a different tree will throw exception.
    try {
      cellTree.setKeyboardSelectedTreeNode(l1Node, 0, true);
      fail("should have thrown");
    } catch (IllegalArgumentException e) {
      assertEquals(e.getMessage(), "The tree node does not belong to the tree.");
    }
  }
  public void testKeyboardNavigationForUpAndDownKeys() {
    CellTree cellTree = (CellTree) tree;
    TreeNode root = cellTree.getRootTreeNode();
    // Open nodes: a and aj
    TreeNode l1Node = root.setChildOpen(0, true); // a
    assertEquals(l1Node.getValue(), "a");
    TreeNode l2Node = l1Node.setChildOpen(9, true); // aj
    assertEquals(l2Node.getValue(), "aj");
    // To force tree structure changes (internal flush()).
    assertEquals(l2Node.getChildCount(), 10);
    // CellTree structure with node 'a' and 'aj' opened.
    List<String> expectedNavigationPath = Arrays.asList("a",
        "aa", "ab", "ac", "ad", "ae", "af", "ag", "ah", "ai", "aj",
        "aja", "ajb", "ajc", "ajd", "aje", "ajf", "ajg", "ajh", "aji", "ajj",
        "b", "c", "d", "e", "f", "g", "h", "i", "j");
    // Default KeyboardSelected is at "a"
    assertEquals(cellTree.getKeyboardSelectedTreeNode().getValue(), "a");
    int steps = expectedNavigationPath.size() - 1;
    assertEquals(expectedNavigationPath, repeatKeyInTree(cellTree, KeyCodes.KEY_DOWN, steps));
    Collections.reverse(expectedNavigationPath);
    assertEquals(expectedNavigationPath, repeatKeyInTree(cellTree, KeyCodes.KEY_UP, steps));
  }
  /**
   * Repeats a keystroke in the CellTree that contains string values.
   * @return the nodes selected before starting and after each keystroke.
   */
  private List<String> repeatKeyInTree(CellTree cellTree, int keyCode, int keyCount) {
    List<String> values = new ArrayList<String>();
    values.add((String) cellTree.getKeyboardSelectedTreeNode().getValue());
    for (int i = 0; i < keyCount; i++) {
      cellTree.handleKeyNavigation(keyCode);
      values.add((String) cellTree.getKeyboardSelectedTreeNode().getValue());
    }
    return values;
  }
  @Override
  protected <T> CellTree createAbstractCellTree(TreeViewModel model, T rootValue) {
    return new CellTree(model, rootValue);
  }
}