package io.smallrye.faulttolerance.core.timeout;

import static io.smallrye.faulttolerance.core.util.TestThread.runOnTestThread;
import static java.util.concurrent.CompletableFuture.completedFuture;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import org.eclipse.microprofile.faulttolerance.exceptions.TimeoutException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import io.smallrye.faulttolerance.core.util.TestException;
import io.smallrye.faulttolerance.core.util.TestThread;
import io.smallrye.faulttolerance.core.util.barrier.Barrier;

public class FutureTimeoutTest {
    private Barrier watcherTimeoutElapsedBarrier;
    private Barrier watcherExecutionInterruptedBarrier;

    private TestTimeoutWatcher timeoutWatcher;
    private ExecutorService asyncExecutor;

    @Before
    public void setUp() {
        watcherTimeoutElapsedBarrier = Barrier.interruptible();
        watcherExecutionInterruptedBarrier = Barrier.interruptible();

        timeoutWatcher = new TestTimeoutWatcher(watcherTimeoutElapsedBarrier, watcherExecutionInterruptedBarrier);
        asyncExecutor = Executors.newFixedThreadPool(4);
    }

    @After
    public void tearDown() throws InterruptedException {
        asyncExecutor.shutdown();
        asyncExecutor.awaitTermination(10, TimeUnit.SECONDS);
    }

    @Test
    public void failOnLackOfExecutor() {
        TestInvocation<Future<String>> invocation = TestInvocation.immediatelyReturning(() -> completedFuture("foobar"));
        Timeout<Future<String>> timeout = new Timeout<>(invocation, "test invocation", 1000,
                timeoutWatcher, null);
        assertThatThrownBy(() -> new AsyncTimeout<>(timeout, null))
                .isExactlyInstanceOf(IllegalArgumentException.class)
                .hasMessage("Executor must be set");
    }

    @Test
    public void immediatelyReturning_value() throws Exception {
        TestThread<Future<String>> testThread = runAsyncTimeoutImmediately(() -> completedFuture("foobar"));
        Future<String> future = testThread.await();
        assertThat(future.get()).isEqualTo("foobar");
        assertThat(timeoutWatcher.timeoutWatchWasCancelled()).isTrue();
    }

    @Test
    public void immediatelyReturning_exception() {
        TestThread<Future<String>> testThread = runAsyncTimeoutImmediately(TestException::doThrow);
        assertThatThrownBy(testThread::await).isExactlyInstanceOf(TestException.class);
        assertThat(timeoutWatcher.timeoutWatchWasCancelled()).isTrue();
    }

    @Test
    public void delayed_value_notTimedOut() throws Exception {
        Barrier invocationDelayBarrier = Barrier.interruptible();

        TestInvocation<Future<String>> invocation = TestInvocation.delayed(invocationDelayBarrier,
                () -> completedFuture("foobar"));
        Timeout<Future<String>> timeout = new Timeout<>(invocation, "test invocation", 1000,
                timeoutWatcher, null);
        TestThread<Future<String>> testThread = runOnTestThread(new AsyncTimeout<>(timeout, asyncExecutor));
        invocationDelayBarrier.open();

        Future<String> future = testThread.await();
        assertThat(future.get()).isEqualTo("foobar");
        assertThat(future.isDone()).isEqualTo(true);
        assertThat(timeoutWatcher.timeoutWatchWasCancelled()).isTrue();
    }

    @Test
    public void delayed_value_timedOut() throws InterruptedException {
        Barrier invocationDelayBarrier = Barrier.interruptible();

        TestInvocation<Future<String>> invocation = TestInvocation.delayed(invocationDelayBarrier,
                () -> completedFuture("foobar"));
        Timeout<Future<String>> timeout = new Timeout<>(invocation, "test invocation", 1000,
                timeoutWatcher, null);
        TestThread<Future<String>> testThread = runOnTestThread(new AsyncTimeout<>(timeout, asyncExecutor));
        watcherTimeoutElapsedBarrier.open();
        watcherExecutionInterruptedBarrier.await();

        assertThatThrownBy(testThread::await)
                .isExactlyInstanceOf(TimeoutException.class)
                .hasMessage("test invocation timed out");
        assertThat(timeoutWatcher.timeoutWatchWasCancelled()).isFalse();
    }

    @Test
    public void delayed_value_timedOutNoninterruptibly() throws InterruptedException {
        Barrier invocationDelayBarrier = Barrier.noninterruptible();

        TestInvocation<Future<String>> invocation = TestInvocation.delayed(invocationDelayBarrier,
                () -> completedFuture("foobar"));

        Timeout<Future<String>> timeout = new Timeout<>(invocation, "test invocation", 1000,
                timeoutWatcher, null);
        TestThread<Future<String>> testThread = runOnTestThread(new AsyncTimeout<>(timeout, asyncExecutor));
        watcherTimeoutElapsedBarrier.open();
        watcherExecutionInterruptedBarrier.await();

        assertThatThrownBy(testThread::await)
                .isExactlyInstanceOf(TimeoutException.class)
                .hasMessage("test invocation timed out");
        assertThat(timeoutWatcher.timeoutWatchWasCancelled()).isFalse(); // watcher should not be canceled if it caused the stop

        invocationDelayBarrier.open();
    }

    @Test
    public void delayed_value_cancelled() throws InterruptedException {
        Barrier invocationStartBarrier = Barrier.interruptible();
        Barrier invocationDelayBarrier = Barrier.interruptible();

        TestInvocation<Future<String>> invocation = TestInvocation.delayed(invocationStartBarrier, invocationDelayBarrier,
                () -> completedFuture("foobar"));

        Timeout<Future<String>> timeout = new Timeout<>(invocation, "test invocation", 1000,
                timeoutWatcher, null);
        TestThread<Future<String>> testThread = runOnTestThread(new AsyncTimeout<>(timeout, asyncExecutor));

        invocationStartBarrier.await();
        testThread.interrupt();

        assertThatThrownBy(testThread::await)
                .isExactlyInstanceOf(InterruptedException.class);
        assertThat(timeoutWatcher.timeoutWatchWasCancelled()).isFalse(); // watcher should not be canceled if it caused the stop

        invocationDelayBarrier.open();
    }

    @Test
    public void delayed_value_selfInterrupted() {
        Barrier delayBarrier = Barrier.interruptible();

        Callable<Future<String>> action = () -> {
            Thread.currentThread().interrupt();
            delayBarrier.await();
            return completedFuture("foobar");
        };
        TestThread<Future<String>> testThread = runAsyncTimeoutImmediately(action);

        delayBarrier.open();

        assertThatThrownBy(testThread::await).isExactlyInstanceOf(InterruptedException.class);
        assertThat(timeoutWatcher.timeoutWatchWasCancelled()).isTrue();
    }

    @Test
    public void immediate_value_nonInterruptibleCancelShouldBePropagated() throws Exception {
        Barrier delayBarrier = Barrier.interruptible();

        Callable<Future<String>> action = () -> CompletableFuture.supplyAsync(() -> {
            try {
                delayBarrier.await();
            } catch (InterruptedException e) {
                throw new CompletionException(e);
            }
            return "foobar";
        });
        TestThread<Future<String>> testThread = runAsyncTimeoutImmediately(action);

        Future<String> future = testThread.await();
        future.cancel(false);

        assertThat(future.isCancelled()).isTrue();
        delayBarrier.open();
    }

    @Test
    public void immediate_value_interruptibleCancelShouldBePropagated() throws Exception {
        Barrier delayBarrier = Barrier.interruptible();

        Callable<Future<String>> action = () -> CompletableFuture.supplyAsync(() -> {
            try {
                delayBarrier.await();
            } catch (InterruptedException e) {
                throw new CompletionException(e);
            }
            return "foobar";
        });
        TestThread<Future<String>> testThread = runAsyncTimeoutImmediately(action);

        Future<String> future = testThread.await();
        future.cancel(true);

        assertThat(future.isCancelled()).isTrue();
        // this changed with 10/12/2019 refactoring, preivously it was interrupted exception
        // looks okay though
        assertThatThrownBy(future::get).isExactlyInstanceOf(CancellationException.class);
        delayBarrier.open();
    }

    @Test
    public void delayed_value_timedGetRethrowsEventualError() throws Exception {
        RuntimeException exception = new RuntimeException("forced");

        Callable<Future<String>> action = () -> CompletableFuture.supplyAsync(() -> {
            throw exception;
        });
        TestThread<Future<String>> testThread = runAsyncTimeoutImmediately(action);

        Future<String> future = testThread.await();

        assertThatThrownBy(() -> future.get(1000, TimeUnit.MILLISECONDS))
                .isExactlyInstanceOf(ExecutionException.class)
                .hasCause(exception);
    }

    @Test
    public void delayed_value_getRethrowsError() throws Exception {
        RuntimeException exception = new RuntimeException("forced");

        Callable<Future<String>> action = () -> CompletableFuture.supplyAsync(() -> {
            throw exception;
        });

        TestThread<Future<String>> testThread = runAsyncTimeoutImmediately(action);

        Future<String> future = testThread.await();

        assertThatThrownBy(future::get)
                .isExactlyInstanceOf(ExecutionException.class)
                .hasCause(exception);
    }

    private TestThread<Future<String>> runAsyncTimeoutImmediately(Callable<Future<String>> action) {
        TestInvocation<Future<String>> invocation = TestInvocation.immediatelyReturning(action);
        Timeout<Future<String>> timeout = new Timeout<>(invocation, "test invocation", 1000,
                timeoutWatcher, null);
        return runOnTestThread(new AsyncTimeout<>(timeout, asyncExecutor));
    }
}