// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.data.cache;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import java.io.IOException;
import java.net.URL;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.commons.jcs.access.behavior.ICacheAccess;
import org.junit.Rule;
import org.junit.Test;
import org.openstreetmap.josm.testutils.JOSMTestRules;
import org.openstreetmap.josm.tools.Logging;
import org.openstreetmap.josm.tools.Utils;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

/**
 * Simple tests for ThreadPoolExecutor / HostLimitQueue veryfing, that this pair works OK
 * @author Wiktor Niesiobedzki
 */
public class HostLimitQueueTest {
    /**
     * Setup test.
     */
    @Rule
    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
    public JOSMTestRules test = new JOSMTestRules().preferences().timeout(20 * 1000);

    private static ThreadPoolExecutor getNewThreadPoolExecutor(String nameFormat, int workers, int queueLimit) {
        HostLimitQueue workQueue = new HostLimitQueue(queueLimit);
        ThreadPoolExecutor executor = new ThreadPoolExecutor(
                0, // 0 so for unused thread pools threads will eventually die, freeing also the threadpool
                workers, // do not this number of threads
                300, // keepalive for thread
                TimeUnit.SECONDS,
                workQueue,
                Utils.newThreadFactory(nameFormat, Thread.NORM_PRIORITY)
                );
        workQueue.setExecutor(executor);
        return executor;
    }

    /**
     * Mock class for tests
     */
    static class Task extends JCSCachedTileLoaderJob<String, CacheEntry> {
        private URL url;
        private AtomicInteger counter;

        Task(ICacheAccess<String, CacheEntry> cache, URL url, AtomicInteger counter) {
            super(cache, 1, 1, null);
            this.url = url;
            this.counter = counter;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Logging.trace(e);
            } finally {
                this.counter.incrementAndGet();
                executionFinished();
            }
        }

        @Override
        public String getCacheKey() {
            return "";
        }

        @Override
        public URL getUrl() throws IOException {
            return this.url;
       }

        @Override
        protected CacheEntry createCacheEntry(byte[] content) {
            return null;
        }
    }

    /**
     * Check if single threaded execution works properly
     * @throws Exception in case of error
     */
    @Test
    public void testSingleThreadPerHost() throws Exception {
        ThreadPoolExecutor tpe = getNewThreadPoolExecutor("test-%d", 3, 1);
        ICacheAccess<String, CacheEntry> cache = JCSCacheManager.getCache("test", 3, 0, "");
        AtomicInteger counter = new AtomicInteger(0);
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10; i++) {
            tpe.execute(new Task(cache, new URL("http://localhost/"+i), counter));
        }
        tpe.shutdown();
        tpe.awaitTermination(15, TimeUnit.SECONDS); // at most it should take ~10 seconds, so after 15 it's already failed
        long duration = System.currentTimeMillis() - start;
        // check that all tasks were executed
        assertEquals(10, counter.get());
        // although there are 3 threads, we can make only 1 parallel call to localhost
        // so it should take ~10 seconds to finish
        // if it's shorter, it means that host limit does not work
        assertTrue("Expected duration between 9 and 11 seconds not met. Actual duration: " + (duration /1000),
                duration < 11*1000 & duration > 9*1000);
    }

    /**
     * Check if two threaded execution work properly
     * @throws Exception in case of error
     */
    @Test
    public void testMultipleThreadPerHost() throws Exception {
        ThreadPoolExecutor tpe = getNewThreadPoolExecutor("test-%d", 3, 2);
        ICacheAccess<String, CacheEntry> cache = JCSCacheManager.getCache("test", 3, 0, "");
        AtomicInteger counter = new AtomicInteger(0);
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10; i++) {
            tpe.execute(new Task(cache, new URL("http://hostlocal/"+i), counter));
        }
        tpe.shutdown();
        tpe.awaitTermination(15, TimeUnit.SECONDS);
        long duration = System.currentTimeMillis() - start;
        // check that all tasks were executed
        assertEquals(10, counter.get());
        // although there are 3 threads, we can make only 2 parallel call to localhost
        // so it should take ~5 seconds to finish
        // if it's shorter, it means that host limit does not work
        assertTrue("Expected duration between 4 and 6 seconds not met. Actual duration: " + (duration /1000),
                duration < 6*1000 & duration > 4*1000);
    }

    /**
     * Check two hosts
     * @throws Exception in case of error
     */
    @Test
    public void testTwoHosts() throws Exception {
        ThreadPoolExecutor tpe = getNewThreadPoolExecutor("test-%d", 3, 1);
        ICacheAccess<String, CacheEntry> cache = JCSCacheManager.getCache("test", 3, 0, "");
        AtomicInteger counter = new AtomicInteger(0);
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10; i++) {
            String url = (i % 2 == 0) ? "http://localhost" : "http://hostlocal";
            tpe.execute(new Task(cache, new URL(url+i), counter));
        }
        tpe.shutdown();
        tpe.awaitTermination(15, TimeUnit.SECONDS);
        long duration = System.currentTimeMillis() - start;
        // check that all tasks were executed
        assertEquals(10, counter.get());
        // although there are 3 threads, we can make only 1 parallel per host, and we have 2 hosts
        // so it should take ~5 seconds to finish
        // if it's shorter, it means that host limit does not work
        assertTrue("Expected duration between 4 and 6 seconds not met. Actual duration: " + (duration /1000),
                duration < 6*1000 & duration > 4*1000);
    }
}
