package org.jboss.cache.api;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.cache.CacheSPI;
import org.jboss.cache.DefaultCacheFactory;
import org.jboss.cache.Fqn;
import org.jboss.cache.Node;
import org.jboss.cache.NodeNotExistsException;
import org.jboss.cache.config.CacheLoaderConfig;
import org.jboss.cache.config.Configuration;
import org.jboss.cache.factories.XmlConfigurationParser;
import org.jboss.cache.loader.DummyInMemoryCacheLoader;
import org.jboss.cache.util.TestingUtil;
import org.jboss.cache.xml.XmlHelper;
import static org.testng.AssertJUnit.*;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import org.w3c.dom.Element;

import javax.transaction.TransactionManager;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.CountDownLatch;

/**
 * Excercises and tests the new move() api
 *
 * @author <a href="mailto:manik@jboss.org">Manik Surtani</a>
 * @since 2.0.0
 */
public class NodeMoveAPITest
{
   protected final Log log = LogFactory.getLog(getClass());

   protected Node<Object, Object> rootNode, nodeA, nodeB, nodeC, nodeD, nodeE;
   protected CacheSPI<Object, Object> cache;
   protected TransactionManager tm;
   protected static final Fqn<String> A = Fqn.fromString("/a"), B = Fqn.fromString("/b"), C = Fqn.fromString("/c"), D = Fqn.fromString("/d"), E = Fqn.fromString("/e");
   protected Object k = "key", vA = "valueA", vB = "valueB", vC = "valueC", vD = "valueD", vE = "valueE";

   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()
   {
      TestingUtil.killCaches(cache);
   }

   @Test(groups = {"functional"})
   public void testBasicMove()
   {
      nodeA = rootNode.addChild(A);
      nodeA.put(k, vA);
      nodeB = rootNode.addChild(B);
      nodeB.put(k, vB);
      nodeC = nodeA.addChild(C);
      nodeC.put(k, vC);
      /*
         /a/c
         /b
       */

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

      // test data
      assertEquals("" + nodeA, vA, nodeA.get(k));
      assertEquals(vB, nodeB.get(k));
      assertEquals(vC, nodeC.get(k));

      // parentage
      assertEquals(nodeA, nodeC.getParent());

      log.info("BEFORE MOVE " + cache);
      // move
      cache.move(nodeC.getFqn(), nodeB.getFqn());

      // re-fetch nodeC
      nodeC = cache.getNode(Fqn.fromRelativeFqn(nodeB.getFqn(), C));

      log.info("POST MOVE " + cache);
      log.info("HC " + nodeC + " " + System.identityHashCode(nodeC));
      Node x = cache.getRoot().getChild(Fqn.fromString("b/c"));
      log.info("HC " + x + " " + System.identityHashCode(x));
      /*
         /a
         /b/c
      */
      assertEquals("NODE C " + nodeC, "/b/c", nodeC.getFqn().toString());

      assertTrue(rootNode.hasChild(A));
      assertTrue(rootNode.hasChild(B));
      assertFalse(rootNode.hasChild(C));
      assertFalse(nodeA.hasChild(C));
      assertTrue(nodeB.hasChild(C));

      // test data
      assertEquals(vA, nodeA.get(k));
      assertEquals(vB, nodeB.get(k));
      assertEquals(vC, nodeC.get(k));

      // parentage
      assertEquals("B is parent of C: " + nodeB, nodeB, nodeC.getParent());
   }

   @SuppressWarnings("unchecked")
   private Node<Object, Object> genericize(Node node)
   {
      return (Node<Object, Object>) node;
   }

   @Test(groups = {"functional"})
   public void testMoveWithChildren()
   {
      nodeA = rootNode.addChild(A);
      nodeA.put(k, vA);
      nodeB = rootNode.addChild(B);
      nodeB.put(k, vB);
      nodeC = nodeA.addChild(C);
      nodeC.put(k, vC);
      nodeD = nodeC.addChild(D);
      nodeD.put(k, vD);
      nodeE = nodeD.addChild(E);
      nodeE.put(k, vE);

      assertTrue(rootNode.hasChild(A));
      assertTrue(rootNode.hasChild(B));
      assertFalse(rootNode.hasChild(C));
      assertTrue(nodeA.hasChild(C));
      assertTrue(nodeC.hasChild(D));
      assertTrue(nodeD.hasChild(E));

      // test data
      assertEquals(vA, nodeA.get(k));
      assertEquals(vB, nodeB.get(k));
      assertEquals(vC, nodeC.get(k));
      assertEquals(vD, nodeD.get(k));
      assertEquals(vE, nodeE.get(k));

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

      // move
      log.info("move " + nodeC + " to " + nodeB);
      cache.move(nodeC.getFqn(), nodeB.getFqn());
      //System.out.println("nodeB " + nodeB);
      //System.out.println("nodeC " + nodeC);

      // child nodes will need refreshing, since existing pointers will be stale.
      nodeC = nodeB.getChild(C);
      nodeD = nodeC.getChild(D);
      nodeE = nodeD.getChild(E);

      assertTrue(rootNode.hasChild(A));
      assertTrue(rootNode.hasChild(B));
      assertFalse(rootNode.hasChild(C));
      assertFalse(nodeA.hasChild(C));
      assertTrue(nodeB.hasChild(C));
      assertTrue(nodeC.hasChild(D));
      assertTrue(nodeD.hasChild(E));

      // test data
      assertEquals(vA, nodeA.get(k));
      assertEquals(vB, nodeB.get(k));
      assertEquals(vC, nodeC.get(k));
      assertEquals(vD, nodeD.get(k));
      assertEquals(vE, nodeE.get(k));

      // parentage
      assertEquals(rootNode, nodeA.getParent());
      assertEquals(rootNode, nodeB.getParent());
      assertEquals(nodeB, nodeC.getParent());
      assertEquals(nodeC, nodeD.getParent());
      assertEquals(nodeD, nodeE.getParent());
   }

   @Test(groups = {"functional"})
   public void testTxCommit() throws Exception
   {
      nodeA = rootNode.addChild(A);
      nodeB = nodeA.addChild(B);

      assertEquals(rootNode, nodeA.getParent());
      assertEquals(nodeA, nodeB.getParent());
      assertEquals(nodeA, rootNode.getChildren().iterator().next());
      assertEquals(nodeB, nodeA.getChildren().iterator().next());

      tm.begin();
      // move node B up to hang off the root
      cache.move(nodeB.getFqn(), Fqn.ROOT);

      tm.commit();

      nodeB = rootNode.getChild(B);

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

      assertTrue(rootNode.getChildren().contains(nodeA));
      assertTrue(rootNode.getChildren().contains(nodeB));

      assertTrue(nodeA.getChildren().isEmpty());
   }

   @Test(groups = {"functional"})
   public void testTxRollback() throws Exception
   {
      nodeA = rootNode.addChild(A);
      nodeB = nodeA.addChild(B);

      assertEquals(rootNode, nodeA.getParent());
      assertEquals(nodeA, nodeB.getParent());
      assertEquals(nodeA, rootNode.getChildren().iterator().next());
      assertEquals(nodeB, nodeA.getChildren().iterator().next());


      tm.begin();
      // move node B up to hang off the root
      cache.move(nodeB.getFqn(), Fqn.ROOT);

      // need to think of a way to test the same with optimistically locked nodes
      if (!optimistic)
      {
         assertEquals(rootNode, nodeA.getParent());
         assertEquals(rootNode, nodeB.getParent());
         assertTrue(rootNode.getChildren().contains(nodeA));
         assertTrue(rootNode.getChildren().contains(nodeB));
         assertTrue(nodeA.getChildren().isEmpty());
      }


      tm.rollback();

      nodeA = rootNode.getChild(A);
      nodeB = nodeA.getChild(B);

      // should revert
      assertEquals(rootNode, nodeA.getParent());
      assertEquals(nodeA, nodeB.getParent());
      assertEquals(nodeA, rootNode.getChildren().iterator().next());
      assertEquals(nodeB, nodeA.getChildren().iterator().next());
   }

   @Test(groups = {"functional"})
   public void testWithCacheloaders() throws Exception
   {
      doCacheLoaderTest(false, false);
   }

   @Test(groups = {"functional"})
   public void testWithPassivation() throws Exception
   {
      doCacheLoaderTest(true, false);
   }

   @Test(groups = {"functional"})
   public void testWithCacheloadersTx() throws Exception
   {
      doCacheLoaderTest(false, true);
   }

   @Test(groups = {"functional"})
   public void testWithPassivationTx() throws Exception
   {
      doCacheLoaderTest(true, true);
   }

   protected void doCacheLoaderTest(boolean pasv, boolean useTx) throws Exception
   {
//      cache.stop();
      cache.destroy();
      cache.getConfiguration().setCacheLoaderConfig(getSingleCacheLoaderConfig(pasv, "", DummyInMemoryCacheLoader.class.getName(), null, false, false, false, false));
      cache.start();

      DummyInMemoryCacheLoader loader = (DummyInMemoryCacheLoader) cache.getCacheLoaderManager().getCacheLoader();

      rootNode.put("key", "value");

      if (!pasv)
      {
         Map m = loader.get(Fqn.ROOT);
         assertNotNull("Should not be null", m);
         assertEquals("value", m.get("key"));
      }

      nodeA = rootNode.addChild(A);
      nodeA.put(k, vA);
      nodeB = rootNode.addChild(B);
      nodeB.put(k, vB);
      nodeC = nodeA.addChild(C);
      nodeC.put(k, vC);
      nodeD = nodeC.addChild(D);
      nodeD.put(k, vD);
      nodeE = nodeD.addChild(E);
      nodeE.put(k, vE);
      cache.evict(Fqn.ROOT, true);

      // move
      if (useTx) tm.begin();
      cache.move(nodeC.getFqn(), nodeB.getFqn());
      if (useTx) tm.commit();

      // after eviction, the node objects we hold are probably stale.
      nodeA = rootNode.getChild(A);
      nodeB = rootNode.getChild(B);
      nodeC = nodeB.getChild(C);
      log.info("nodeC get child B ");
      nodeD = nodeC.getChild(D);
      log.info("nodeD get child E ");
      nodeE = nodeD.getChild(E);

      Fqn old_C = C.clone();
      Fqn old_D = Fqn.fromRelativeFqn(old_C, D);
      Fqn old_E = Fqn.fromRelativeFqn(old_D, E);

      // test data
      assertEquals(vA, nodeA.get(k));
      assertEquals(vB, nodeB.get(k));
      assertEquals(vC, nodeC.get(k));
      assertEquals(vD, nodeD.get(k));
      assertEquals(vE, nodeE.get(k));

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


      if (pasv) cache.evict(Fqn.ROOT, true);

      //now inspect the loader.
      assertEquals(vA, loader.get(nodeA.getFqn()).get(k));
      assertEquals(vB, loader.get(nodeB.getFqn()).get(k));
      assertEquals(vC, loader.get(nodeC.getFqn()).get(k));
      assertEquals(vD, loader.get(nodeD.getFqn()).get(k));
      assertEquals(vE, loader.get(nodeE.getFqn()).get(k));

      assertNull(loader.get(old_C));
      assertNull(loader.get(old_D));
      assertNull(loader.get(old_E));

   }

   @Test(groups = {"functional"})
   public void testLocksDeepMove() throws Exception
   {
      nodeA = rootNode.addChild(A);
      nodeB = nodeA.addChild(B);
      nodeD = nodeB.addChild(D);
      nodeC = rootNode.addChild(C);
      assertEquals(0, cache.getNumberOfLocksHeld());
      tm.begin();

      cache.move(nodeC.getFqn(), nodeB.getFqn());
      //       nodeC should have a RL, nodeA should have a RL, nodeB should have a WL, nodeD should have a WL

      assertEquals("ROOT should have a RL, nodeC should have a RL, nodeA should have a RL, nodeB should have a WL, nodeD should have a WL", 5, cache.getNumberOfLocksHeld());

      tm.commit();

      assertEquals(0, cache.getNumberOfLocksHeld());
   }

   @Test(groups = {"functional"})
   public void testLocks() throws Exception
   {
      nodeA = rootNode.addChild(A);
      nodeB = nodeA.addChild(B);
      nodeC = rootNode.addChild(C);
      assertEquals(0, cache.getNumberOfLocksHeld());
      tm.begin();

      cache.move(nodeC.getFqn(), nodeB.getFqn());

      assertEquals("ROOT should have a RL, nodeC should have a RL, nodeA should have a RL, nodeB should have a WL", 4, cache.getNumberOfLocksHeld());

      tm.commit();

      assertEquals(0, cache.getNumberOfLocksHeld());
   }


   @Test(groups = {"functional"})
   public void testConcurrency() throws InterruptedException
   {
      // FIXME: investigate intermittent failure when in optimistic mode.
      if (optimistic) return;

      final int N = 3;// number of threads
      final int loops = 1 << 6;// number of loops
      // tests a tree structure as such:
      // /a
      // /b
      // /c
      // /d
      // /e
      // /x
      // /y

      // N threads constantly move /x and /y around to hang off either /a ~ /e randomly.

      final Fqn<String> FQN_A = A, FQN_B = B, FQN_C = C, FQN_D = D, FQN_E = E, FQN_X = Fqn.fromString("/x"), FQN_Y = Fqn.fromString("/y");

      // set up the initial structure.
      final Node[] NODES = {
            rootNode.addChild(FQN_A), rootNode.addChild(FQN_B),
            rootNode.addChild(FQN_C), rootNode.addChild(FQN_D), rootNode.addChild(FQN_E)
      };

      final Node<Object, Object> NODE_X = genericize(NODES[0]).addChild(FQN_X);
      final Node<Object, Object> NODE_Y = genericize(NODES[1]).addChild(FQN_Y);

      Thread[] movers = new Thread[N];
      final CountDownLatch latch = new CountDownLatch(1);
      final Random r = new Random();

      for (int i = 0; i < N; i++)
      {
         movers[i] = new Thread("Mover-" + i)
         {
            public void run()
            {
               try
               {
                  latch.await();
               }
               catch (InterruptedException e)
               {
               }

               for (int counter = 0; counter < loops; counter++)
               {

                  System.out.println(getName() + ": Attempt " + counter);
                  try
                  {
                     cache.move(NODE_X.getFqn(), NODES[r.nextInt(NODES.length)].getFqn());
                  }
                  catch (NodeNotExistsException e)
                  {
                     // this may happen ...
                  }
                  TestingUtil.sleepRandom(250);
                  try
                  {
                     cache.move(NODE_Y.getFqn(), NODES[r.nextInt(NODES.length)].getFqn());
                  }
                  catch (NodeNotExistsException e)
                  {
                     // this may happen ...
                  }
                  TestingUtil.sleepRandom(250);
               }
            }
         };
         movers[i].start();
      }

      latch.countDown();

      for (Thread t : movers)
      {
         t.join();
      }

      assertEquals(0, cache.getNumberOfLocksHeld());
      boolean found_x = false, found_x_again = false;
      for (Node erased : NODES)
      {
         Node<Object, Object> n = genericize(erased);
         if (!found_x)
         {
            found_x = n.hasChild(FQN_X);
         }
         else
         {
            found_x_again = found_x_again || n.hasChild(FQN_X);
         }
      }
      boolean found_y = false, found_y_again = false;
      for (Node erased : NODES)
      {
         Node<Object, Object> n = genericize(erased);
         if (!found_y)
         {
            found_y = n.hasChild(FQN_Y);
         }
         else
         {
            found_y_again = found_y_again || n.hasChild(FQN_Y);
         }
      }

      assertTrue("Should have found x", found_x);
      assertTrue("Should have found y", found_y);
      assertFalse("Should have only found x once", found_x_again);
      assertFalse("Should have only found y once", found_y_again);
   }

   @Test(groups = {"functional"})
   public void testMoveInSamePlace()
   {
      final Fqn<String> FQN_X = Fqn.fromString("/x");
      // set up the initial structure.
      Node aNode = rootNode.addChild(A);
      Node xNode = aNode.addChild(FQN_X);
      assertEquals(aNode.getChildren().size(), 1);
      cache.move(xNode.getFqn(), aNode.getFqn());
      assertEquals(aNode.getChildren().size(), 1);
   }

   protected CacheLoaderConfig getSingleCacheLoaderConfig(boolean passivation, String preload, String cacheloaderClass, String properties, boolean async, boolean fetchPersistentState, boolean shared, boolean purgeOnStartup) throws Exception
   {
      String xml = "<config>\n" +
            "<passivation>" + passivation + "</passivation>\n" +
            "<preload>" + preload + "</preload>\n" +
            "<cacheloader>\n" +
            "<class>" + cacheloaderClass + "</class>\n" +
            "<properties>" + properties + "</properties>\n" +
            "<async>" + async + "</async>\n" +
            "<shared>" + shared + "</shared>\n" +
            "<fetchPersistentState>" + fetchPersistentState + "</fetchPersistentState>\n" +
            "<purgeOnStartup>" + purgeOnStartup + "</purgeOnStartup>\n" +
            "</cacheloader>\n" +
            "</config>";
      Element element = XmlHelper.stringToElement(xml);
      return XmlConfigurationParser.parseCacheLoaderConfig(element);
   }
}
