package org.infinispan.api;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.testng.AssertJUnit.assertTrue;

import java.util.Collections;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import org.infinispan.Cache;
import org.infinispan.container.entries.InternalCacheEntry;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.test.SingleCacheManagerTest;
import org.infinispan.test.TestingUtil;
import org.infinispan.test.fwk.TestCacheManagerFactory;
import org.infinispan.util.DefaultTimeService;
import org.infinispan.util.TimeService;
import org.testng.annotations.Test;

@Test(groups = "functional", testName = "api.AsyncAPITest")
public class AsyncAPITest extends SingleCacheManagerTest {

   private Cache<String, String> c;
   private ControlledTimeService timeService = new ControlledTimeService();
   private Long startTime;

   @Override
   protected EmbeddedCacheManager createCacheManager() throws Exception {
      EmbeddedCacheManager cm = TestCacheManagerFactory.createCacheManager(false);
      TestingUtil.replaceComponent(cm, TimeService.class, timeService, true);
      c = cm.getCache();
      return cm;
   }

   public void testAsyncMethods() throws Exception {
      // get
      Future<String> f = c.getAsync("k");
      assert f != null;
      assert !f.isCancelled();
      assertNull(f.get());
      assert f.isDone();
      assert c.get("k") == null;

      // put
      f = c.putAsync("k", "v");
      assert f != null;
      assert !f.isCancelled();
      assertEquals(f.get(), null);
      assert f.isDone();
      assert c.get("k").equals("v");

      f = c.putAsync("k", "v2");
      assert f != null;
      assert !f.isCancelled();
      assert f.get().equals("v");
      assert f.isDone();
      assert c.get("k").equals("v2");

      // putAll
      Future<Void> f2 = c.putAllAsync(Collections.singletonMap("k", "v3"));
      assert f2 != null;
      assert !f2.isCancelled();
      assert f2.get() == null;
      assert f2.isDone();
      assert c.get("k").equals("v3");

      // putIfAbsent
      f = c.putIfAbsentAsync("k", "v4");
      assert f != null;
      assert !f.isCancelled();
      assert f.get().equals("v3");
      assert f.isDone();
      assert c.get("k").equals("v3");

      // remove
      f = c.removeAsync("k");
      assert f != null;
      assert !f.isCancelled();
      assert f.get().equals("v3");
      assert f.isDone();
      assert c.get("k") == null;

      // putIfAbsent again
      f = c.putIfAbsentAsync("k", "v4");
      assert f != null;
      assert !f.isCancelled();
      assert f.get() == null;
      assert f.isDone();
      assert c.get("k").equals("v4");

      // get
      f = c.getAsync("k");
      assert f != null;
      assert !f.isCancelled();
      assert f.get().equals("v4");
      assert f.isDone();
      assert c.get("k").equals("v4");

      // removecond
      Future<Boolean> f3 = c.removeAsync("k", "v_nonexistent");
      assert f3 != null;
      assert !f3.isCancelled();
      assert f3.get().equals(false);
      assert f3.isDone();
      assert c.get("k").equals("v4");

      f3 = c.removeAsync("k", "v4");
      assert f3 != null;
      assert !f3.isCancelled();
      assert f3.get().equals(true);
      assert f3.isDone();
      assert c.get("k") == null;

      // replace
      f = c.replaceAsync("k", "v5");
      assert f != null;
      assert !f.isCancelled();
      assert f.get() == null;
      assert f.isDone();
      assert c.get("k") == null;

      c.put("k", "v");
      f = c.replaceAsync("k", "v5");
      assert f != null;
      assert !f.isCancelled();
      assert f.get().equals("v");
      assert f.isDone();
      assert c.get("k").equals("v5");

      //replace2
      f3 = c.replaceAsync("k", "v_nonexistent", "v6");
      assert f3 != null;
      assert !f3.isCancelled();
      assert f3.get().equals(false);
      assert f3.isDone();
      assert c.get("k").equals("v5");

      f3 = c.replaceAsync("k", "v5", "v6");
      assert f3 != null;
      assert !f3.isCancelled();
      assert f3.get().equals(true);
      assert f3.isDone();
      assert c.get("k").equals("v6");
   }

   public void testAsyncMethodWithLifespanAndMaxIdle() throws Exception {

      // lifespan only
      Future<String> f = c.putAsync("k", "v", 1000, TimeUnit.MILLISECONDS);
      markStartTime();
      assert f != null;
      assert !f.isCancelled();
      assert f.get() == null;
      assert f.isDone();
      verifyEviction("k", "v", 1000, 500, true);

      log.warn("STARTING FAILING ONE");

      // lifespan and max idle (test max idle)
      f = c.putAsync("k", "v", 3000, TimeUnit.MILLISECONDS, 1000, TimeUnit.MILLISECONDS);
      markStartTime();
      assert f != null;
      assert !f.isCancelled();
      assert f.get() == null;
      assert f.isDone();
      verifyEviction("k", "v", 1000, 500, false);

      // lifespan and max idle (test lifespan)
      f = c.putAsync("k", "v", 3000, TimeUnit.MILLISECONDS, 1000, TimeUnit.MILLISECONDS);
      markStartTime();
      assert f != null;
      assert !f.isCancelled();
      assert f.get() == null;
      assert f.isDone();
      verifyEviction("k", "v", 3000, 500, true);

      // putAll lifespan only
      Future<Void> f2 = c.putAllAsync(Collections.singletonMap("k", "v3"), 1000, TimeUnit.MILLISECONDS);
      markStartTime();
      assert f2 != null;
      assert !f2.isCancelled();
      assert f2.get() == null;
      assert f2.isDone();
      verifyEviction("k", "v3", 1000, 500, true);

      // putAll lifespan and max idle (test max idle)
      f2 = c.putAllAsync(Collections.singletonMap("k", "v4"), 3000, TimeUnit.MILLISECONDS, 1000, TimeUnit.MILLISECONDS);
      markStartTime();
      assert f2 != null;
      assert !f2.isCancelled();
      assert f2.get() == null;
      assert f2.isDone();
      verifyEviction("k", "v4", 1000, 500, false);

      // putAll lifespan and max idle (test lifespan)
      f2 = c.putAllAsync(Collections.singletonMap("k", "v5"), 3000, TimeUnit.MILLISECONDS, 1000, TimeUnit.MILLISECONDS);
      markStartTime();
      assert f2 != null;
      assert !f2.isCancelled();
      assert f2.get() == null;
      assert f2.isDone();
      verifyEviction("k", "v5", 3000, 500, true);

      // putIfAbsent lifespan only
      f = c.putAsync("k", "v3");
      assertNull(f.get());
      f = c.putIfAbsentAsync("k", "v4", 1000, TimeUnit.MILLISECONDS);
      markStartTime();
      assert f != null;
      assert !f.isCancelled();
      assertEquals("v3", f.get());
      assert f.isDone();
      assert c.get("k").equals("v3");
      assert !c.get("k").equals("v4");
      Thread.sleep(300);
      assert c.get("k").equals("v3");
      f = c.removeAsync("k");
      assert f.get().equals("v3");
      assert c.get("k") == null;

      // now really put (k removed) lifespan only
      f = c.putIfAbsentAsync("k", "v", 1000, TimeUnit.MILLISECONDS);
      markStartTime();
      assert f != null;
      assert !f.isCancelled();
      assert f.get() == null;
      assert f.isDone();
      verifyEviction("k", "v", 1000, 500, true);

      // putIfAbsent lifespan and max idle (test max idle)
      f = c.putIfAbsentAsync("k", "v", 3000, TimeUnit.MILLISECONDS, 1000, TimeUnit.MILLISECONDS);
      markStartTime();
      assert f != null;
      assert !f.isCancelled();
      assert f.get() == null;
      assert f.isDone();
      verifyEviction("k", "v", 1000, 500, false);

      // putIfAbsent lifespan and max idle (test lifespan)
      f = c.putIfAbsentAsync("k", "v", 3000, TimeUnit.MILLISECONDS, 1000, TimeUnit.MILLISECONDS);
      markStartTime();
      assert f != null;
      assert !f.isCancelled();
      assert f.get() == null;
      assert f.isDone();
      verifyEviction("k", "v", 3000, 500, true);

      // replace
      f = c.replaceAsync("k", "v5", 1000, TimeUnit.MILLISECONDS);
      markStartTime();
      assert f != null;
      assert !f.isCancelled();
      assert f.get() == null;
      assert f.isDone();
      assert c.get("k") == null;

      // replace lifespan only
      c.put("k", "v");
      f = c.replaceAsync("k", "v5", 1000, TimeUnit.MILLISECONDS);
      markStartTime();
      assert f != null;
      assert !f.isCancelled();
      assert f.get().equals("v");
      assert f.isDone();
      verifyEviction("k", "v5", 1000, 500, true);

      // replace lifespan and max idle (test max idle)
      c.put("k", "v");
      f = c.replaceAsync("k", "v5", 5000, TimeUnit.MILLISECONDS, 1000, TimeUnit.MILLISECONDS);
      markStartTime();
      assert f != null;
      assert !f.isCancelled();
      assert f.get().equals("v");
      assert f.isDone();
      verifyEviction("k", "v5", 1000, 500, false);

      // replace lifespan and max idle (test lifespan)
      c.put("k", "v");
      f = c.replaceAsync("k", "v5", 3000, TimeUnit.MILLISECONDS, 1000, TimeUnit.MILLISECONDS);
      markStartTime();
      assert f != null;
      assert !f.isCancelled();
      assert f.get().equals("v");
      assert f.isDone();
      verifyEviction("k", "v5", 3000, 500, true);

      //replace2
      c.put("k", "v5");
      Future<Boolean> f3 = c.replaceAsync("k", "v_nonexistent", "v6", 1000, TimeUnit.MILLISECONDS);
      markStartTime();
      assert f3 != null;
      assert !f3.isCancelled();
      assert f3.get().equals(false);
      assert f3.isDone();
      Thread.sleep(300);
      assert c.get("k").equals("v5");

      // replace2 lifespan only
      f3 = c.replaceAsync("k", "v5", "v6", 1000, TimeUnit.MILLISECONDS);
      markStartTime();
      assert f3 != null;
      assert !f3.isCancelled();
      assert f3.get().equals(true);
      assert f3.isDone();
      verifyEviction("k", "v6", 1000, 500, true);

      // replace2 lifespan and max idle (test max idle)
      c.put("k", "v5");
      f3 = c.replaceAsync("k", "v5", "v6", 5000, TimeUnit.MILLISECONDS, 1000, TimeUnit.MILLISECONDS);
      markStartTime();
      assert f3 != null;
      assert !f3.isCancelled();
      assert f3.get().equals(true);
      assert f3.isDone();
      verifyEviction("k", "v6", 1000, 500, false);

      // replace2 lifespan and max idle (test lifespan)
      c.put("k", "v5");
      f3 = c.replaceAsync("k", "v5", "v6", 3000, TimeUnit.MILLISECONDS, 1000, TimeUnit.MILLISECONDS);
      markStartTime();
      assert f3 != null;
      assert !f3.isCancelled();
      assert f3.get().equals(true);
      assert f3.isDone();
      verifyEviction("k", "v6", 3000, 500, true);
   }

   private void markStartTime() {
      startTime = timeService.wallClockTime();
   }

   /**
    * Verifies if a key is evicted after a certain time.
    *  @param key the key to check
    * @param expectedValue expected key value at the beginning
    * @param expectedLifetime expected life of the key
    * @param checkPeriod period between executing checks. If the check modifies the idle time. this is important to block idle expiration.
    * @param touchKey indicates if the poll for key existence should read the key and cause idle time to be reset
    */
   private void verifyEviction(final String key, final String expectedValue, final long expectedLifetime, long checkPeriod, final boolean touchKey) {
      if (startTime == null) {
         throw new IllegalStateException("markStartTime() must be called before verifyEviction(..)");
      }

      try {
         long expectedEndTime = startTime + expectedLifetime;
         Condition condition = () -> {
            if (touchKey) {
               return !c.containsKey(key);        //this check DOES read the key so it resets the idle time
            } else {
               //this check DOES NOT read the key so it does not interfere with idle time
               InternalCacheEntry entry = c.getAdvancedCache().getDataContainer().peek(key);
               return entry == null || entry.isExpired(timeService.wallClockTime());
            }
         };
         assertTrue(expectedValue.equals(c.get(key)) || timeService.wallClockTime() > expectedEndTime);
         // we need to loop to keep touching the entry and protect against idle expiration
         while (timeService.wallClockTime() <= expectedEndTime) {
            assertFalse("Entry evicted too soon!", condition.isSatisfied());
            timeService.advance(checkPeriod);
         }
         assertTrue(timeService.wallClockTime() > expectedEndTime);
         assertTrue(condition.isSatisfied());
         Object value = c.get(key);
         assertNull(value);

      } catch (RuntimeException e) {
         throw e;
      } catch (Exception e) {
         throw new RuntimeException(e);
      } finally {
         startTime = null;
      }
   }

   private static class ControlledTimeService extends DefaultTimeService {
      private long time = super.wallClockTime();

      @Override
      public long wallClockTime() {
         return time;
      }

      @Override
      public long time() {
         return TimeUnit.MILLISECONDS.toNanos(time);
      }

      public void advance(long millis) {
         time += millis;
      }
   }
}
