package org.infinispan.lock.singlelock;

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

import java.util.HashMap;
import java.util.Map;

import jakarta.transaction.RollbackException;
import jakarta.transaction.Transaction;

import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.container.entries.InternalCacheEntry;
import org.infinispan.distribution.LocalizedCacheTopology;
import org.infinispan.test.MultipleCacheManagersTest;
import org.infinispan.test.TestDataSCI;
import org.infinispan.test.TestingUtil;
import org.infinispan.test.fwk.CleanupAfterMethod;
import org.infinispan.transaction.lookup.EmbeddedTransactionManagerLookup;
import org.infinispan.transaction.tm.EmbeddedTransaction;
import org.infinispan.transaction.tm.EmbeddedTransactionManager;
import org.testng.annotations.Test;

/**
 * Main owner changes due to state transfer in a distributed cluster using optimistic locking.
 *
 * @since 5.1
 */
@Test(groups = "functional", testName = "lock.singlelock.MainOwnerChangesLockTest")
@CleanupAfterMethod
public class MainOwnerChangesLockTest extends MultipleCacheManagersTest {

   public static final int NUM_KEYS = 100;
   private ConfigurationBuilder dccc;

   @Override
   protected void createCacheManagers() throws Throwable {
      dccc = getDefaultClusteredCacheConfig(CacheMode.DIST_SYNC, true, true);
      dccc.transaction().transactionManagerLookup(new EmbeddedTransactionManagerLookup());
      dccc.clustering().hash().l1().disable().locking().lockAcquisitionTimeout(TestingUtil.shortTimeoutMillis());
      dccc.clustering().stateTransfer().fetchInMemoryState(true);
      createCluster(TestDataSCI.INSTANCE, dccc, 2);
      waitForClusterToForm();
   }

   public void testLocalTxLockMigration() throws Exception {
      testLockMigration(0);
   }

   public void testRemoteTxLockMigration() throws Exception {
      testLockMigration(1);
   }

   private void testLockMigration(int nodeThatPuts) throws Exception {
      Map<Object, EmbeddedTransaction> key2Tx = new HashMap<>();
      for (int i = 0; i < NUM_KEYS; i++) {
         Object key = getKeyForCache(0);
         if (key2Tx.containsKey(key)) continue;

         embeddedTm(nodeThatPuts).begin();
         cache(nodeThatPuts).put(key, key);
         EmbeddedTransaction tx = embeddedTm(nodeThatPuts).getTransaction();
         tx.runPrepare();
         embeddedTm(nodeThatPuts).suspend();
         key2Tx.put(key, tx);

         assertLocked(0, key);
      }

      log.trace("Lock transfer happens here");

      addClusterEnabledCacheManager(TestDataSCI.INSTANCE, dccc);
      waitForClusterToForm();

      Object migratedKey = null;
      LocalizedCacheTopology cacheTopology = advancedCache(2).getDistributionManager().getCacheTopology();
      for (Object key : key2Tx.keySet()) {
         if (cacheTopology.getDistribution(key).isPrimary()) {
            migratedKey = key;
            break;
         }
      }
      if (migratedKey == null) {
         log.trace("No key migrated to new owner.");
      } else {
         log.trace("migratedKey = " + migratedKey);
         embeddedTm(2).begin();
         cache(2).put(migratedKey, "someValue");
         try {
            embeddedTm(2).commit();
            fail("RollbackException should have been thrown here.");
         } catch (RollbackException e) {
            //expected
         }
      }

      log.trace("About to commit existing transactions.");

      log.trace("Committing the tx to the new node.");
      for (Transaction tx : key2Tx.values()) {
         tm(nodeThatPuts).resume(tx);
         embeddedTm(nodeThatPuts).getTransaction().runCommit(false);
      }

      for (Object key : key2Tx.keySet()) {
         Object value = getValue(key);//make sure that data from the container, just to make sure all replicas are correctly set
         assertEquals(key, value);
      }
   }

   private Object getValue(Object key) {
      log.tracef("Checking key: %s", key);
      InternalCacheEntry d0 = advancedCache(0).getDataContainer().get(key);
      InternalCacheEntry d1 = advancedCache(1).getDataContainer().get(key);
      InternalCacheEntry d2 = advancedCache(2).getDataContainer().get(key);
      if (d0 == null) {
         assert sameValue(d1, d2);
         return d1.getValue();
      } else if (d1 == null)  {
         assert sameValue(d0, d2);
         return d0.getValue();
      } else  if (d2 == null) {
         assert sameValue(d0, d1);
         return d0.getValue();
      }
      throw new RuntimeException();
   }

   private boolean sameValue(InternalCacheEntry d1, InternalCacheEntry d2) {
      return d1.getValue().equals(d2.getValue());
   }

   private EmbeddedTransactionManager embeddedTm(int cacheIndex) {
      return (EmbeddedTransactionManager) tm(cacheIndex);
   }
}
