package org.jboss.cache.api;

import org.jboss.cache.CacheSPI;
import org.jboss.cache.DefaultCacheFactory;
import org.jboss.cache.Fqn;
import org.jboss.cache.Node;
import org.jboss.cache.config.Configuration;
import org.jboss.cache.interceptors.OptimisticNodeInterceptor;
import org.jboss.cache.interceptors.PessimisticLockInterceptor;
import org.jboss.cache.interceptors.base.CommandInterceptor;
import org.jboss.cache.optimistic.TransactionWorkspace;
import org.jboss.cache.transaction.OptimisticTransactionEntry;
import static org.testng.AssertJUnit.*;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import javax.transaction.TransactionManager;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Tests {@link org.jboss.cache.Node}-centric operations
 *
 * @author <a href="mailto:manik@jboss.org">Manik Surtani</a>
 * @since 2.0.0
 */
@Test(groups = "functional")
public class NodeAPITest
{
   private Node<Object, Object> rootNode;

   private CacheSPI<Object, Object> cache;

   private TransactionManager tm;

   private static final Fqn<String> A = Fqn.fromString("/a"), B = Fqn.fromString("/b"), C = Fqn.fromString("/c"), D = Fqn
         .fromString("/d");

   protected boolean optimistic = false;

   @BeforeMethod(alwaysRun = true)
   public void setUp() throws Exception
   {
      // start a single cache instance
      cache = (CacheSPI<Object, Object>) new DefaultCacheFactory().createCache("META-INF/conf-test/local-tx-service.xml", false);
      cache.getConfiguration().setNodeLockingScheme(optimistic ? Configuration.NodeLockingScheme.OPTIMISTIC : Configuration.NodeLockingScheme.PESSIMISTIC);
      cache.start();
      rootNode = cache.getRoot();
      tm = cache.getTransactionManager();
   }

   @AfterMethod(alwaysRun = true)
   public void tearDown()
   {
      if (cache != null)
      {
         if (cache.getTransactionManager() != null)
         {
            try
            {
               cache.getTransactionManager().rollback();
            }
            catch (Exception e)
            {
               // don't care
            }
         }
         cache.stop();
      }
      if (rootNode != null)
      {
         rootNode = null;
      }
   }

   private void assertOptimistic()
   {
      assert cache.getConfiguration().isNodeLockingOptimistic();
      boolean interceptorChainOK = false;

      for (CommandInterceptor i : cache.getInterceptorChain())
      {
         if (i instanceof PessimisticLockInterceptor) assert false : "Not an optimistic locking chain!!";
         if (i instanceof OptimisticNodeInterceptor) interceptorChainOK = true;
      }

      assert interceptorChainOK : "Not an optimistic locking chain!!";
   }

   public void testAddingData()
   {
      if (optimistic) assertOptimistic();

      Node<Object, Object> nodeA = rootNode.addChild(A);
      nodeA.put("key", "value");

      assertEquals("value", nodeA.get("key"));
   }

   public void testAddingDataTx() throws Exception
   {
      tm.begin();
      Node<Object, Object> nodeA = rootNode.addChild(A);
      nodeA.put("key", "value");

      assertEquals("value", nodeA.get("key"));
      tm.commit();
   }

   public void testOverwritingDataTx() throws Exception
   {
      Node<Object, Object> nodeA = rootNode.addChild(A);
      nodeA.put("key", "value");
      assertEquals("value", nodeA.get("key"));
      tm.begin();
      rootNode.removeChild(A);
      cache.put(A, "k2", "v2");
      tm.commit();
      assertNull(nodeA.get("key"));
      assertEquals("v2", nodeA.get("k2"));
   }


   /**
    * Remember, Fqns are relative!!
    */
   public void testParentsAndChildren()
   {
      Node<Object, Object> nodeA = rootNode.addChild(A);
      Node<Object, Object> nodeB = nodeA.addChild(B);
      Node<Object, Object> nodeC = nodeA.addChild(C);
      Node<Object, Object> nodeD = rootNode.addChild(D);

      assertEquals(rootNode, nodeA.getParent());
      assertEquals(nodeA, nodeB.getParent());
      assertEquals(nodeA, nodeC.getParent());
      assertEquals(rootNode, nodeD.getParent());

      assertTrue(rootNode.hasChild(A));
      assertFalse(rootNode.hasChild(B));
      assertFalse(rootNode.hasChild(C));
      assertTrue(rootNode.hasChild(D));

      assertTrue(nodeA.hasChild(B));
      assertTrue(nodeA.hasChild(C));

      assertEquals(nodeA, rootNode.getChild(A));
      assertEquals(nodeD, rootNode.getChild(D));
      assertEquals(nodeB, nodeA.getChild(B));
      assertEquals(nodeC, nodeA.getChild(C));

      assertTrue(nodeA.getChildren().contains(nodeB));
      assertTrue(nodeA.getChildren().contains(nodeC));
      assertEquals(2, nodeA.getChildren().size());

      assertTrue(rootNode.getChildren().contains(nodeA));
      assertTrue(rootNode.getChildren().contains(nodeD));
      assertEquals(2, rootNode.getChildren().size());

      assertEquals(true, rootNode.removeChild(A));
      assertFalse(rootNode.getChildren().contains(nodeA));
      assertTrue(rootNode.getChildren().contains(nodeD));
      assertEquals(1, rootNode.getChildren().size());

      assertEquals("double remove", false, rootNode.removeChild(A));
      assertEquals("double remove", false, rootNode.removeChild(A.getLastElement()));
   }

   public void testLocking() throws Exception
   {
      tm.begin();
      Node<Object, Object> nodeA = rootNode.addChild(A);
      Node<Object, Object> nodeB = nodeA.addChild(B);
      Node<Object, Object> nodeC = nodeB.addChild(C);

      if (!optimistic)
      {
         assertEquals(3, cache.getNumberOfNodes());
         assertEquals(4, cache.getNumberOfLocksHeld());
      }
      tm.commit();

      tm.begin();
      assertEquals(0, cache.getNumberOfLocksHeld());
      nodeC.put("key", "value");
      if (!optimistic) assertEquals(4, cache.getNumberOfLocksHeld());
      tm.commit();
   }

   public void testImmutabilityOfData()
   {
      rootNode.put("key", "value");
      Map<Object, Object> m = rootNode.getData();
      try
      {
         m.put("x", "y");
         fail("Map should be immutable!!");
      }
      catch (Exception e)
      {
         // expected
      }

      try
      {
         rootNode.getKeys().add(new Object());
         fail("Key set should be immutable");
      }
      catch (Exception e)
      {
         // expected
      }
   }

   public void testDefensiveCopyOfData()
   {
      rootNode.put("key", "value");
      Map<Object, Object> data = rootNode.getData();
      Set<Object> keys = rootNode.getKeys();

      assert keys.size() == 1;
      assert keys.contains("key");

      assert data.size() == 1;
      assert data.containsKey("key");

      // now change stuff.

      rootNode.put("key2", "value2");

      // assert that the collections we initially got have not changed.
      assert keys.size() == 1;
      assert keys.contains("key");

      assert data.size() == 1;
      assert data.containsKey("key");
   }

   public void testDefensiveCopyOfChildren()
   {
      Fqn childFqn = Fqn.fromString("/child");
      rootNode.addChild(childFqn).put("k", "v");
      Set<Node<Object, Object>> children = rootNode.getChildren();
      Set<Object> childrenNames = rootNode.getChildrenNames();

      assert childrenNames.size() == 1;
      assert childrenNames.contains(childFqn.getLastElement());

      assert children.size() == 1;
      assert children.iterator().next().getFqn().equals(childFqn);

      // now change stuff.

      rootNode.addChild(Fqn.fromString("/child2"));

      // assert that the collections we initially got have not changed.
      assert childrenNames.size() == 1;
      assert childrenNames.contains(childFqn.getLastElement());

      assert children.size() == 1;
      assert children.iterator().next().getFqn().equals(childFqn);
   }


   public void testImmutabilityOfChildren()
   {
      rootNode.addChild(A);

      try
      {
         rootNode.getChildren().clear();
         fail("Collection of child nodes returned in getChildrenDirect() should be immutable");
      }
      catch (Exception e)
      {
         // expected
      }
   }

   public void testGetChildrenUnderTx() throws Exception
   {
      Fqn A_B = Fqn.fromRelativeFqn(A, B);
      Fqn A_C = Fqn.fromRelativeFqn(A, C);
      tm.begin();
      cache.put(A_B, "1", "1");
      cache.put(A_C, "2", "2");

      if (!optimistic)
      {
         assertEquals(3, cache.getNumberOfNodes());
         assertEquals(4, cache.getNumberOfLocksHeld());
      }
      else
      {
         TransactionWorkspace<Object, Object> w = getTransactionWorkspace();
         assert w.getNodes().size() == 4 : "Should be 4 nodes in the workspace, not " + w.getNodes().size();
         // test deltas
         List<Set<Fqn>> deltas = w.getNodes().get(Fqn.ROOT).getMergedChildren();
         assert deltas.get(0).size() == 1 : "/ should have 1 child added";
         assert deltas.get(1).size() == 0 : "/ should have 0 children removed";

         deltas = w.getNodes().get(A).getMergedChildren();
         assert deltas.get(0).size() == 2 : "/ should have 2 children added";
         assert deltas.get(1).size() == 0 : "/ should have 0 children removed";

         deltas = w.getNodes().get(A_B).getMergedChildren();
         assert deltas.get(0).size() == 0 : "/a/b should have 0 children added";
         assert deltas.get(1).size() == 0 : "/a/b should have 0 children removed";

         deltas = w.getNodes().get(A_C).getMergedChildren();
         assert deltas.get(0).size() == 0 : "/a/c should have 0 children added";
         assert deltas.get(1).size() == 0 : "/a/c should have 0 children removed";
      }

      assertEquals("Number of child", 2, cache.getRoot().getChild(A).getChildren().size());
      tm.commit();
   }

   @SuppressWarnings("unchecked")
   private TransactionWorkspace<Object, Object> getTransactionWorkspace() throws Exception
   {
      return ((OptimisticTransactionEntry) cache.getTransactionTable().get(cache.getTransactionTable().get(tm.getTransaction()))).getTransactionWorkSpace();
   }

   public void testGetChildAPI()
   {
      // creates a Node<Object, Object> with fqn /a/b/c
      rootNode.addChild(A).addChild(B).addChild(C);

      rootNode.getChild(A).put("key", "value");
      rootNode.getChild(A).getChild(B).put("key", "value");
      rootNode.getChild(A).getChild(B).getChild(C).put("key", "value");

      assertEquals("value", rootNode.getChild(A).get("key"));
      assertEquals("value", rootNode.getChild(A).getChild(B).get("key"));
      assertEquals("value", rootNode.getChild(A).getChild(B).getChild(C).get("key"));

      assertNull(rootNode.getChild(Fqn.fromElements("nonexistent")));
   }

   public void testClearingData()
   {
      rootNode.put("k", "v");
      rootNode.put("k2", "v2");
      assertEquals(2, rootNode.getKeys().size());
      rootNode.clearData();
      assertEquals(0, rootNode.getKeys().size());
      assertTrue(rootNode.getData().isEmpty());
   }

   public void testClearingDataTx() throws Exception
   {
      tm.begin();
      rootNode.put("k", "v");
      rootNode.put("k2", "v2");
      assertEquals(2, rootNode.getKeys().size());
      rootNode.clearData();
      assertEquals(0, rootNode.getKeys().size());
      assertTrue(rootNode.getData().isEmpty());
      tm.commit();
      assertTrue(rootNode.getData().isEmpty());
   }

   public void testPutData()
   {
      assertTrue(rootNode.getData().isEmpty());

      Map<Object, Object> map = new HashMap<Object, Object>();
      map.put("k1", "v1");
      map.put("k2", "v2");

      rootNode.putAll(map);

      assertEquals(2, rootNode.getData().size());
      assertEquals("v1", rootNode.get("k1"));
      assertEquals("v2", rootNode.get("k2"));

      map.clear();
      map.put("k3", "v3");

      rootNode.putAll(map);
      assertEquals(3, rootNode.getData().size());
      assertEquals("v1", rootNode.get("k1"));
      assertEquals("v2", rootNode.get("k2"));
      assertEquals("v3", rootNode.get("k3"));

      map.clear();
      map.put("k4", "v4");
      map.put("k5", "v5");

      rootNode.replaceAll(map);
      assertEquals(2, rootNode.getData().size());
      assertEquals("v4", rootNode.get("k4"));
      assertEquals("v5", rootNode.get("k5"));
   }

   public void testGetChildrenNames() throws Exception
   {
      rootNode.addChild(A).put("k", "v");
      rootNode.addChild(B).put("k", "v");

      Set<String> childrenNames = new HashSet<String>();
      childrenNames.add(A.getLastElement());
      childrenNames.add(B.getLastElement());

      assertEquals(childrenNames, rootNode.getChildrenNames());

      // now delete a child, within a tx
      tm.begin();
      rootNode.removeChild(B);
      assertFalse(rootNode.hasChild(B));
      childrenNames.remove(B.getLastElement());
      assertEquals(childrenNames, rootNode.getChildrenNames());
      tm.commit();
      assertEquals(childrenNames, rootNode.getChildrenNames());
   }

   public void testDoubleRemovalOfData() throws Exception
   {
      cache.put("/foo/1/2/3", "item", 1);
      tm.begin();
      assertEquals(cache.get("/foo/1/2/3", "item"), 1);
      cache.removeNode("/foo/1");
      assertNull(cache.getNode("/foo/1"));
      assertNull(cache.get("/foo/1", "item"));
      cache.removeNode("/foo/1/2/3");
      assertNull(cache.get("/foo/1/2/3", "item"));
      assertNull(cache.get("/foo/1", "item"));
      tm.commit();
      assertFalse(cache.exists("/foo/1"));
      assertNull(cache.get("/foo/1/2/3", "item"));
      assertNull(cache.get("/foo/1", "item"));
   }

   public void testDoubleRemovalOfData2() throws Exception
   {
      cache.put("/foo/1/2", "item", 1);
      tm.begin();
      assertEquals(cache.get("/foo/1", "item"), null);
      cache.removeNode("/foo/1");
      assertNull(cache.get("/foo/1", "item"));
      cache.removeNode("/foo/1/2");
      assertNull(cache.get("/foo/1", "item"));
      tm.commit();
      assertFalse(cache.exists("/foo/1"));
      assertNull(cache.get("/foo/1/2", "item"));
      assertNull(cache.get("/foo/1", "item"));
   }
}
