package org.infinispan.statetransfer;

import static org.infinispan.test.concurrent.StateSequencerUtil.advanceOnInterceptor;
import static org.infinispan.test.concurrent.StateSequencerUtil.matchCommand;
import static org.testng.AssertJUnit.assertEquals;
import static org.testng.AssertJUnit.assertFalse;
import static org.testng.AssertJUnit.assertTrue;
import static org.testng.AssertJUnit.fail;

import java.util.Arrays;

import org.infinispan.Cache;
import org.infinispan.commands.tx.RollbackCommand;
import org.infinispan.commands.tx.VersionedPrepareCommand;
import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.test.MultipleCacheManagersTest;
import org.infinispan.test.TestingUtil;
import org.infinispan.test.concurrent.StateSequencer;
import org.infinispan.test.fwk.CleanupAfterMethod;
import org.infinispan.transaction.TransactionMode;
import org.infinispan.transaction.xa.GlobalTransaction;
import org.infinispan.util.ControlledConsistentHashFactory;
import org.infinispan.util.concurrent.locks.LockManager;
import org.testng.annotations.Test;

/**
 * Test that a commit command that has timed out on a backup owner cannot write entries after the locks have been
 * released on the primary owner.
 */
@Test(groups = "functional", testName = "statetransfer.PrepareTimeoutTest")
@CleanupAfterMethod
public class PrepareTimeoutTest extends MultipleCacheManagersTest {
   private static final String TEST_KEY = "key";
   private static final String TX1_VALUE = "value1";
   private static final java.lang.Object TX2_VALUE = "value2";
   public static final int COMPLETED_TX_TIMEOUT = 2000;

   @Override
   protected void createCacheManagers() throws Throwable {
      ControlledConsistentHashFactory<?> consistentHashFactory = new ControlledConsistentHashFactory.Default(1, 2);

      ConfigurationBuilder builder = new ConfigurationBuilder();
      builder.clustering().cacheMode(CacheMode.DIST_SYNC);
      builder.clustering().remoteTimeout(2000);
      builder.clustering().hash().numSegments(1).consistentHashFactory(consistentHashFactory);
      builder.transaction().transactionMode(TransactionMode.TRANSACTIONAL);
      builder.transaction().completedTxTimeout(COMPLETED_TX_TIMEOUT);

      addClusterEnabledCacheManager(builder);
      addClusterEnabledCacheManager(builder);
      addClusterEnabledCacheManager(builder);
      waitForClusterToForm();
   }

   public void testCommitDoesntWriteAfterRollback() throws Exception {
      // Start a tx on A: put(k, v1), owners(k) = [B (primary) and C (backup)]
      // Block the prepare on B and C so that it times out
      // Wait for the rollback command to be executed on B and C
      // Unblock the prepare on B and C
      // Check that there are no locked keys or remote transactions on B and C
      StateSequencer sequencer = new StateSequencer();
      sequencer.logicalThread("main", "main:start", "main:check");
      sequencer.logicalThread("primary", "primary:block_prepare", "primary:after_rollback", "primary:resume_prepare",
            "primary:after_prepare");
      sequencer.logicalThread("backup", "backup:block_prepare", "backup:after_rollback", "backup:resume_prepare",
            "backup:after_prepare");

      sequencer.order("main:start", "primary:block_prepare", "primary:after_prepare", "main:check");
      sequencer.order("main:start", "backup:block_prepare", "backup:after_prepare", "main:check");

      advanceOnInterceptor(sequencer, cache(1), StateTransferInterceptor.class,
            matchCommand(VersionedPrepareCommand.class).matchCount(0).build())
            .before("primary:block_prepare", "primary:resume_prepare").after("primary:after_prepare");

      advanceOnInterceptor(sequencer, cache(1), StateTransferInterceptor.class,
            matchCommand(RollbackCommand.class).build())
            .after("primary:after_rollback");

      advanceOnInterceptor(sequencer, cache(2), StateTransferInterceptor.class,
            matchCommand(VersionedPrepareCommand.class).matchCount(0).build())
            .before("backup:block_prepare", "backup:resume_prepare").after("backup:after_prepare");

      advanceOnInterceptor(sequencer, cache(2), StateTransferInterceptor.class,
            matchCommand(RollbackCommand.class).build())
            .after("backup:after_rollback");


      assertEquals(Arrays.asList(address(1), address(2)),
                   cacheTopology(0).getDistribution(TEST_KEY).writeOwners());
      sequencer.advance("main:start");

      tm(0).begin();
      cache(0).put(TEST_KEY, TX1_VALUE);
      try {
         tm(0).commit();
         fail("Exception expected during commit");
      } catch (Exception e) {
         // expected
      }

      tm(0).begin();
      cache(0).put(TEST_KEY, TX2_VALUE);
      GlobalTransaction gtx1 = transactionTable(0).getLocalTransaction(tm(0).getTransaction()).getGlobalTransaction();
      tm(0).commit();

      // Wait for the 1st tx to be removed from the completed txs table
      Thread.sleep(COMPLETED_TX_TIMEOUT + 1000);

      assertTrue(transactionTable(1).isTransactionCompleted(gtx1));
      assertTrue(transactionTable(2).isTransactionCompleted(gtx1));

      sequencer.advance("main:check");

      LockManager lockManager1 = TestingUtil.extractLockManager(cache(1));
      assertFalse(lockManager1.isLocked(TEST_KEY));

      assertFalse(transactionTable(1).containRemoteTx(gtx1));
      assertFalse(transactionTable(2).containRemoteTx(gtx1));

      for (Cache<?, ?> cache : caches()) {
         assertEquals(TX2_VALUE, cache.get(TEST_KEY));
      }
   }
}
