package com.atlassian.bitbucket.mesh.concurrent;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;

import java.time.Duration;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class CallerBlocksPolicyTest {

    private ThreadPoolExecutor executor;
    @Mock
    private Runnable task;

    @Before
    public void setUp() {
        executor = new ThreadPoolExecutor(1, 1, 0, TimeUnit.SECONDS, new SynchronousQueue<>());
        executor.setRejectedExecutionHandler(new CallerBlocksPolicy(Duration.ofMillis(100L)));
    }

    @After
    public void tearDown() {
        executor.shutdown();
    }

    @Test
    public void testCallerBlocksThenRuns() throws Exception {
        long durationFirstMs = time(() -> executor.execute(() -> blockMillis(50L)));
        //This will block until the _first_ task has run, to allow the second task to be _queued_, but it does
        //not guarantee the task has _completed_
        long durationSecondMs = time(() -> executor.execute(task));

        //To guarantee the second task has completed, we terminate the executor and wait for it to stop. That
        //way we know there are no more tasks to run
        executor.shutdown();
        assertTrue(executor.awaitTermination(5L, TimeUnit.SECONDS));

        verify(task).run();

        //The first submit should be picked up immediately
        assertThat(durationFirstMs, lessThan(25L));
        //The second submit should trigger the CallerBlocksPolicy and queue for up to 50ms. We'll accept anything
        //between 25ms and 75ms to account for thread switching effects on build agents.
        assertThat(durationSecondMs, both(lessThanOrEqualTo(75L)).and(greaterThanOrEqualTo(25L)));
    }

    @Test
    public void testCallerBlocksThenTimesOut() {
        executor.submit(() -> blockMillis(150L));
        assertThrows(RejectedExecutionException.class, () -> executor.submit(task));

        verify(task, never()).run();
    }

    @Test
    public void testThrowsWhenShutdown() {
        executor.shutdown();

        assertThrows(RejectedExecutionException.class, () -> executor.submit(task));
    }

    private static void blockMillis(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    private static long time(Runnable task) {
        long start = System.nanoTime();
        task.run();
        return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
    }
}