package org.infinispan.tx.locking;

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

import jakarta.transaction.Transaction;

import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.test.MultipleCacheManagersTest;
import org.infinispan.test.TestDataSCI;
import org.infinispan.transaction.LockingMode;
import org.infinispan.transaction.lookup.EmbeddedTransactionManagerLookup;
import org.infinispan.util.concurrent.IsolationLevel;
import org.testng.annotations.Test;

/**
 * @author Martin Gencur
 */
@Test(groups = "functional", testName = "tx.locking.SizeDistTxRepeatableReadTest")
public class SizeDistTxRepeatableReadTest extends MultipleCacheManagersTest {

   IsolationLevel isolation;

   Object k0;
   Object k1;

   public SizeDistTxRepeatableReadTest() {
      this.isolation = IsolationLevel.REPEATABLE_READ;
   }

   @Override
   protected void createCacheManagers() throws Throwable {
      final ConfigurationBuilder conf = getDefaultClusteredCacheConfig(CacheMode.DIST_SYNC, true);
      conf.clustering().hash().numOwners(1)
            .locking().isolationLevel(isolation)
            .transaction()
            .lockingMode(LockingMode.OPTIMISTIC)
            .transactionManagerLookup(new EmbeddedTransactionManagerLookup());
      createCluster(TestDataSCI.INSTANCE, conf, 2);
      waitForClusterToForm();
      k0 = getKeyForCache(0);
      k1 = getKeyForCache(1);
   }

   public void testSizeWithPreviousRead() throws Exception {
      preloadCacheAndCheckSize();

      tm(0).begin();
      //make sure we read k1 in this transaction
      assertEquals("v1", cache(0).get(k1));
      final Transaction tx1 = tm(0).suspend();

      //another tx working on the same keys
      tm(0).begin();
      //remove the key that was previously read in another tx
      cache(0).remove(k1);
      cache(0).put(k0, "v2");
      tm(0).commit();

      assertEquals(1, cache(0).size());
      assertEquals("v2", cache(0).get(k0));

      tm(0).resume(tx1);
      //we read k1 earlier so size() should take the key into account even though it was removed in another tx
      assertEquals(2, cache(0).size());
      //we did not read k0 previosly - getting the changed value
      assertEquals("v2", cache(0).get(k0));
      //we've read it before - getting original value
      assertEquals("v1", cache(0).get(k1));
      tm(0).commit();

      assertNull(cache(1).get(k1));
   }

   public void testSizeWithReadFromRemoteNode() throws Exception {
      preloadCacheAndCheckSize();

      tm(0).begin();
      assertEquals("v1", cache(0).get(k1));
      assertEquals(2, cache(0).size());
      tm(0).rollback();
   }

   public void testSizeWithoutPreviousRead() throws Exception {
      preloadCacheAndCheckSize();

      tm(0).begin();
      //no reading of k1 here
      final Transaction tx1 = tm(0).suspend();

      //another tx working on the same keys
      tm(0).begin();
      //remove the key that was previously read in another tx
      cache(0).remove(k1);
      cache(0).put(k0, "v2");
      tm(0).commit();

      assertEquals(1, cache(0).size());
      assertEquals("v2", cache(0).get(k0));

      tm(0).resume(tx1);
      //we did not read k1 earlier so size() should reflect the other transaction's changes
      assertEquals(1, cache(0).size());
      //we did not read any keys previosly - getting original values
      assertEquals("v2", cache(0).get(k0));
      assertNull(cache(0).get(k1));
      tm(0).commit();

      assertNull(cache(1).get(k1));
   }

   protected void preloadCacheAndCheckSize() throws Exception {
      tm(0).begin();
      cache(0).put(k0, "v0");
      cache(0).put(k1, "v1");
      assertEquals(2, cache(0).size());
      tm(0).commit();
   }
}
