package org.infinispan.notifications.cachelistener.cluster;

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

import java.util.Collection;

import jakarta.transaction.HeuristicMixedException;
import jakarta.transaction.HeuristicRollbackException;
import jakarta.transaction.NotSupportedException;
import jakarta.transaction.RollbackException;
import jakarta.transaction.SystemException;
import jakarta.transaction.TransactionManager;

import org.infinispan.Cache;
import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.distribution.MagicKey;
import org.infinispan.notifications.cachelistener.event.CacheEntryEvent;
import org.infinispan.notifications.cachelistener.event.Event;
import org.testng.annotations.Test;

/**
 * This is a base class defining tests that are common across transactional tests when dealing with a cluster listener
 *
 * @author wburns
 * @since 4.0
 */
@Test(groups = "functional")
public abstract class AbstractClusterListenerTxTest extends AbstractClusterListenerTest {
   protected AbstractClusterListenerTxTest(CacheMode cacheMode) {
      super(true, cacheMode);
   }

   // Checks the given events to see if an entry was marked as being created for the provided key and value
   // Note this will have to iterate over all the events until one is found, since it is possible to get the events in
   // non-deterministic order since they aren't from the same node
   protected void verifyCreation(Collection<CacheEntryEvent> events, Object key, Object expectedValue) {
      boolean found = false;
      for (CacheEntryEvent event : events) {
         if (Event.Type.CACHE_ENTRY_CREATED == event.getType() && key.equals(event.getKey()) &&
               expectedValue.equals(event.getValue())) {
            found = true;
            break;
         }
      }
      assertTrue(found, "No entry was created in provided events " + events + " matching key " + key + " and value " +
            expectedValue);
   }

   @Test
   public void testBatchedCommitOriginatorNotLocal() throws SystemException, NotSupportedException, HeuristicRollbackException,
                                                  HeuristicMixedException, RollbackException {
      Cache<Object, String> cache0 = cache(0, CACHE_NAME);
      Cache<Object, String> cache1 = cache(1, CACHE_NAME);
      Cache<Object, String> cache2 = cache(2, CACHE_NAME);

      ClusterListener clusterListener = new ClusterListener();
      cache0.addListener(clusterListener);

      MagicKey key1 = new MagicKey(cache1, cache2);
      MagicKey key2 = new MagicKey(cache2, cache1);

      TransactionManager tm = cache2.getAdvancedCache().getTransactionManager();
      tm.begin();

      cache2.put(key1, FIRST_VALUE);
      assertEquals(clusterListener.events.size(), 0);

      cache2.put(key2, SECOND_VALUE);
      assertEquals(clusterListener.events.size(), 0);

      tm.commit();

      assertEquals(clusterListener.events.size(), 2);
      verifyCreation(clusterListener.events, key1, FIRST_VALUE);
      verifyCreation(clusterListener.events, key2, SECOND_VALUE);
   }

   @Test
   public void testBatchedCommitKeyNotLocalLocal() throws HeuristicRollbackException, RollbackException, HeuristicMixedException,
                                               SystemException, NotSupportedException {
      Cache<Object, String> cache0 = cache(0, CACHE_NAME);
      Cache<Object, String> cache1 = cache(1, CACHE_NAME);
      Cache<Object, String> cache2 = cache(2, CACHE_NAME);

      ClusterListener clusterListener = new ClusterListener();
      cache0.addListener(clusterListener);

      MagicKey key1 = new MagicKey(cache1, cache2);
      MagicKey key2 = new MagicKey(cache2, cache1);

      TransactionManager tm = cache2.getAdvancedCache().getTransactionManager();
      tm.begin();

      cache0.put(key1, FIRST_VALUE);
      assertEquals(clusterListener.events.size(), 0);

      cache0.put(key2, SECOND_VALUE);
      assertEquals(clusterListener.events.size(), 0);

      tm.commit();

      assertEquals(clusterListener.events.size(), 2);
      verifyCreation(clusterListener.events, key1, FIRST_VALUE);
      verifyCreation(clusterListener.events, key2, SECOND_VALUE);
   }

   @Test
   public void testBatchedCommitLocal() throws HeuristicRollbackException, RollbackException, HeuristicMixedException,
                                               SystemException, NotSupportedException {
      Cache<Object, String> cache0 = cache(0, CACHE_NAME);
      Cache<Object, String> cache1 = cache(1, CACHE_NAME);
      Cache<Object, String> cache2 = cache(2, CACHE_NAME);

      ClusterListener clusterListener = new ClusterListener();
      cache0.addListener(clusterListener);

      MagicKey key1 = new MagicKey(cache0);
      MagicKey key2 = new MagicKey(cache1, cache0);

      TransactionManager tm = cache2.getAdvancedCache().getTransactionManager();
      tm.begin();

      cache0.put(key1, FIRST_VALUE);
      assertEquals(clusterListener.events.size(), 0);

      cache0.put(key2, SECOND_VALUE);
      assertEquals(clusterListener.events.size(), 0);

      tm.commit();

      assertEquals(clusterListener.events.size(), 2);
      verifyCreation(clusterListener.events, key1, FIRST_VALUE);
      verifyCreation(clusterListener.events, key2, SECOND_VALUE);
   }

   @Test
   public void testRolledBackNotLocal() throws SystemException, NotSupportedException {
      Cache<Object, String> cache0 = cache(0, CACHE_NAME);
      Cache<Object, String> cache1 = cache(1, CACHE_NAME);
      Cache<Object, String> cache2 = cache(2, CACHE_NAME);

      ClusterListener clusterListener = new ClusterListener();
      cache0.addListener(clusterListener);

      MagicKey key1 = new MagicKey(cache1, cache2);
      MagicKey key2 = new MagicKey(cache2, cache1);

      TransactionManager tm = cache2.getAdvancedCache().getTransactionManager();
      tm.begin();

      cache2.put(key1, FIRST_VALUE);
      assertEquals(clusterListener.events.size(), 0);

      cache2.put(key2, SECOND_VALUE);
      assertEquals(clusterListener.events.size(), 0);

      tm.rollback();

      assertEquals(clusterListener.events.size(), 0);
   }

   @Test
   public void testRolledBackOriginatorNotLocal() throws SystemException, NotSupportedException {
      Cache<Object, String> cache0 = cache(0, CACHE_NAME);
      Cache<Object, String> cache1 = cache(1, CACHE_NAME);
      Cache<Object, String> cache2 = cache(2, CACHE_NAME);

      ClusterListener clusterListener = new ClusterListener();
      cache0.addListener(clusterListener);

      MagicKey key1 = new MagicKey(cache0);
      MagicKey key2 = new MagicKey(cache1, cache0);

      TransactionManager tm = cache2.getAdvancedCache().getTransactionManager();
      tm.begin();

      cache2.put(key1, FIRST_VALUE);
      assertEquals(clusterListener.events.size(), 0);

      cache2.put(key2, SECOND_VALUE);
      assertEquals(clusterListener.events.size(), 0);

      tm.rollback();

      assertEquals(clusterListener.events.size(), 0);
   }

   @Test
   public void testRolledBackLocal() throws SystemException, NotSupportedException {
      Cache<Object, String> cache0 = cache(0, CACHE_NAME);
      Cache<Object, String> cache1 = cache(1, CACHE_NAME);
      Cache<Object, String> cache2 = cache(2, CACHE_NAME);

      ClusterListener clusterListener = new ClusterListener();
      cache0.addListener(clusterListener);

      MagicKey key1 = new MagicKey(cache0);
      MagicKey key2 = new MagicKey(cache1, cache0);

      TransactionManager tm = cache2.getAdvancedCache().getTransactionManager();
      tm.begin();

      cache0.put(key1, FIRST_VALUE);
      assertEquals(clusterListener.events.size(), 0);

      cache0.put(key2, SECOND_VALUE);
      assertEquals(clusterListener.events.size(), 0);

      tm.rollback();

      assertEquals(clusterListener.events.size(), 0);
   }

   @Test
   public void testMultipleKeysSameOwnerBatchNotified() throws SystemException, NotSupportedException, HeuristicRollbackException,
                                                          HeuristicMixedException, RollbackException {
      Cache<Object, String> cache0 = cache(0, CACHE_NAME);
      Cache<Object, String> cache1 = cache(1, CACHE_NAME);
      Cache<Object, String> cache2 = cache(2, CACHE_NAME);

      ClusterListener clusterListener = new ClusterListener();
      cache0.addListener(clusterListener);

      MagicKey key1 = new MagicKey(cache1);
      MagicKey key2 = new MagicKey(cache1);

      TransactionManager tm = cache2.getAdvancedCache().getTransactionManager();
      tm.begin();

      cache2.put(key1, FIRST_VALUE);
      assertEquals(clusterListener.events.size(), 0);

      cache2.put(key2, SECOND_VALUE);
      assertEquals(clusterListener.events.size(), 0);

      tm.commit();

      assertEquals(clusterListener.events.size(), 2);
      verifyCreation(clusterListener.events, key1, FIRST_VALUE);
      verifyCreation(clusterListener.events, key2, SECOND_VALUE);
   }
}
