/*
 * JBoss, Home of Professional Open Source
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */
package org.jboss.cache.buddyreplication;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jboss.cache.CacheSPI;
import org.jboss.cache.Fqn;
import org.jboss.cache.util.TestingUtil;
import org.jboss.cache.notifications.annotation.CacheBlocked;
import org.jboss.cache.notifications.annotation.CacheListener;
import org.jboss.cache.notifications.annotation.CacheUnblocked;
import org.jboss.cache.notifications.event.Event;
import org.jboss.cache.util.CachePrinter;
import static org.testng.AssertJUnit.*;
import org.testng.annotations.Test;

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

/**
 * Teststhe transfer of content under *normal* operation
 *
 * @author <a href="mailto:manik@jboss.org">Manik Surtani (manik@jboss.org)</a>
 */
@Test(groups = {"functional"})
public class BuddyReplicationContentTest extends BuddyReplicationTestsBase
{
   private String key = "key";
   private String value = "value";
   private Log log = LogFactory.getLog(BuddyGroupAssignmentTest.class);
   BuddyFqnTransformer fqnTransformer = new BuddyFqnTransformer();


   private void assertNoStaleLocks(List<CacheSPI<Object, Object>> caches)
   {
      for (CacheSPI<Object, Object> cache : caches) assertNoStaleLocks(cache);
   }

   private void assertNoStaleLocks(CacheSPI<Object, Object> cache)
   {
      assertEquals("Number of locks in cache instance " + cache + " should be 0", 0, cache.getNumberOfLocksHeld());
   }

   public void testSimplePut() throws Exception
   {
      log.debug("Running testSimplePut");
      caches = createCaches(3, false);

      String fqn = "/test";
      String backupFqn = "/" + BuddyManager.BUDDY_BACKUP_SUBTREE + "/" + fqnTransformer.getGroupNameFromAddress(caches.get(0).getLocalAddress()) + fqn;

      assertNoStaleLocks(caches);

      // put something in cache 1
      caches.get(0).put(fqn, key, value);

      assertNoStaleLocks(caches);

      // this should be in neither of the other caches' "main" trees
      assertEquals(value, caches.get(0).get(fqn, key));
      assertNull("Should be null", caches.get(1).get(fqn, key));
      assertNull("Should be null", caches.get(2).get(fqn, key));

      // check the backup trees

      assertEquals("Buddy should have data in backup tree", value, caches.get(1).get(backupFqn, key));
      assertNull("Should be null", caches.get(2).get(backupFqn, key));

      assertNoStaleLocks(caches);

      System.out.println("Cache 0 = " + CachePrinter.printCacheLockingInfo(caches.get(0)));
      System.out.println("Cache 1 = " + CachePrinter.printCacheLockingInfo(caches.get(1)));
      System.out.println("Cache 2 = " + CachePrinter.printCacheLockingInfo(caches.get(2)));
   }

   public void testPutAndRemove() throws Exception
   {
      log.debug("Running testPutAndRemove");
      caches = createCaches(3, false);

      String fqn = "/test";
      String backupFqn = "/" + BuddyManager.BUDDY_BACKUP_SUBTREE + "/" + fqnTransformer.getGroupNameFromAddress(caches.get(0).getLocalAddress()) + fqn;

      assertNoStaleLocks(caches);

      // put something in cache 1
      caches.get(0).put(fqn, key, value);

      assertNoStaleLocks(caches);

      // this should be in neither of the other caches' "main" trees
      assertEquals(value, caches.get(0).get(fqn, key));
      assertNull("Should be null", caches.get(1).get(fqn, key));
      assertNull("Should be null", caches.get(2).get(fqn, key));

      // check the backup trees

      assertEquals("Buddy should have data in backup tree", value, caches.get(1).get(backupFqn, key));
      assertNull("Should be null", caches.get(2).get(backupFqn, key));

      assertNoStaleLocks(caches);

      // now remove
      caches.get(0).removeNode(fqn);
      assertNoStaleLocks(caches);


      assertNull("Should be null", caches.get(0).get(fqn, key));
      assertNull("Should be null", caches.get(1).get(fqn, key));
      assertNull("Should be null", caches.get(2).get(fqn, key));

      // check the backup trees
      assertNull("Should be null", caches.get(0).get(backupFqn, key));
      assertNull("Should be null", caches.get(1).get(backupFqn, key));
      assertNull("Should be null", caches.get(2).get(backupFqn, key));

      assertNoStaleLocks(caches);
   }

   public void testPutAndRemove2() throws Exception
   {
      log.debug("Running testPutAndRemove2");
      caches = createCaches(2, 4, false);

      String fqn = "/test";
      String backupFqn = "/" + BuddyManager.BUDDY_BACKUP_SUBTREE + "/" + fqnTransformer.getGroupNameFromAddress(caches.get(0).getLocalAddress()) + fqn;

      // put something in cache 1
      assertNoStaleLocks(caches);

      caches.get(0).put(fqn, key, value);

      assertNoStaleLocks(caches);

      // this should be in neither of the other caches' "main" trees
      assertEquals(value, caches.get(0).get(fqn, key));
      assertNull("Should be null", caches.get(1).get(fqn, key));
      assertNull("Should be null", caches.get(2).get(fqn, key));
      assertNull("Should be null", caches.get(3).get(fqn, key));

      // check the backup trees
      assertEquals("Buddy should have data in backup tree", value, caches.get(1).get(backupFqn, key));
      assertEquals("Buddy should have data in backup tree", value, caches.get(2).get(backupFqn, key));
      assertNull("Should be null", caches.get(3).get(backupFqn, key));

      assertNoStaleLocks(caches);

      // now remove
      caches.get(0).removeNode(fqn);
      assertNoStaleLocks(caches);

      assertNull("Should be null", caches.get(0).get(fqn, key));
      assertNull("Should be null", caches.get(1).get(fqn, key));
      assertNull("Should be null", caches.get(2).get(fqn, key));
      assertNull("Should be null", caches.get(3).get(fqn, key));

      // check the backup trees
      assertNull("Should be null", caches.get(0).get(backupFqn, key));
      assertNull("Should be null", caches.get(1).get(backupFqn, key));
      assertNull("Should be null", caches.get(2).get(backupFqn, key));
      assertNull("Should be null", caches.get(3).get(backupFqn, key));

      assertNoStaleLocks(caches);
   }

   public void testBuddyJoin() throws Exception
   {
      log.debug("Running testBuddyJoin");
      caches = createCaches(2, false);
      CacheSPI<Object, Object> cache2 = null;

      try
      {
         Fqn fqn = Fqn.fromString("/test");
         Fqn backupFqn = fqnTransformer.getBackupFqn(caches.get(1).getLocalAddress(), fqn);

         assertNoStaleLocks(caches);

         // put something in cache 1
         caches.get(1).put(fqn, key, value);

         assertNoStaleLocks(caches);

         // this should be in neither of the other caches' "main" trees
         assertEquals(value, caches.get(1).get(fqn, key));
         assertFalse("Should be false", caches.get(0).exists(fqn));

         // check the backup trees
         assertEquals("Buddy should have data in backup tree", value, caches.get(0).get(backupFqn, key));

         assertNoStaleLocks(caches);

         // now add a new cache to the cluster
         cache2 = createCache(1, null);

         // allow this cache a few msecs to join
         TestingUtil.blockUntilViewsReceived(3000, caches.get(0), caches.get(1), cache2);

         TestingUtil.sleepThread(2000); // allow buddy group reorg

         List<CacheSPI<Object, Object>> dump = new ArrayList<CacheSPI<Object, Object>>(caches);
         dump.add(cache2);
         TestingUtil.dumpCacheContents(dump);

         // now caches.get(1)'s buddy should be cache2, not cache[0]
         assertIsBuddy(caches.get(1), cache2, true);
         // this should still be the same
         assertIsBuddy(caches.get(0), caches.get(1), true);
         // and cache2's buddy should be cache[0]
         assertIsBuddy(cache2, caches.get(0), true);

         // so now the backup data we saw on cache[0] should have been removed.
         assertFalse("This backup data should have been removed", caches.get(0).exists(backupFqn));

         // while cache2 should now posess this backup (due to a state transfer)
         assertEquals("Backup state should have been transferred to this new cache instance", value, cache2.get(backupFqn, key));

         caches.get(1).removeNode(fqn);
         assertNoStaleLocks(caches);


         assertFalse("Should be null", caches.get(0).exists(fqn));
         assertFalse("Should be null", caches.get(1).exists(fqn));
         assertFalse("Should be null", cache2.exists(fqn));

         // check the backup trees
         assertFalse("Should be null", caches.get(0).exists(backupFqn));
         assertFalse("Should be null", caches.get(1).exists(backupFqn));
         assertFalse("Should be null", cache2.exists(backupFqn));

         assertNoStaleLocks(caches);

      }
      finally
      {
         if (cache2 != null) cache2.stop();
      }
   }

   //@Test (invocationCount = 25)
   public void testCompleteStateSurvival() throws Exception
   {
      log.debug("Running testCompleteStateSurvival");
      caches = null;

      caches = createCaches(3, false, true);
      CacheBlockListener blockListener = new CacheBlockListener();

      caches.get(0).addCacheListener(blockListener);
      caches.get(1).addCacheListener(blockListener);
      caches.get(2).addCacheListener(blockListener);

      caches.get(0).put("/0", "key", "value");
      caches.get(1).put("/1", "key", "value");
      caches.get(2).put("/2", "key", "value");

//      TestingUtil.sleepThread(getSleepTimeout());
      //TestingUtil.sleepThread(caches.get(0).getConfiguration().getStateRetrievalTimeout() * 3);
      blockListener.blockUntilAllCachesAreUnblocked(caches.get(0).getConfiguration().getStateRetrievalTimeout() * 3);

      log.info("stopping 2");
      caches.get(2).stop();

      blockListener.blockUntilAllCachesAreUnblocked(caches.get(0).getConfiguration().getStateRetrievalTimeout() * 5);

      assertEquals("value", caches.get(0).get("/2", "key"));

      blockListener.blockUntilAllCachesAreUnblocked(caches.get(0).getConfiguration().getStateRetrievalTimeout() * 5);

      caches.get(1).stop();

      // cache[0] is all thats left!!

      assertEquals("value", caches.get(0).get("/0", "key"));

      try
      {
         assertEquals("value", caches.get(0).get("/1", "key"));
      }
      catch (RuntimeException e)
      {
         // may barf the first time around since we are unable to contact our buddy and store this data.
         assertEquals(IllegalArgumentException.class, e.getCause().getClass());
      }

      // now try the assertion again since the local gravitation would have worked.

      System.out.println("Cache contents " + CachePrinter.printCacheDetails(caches.get(0)));

      assertEquals("value", caches.get(0).get("/1", "key"));
      assertEquals("value", caches.get(0).get("/2", "key"));
   }

   @CacheListener
   public static class CacheBlockListener
   {
      private int blocks = 0;

      @CacheBlocked
      public void processBlock(Event e)
      {
         if (e.isPre())
         {
            System.out.println(">>>>>>>> Got BLOCK on cache " + e.getCache().getLocalAddress());
            synchronized (this)
            {
               blocks++;
               notifyAll();
            }
         }
      }

      @CacheUnblocked
      public void processUnblock(Event e)
      {
         if (e.isPre())
         {
            System.out.println(">>>>>>>> Got UNBLOCK on cache " + e.getCache().getLocalAddress());
            synchronized (this)
            {
               blocks--;
               notifyAll();
            }
         }
      }

      public void blockUntilAllCachesAreUnblocked(long maxWait) throws InterruptedException
      {
         synchronized (this)
         {
            if (blocks > 1)
            {
               wait(maxWait);
            }
            // try returning anyway?
            blocks = 0;
            /*
            if (blocks > 1)
               throw new RuntimeException("Timed out waiting for unblocks.  Number of blocks = " + blocks);
            if (blocks == 1) blocks = 0;
            */
         }
      }
   }
}
