package org.infinispan.tx.totalorder;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.fail;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import javax.transaction.RollbackException;

import org.infinispan.Cache;
import org.infinispan.commands.tx.PrepareCommand;
import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.context.impl.TxInvocationContext;
import org.infinispan.distribution.MagicKey;
import org.infinispan.interceptors.AsyncInterceptorChain;
import org.infinispan.interceptors.BaseCustomAsyncInterceptor;
import org.infinispan.test.MultipleCacheManagersTest;
import org.infinispan.test.TestingUtil;
import org.infinispan.transaction.TransactionProtocol;
import org.infinispan.transaction.totalorder.TotalOrderManager;
import org.infinispan.util.concurrent.IsolationLevel;
import org.testng.annotations.Test;

/**
 * Tests if the locks are cleanup after a TimeoutException
 *
 * @author Pedro Ruivo
 * @since 6.0
 */
@Test(groups = "functional", testName = "tx.totalorder.CleanupAfterFailTest")
public class CleanupAfterFailTest extends MultipleCacheManagersTest {

   public void testTimeoutCleanup() throws Exception {
      final CountDownLatch block = new CountDownLatch(1);
      final BaseCustomAsyncInterceptor interceptor = new BaseCustomAsyncInterceptor() {
         @Override
         public Object visitPrepareCommand(TxInvocationContext ctx, PrepareCommand command) throws Throwable {
            block.await();
            return invokeNext(ctx, command);
         }
      };
      final AsyncInterceptorChain chain = TestingUtil.extractComponent(cache(1), AsyncInterceptorChain.class);
      final Object key = new MagicKey(cache(1));
      try {
         chain.addInterceptor(interceptor, 0);
         tm(0).begin();
         cache(0).put(key, "v");
         tm(0).commit();
         fail("Rollback expected!");
      } catch (RollbackException e) {
         //expected
      } finally {
         block.countDown();
         chain.removeInterceptor(0);
      }

      assertNoTransactions();
      assertNoLocks();
   }

   public void testTimeoutCleanupInLocalNode() throws Exception {
      final CountDownLatch block = new CountDownLatch(1);
      final BaseCustomAsyncInterceptor interceptor = new BaseCustomAsyncInterceptor() {
         @Override
         public Object visitPrepareCommand(TxInvocationContext ctx, PrepareCommand command)
               throws Throwable {
            if (!ctx.isOriginLocal()) {
               block.await();
            }
            return invokeNext(ctx, command);
         }
      };
      final AsyncInterceptorChain chain = TestingUtil.extractComponent(cache(0), AsyncInterceptorChain.class);
      final Object key1 = new MagicKey(cache(0));
      final Object key2 = new MagicKey(cache(1));
      try {
         chain.addInterceptor(interceptor, 0);
         tm(0).begin();
         cache(0).put(key1, "v1");
         cache(0).put(key2, "v2");
         tm(0).commit();
         fail("Rollback expected!");
      } catch (RollbackException e) {
         //expected
      } finally {
         block.countDown();
         chain.removeInterceptor(0);
      }

      cache(0).put(key1, "v3");
      cache(0).put(key2, "v4");

      assertCacheValue(key1, "v3");
      assertCacheValue(key2, "v4");

      assertNoTransactions();
      assertNoLocks();
   }

   @Override
   protected final void createCacheManagers() throws Throwable {
      ConfigurationBuilder dcc = getDefaultClusteredCacheConfig(CacheMode.DIST_SYNC, true);
      dcc.transaction()
            .transactionProtocol(TransactionProtocol.TOTAL_ORDER)
            .useSynchronization(false)
            .recovery().disable();
      dcc.locking().isolationLevel(IsolationLevel.REPEATABLE_READ);
      dcc.clustering().hash()
            .numOwners(1)
            .numSegments(60);
      dcc.clustering().remoteTimeout(1, TimeUnit.SECONDS);
      createCluster(dcc, 2);
      waitForClusterToForm();
   }

   private void assertCacheValue(Object key, Object value) {
      for (Cache cache : caches()) {
         assertEquals(cache.get(key), value, "Wrong value for cache " + address(cache) + ". key=" + key);
      }
   }

   private void assertNoLocks() {
      eventually(() -> {
         for (Cache cache : caches()) {
            if (TestingUtil.extractComponent(cache, TotalOrderManager.class).hasAnyLockAcquired()) {
               return false;
            }
         }
         return true;
      });
   }

}
