package org.infinispan.notifications.cachelistener;

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

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.notifications.Listener;
import org.infinispan.notifications.cachelistener.annotation.CacheEntriesEvicted;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryCreated;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryModified;
import org.infinispan.notifications.cachelistener.annotation.CacheEntryRemoved;
import org.infinispan.notifications.cachelistener.event.CacheEntryEvent;
import org.infinispan.notifications.cachelistener.event.Event;
import org.infinispan.test.SingleCacheManagerTest;
import org.infinispan.test.TestingUtil;
import org.infinispan.test.fwk.CleanupAfterMethod;
import org.infinispan.test.fwk.TestCacheManagerFactory;
import org.infinispan.util.logging.Log;
import org.infinispan.util.logging.LogFactory;
import org.testng.annotations.Test;

/**
 * Tests visibility of effects of cache operations on a separate thread once
 * a cache listener event has been consumed for the corresponding cache
 * operation.
 *
 * @author Galder Zamarreño
 * @since 5.1
 */
@Test(groups = "functional", testName = "notifications.cachelistener.CacheListenerVisibilityTest")
@CleanupAfterMethod
public class CacheListenerVisibilityTest extends SingleCacheManagerTest {

   @Override
   protected EmbeddedCacheManager createCacheManager() throws Exception {
      return TestCacheManagerFactory.createCacheManager(false);
   }

   public void testSizeVisibility() throws Exception {
      updateCache(Visibility.SIZE);
   }

   public void testGetVisibility() throws Exception {
      updateCache(Visibility.GET);
   }

   public void testGetVisibilityWithinEntryCreatedListener() throws Exception {
      updateCacheAssertInListener(
            new EntryCreatedWithAssertListener(new CountDownLatch(1)));
   }

   public void testGetVisibilityWithinEntryModifiedListener() throws Exception {
      updateCacheAssertInListener(
            new EntryModifiedWithAssertListener(new CountDownLatch(1)));
   }

   public void testRemoveVisibility() throws Exception {
      cache.put(1, "v1");

      final CountDownLatch after = new CountDownLatch(1);
      final CountDownLatch afterContinue = new CountDownLatch(1);
      final CountDownLatch before = new CountDownLatch(1);
      cache.addListener(new EntryListener(before, afterContinue, after));

      assertEquals("v1", cache.get(1));

      Future<Void> ignore = fork(new Callable<Void>() {
         @Override
         public Void call() throws Exception {
            cache.remove(1);
            return null;
         }
      });

      // With removes, there's a before/after event acknowledgment, so verify it
      // Evicts on the other hand only emit a single event, after
      boolean signalled = before.await(30, TimeUnit.SECONDS);
      assertTrue("Timed out while waiting for before listener notification",
            signalled);

      assertEquals("v1", cache.get(1));

      // Let the isPre=false callback continue
      afterContinue.countDown();

      signalled = after.await(30, TimeUnit.SECONDS);
      assertTrue("Timed out while waiting for after listener notification",
            signalled);

      assertEquals(null, cache.get(1));

      ignore.get(5, TimeUnit.SECONDS);
   }

   public void testEvictOnCacheEntryEvictedVisibility() throws Exception {
      checkEvictVisibility(false);
   }

   public void testEvictOnCacheEntriesEvictedVisibility() throws Exception {
      checkEvictVisibility(true);
   }

   private void checkEvictVisibility(boolean isCacheEntriesEvicted) throws Exception {
      cache.put(1, "v1");

      final CountDownLatch after = new CountDownLatch(1);
      Object listener = isCacheEntriesEvicted
            ? new EntriesEvictedListener(after)
            : new EntryListener(null, null, after);

      cache.addListener(listener);
      assertEquals("v1", cache.get(1));

      Future<Void> ignore = fork(new Callable<Void>() {
         @Override
         public Void call() throws Exception {
            cache.evict(1);
            return null;
         }
      });

      boolean signalled = after.await(30, TimeUnit.SECONDS);
      assertTrue("Timed out while waiting for after listener notification",
            signalled);

      assertEquals(null, cache.get(1));

      ignore.get(5, TimeUnit.SECONDS);
   }

   public void testClearVisibility() throws Exception {
      cache.put(1, "v1");
      cache.put(2, "v1");
      cache.put(3, "v1");

      final CyclicBarrier after = new CyclicBarrier(2);
      final CountDownLatch afterContinue = new CountDownLatch(1);
      final CountDownLatch before = new CountDownLatch(1);
      cache.addListener(new CacheClearListener(before, afterContinue, after));

      assertEquals("v1", cache.get(1));
      assertEquals("v1", cache.get(2));
      assertEquals("v1", cache.get(3));

      Future<Void> ignore = fork(new Callable<Void>() {
         @Override
         public Void call() throws Exception {
            cache.clear();
            return null;
         }
      });

      boolean signalled = before.await(30, TimeUnit.SECONDS);
      assertTrue("Timed out while waiting for before listener notification",
            signalled);

      assertEquals("v1", cache.get(1));
      assertEquals("v1", cache.get(2));
      assertEquals("v1", cache.get(3));

      // Let the isPre=false callback continue
      afterContinue.countDown();

      // Wait for isPre=false remove notification for k=1
      after.await(30, TimeUnit.SECONDS);
      assertEquals(null, cache.get(1));

      // Wait for isPre=false remove notification for k=2
      after.await(30, TimeUnit.SECONDS);
      assertEquals(null, cache.get(2));

      // Wait for isPre=false remove notification for k=3
      after.await(30, TimeUnit.SECONDS);
      assertEquals(null, cache.get(3));

      assertTrue(cache.isEmpty());
      ignore.get(5, TimeUnit.SECONDS);
   }

   private void updateCacheAssertInListener(WithAssertListener listener)
         throws Exception {
      cache.addListener(listener);

      Future<Void> ignore = fork(new Callable<Void>() {
         @Override
         public Void call() throws Exception {
            cache.put("k", "v");
            return null;
         }
      });

      listener.latch.await(30, TimeUnit.SECONDS);
      assert listener.assertNotNull;
      assert listener.assertValue;
      ignore.get(5, TimeUnit.SECONDS);
   }

   private void updateCache(Visibility visibility) throws Exception {
      final String key = "k-" + visibility;
      final String value = "k-" + visibility;
      final CountDownLatch after = new CountDownLatch(1);
      final CountDownLatch afterContinue = new CountDownLatch(1);
      final CountDownLatch before = new CountDownLatch(1);
      cache.addListener(new EntryListener(before, afterContinue, after));

      switch (visibility) {
         case SIZE:
            assertEquals(0, cache.size());
            break;
         case GET:
            assertNull(cache.get(key));
            break;
      }

      Future<Void> ignore = fork(new Callable<Void>() {
         @Override
         public Void call() throws Exception {
            cache.put(key, value);
            return null;
         }
      });

      boolean signalled = before.await(30, TimeUnit.SECONDS);
      assertTrue("Timed out while waiting for before listener notification",
            signalled);

      switch (visibility) {
         case SIZE:
            assertEquals(0, cache.size());
            break;
         case GET:
            assertNull(cache.get(key));
            break;
      }

      // Let the isPre=false callback continue
      afterContinue.countDown();

      signalled = after.await(30, TimeUnit.SECONDS);
      assertTrue("Timed out while waiting for after listener notification",
            signalled);

      switch (visibility) {
         case SIZE:
            assertEquals(1, cache.size());
            break;
         case GET:
            Object retVal = cache.get(key);
            assertNotNull(retVal);
            assertEquals(retVal, value);
            break;
      }
      ignore.get(5, TimeUnit.SECONDS);
   }

   @Listener
   public static class EntriesEvictedListener {
      // Use a different listener class for @CacheEntriesEvicted vs
      // @CacheEntryEvicted to tests both callbacks separately

      Log log = LogFactory.getLog(EntriesEvictedListener.class);

      final CountDownLatch after;

      public EntriesEvictedListener(CountDownLatch after) {
         this.after = after;
      }

      @CacheEntriesEvicted
      @SuppressWarnings("unused")
      public void entryEvicted(Event e) {
         log.info("Cache entries evicted, now check in different thread");
         after.countDown();
         // Force a bit of delay in the listener so that lack of visibility
         // of changes in container can be appreciated more easily
         TestingUtil.sleepThread(1000);
      }
   }

   @Listener
   public static class CacheClearListener {

      Log log = LogFactory.getLog(CacheClearListener.class);

      final CyclicBarrier after;
      final CountDownLatch before;
      final CountDownLatch afterContinue;

      public CacheClearListener(CountDownLatch before,
            CountDownLatch afterContinue, CyclicBarrier after) {
         this.before = before;
         this.after = after;
         this.afterContinue = afterContinue;
      }

      @CacheEntryRemoved
      @SuppressWarnings("unused")
      public void entryTouched(Event e) {
         if (!e.isPre()) {
            log.infof("Cache entry removed, event is: %s", e);
            try {
               after.await(30, TimeUnit.SECONDS);
            } catch (InterruptedException e1) {
               Thread.currentThread().interrupt();
            } catch (BrokenBarrierException e1) {
               throw new IllegalStateException(e1);
            } catch (TimeoutException e1) {
               throw new IllegalStateException(e1);
            }
            // Force a bit of delay in the listener so that lack of visibility
            // of changes in container can be appreciated more easily
            TestingUtil.sleepThread(1000);
         } else {
            before.countDown();
            try {
               boolean signalled = afterContinue.await(30, TimeUnit.SECONDS);
               assertTrue("Timed out while waiting for post listener event to execute",
                     signalled);
            } catch (InterruptedException e1) {
               Thread.currentThread().interrupt();
            }
         }
      }

   }


   @Listener
   public static class EntryListener {

      Log log = LogFactory.getLog(EntryListener.class);

      final CountDownLatch after;
      final CountDownLatch before;
      final CountDownLatch afterContinue;

      public EntryListener(CountDownLatch before,
            CountDownLatch afterContinue, CountDownLatch after) {
         this.before = before;
         this.after = after;
         this.afterContinue = afterContinue;
      }

      @CacheEntriesEvicted
      @SuppressWarnings("unused")
      public void entryEvicted(Event e) {
         log.info("Cache entry evicted, now check in different thread");
         after.countDown();
         // Force a bit of delay in the listener so that lack of visibility
         // of changes in container can be appreciated more easily
         TestingUtil.sleepThread(1000);
      }

      @CacheEntryCreated
      @CacheEntryRemoved
      @SuppressWarnings("unused")
      public void entryTouched(Event e) {
         if (!e.isPre()) {
            log.info("Cache entry touched, now check in different thread");
            after.countDown();
            // Force a bit of delay in the listener so that lack of visibility
            // of changes in container can be appreciated more easily
            TestingUtil.sleepThread(1000);
         } else {
            before.countDown();
            try {
               boolean signalled = afterContinue.await(30, TimeUnit.SECONDS);
               assertTrue("Timed out while waiting for post listener event to execute",
                     signalled);
            } catch (InterruptedException e1) {
               Thread.currentThread().interrupt();
            }
         }
      }

   }

   public static abstract class WithAssertListener {

      Log log = LogFactory.getLog(WithAssertListener.class);

      final CountDownLatch latch;
      volatile boolean assertNotNull;
      volatile boolean assertValue;

      protected WithAssertListener(CountDownLatch latch) {
         this.latch = latch;
      }

      protected void assertCacheContents(CacheEntryEvent e) {
         if (!e.isPre()) {
            log.info("Cache entry created, now check cache contents");
            Object value = e.getCache().get("k");
            if (value == null) {
               assertNotNull = false;
               assertValue = false;
            } else {
               assertNotNull = true;
               assertValue = value.equals("v");
            }
            // Force a bit of delay in the listener
            latch.countDown();
         }
      }

   }

   @Listener
   public static class EntryCreatedWithAssertListener extends WithAssertListener {

      protected EntryCreatedWithAssertListener(CountDownLatch latch) {
         super(latch);
      }

      @CacheEntryCreated
      @SuppressWarnings("unused")
      public void entryCreated(CacheEntryEvent e) {
         assertCacheContents(e);
      }

   }

   @Listener
   public static class EntryModifiedWithAssertListener extends WithAssertListener {

      protected EntryModifiedWithAssertListener(CountDownLatch latch) {
         super(latch);
      }

      @CacheEntryCreated
      @CacheEntryModified
      @SuppressWarnings("unused")
      public void entryCreated(CacheEntryEvent e) {
         assertCacheContents(e);
      }

   }

   private enum Visibility {
      SIZE, GET
   }

}
