package org.jboss.cache.buddyreplication;

import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import org.jboss.cache.CacheSPI;
import org.jboss.cache.Fqn;
import org.jboss.cache.notifications.annotation.CacheListener;
import org.jboss.cache.notifications.annotation.NodeCreated;
import org.jboss.cache.notifications.event.NodeCreatedEvent;
import org.jboss.cache.transaction.DummyTransactionManagerLookup;
import org.jboss.cache.util.TestingUtil;
import org.testng.annotations.Test;

/**
 * Test for handling of JBCACHE-1530.
 *
 * @author Brian Stansberry
 */
@Test(groups = "functional", testName = "buddyreplication.DataGravitationCleanupFromDefunctTreeTest")
public class DataGravitationCleanupFromDefunctTreeTest extends BuddyReplicationTestsBase
{   
   @CacheListener
   public static class GravitationBlocker
   {
      private final Fqn<String> toBlock;
      private final CountDownLatch toTrigger;
      private final CountDownLatch toAwait;
      private boolean blocked;      

      GravitationBlocker(Fqn<String> toBlock, CountDownLatch toTrigger, CountDownLatch toAwait)
      {
         this.toBlock = toBlock;
         this.toTrigger = toTrigger;
         this.toAwait = toAwait;
      }
      
      @NodeCreated
      public void nodeAdded(NodeCreatedEvent event)
      {
         if (!blocked && event.isOriginLocal() && event.getFqn().equals(toBlock))
         {
            blocked = true;
            toTrigger.countDown();
            try
            {
//               System.out.println("blocking " + System.currentTimeMillis());
               toAwait.await(10, TimeUnit.SECONDS);
//               System.out.println("released " + System.currentTimeMillis());
            }
            catch (InterruptedException e)
            {
               Thread.currentThread().interrupt();
            }
         }
      }
   }

   public void testOwnerDiesInMidGravitation() throws Exception
   {
      final List<CacheSPI<Object, Object>> caches = createCaches(1, 4, false, false, false, false);
      cachesTL.set(caches);

      for (CacheSPI<Object, Object> c : caches)
      {
         c.getConfiguration().setFetchInMemoryState(false);
         c.getConfiguration().setTransactionManagerLookupClass(DummyTransactionManagerLookup.class.getName());
         c.start();
      }

      waitForBuddy(caches.get(0), caches.get(1), false);
      waitForBuddy(caches.get(1), caches.get(2), false);
      waitForBuddy(caches.get(2), caches.get(3), false);
      waitForBuddy(caches.get(3), caches.get(0), false);
      Thread.sleep(2000);//wait for state transfer

      final Fqn<String> fqn = Fqn.fromString("/0");
      caches.get(0).put(fqn, "k", "v");
      Fqn backup = fqnTransformer.getBackupFqn(caches.get(0).getLocalAddress(), fqn);
      assert (caches.get(1).exists(backup));

      CountDownLatch toTrigger = new CountDownLatch(1);
      CountDownLatch toAwait = new CountDownLatch(1);
      GravitationBlocker blocker = new GravitationBlocker(fqn, toTrigger, toAwait);
      caches.get(2).addCacheListener(blocker);
      
      Future<?> future = Executors.newSingleThreadExecutor().submit(new Runnable() {
         public void run() {
            // Trigger a gravitation
            caches.get(2).getInvocationContext().getOptionOverrides().setForceDataGravitation(true);
            caches.get(2).get(fqn, "k");
         }
      });

      assert (toTrigger.await(5, TimeUnit.SECONDS));
      
      // Gravitation is now blocking on adding data to cache2
      
      caches.get(0).stop();
//      System.out.println("stopped 0 " + System.currentTimeMillis());
      // TODO need a way to know when this is done
      TestingUtil.sleepThread(1000); // buddy group re-formation is async 
      
      toAwait.countDown(); // gravitation can now complete      
      assert (future.get(5, TimeUnit.SECONDS) == null); // and it now is complete
      
      assert (blocker.blocked);
      
      backup = fqnTransformer.getBackupFqn(caches.get(2).getLocalAddress(), fqn);
      assert (caches.get(3).exists(backup));
      
      assert ("v".equals(caches.get(2).put(fqn, "k", "v1")));
      
      // Now we gravitate to cache1. IF it has original "v" in its :DEAD tree
      // it will gravitate that rather than asking cache3 and the assert will fail
      
      caches.get(1).getInvocationContext().getOptionOverrides().setForceDataGravitation(true);
      Object val = caches.get(1).get(fqn, "k");
      assert "v1".equals(val) : val + " is 'v1'";
   }
}
