package org.infinispan.statetransfer;

import static org.testng.AssertJUnit.assertEquals;
import static org.testng.AssertJUnit.assertNull;

import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import org.infinispan.Cache;
import org.infinispan.commands.write.InvalidateCommand;
import org.infinispan.commons.CacheConfigurationException;
import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.container.entries.InternalCacheEntry;
import org.infinispan.context.InvocationContext;
import org.infinispan.interceptors.BaseCustomAsyncInterceptor;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.persistence.dummy.DummyInMemoryStoreConfigurationBuilder;
import org.infinispan.test.Exceptions;
import org.infinispan.test.MultipleCacheManagersTest;
import org.infinispan.test.TestingUtil;
import org.infinispan.test.fwk.CleanupAfterMethod;
import org.testng.annotations.Test;

/**
 * Test if state transfer happens properly on a non-tx invalidation cache.
 *
 * @since 7.0
 */
@Test(groups = "functional", testName = "statetransfer.NonTxStateTransferInvalidationTest")
@CleanupAfterMethod
public class NonTxStateTransferInvalidationTest extends MultipleCacheManagersTest {

   public static final int NUM_KEYS = 10;
   private ConfigurationBuilder dccc;

   @Override
   protected void createCacheManagers() throws Throwable {
      dccc = getDefaultClusteredCacheConfig(CacheMode.INVALIDATION_SYNC, false, true);
      createCluster(dccc, 2);
      waitForClusterToForm();
   }

   public void testStateTransferDisabled() throws Exception {
      // Insert initial data in the cache
      Set<Object> keys = new HashSet<Object>();
      for (int i = 0; i < NUM_KEYS; i++) {
         Object key = "key" + i;
         keys.add(key);
         cache(0).put(key, key);
      }

      log.trace("State transfer happens here");
      // add a third node
      addClusterEnabledCacheManager(dccc);
      waitForClusterToForm();

      log.trace("Checking the values from caches...");
      for (Object key : keys) {
         log.tracef("Checking key: %s", key);
         // check them directly in data container
         InternalCacheEntry d0 = advancedCache(0).getDataContainer().get(key);
         InternalCacheEntry d1 = advancedCache(1).getDataContainer().get(key);
         InternalCacheEntry d2 = advancedCache(2).getDataContainer().get(key);
         assertEquals(key, d0.getValue());
         assertNull(d1);
         assertNull(d2);
      }
   }

   public void testConfigValidation() {
      ConfigurationBuilder builder1 = new ConfigurationBuilder();
      builder1.clustering().cacheMode(CacheMode.INVALIDATION_ASYNC).stateTransfer();
      builder1.validate();

      ConfigurationBuilder builder2 = new ConfigurationBuilder();
      builder2.clustering().cacheMode(CacheMode.INVALIDATION_ASYNC).stateTransfer().fetchInMemoryState(true);
      Exceptions.expectException(CacheConfigurationException.class, builder2::validate);

      ConfigurationBuilder builder3 = new ConfigurationBuilder();
      builder3.clustering().cacheMode(CacheMode.INVALIDATION_ASYNC).persistence()
            .addStore(DummyInMemoryStoreConfigurationBuilder.class);
      builder3.validate();

      ConfigurationBuilder builder4 = new ConfigurationBuilder();
      builder4.clustering().cacheMode(CacheMode.INVALIDATION_ASYNC).persistence()
            .addStore(DummyInMemoryStoreConfigurationBuilder.class).fetchPersistentState(true);
      Exceptions.expectException(CacheConfigurationException.class, builder4::validate);
   }

   public void testInvalidationDuringStateTransfer() throws Exception {
      EmbeddedCacheManager node1 = manager(0);
      Cache<String, Object> node1Cache = node1.getCache();
      EmbeddedCacheManager node2 = manager(1);
      Cache<String, Object> node2Cache = node2.getCache();
      CountDownLatch latch = new CountDownLatch(1);
      node2Cache.getAdvancedCache().getAsyncInterceptorChain().addInterceptor(new BaseCustomAsyncInterceptor() {
         @Override
         public Object visitInvalidateCommand(InvocationContext ctx, InvalidateCommand command) throws
               Throwable {
            latch.await(10, TimeUnit.SECONDS);
            return super.visitInvalidateCommand(ctx, command);
         }
      }, 0);

      String key = "key";
      Future<?> future = fork(() -> {
         node1Cache.putForExternalRead(key, new Object());
         node1Cache.remove(key);
      });

      EmbeddedCacheManager node3 = addClusterEnabledCacheManager(dccc);
      Cache<Object, Object> node3Cache = node3.getCache();
      TestingUtil.waitForNoRebalance(caches());
      log.info("Node 3 started");
      latch.countDown();

      future.get(30, TimeUnit.SECONDS);
      assertNull(node1Cache.get(key));
      assertNull(node2Cache.get(key));
      assertNull(node3Cache.get(key));
   }

}
