package org.infinispan.distribution;

import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

import org.infinispan.Cache;
import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.manager.CacheContainer;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.test.AbstractInfinispanTest;
import org.infinispan.test.MultipleCacheManagersTest;
import org.infinispan.test.TestingUtil;
import org.infinispan.test.fwk.TestCacheManagerFactory;
import org.infinispan.commons.test.TestResourceTracker;
import org.infinispan.commons.util.concurrent.CompletableFutures;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

/**
 * Tests concurrent startup of replicated and distributed caches
 *
 * @author Mircea.Markus@jboss.com
 * @since 4.1
 */
@Test(testName = "distribution.ConcurrentStartWithReplTest", groups = "functional")
public class ConcurrentStartWithReplTest extends AbstractInfinispanTest {

   private ConfigurationBuilder replCfg, distCfg;

   @BeforeClass(alwaysRun = true)
   public void setUp() {
      replCfg = MultipleCacheManagersTest.getDefaultClusteredCacheConfig(CacheMode.REPL_SYNC, false);
      replCfg.clustering().stateTransfer().fetchInMemoryState(true);

      distCfg = MultipleCacheManagersTest.getDefaultClusteredCacheConfig(CacheMode.DIST_SYNC, false);
      distCfg.clustering().stateTransfer().fetchInMemoryState(true);
   }

   @Test(timeOut = 60000)
   public void testSequence1() throws ExecutionException, InterruptedException {
      TestResourceTracker.testThreadStarted(this.getTestName());
      /*

      Sequence 1:

         C1 (repl) (becomes coord)
         C2 (dist)
         C1 (repl)
         C2 (dist)

         in the same thread.

       */

      doTest(true, false);

   }

   @Test(timeOut = 60000)
   public void testSequence2() throws ExecutionException, InterruptedException {
      TestResourceTracker.testThreadStarted(this.getTestName());
      /*

      Sequence 2:

         C1 (repl) (becomes coord)
         C2 (repl)
         C2 (dist)
         C1 (dist)

         in the same thread.

       */

      doTest(false, false);
   }

   @Test(timeOut = 60000)
   public void testSequence3() throws ExecutionException, InterruptedException {
      TestResourceTracker.testThreadStarted(this.getTestName());
      /*

      Sequence 3:

         C1 (repl) (becomes coord)
         C2 (repl)
         C1 (dist) (async thread)
         C2 (dist) (async thread)

         in the same thread, except the last two which are in separate threads

       */
      doTest(true, true);
   }

   @Test(timeOut = 60000)
   public void testSequence4() throws ExecutionException, InterruptedException {
      TestResourceTracker.testThreadStarted(this.getTestName());
      /*

      Sequence 4:

         C1 (repl) (becomes coord)
         C2 (repl)
         C2 (dist) (async thread)
         C1 (dist) (async thread)

         in the same thread, except the last two which are in separate threads

       */
      doTest(false, true);
   }

   private void doTest(boolean inOrder, boolean nonBlockingStartupForDist) throws ExecutionException, InterruptedException {
      EmbeddedCacheManager cm1 = TestCacheManagerFactory.createClusteredCacheManager(new ConfigurationBuilder());
      EmbeddedCacheManager cm2 = TestCacheManagerFactory.createClusteredCacheManager(new ConfigurationBuilder());
      try {
         cm1.defineConfiguration("r", replCfg.build());
         cm1.defineConfiguration("d", distCfg.build());
         cm2.defineConfiguration("r", replCfg.build());
         cm2.defineConfiguration("d", distCfg.build());

         // first start the repl caches
         Cache<String, String> c1r = startCache(cm1, "r", false).get();
         c1r.put("key", "value");
         Cache<String, String> c2r = startCache(cm2, "r", false).get();
         TestingUtil.blockUntilViewsReceived(10000, c1r, c2r);
         TestingUtil.waitForNoRebalance(c1r, c2r);
         assert "value".equals(c2r.get("key"));

         // now the dist ones
         Future<Cache<String, String>> c1df = startCache(inOrder ? cm1 : cm2, "d", nonBlockingStartupForDist);
         Future<Cache<String, String>> c2df = startCache(inOrder ? cm2 : cm1, "d", nonBlockingStartupForDist);
         Cache<String, String> c1d = c1df.get();
         Cache<String, String> c2d = c2df.get();

         c1d.put("key", "value");
         assert "value".equals(c2d.get("key"));
      } finally {
         TestingUtil.killCacheManagers(cm1, cm2);
      }
   }

   private Future<Cache<String, String>> startCache(final CacheContainer cm, final String cacheName, boolean nonBlockingStartup) {
      final Callable<Cache<String, String>> cacheCreator = () -> cm.getCache(cacheName);
      if (nonBlockingStartup) {
         return fork(cacheCreator);
      } else {
         try {
            Cache<String, String> cache = cacheCreator.call();
            return CompletableFuture.completedFuture(cache);
         } catch (Exception e) {
            return CompletableFutures.completedExceptionFuture(e);
         }
      }
   }

}
