package org.infinispan.atomic;

import static org.testng.AssertJUnit.assertEquals;
import static org.testng.AssertJUnit.assertTrue;
import static org.testng.AssertJUnit.fail;

import java.util.Arrays;
import java.util.HashSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.test.SingleCacheManagerTest;
import org.infinispan.test.TestingUtil;
import org.infinispan.test.ValueFuture;
import org.infinispan.test.fwk.TestCacheManagerFactory;
import org.infinispan.transaction.LockingMode;
import org.infinispan.transaction.lookup.EmbeddedTransactionManagerLookup;
import org.infinispan.util.concurrent.IsolationLevel;
import org.infinispan.util.concurrent.TimeoutException;
import org.infinispan.util.logging.Log;
import org.infinispan.util.logging.LogFactory;
import org.testng.annotations.Test;


/**
 * Test class for AtomicHashMap.
 *
 * @author Mircea.Markus@jboss.com
 * @author Dan Berindei
 */
@Test(groups = "functional", testName = "atomic.AtomicHashMapPessimisticConcurrencyTest")
public class AtomicHashMapPessimisticConcurrencyTest extends SingleCacheManagerTest {
   private static final Log log = LogFactory.getLog(AtomicHashMapPessimisticConcurrencyTest.class);

   public static final String KEY = "key";
   private LockingMode lockingMode = LockingMode.PESSIMISTIC;

   @Override
   protected EmbeddedCacheManager createCacheManager() throws Exception {
      ConfigurationBuilder builder = TestCacheManagerFactory.getDefaultCacheConfiguration(true);
      builder.locking().lockAcquisitionTimeout(TestingUtil.shortTimeoutMillis());
      builder.invocationBatching().enable();
      builder.locking().isolationLevel(IsolationLevel.READ_COMMITTED);
      builder.transaction().transactionManagerLookup(new EmbeddedTransactionManagerLookup()).lockingMode(lockingMode);

      return TestCacheManagerFactory.createCacheManager(builder);
   }

   public void testConcurrentCreate() throws Exception {
      tm().begin();
      AtomicMapLookup.getAtomicMap(cache, KEY);

      final AtomicBoolean gotTimeoutException = new AtomicBoolean();
      fork(() -> {
         try {
            tm().begin();

            try {
               AtomicMapLookup.getAtomicMap(cache, KEY);
            } catch (TimeoutException e) {
               // this is the exception we were expecting
               gotTimeoutException.set(true);
            } finally {
               tm().rollback();
            }
         } catch (Exception e) {
            log.error("Unexpected error performing transaction", e);
         }
      }).get(10, TimeUnit.SECONDS);

      assert gotTimeoutException.get();
   }

   public void testLockTimeout() throws Exception {
      AtomicMap<Integer, String> atomicMap = AtomicMapLookup.getAtomicMap(cache, KEY);
      tm().begin();
      atomicMap.put(1, "");

      Future<Object> future = fork(() -> {
         tm().begin();

         try {
            AtomicMap<Integer, String> otMap = AtomicMapLookup.getAtomicMap(cache, KEY);
            otMap.put(1, "val");
         } finally {
            tm().rollback();
         }
         return null;
      });

      try {
         future.get(10, TimeUnit.SECONDS);
         fail("Should have failed with a TimeoutException");
      } catch (ExecutionException e) {
         assertTrue(e.getCause() instanceof TimeoutException);
      }
   }

   public void testConcurrentPut() throws Exception {
      final CountDownLatch readLatch = new CountDownLatch(1);
      final CountDownLatch commitLatch = new CountDownLatch(1);

      AtomicMap<Integer, String> atomicMap = AtomicMapLookup.getAtomicMap(cache, KEY);
      tm().begin();
      atomicMap.put(1, "value1");

      Future<Object> future = fork(() -> {
         try {
            tm().begin();
            AtomicMap<Integer, String> otMap = AtomicMapLookup.getAtomicMap(cache, KEY);

            assertEquals(0, otMap.size());
            readLatch.countDown();

            otMap.put(2, "value2");
            commitLatch.await(10, TimeUnit.SECONDS);

            tm().commit();
         } catch (Exception e) {
            tm().rollback();
            throw e;
         }
         return null;
      });

      readLatch.await(10, TimeUnit.SECONDS);
      tm().commit();
      commitLatch.countDown();

      future.get(10, TimeUnit.SECONDS);
      assertEquals(new HashSet<>(Arrays.asList(1, 2)), atomicMap.keySet());
   }

   public void testConcurrentRemove() throws Exception {
      final CountDownLatch readLatch = new CountDownLatch(1);
      final CountDownLatch commitLatch = new CountDownLatch(1);

      AtomicMap<Integer, String> atomicMap = AtomicMapLookup.getAtomicMap(cache, KEY);
      tm().begin();
      atomicMap.put(1, "value1");
      atomicMap.put(2, "value2");
      atomicMap.put(3, "value3");
      tm().commit();

      tm().begin();
      atomicMap.remove(1);

      Future<Object> future = fork(() -> {
         try {
            tm().begin();
            AtomicMap<Integer, String> otMap = AtomicMapLookup.getAtomicMap(cache, KEY);

            assertEquals(3, otMap.size());
            readLatch.countDown();

            otMap.remove(2);
            commitLatch.await(10, TimeUnit.SECONDS);

            tm().commit();
         } catch (Exception e) {
            tm().rollback();
            throw e;
         }
         return null;
      });

      readLatch.await(10, TimeUnit.SECONDS);
      tm().commit();
      commitLatch.countDown();

      future.get(10, TimeUnit.SECONDS);
      assertEquals(new HashSet<>(Arrays.asList(3)), atomicMap.keySet());
   }

   public void testReadAfterTxStarted() throws Exception {
      AtomicMap<Integer, String> atomicMap = AtomicMapLookup.getAtomicMap(cache, KEY);
      atomicMap.put(1, "existing");
      tm().begin();
      atomicMap.put(1, "newVal");

      final ValueFuture responseBeforeCommit = new ValueFuture();
      final ValueFuture responseAfterCommit = new ValueFuture();
      final CountDownLatch commitLatch = new CountDownLatch(1);

      Future<Object> future = fork(() -> {
         tm().begin();

         try {
            AtomicMap<Integer, String> otMap = AtomicMapLookup.getAtomicMap(cache, KEY);

            responseBeforeCommit.set(otMap.get(1));

            // wait until the main thread commits the transaction
            commitLatch.await();

            responseAfterCommit.set(otMap.get(1));
         } finally {
            tm().rollback();
         }
         return null;
      });

      assertEquals("existing", responseBeforeCommit.get());

      tm().commit();
      commitLatch.countDown();

      future.get(10, TimeUnit.SECONDS);
      assertEquals("newVal", atomicMap.get(1));
      assertEquals("newVal", responseAfterCommit.get());
   }
}
