package org.infinispan.query.backend;

import static org.infinispan.test.TestingUtil.replaceComponent;
import static org.infinispan.test.TestingUtil.withCacheManagers;
import static org.mockito.Matchers.anyCollection;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.testng.Assert.assertTrue;

import org.infinispan.Cache;
import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.configuration.cache.Index;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.query.indexmanager.IndexUpdateCommand;
import org.infinispan.query.test.Person;
import org.infinispan.remoting.rpc.RpcManager;
import org.infinispan.remoting.rpc.RpcOptions;
import org.infinispan.remoting.transport.Transport;
import org.infinispan.test.AbstractInfinispanTest;
import org.infinispan.test.MultiCacheManagerCallable;
import org.infinispan.test.fwk.TestCacheManagerFactory;
import org.mockito.ArgumentCaptor;
import org.testng.annotations.Test;

/**
 * Tests for configuring a RemoteIndexingBackend so that it does not block on RPC
 *
 * @author gustavonalle
 * @since 7.0
 */
@Test(groups = "functional", testName = "query.backend.AsyncBackendTest")
public class AsyncBackendTest extends AbstractInfinispanTest {

   @Test
   public void testWithEnablingAsync() {
      test(getBaseConfigPlus("default.worker.execution", "async"), new Assertion() {
         @Override
         public void doAssertion(RpcManager rpcManager) {
            calledIndexAsynchronously(rpcManager, "person");
         }
      });
   }

   @Test
   public void testWithEnablingAsyncForDifferentIndex() {
      test(getBaseConfigPlus("cat.worker.execution", "async"), new Assertion() {
         @Override
         public void doAssertion(RpcManager rpcManager) {
            calledIndexSynchronously(rpcManager, "person");
         }
      });
   }

   @Test
   public void testWithDefaultSettings() {
      test(getBaseConfig(), new Assertion() {
         @Override
         public void doAssertion(RpcManager rpcManager) {
            calledIndexSynchronously(rpcManager, "person");
         }
      });
   }

   @Test
   public void testWithShardedIndex() {
      ConfigurationBuilder cfg = getBaseConfigPlus(
            "default.sharding_strategy.nbr_of_shards", "2",
            "person.0.worker.execution", "async"
      );
      test(cfg, new Assertion() {
         @Override
         public void doAssertion(RpcManager rpcManager) {
            calledIndexAsynchronously(rpcManager, "person.0");
            calledIndexSynchronously(rpcManager, "person.1");
         }
      });
   }

   @Test
   public void testOverridingDefault() {
      ConfigurationBuilder cfg = getBaseConfigPlus(
            "default.worker.execution", "async",
            "person.worker.execution", "sync"
      );
      test(cfg, new Assertion() {
         @Override
         public void doAssertion(RpcManager rpcManager) {
            calledIndexSynchronously(rpcManager, "person");
         }
      });
   }

   @Test
   public void testHierarchy() {
      ConfigurationBuilder cfg = getBaseConfigPlus(
            "person.sharding_strategy.nbr_of_shards", "3",
            "default.worker.execution", "sync",
            "person.worker.execution", "async",
            "person.1.worker.execution", "sync"
      );
      test(cfg, new Assertion() {
         @Override
         public void doAssertion(RpcManager rpcManager) {
            calledIndexAsynchronously(rpcManager, "person.0");
            calledIndexSynchronously(rpcManager, "person.1");
            calledIndexAsynchronously(rpcManager, "person.2");
         }
      });
   }

   private ConfigurationBuilder getBaseConfig() {
      ConfigurationBuilder cfg = new ConfigurationBuilder();
      cfg.clustering().cacheMode(CacheMode.REPL_SYNC).indexing().index(Index.ALL)
            .addProperty("default.indexmanager", "org.infinispan.query.indexmanager.InfinispanIndexManager")
            .addProperty("lucene_version", "LUCENE_CURRENT");
      return cfg;
   }

   private ConfigurationBuilder getBaseConfigPlus(String... props) {
      assert props != null && props.length % 2 == 0;
      ConfigurationBuilder cfg = getBaseConfig();
      for (int i = 0; i < props.length; i += 2) {
         cfg.indexing().addProperty(props[i], props[i + 1]);
      }
      return cfg;
   }

   @SuppressWarnings("unchecked")
   private void calledIndexSynchronously(RpcManager rpcManager, String indexName) {
      assertIndexCall(rpcManager, indexName, true);
   }

   @SuppressWarnings("unchecked")
   private void calledIndexAsynchronously(RpcManager rpcManager, String indexName) {
      assertIndexCall(rpcManager, indexName, false);
   }

   @SuppressWarnings("unchecked")
   private void assertIndexCall(RpcManager rpcManager, String indexName, boolean sync) {
      ArgumentCaptor<IndexUpdateCommand> argument = ArgumentCaptor.forClass(IndexUpdateCommand.class);
      RpcOptions rpcOptions = rpcManager.getDefaultRpcOptions(sync);
      verify(rpcManager, atLeastOnce()).invokeRemotely(anyCollection(), argument.capture(), eq(rpcOptions));
      boolean indexCalled = false;
      for (IndexUpdateCommand updateCommand : argument.getAllValues()) {
         indexCalled |= updateCommand.getIndexName().equals(indexName);
      }
      assertTrue(indexCalled);
   }

   private static interface Assertion {
      void doAssertion(RpcManager rpcManager);
   }

   private void test(ConfigurationBuilder cfg, final Assertion assertion) {
      withCacheManagers(new MultiCacheManagerCallable(
            TestCacheManagerFactory.createClusteredCacheManager(cfg),
            TestCacheManagerFactory.createClusteredCacheManager(cfg)) {
         @Override
         public void call() {
            EmbeddedCacheManager slave = isMaster(cms[0].getCache()) ? cms[1] : cms[0];
            RpcManager wireTappedRpcManager = spyOnTransport(slave.getCache());
            slave.getCache().put(1, new Person("person1", "blurb1", 20));
            slave.getCache().put(2, new Person("person2", "blurb2", 27));
            slave.getCache().put(3, new Person("person3", "blurb3", 56));
            assertion.doAssertion(wireTappedRpcManager);
         }
      });
   }

   private boolean isMaster(Cache<?, ?> cm) {
      Transport transport = cm.getAdvancedCache().getRpcManager().getTransport();
      return transport.getCoordinator().equals(transport.getAddress());
   }

   private RpcManager spyOnTransport(Cache<?, ?> cache) {
      RpcManager rpcManager = spy(cache.getAdvancedCache().getRpcManager());
      replaceComponent(cache, RpcManager.class, rpcManager, false);
      return rpcManager;
   }

}
