/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.facebook.presto.raptor.util;

import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import static com.google.common.util.concurrent.Uninterruptibles.awaitUninterruptibly;
import static java.util.concurrent.Executors.newCachedThreadPool;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertTrue;

@Test(singleThreaded = true)
public class TestPrioritizedFifoExecutor
{
    private static final Comparator<Runnable> DUMMY_COMPARATOR = (o1, o2) -> 0;

    private ExecutorService executor;

    @BeforeMethod
    public void setUp()
            throws Exception
    {
        executor = newCachedThreadPool();
    }

    @AfterMethod(alwaysRun = true)
    public void tearDown()
            throws Exception
    {
        executor.shutdownNow();
    }

    @Test
    public void testCounter()
            throws Exception
    {
        // enforce single thread
        PrioritizedFifoExecutor<Runnable> executor = new PrioritizedFifoExecutor<>(this.executor, 1, DUMMY_COMPARATOR);

        int totalTasks = 100_000;
        AtomicInteger counter = new AtomicInteger();
        CountDownLatch startLatch = new CountDownLatch(1);
        CountDownLatch completeLatch = new CountDownLatch(totalTasks);
        List<Future<?>> futures = new ArrayList<>();

        for (int i = 0; i < totalTasks; i++) {
            int taskNumber = i;
            futures.add(executor.submit(() -> {
                try {
                    // wait for the go signal
                    awaitUninterruptibly(startLatch, 1, TimeUnit.MINUTES);

                    assertFalse(futures.get(taskNumber).isDone());

                    // intentional distinct read and write calls
                    int initialCount = counter.get();
                    counter.set(initialCount + 1);
                }
                finally {
                    completeLatch.countDown();
                }
            }));
        }

        for (Future<?> future : futures) {
            assertFalse(future.isDone());
        }

        // signal go and wait for tasks to complete
        startLatch.countDown();
        awaitUninterruptibly(completeLatch, 1, TimeUnit.MINUTES);

        assertEquals(counter.get(), totalTasks);
        // since this is a fifo executor with one thread and completeLatch is decremented inside the future,
        // the last future may not be done yet, but all the rest must be
        futures.get(futures.size() - 1).get(1, TimeUnit.MINUTES);
        for (Future<?> future : futures) {
            assertTrue(future.isDone());
        }
    }

    @Test
    public void testSingleThreadBound()
            throws Exception
    {
        testBound(1, 100_000);
    }

    @Test
    public void testDoubleThreadBound()
            throws Exception
    {
        testBound(2, 100_000);
    }

    private void testBound(int maxThreads, int totalTasks)
    {
        PrioritizedFifoExecutor<Runnable> boundedExecutor = new PrioritizedFifoExecutor<>(executor, maxThreads, DUMMY_COMPARATOR);

        AtomicInteger activeThreadCount = new AtomicInteger();
        CountDownLatch startLatch = new CountDownLatch(1);
        CountDownLatch completeLatch = new CountDownLatch(totalTasks);
        AtomicBoolean failed = new AtomicBoolean();

        for (int i = 0; i < totalTasks; i++) {
            boundedExecutor.submit(() -> {
                try {
                    // wait for the go signal
                    awaitUninterruptibly(startLatch);

                    int count = activeThreadCount.incrementAndGet();
                    if (count < 1 || count > maxThreads) {
                        failed.set(true);
                    }
                    activeThreadCount.decrementAndGet();
                }
                finally {
                    completeLatch.countDown();
                }
            });
        }

        // signal go and wait for tasks to complete
        startLatch.countDown();
        awaitUninterruptibly(completeLatch, 1, TimeUnit.MINUTES);

        assertFalse(failed.get());
    }
}
