package org.infinispan.client.hotrod.stress.near;

import static org.infinispan.client.hotrod.test.HotRodClientTestingUtil.killRemoteCacheManagers;
import static org.infinispan.client.hotrod.test.HotRodClientTestingUtil.withRemoteCacheManager;
import static org.infinispan.server.hotrod.test.HotRodTestingUtil.hotRodCacheConfiguration;
import static org.testng.AssertJUnit.assertNotNull;
import static org.testng.AssertJUnit.assertNull;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.Callable;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadLocalRandom;

import org.infinispan.client.hotrod.RemoteCache;
import org.infinispan.client.hotrod.RemoteCacheManager;
import org.infinispan.client.hotrod.configuration.ConfigurationBuilder;
import org.infinispan.client.hotrod.configuration.NearCacheMode;
import org.infinispan.client.hotrod.test.InternalRemoteCacheManager;
import org.infinispan.client.hotrod.test.RemoteCacheManagerCallable;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.test.TestingUtil;
import org.infinispan.test.fwk.TestCacheManagerFactory;
import org.testng.annotations.AfterClass;
import org.testng.annotations.Test;

/**
 * Manual test, requires external Hot Rod server.
 */
@Test(groups = "manual", testName = "client.hotrod.stress.near.EagerNearCacheStressTest")
public class EagerNearCacheStressTest {

   static int NUM_CLIENTS = 3;
   static int NUM_THREADS_PER_CLIENT = 10;
   static ExecutorService EXEC = Executors.newCachedThreadPool();

   static final int NUM_OPERATIONS = 10_000_000;
   static final int NUM_KEYS_PRELOAD = 1_000;
   static final int KEY_RANGE = 1_000;

   @AfterClass
   public static void shutdownExecutor() {
      EXEC.shutdown();
   }

   EmbeddedCacheManager createCacheManager() {
      return TestCacheManagerFactory.createCacheManager(hotRodCacheConfiguration());
   }

   RemoteCacheManager getRemoteCacheManager(int port) {
      return getRemoteCacheManager(port, NearCacheMode.DISABLED, -1);
   }

   RemoteCacheManager getRemoteCacheManager(int port, NearCacheMode nearCacheMode, int maxEntries) {
      ConfigurationBuilder builder = new ConfigurationBuilder();
      builder.nearCache().mode(nearCacheMode).maxEntries(maxEntries);
      builder.addServer().host("127.0.0.1").port(port);
      return new InternalRemoteCacheManager(builder.build());
   }

   public void testLocalPreloadAndGetPut10to1() {
      runPreloadAndOps(NearCacheMode.INVALIDATED, -1, 0.90);
   }

   void runPreloadAndOps(NearCacheMode nearCacheMode, int maxEntries, double getRatio) {
      EmbeddedCacheManager cm = createCacheManager();
      //HotRodServer server = createHotRodServer(cm);
      int port = 11222;
      preloadData(port);
      RemoteCacheManager[] remotecms = new RemoteCacheManager[NUM_CLIENTS];
      for (int i = 0; i < NUM_CLIENTS; i++)
         remotecms[i] = getRemoteCacheManager(port, nearCacheMode, maxEntries);
      try {
         ops(remotecms, getRatio);
      } finally {
         killRemoteCacheManagers(remotecms);
         //killServers(server);
         TestingUtil.killCacheManagers(cm);
      }
   }

   void ops(RemoteCacheManager[] remotecms, double getRatio) {
      CyclicBarrier barrier = new CyclicBarrier((NUM_CLIENTS * NUM_THREADS_PER_CLIENT) + 1);
      List<Future<Void>> futures = new ArrayList<>(NUM_CLIENTS * NUM_THREADS_PER_CLIENT);
      for (RemoteCacheManager remotecm : remotecms) {
         RemoteCache<Integer, String> remote = remotecm.getCache();
         for (int i = 0; i < NUM_THREADS_PER_CLIENT; i++) {
            Callable<Void> call = new Main(barrier, remote, getRatio);
            futures.add(EXEC.submit(call));
         }
      }
      barrierAwait(barrier); // wait for all threads to be ready
      barrierAwait(barrier); // wait for all threads to finish

      for (Future<Void> f : futures)
         futureGet(f);
   }

   void preloadData(int port) {
      // Preload data
      withRemoteCacheManager(new RemoteCacheManagerCallable(getRemoteCacheManager(port)) {
         @Override
         public void call() {
            RemoteCache<Integer, String> remote = rcm.getCache();
            Map<Integer, String> map = new HashMap<>();
            for (int i = 0; i < NUM_KEYS_PRELOAD; ++i)
               map.put(i, TestingUtil.generateRandomString(512));
            remote.putAll(map);
         }
      });
   }

   static abstract class Runner implements Callable<Void> {

      final CyclicBarrier barrier;
      final RemoteCache<Integer, String> remote;
      final double getRatio;

      Runner(CyclicBarrier barrier, RemoteCache<Integer, String> remote, double getRatio) {
         this.barrier = barrier;
         this.remote = remote;
         this.getRatio = getRatio;
      }

      @Override
      public Void call() throws Exception {
         barrierAwait(barrier);
         try {
            run();
            return null;
         } finally {
            barrierAwait(barrier);
         }
      }

      abstract void run();
   }

   final static class Main extends Runner {
      static final ThreadLocalRandom R = ThreadLocalRandom.current();

      Main(CyclicBarrier barrier, RemoteCache<Integer, String> remote, double getRatio) {
         super(barrier, remote, getRatio);
      }

      @Override
      void run() {
         double maxGetKey = KEY_RANGE * getRatio;
         for (int i = 0; i < NUM_OPERATIONS; i++) {
            int key = R.nextInt(KEY_RANGE);
            if (key < maxGetKey) {
               String value = remote.get(key);
               assertNotNull(value);
            } else {
               String prev = remote.put(key, TestingUtil.generateRandomString(512));
               assertNull(prev);
            }
         }
      }
   }

   static int barrierAwait(CyclicBarrier barrier) {
      try {
         return barrier.await();
      } catch (InterruptedException | BrokenBarrierException e) {
         throw new AssertionError(e);
      }
   }

   <T> T futureGet(Future<T> future) {
      try {
         return future.get();
      } catch (InterruptedException | ExecutionException e) {
         throw new AssertionError(e);
      }
   }

}
