package org.infinispan.executors;

import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.testng.AssertJUnit.assertEquals;
import static org.testng.AssertJUnit.assertFalse;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;

import org.infinispan.test.AbstractInfinispanTest;
import org.infinispan.util.concurrent.WithinThreadExecutor;
import org.testng.annotations.AfterClass;
import org.testng.annotations.Test;

/**
 * Basic tests for {@link LimitedExecutor}
 *
 * @author Dan Berindei
 * @since 9.0
 */
@Test(groups = "functional", testName = "executors.LimitedExecutorTest")
public class LimitedExecutorTest extends AbstractInfinispanTest {
   public static final String NAME = "Test";
   private final ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 2,
         0L, MILLISECONDS,
         new SynchronousQueue<>(),
         getTestThreadFactory(NAME));

   @AfterClass(alwaysRun = true)
   public void stopExecutors() {
      executor.shutdownNow();
   }

   public void testBasicWithinThread() throws Exception {
      LimitedExecutor limitedExecutor = new LimitedExecutor(NAME, new WithinThreadExecutor(), 1);

      CompletableFuture<String> cf = new CompletableFuture<>();
      limitedExecutor.execute(() -> cf.complete("value"));
      assertEquals("value", cf.getNow("task did not run synchronously"));
   }

   /**
    * Test that no more than 1 task runs at a time.
    */
   public void testConcurrencyLimit() throws Exception {
      eventuallyEquals(0, executor::getActiveCount);
      LimitedExecutor limitedExecutor = new LimitedExecutor(NAME, executor, 1);

      CompletableFuture<String> blocker1 = new CompletableFuture<>();
      CompletableFuture<String> cf1 = new CompletableFuture<>();

      limitedExecutor.execute(() -> {
         try {
            cf1.complete(blocker1.get(10, SECONDS));
         } catch (Exception e) {
            cf1.completeExceptionally(e);
         }
      });

      verifyTaskIsBlocked(limitedExecutor, blocker1, cf1);
   }

   /**
    * Test that an async task ({@code executeAsync()}) will block another task from running
    * until its {@code CompletableFuture} is completed.
    */
   public void testConcurrencyLimitExecuteAsync() throws Exception {
      eventuallyEquals(0, executor::getActiveCount);
      LimitedExecutor limitedExecutor = new LimitedExecutor(NAME, executor, 1);

      CompletableFuture<String> blocker1 = new CompletableFuture<>();
      CompletableFuture<String> cf1 = new CompletableFuture<>();

      limitedExecutor.executeAsync(() -> blocker1.thenAccept(cf1::complete));

      verifyTaskIsBlocked(limitedExecutor, blocker1, cf1);
   }

   /**
    * Test that no more than 1 task runs at a time when using a {@link WithinThreadExecutor}.
    */
   public void testConcurrencyLimitWithinThread() throws Exception {
      LimitedExecutor limitedExecutor = new LimitedExecutor(NAME, new WithinThreadExecutor(), 1);

      CompletableFuture<String> blocker1 = new CompletableFuture<>();
      CompletableFuture<String> blocker2 = new CompletableFuture<>();
      CompletableFuture<String> cf1 = new CompletableFuture<>();

      // execute() will block
      Future<?> fork1 = fork(() -> {
         limitedExecutor.execute(() -> {
            blocker2.complete("blocking");
            try {
               cf1.complete(blocker1.get(10, SECONDS));
            } catch (Exception e) {
               cf1.completeExceptionally(e);
            }
         });
      });
      assertEquals("blocking", blocker2.get(10, SECONDS));

      verifyTaskIsBlocked(limitedExecutor, blocker1, cf1);
      fork1.get(10, SECONDS);
   }

   /**
    * Test that an async task ({@code executeAsync()}) will block another task from running
    * until its {@code CompletableFuture} is completed, when using a {@link WithinThreadExecutor}.
    */
   public void testConcurrencyLimitExecuteAsyncWithinThread() throws Exception {
      LimitedExecutor limitedExecutor = new LimitedExecutor(NAME, new WithinThreadExecutor(), 1);

      CompletableFuture<String> blocker1 = new CompletableFuture<>();
      CompletableFuture<String> cf1 = new CompletableFuture<>();

      // executeAsync() will not block
      limitedExecutor.executeAsync(() -> blocker1.thenAccept(cf1::complete));

      verifyTaskIsBlocked(limitedExecutor, blocker1, cf1);
   }

   private void verifyTaskIsBlocked(LimitedExecutor limitedExecutor, CompletableFuture<String> blocker1,
                                    CompletableFuture<String> cf1) throws Exception {
      CompletableFuture<String> blocker2 = new CompletableFuture<>();
      CompletableFuture<String> cf2 = new CompletableFuture<>();

      // execute() may block
      Future<?> fork2 = fork(() -> {
         limitedExecutor.execute(() -> {
            try {
               cf2.complete(cf1.getNow("task 2 ran too early") + " " + blocker2.get(10, SECONDS));
            } catch (Exception e) {
               cf2.completeExceptionally(e);
            }
         });
      });

      assertFalse(cf1.isDone());
      assertFalse(cf2.isDone());

      blocker1.complete("value1");
      assertEquals("value1", cf1.get(10, SECONDS));
      assertFalse(cf2.isDone());

      blocker2.complete("value2");
      assertEquals("value1 value2", cf2.get(10, SECONDS));
      fork2.get(10, SECONDS);
      eventuallyEquals(0, executor::getActiveCount);
   }
}
