package org.infinispan.statetransfer;

import static java.util.concurrent.TimeUnit.SECONDS;
import static org.infinispan.test.TestingUtil.blockUntilViewsReceived;
import static org.infinispan.test.TestingUtil.extractGlobalComponentRegistry;
import static org.infinispan.test.TestingUtil.waitForNoRebalance;
import static org.testng.AssertJUnit.assertEquals;

import java.io.ByteArrayInputStream;
import java.util.concurrent.Future;

import org.infinispan.Cache;
import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.configuration.global.GlobalConfigurationBuilder;
import org.infinispan.lifecycle.ComponentStatus;
import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.test.MultipleCacheManagersTest;
import org.infinispan.test.fwk.CleanupAfterMethod;
import org.infinispan.test.fwk.JGroupsConfigBuilder;
import org.infinispan.test.fwk.TestResourceTracker;
import org.infinispan.test.fwk.TransportFlags;
import org.jgroups.JChannel;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

/**
 * Tests concurrent startup of cache managers when the channel is started externally
 * and injected with a JGroupsChannelLookup.
 *
 * @author Dan Berindei
 * @since 8.2
 */
@Test(testName = "statetransfer.ConcurrentStartChanelLookupTest", groups = "functional")
@CleanupAfterMethod
public class ConcurrentStartChanelLookupTest extends MultipleCacheManagersTest {

   public static final String CACHE_NAME = "repl";

   @Override
   protected void createCacheManagers() throws Throwable {
      // The test method will create the cache managers
   }

   @DataProvider(name = "startOrder")
   public Object[][] startOrder() {
      return new Object[][]{{0, 1}, {1, 0}};
   }

   @Test(timeOut = 60000, dataProvider = "startOrder")
   public void testConcurrentStart(int eagerManager, int lazyManager) throws Exception {
      TestResourceTracker.testThreadStarted(this);
      String name1 = TestResourceTracker.getNextNodeName();
      String name2 = TestResourceTracker.getNextNodeName();

      // Create and connect both channels beforehand
      // We need both nodes in the view when the coordinator's ClusterTopologyManagerImpl starts
      // in order to reproduce the ISPN-5106 deadlock
      JChannel ch1 = createChannel(name1, 0);
      JChannel ch2 = createChannel(name2, 1);

      // Use a JGroupsChannelLookup to pass the created channels to the transport
      EmbeddedCacheManager cm1 = createCacheManager(name1, ch1);
      EmbeddedCacheManager cm2 = createCacheManager(name2, ch2);

      try {
         assertEquals(ComponentStatus.INSTANTIATED, extractGlobalComponentRegistry(cm1).getStatus());
         assertEquals(ComponentStatus.INSTANTIATED, extractGlobalComponentRegistry(cm2).getStatus());

         log.debugf("Channels created. Starting the caches");
         Future<Object> repl1Future = fork(() -> manager(eagerManager).getCache(CACHE_NAME));

         // If eagerManager == 0, the coordinator broadcasts a GET_STATUS command.
         // If eagerManager == 1, the non-coordinator sends a JOIN command to the coordinator.
         // We want to start the lazyManager without receiving these commands, then the eagerManager should
         // retry and succeed.
         // Bundling and retransmission in NAKACK2 mean we need an extra wait after lazyManager sent its
         // command, so we don't try to wait for a precise amount of time.
         Thread.sleep(1000);

         Future<Object> repl2Future = fork(() -> manager(lazyManager).getCache(CACHE_NAME));

         repl1Future.get(10, SECONDS);
         repl2Future.get(10, SECONDS);

         Cache<String, String> c1r = cm1.getCache(CACHE_NAME);
         Cache<String, String> c2r = cm2.getCache(CACHE_NAME);

         blockUntilViewsReceived(10000, cm1, cm2);
         waitForNoRebalance(c1r, c2r);

         c1r.put("key", "value");
         assertEquals("value", c2r.get("key"));
      } finally {
         cm1.stop();
         cm2.stop();

         ch1.close();
         ch2.close();
      }
   }

   private EmbeddedCacheManager createCacheManager(String name1, JChannel ch1) {
      GlobalConfigurationBuilder gcb1 = new GlobalConfigurationBuilder();
      gcb1.transport().nodeName(ch1.getName()).distributedSyncTimeout(10, SECONDS);
      CustomChannelLookup.configureTransportWithChannel(gcb1, ch1, name1, false);

      ConfigurationBuilder replCfg = new ConfigurationBuilder();
      replCfg.clustering().cacheMode(CacheMode.REPL_SYNC);
      replCfg.clustering().stateTransfer().timeout(10, SECONDS);

      EmbeddedCacheManager cm1 = new DefaultCacheManager(gcb1.build(), replCfg.build(), false);
      registerCacheManager(cm1);
      cm1.defineConfiguration(CACHE_NAME, replCfg.build());
      return cm1;
   }

   private JChannel createChannel(String name, int portRange) throws Exception {
      String configString = JGroupsConfigBuilder
            .getJGroupsConfig(ConcurrentStartChanelLookupTest.class.getName(),
                  new TransportFlags().withPortRange(portRange));
      JChannel channel = new JChannel(new ByteArrayInputStream(configString.getBytes()));
      channel.setName(name);
      channel.connect(ConcurrentStartChanelLookupTest.class.getSimpleName());
      log.tracef("Channel %s connected: %s", channel, channel.getViewAsString());
      return channel;
   }
}
