package org.infinispan.stats;

import static java.lang.String.format;
import static org.infinispan.distribution.DistributionTestHelper.addressOf;

import org.infinispan.Cache;
import org.infinispan.commands.tx.PrepareCommand;
import org.infinispan.context.impl.TxInvocationContext;
import org.infinispan.distribution.DistributionManager;
import org.infinispan.interceptors.AsyncInterceptorChain;
import org.infinispan.interceptors.DDAsyncInterceptor;
import org.infinispan.interceptors.impl.TxInterceptor;
import org.infinispan.stats.topK.CacheUsageInterceptor;
import org.infinispan.test.MultipleCacheManagersTest;

/**
 * @author Pedro Ruivo
 * @since 9.0
 */
public abstract class AbstractTopKeyTest extends MultipleCacheManagersTest {

   private static boolean isOwner(Cache<?, ?> cache, Object key) {
      DistributionManager dm = cache.getAdvancedCache().getDistributionManager();
      return dm == null || dm.locate(key).contains(addressOf(cache));
   }

   protected CacheUsageInterceptor getTopKey(Cache<?, ?> cache) {
      AsyncInterceptorChain interceptorChain =
            cache.getAdvancedCache().getAsyncInterceptorChain();
      return interceptorChain.findInterceptorExtending(CacheUsageInterceptor.class);
   }

   protected void assertTopKeyAccesses(Cache<?, ?> cache, String key, long expected, boolean readAccesses) {
      final CacheUsageInterceptor topK = getTopKey(cache);
      final boolean isLocal = isOwner(cache, key);
      eventuallyEquals(format("Wrong number of accesses for key '%s' and cache '%s'.", key, addressOf(cache)),
                       expected,
                       () -> {
                          if (readAccesses) {
                             return (isLocal ? topK.getLocalTopGets() : topK.getRemoteTopGets()).getOrDefault(key, 0L);
                          } else {
                             return (isLocal ? topK.getLocalTopPuts() : topK.getRemoteTopPuts()).getOrDefault(key, 0L);
                          }
                       });
   }

   protected void assertWriteSkew(Cache<?, ?> cache, String key, long expected) {
      final CacheUsageInterceptor topK = getTopKey(cache);
      eventuallyEquals(format("Wrong number of write skew for key '%s' and cache '%s'.", key, addressOf(cache)),
                       expected,
                       () -> topK.getTopWriteSkewFailedKeys().getOrDefault(key, 0L));
   }

   private void assertTopKeyLocked(Cache<?, ?> cache, String key, long expected) {
      final CacheUsageInterceptor topK = getTopKey(cache);
      eventuallyEquals(format("Wrong number of locked key for key '%s' and cache '%s'.", key, addressOf(cache)),
                       expected,
                       () -> topK.getTopLockedKeys().getOrDefault(key, 0L));
   }

   private void assertTopKeyLockContented(Cache<?, ?> cache, String key, long expected) {
      final CacheUsageInterceptor topK = getTopKey(cache);
      eventuallyEquals(format("Wrong number of contented key for key '%s' and cache '%s'.", key, addressOf(cache)),
                       expected,
                       () -> topK.getTopContendedKeys().getOrDefault(key, 0L));
   }

   private void assertTopKeyLockFailed(Cache<?, ?> cache, String key, long expected) {
      final CacheUsageInterceptor topK = getTopKey(cache);
      eventuallyEquals(format("Wrong number of failed locked key for key '%s' and cache '%s'.", key, addressOf(cache)),
                       expected,
                       () -> topK.getTopLockFailedKeys().getOrDefault(key, 0L));
   }

   protected void assertLockInformation(Cache<?, ?> cache, String key, long locked, long contented, long failed) {
      assertTopKeyLocked(cache, key, locked);
      assertTopKeyLockContented(cache, key, contented);
      assertTopKeyLockFailed(cache, key, failed);
   }

   protected PrepareCommandBlocker addPrepareBlockerIfAbsent(Cache<?, ?> cache) {
      AsyncInterceptorChain chain = cache.getAdvancedCache().getAsyncInterceptorChain();

      PrepareCommandBlocker blocker = chain.findInterceptorWithClass(PrepareCommandBlocker.class);
      if (blocker != null)
         return blocker;

      blocker = new PrepareCommandBlocker();
      chain.addInterceptorBefore(blocker, TxInterceptor.class);
      return blocker;
   }

   protected class PrepareCommandBlocker extends DDAsyncInterceptor {

      private boolean unblock = false;
      private boolean prepareBlocked = false;

      @Override
      public Object visitPrepareCommand(TxInvocationContext ctx, PrepareCommand command) throws Throwable {
         return invokeNextThenAccept(ctx, command, (rCtx, rCommand, rv) ->  {
            synchronized (this) {
               prepareBlocked = true;
               notifyAll();
               while (!unblock) {
                  wait();
               }
            }
         });
      }

      public synchronized void reset() {
         unblock = false;
         prepareBlocked = false;
      }

      public synchronized void unblock() {
         unblock = true;
         notifyAll();
      }

      public synchronized void awaitUntilPrepareBlocked() throws InterruptedException {
         while (!prepareBlocked) {
            wait();
         }
      }
   }

}
