package com.atlassian.bitbucket.internal.mirroring.mirror.sync;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

import static org.junit.Assert.*;

public class DedupingExecutorTest {

    private AtomicInteger concurrency;
    private AtomicInteger maxConcurrency;
    private DedupingExecutor executor;
    private ExecutorService pool;

    @Before
    public void setUp() {
        concurrency = new AtomicInteger(0);
        maxConcurrency = new AtomicInteger(0);

        pool = Executors.newCachedThreadPool();
        executor = new DedupingExecutor(pool);
    }

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

    @Test(timeout = 2000L)
    public void testSubmitSequentially() throws ExecutionException, InterruptedException {
        TestTask task1 = new TestTask();
        TestTask task2 = new TestTask();

        Future<?> future1 = executor.submit("id", task1);
        task1.release();
        future1.get();

        Future<?> future2 = executor.submit("id", task2);
        task2.release();
        future2.get();

        assertTrue(task1.done);
        assertTrue(task2.done);
        assertEquals(1, maxConcurrency.get());
    }

    @Test(timeout = 2000L)
    public void testSubmitWhileTaskRunning() throws ExecutionException, InterruptedException {
        TestTask task1 = new TestTask();
        TestTask task2 = new TestTask();
        TestTask task3 = new TestTask();
        TestTask task4 = new TestTask();

        Future<?> future1 = executor.submit("id", task1);
        task1.awaitStart();

        // task1 has started but hasn't finished yet. Submit 2 more tasks; only 1 should be queued
        Future<?> future2 = executor.submit("id", task2);
        Future<?> future3 = executor.submit("id", task3);

        // task2 and task3 should _not_ execute while task1 is running
        assertFalse(task2.awaitStart(100));
        assertFalse(task3.awaitStart(1)); // task3 should never execute

        // wait for task1 to complete and task2 to start
        task1.release();
        task2.awaitStart();

        // task2 is now running, schedule another task
        Future<?> future4 = executor.submit("id", task4);

        // task4 should _not_ execute while task2 is running
        assertFalse(task4.awaitStart(100));

        // let all the tasks finish
        task2.release();
        task3.release();
        task4.release();

        future1.get();
        future2.get();
        future3.get();
        future4.get();

        assertTrue(task1.done);
        assertTrue(task2.done);
        assertFalse(task3.done); // task2 was already queued when task3 was submitted
        assertTrue(task4.done);
        assertSame(future2, future3);
        assertEquals(1, maxConcurrency.get());
    }

    @Test(timeout = 2000L)
    public void testSubmitDifferentIdsWhileTaskRunning() throws ExecutionException, InterruptedException {
        TestTask task1 = new TestTask();
        TestTask task2 = new TestTask();
        TestTask task3 = new TestTask();

        Future<?> future1 = executor.submit("id", task1);
        task1.awaitStart();

        // task1 has started but hasn't finished yet
        Future<?> future2 = executor.submit("id", task2);

        // task3 is submitted under a different ID, so it should not be blocked
        Future<?> future3 = executor.submit("other-id", task3);
        task3.awaitStart();

        task1.release();
        task2.release();
        task3.release();

        future1.get();
        future2.get();
        future3.get();

        assertTrue(task1.done);
        assertTrue(task2.done);
        assertTrue(task3.done);
        assertEquals(2, maxConcurrency.get());
    }


    private class TestTask implements Runnable {
        private final CountDownLatch startedLatch = new CountDownLatch(1);
        private final CountDownLatch finishLatch = new CountDownLatch(1);
        private volatile boolean done;

        public void release() {
            finishLatch.countDown();
        }

        @Override
        public void run() {
            int current = concurrency.incrementAndGet();
            maxConcurrency.accumulateAndGet(current, Math::max);
            startedLatch.countDown();
            try {
                finishLatch.await();
                done = true;
            } catch (InterruptedException e) {
                fail("Interrupted while waiting for task to finish");
            } finally {
                concurrency.decrementAndGet();
            }
        }

        public void awaitStart() throws InterruptedException {
            startedLatch.await();
        }

        public boolean awaitStart(long millis) throws InterruptedException {
            return startedLatch.await(millis, TimeUnit.MILLISECONDS);
        }
    }
}