package de.renew.engine.thread;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import de.renew.util.Semaphor;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;


/**
 * Test class for the {@link SimulationThreadPool}.
 * <p>
 * Instances of {@link Callable}, {@link BlockingSimulationCallable} and {@link WrappedFutureTask}
 * are used to help test this class.
 *
 * @author Felix Ortmann
 */
public class SimulationThreadPoolTest {
    private static final String TEST_STRING = "checked";
    private static final String TEST_THREAD_NAME = "TestThread";
    private static final int LOW_PRIORITY = 1;
    private static final int MID_PRIORITY = 5;
    private static final int MAX_PRIORITY = 10;

    private Object _returnValue;
    private Callable<Object> _sampleCallable;
    private WrappedFutureTask<Object> _sampleFTask;
    private BlockingSimulationCallable<Object> _sampleBlock;
    private Runnable _sampleRun;
    private String _checkValue;
    private SimulationThreadPool _pool;

    /**
     * Setup method to initialize this test environment.
     */
    @BeforeEach
    public void setUp() {
        _returnValue = new Object();
        _checkValue = "";

        _pool = SimulationThreadPool.getNew();

        _sampleCallable = () -> {
            // when the call method is invoked, we want something to be computed here
            // which is kept simple for this test
            return _returnValue;
        };

        // Blocks the thread in which this test is executed. Calls _sampleCallable
        _sampleBlock = new BlockingSimulationCallable<>(
            _sampleCallable, new Semaphor(), Thread.currentThread());

        _sampleFTask = new WrappedFutureTask<>(_sampleBlock);

        // we needed to define an extra String (or any other datatype) to verify executive 
        // behavior within a runnable.
        _sampleRun = () -> _checkValue = TEST_STRING;
    }

    /**
     * Test method for {@link SimulationThreadPool#submitAndWait}
     * with a normal {@link Callable}.
     */
    @Test
    public final void testSubmitAndWaitNormal() {
        //given
        FutureTask<Object> compareTask = new FutureTask<>(_sampleCallable);
        _pool.execute(compareTask);
        System.out.println("submitAndWait setup with normal Callable successful");

        //when/then
        try {
            System.out.println("Wait complete, compare results.");
            assertEquals(compareTask.get(), _pool.submitAndWait(_sampleCallable).get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

    /**
     * Test method for {@link SimulationThreadPool#submitAndWait}
     * with a {@link BlockingSimulationCallable}.
     */
    @Test
    public final void testSubmitAndWaitBlockingSimulationCallable() {
        //given
        FutureTask<Object> compareTask = new FutureTask<>(_sampleBlock);
        _pool.execute(compareTask);
        System.out.println("submitAndWait setup with BlockingSimulationCallable successful");

        //when/then
        try {
            System.out.println("Wait complete, compare results.");
            assertEquals(compareTask.get(), _pool.submitAndWait(_sampleBlock).get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

    /**
     * Test method for {@link SimulationThreadPool#submit}
     * with a normal {@link Callable}.
     */
    @Test
    public final void testSubmitCallable() {
        //given
        FutureTask<Object> compareTask = new FutureTask<>(_sampleCallable);
        _pool.execute(compareTask);
        System.out.println("submit setup with normal Callable successful");

        //when/then
        try {
            System.out.println("try to compare submit result with expected.");
            assertEquals(compareTask.get(), _pool.submit(_sampleCallable).get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

    /**
     * Test method for {@link SimulationThreadPool#submit}
     * with a {@link BlockingSimulationCallable}.
     */
    @Test
    public final void testSubmitBlockingSimulationCallable() {
        //given
        FutureTask<Object> compareTask = new FutureTask<>(_sampleBlock);
        _pool.execute(compareTask);
        System.out.println("submit setup with BlockingSimulationCallable successful");

        //when/then
        try {
            System.out.println("try to compare submit result with expected.");
            assertEquals(compareTask.get(), _pool.submit(_sampleBlock).get());
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

    /**
     * Test method for {@link SimulationThreadPool#getNew}.
     */
    @Test
    public final void testGetNew() {
        //when/then
        assertNotSame(SimulationThreadPool.getNew(), SimulationThreadPool.getCurrent());
    }

    /**
     * Test method for {@link SimulationThreadPool#getCurrent}.
     */
    @Test
    public final void testGetCurrent() {
        // this is quite difficult to test. 
        //TODO An appropriate solution has to be worked out.

        //when/then
        assertEquals(SimulationThreadPool.getCurrent(), SimulationThreadPool.getCurrent());
    }

    /**
     * Test method for {@link SimulationThreadPool#getSimulationThreadPool}.
     */
    @Test
    public final void testGetSimulationThreadPool() {
        //when/then
        assertEquals(
            SimulationThreadPool.getCurrent(), SimulationThreadPool.getSimulationThreadPool());
    }

    /**
     * Test method for {@link SimulationThreadPool#isMyThread()}.
     */
    @Test
    public final void testIsMyThread() {
        // Test only negative case, since this JUnit thread is not a simulation thread.
        // cannot execute this test within a simulation thread.

        //when/then
        assertFalse(_pool.isMyThread());
    }

    /**
     * Test method for {@link SimulationThreadPool#isMyThread(java.lang.Thread)}.
     */
    @Test
    public final void testIsMyThreadWithCurrentThread() {
        // Test only negative case, since this JUnit thread is not a simulation thread.
        // cannot execute this test within a simulation thread.

        //when/then
        assertFalse(_pool.isMyThread(Thread.currentThread()));
    }

    /**
     * Test method for {@link SimulationThreadPool#cleanup}.
     */
    @Test
    public final void testCleanup() {
        // difficult to test. method returns always true (why not void?)

        //given
        SimulationThreadPool oldPool = SimulationThreadPool.getCurrent();

        //when/then
        assertTrue(SimulationThreadPool.cleanup());

        //then
        assertNotSame(oldPool, SimulationThreadPool.getCurrent());
    }

    /*
     * NON JAVA DOC
     *
     * FIXME
     *
     * Unable to test discardNew()
     *
     * Unable to test isSimulationThread()
     *
     * Unclear how to test ExecuteAndWait() since no return value exists.
     *
     */

    /**
     * Test method for {@link SimulationThreadPool#executeAndWait}.
     * The method is invoked from within a {@link SimulationThread}.
     */
    @Test
    public final synchronized void testExecuteAndWaitWithinSimulationThread() {
        //when
        Runnable invoker = () -> {
            // we call SimulationThreadPool#executeAndWait from within this runnable,
            // which will be executed from inside a simulationThread.
            SimulationThreadPool.getNew().executeAndWait(_sampleRun);
        };

        SimulationThread simThread = new SimulationThread(
            _pool.getThreadFactory().newThread(_sampleRun).getThreadGroup(), invoker,
            TEST_THREAD_NAME, MID_PRIORITY);
        _pool.execute(simThread);
        // now we have concurrent behaviour: the pool executes another thread, which waits for the execution of
        // sampleRun - meanwhile the junit control flow carries on. So if we compare the computational result now,
        // it is unclear which result we get, cause the above code may not have already been executed. 
        // Thus, we wait a little (no perfect solution!)
        try {
            System.out.println("Waiting 3 sec. for a concurrent thread to finish.");
            wait(3000); // 3 sec. should be enough.
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        //then
        assertEquals(TEST_STRING, _checkValue);
    }

    /**
     * Test method for {@link SimulationThreadPool#executeAndWait}.
     * The calling thread is different from the {@code SimulationThreadGroup}.
     */
    @Test
    public final synchronized void testExecuteAndWaitFromDifferentThreadGroup() {
        //when
        // the calling thread is the junit thread
        // we know, that internally a blockingSimulationRunnable will be built, so we enforce this testcase.
        _pool.executeAndWait(_sampleRun);

        //then
        // we do not need to check any threadBlock-behaviour - this is the wait. If the wait, implementation
        // would be faulty, the assertStatement would simply fail (sometimes) due to concurrency.
        assertEquals(TEST_STRING, _checkValue);
    }

    /**
     * Test method for {@link SimulationThreadPool#setMaxPriority} and
     * {@link SimulationThreadPool#getMaxPriority}.
     */
    @Test
    public final void testSetGetMaxPriority() {
        //when/then
        //test all accepted values (from 1 to 10)
        for (int i = LOW_PRIORITY; i <= MAX_PRIORITY; i++) {
            _pool.setMaxPriority(i);
            assertEquals(i, _pool.getMaxPriority());
        }
        //test the outer extrema
        _pool.setMaxPriority(Integer.MAX_VALUE);
        assertEquals(MAX_PRIORITY, _pool.getMaxPriority()); //should have no effect
        _pool.setMaxPriority(Integer.MIN_VALUE);
        assertEquals(MAX_PRIORITY, _pool.getMaxPriority()); //should have no effect
    }

    /**
     * Test method for {@link SimulationThreadPool#beforeExecute}
     * with a {@link WrappedFutureTask} as a {@link Runnable}.
     */
    @Test
    public final void testBeforeExecute() {
        //given
        // test with a WrappedFutureTask as runnable
        SimulationThread compareThread = new SimulationThread(
            _pool.getThreadFactory().newThread(_sampleFTask).getThreadGroup(), _sampleFTask,
            TEST_THREAD_NAME, MAX_PRIORITY);
        assertNull(compareThread.getAncestor()); //no one is expected

        //when
        _pool.beforeExecute(compareThread, _sampleFTask);

        //then
        // now compareThread should have an ancestor
        assertNotNull(compareThread.getAncestor());
    }

    /**
     * Test method for {@link SimulationThreadPool#beforeExecute} with a
     * {@link BlockingSimulationRunnable}.
     */
    @Test
    public final void testBeforeExecuteBlocking() {
        //given
        //do it again with an BlockingSimulationRunnable
        SimulationThread compareThread = new SimulationThread(
            _pool.getThreadFactory().newThread(_sampleFTask).getThreadGroup(), _sampleFTask,
            TEST_THREAD_NAME, MAX_PRIORITY);
        assertNull(compareThread.getAncestor()); //no one is expected

        BlockingSimulationRunnable run = new BlockingSimulationRunnable(() -> {
            //just dont anything
        }, new Semaphor(), Thread.currentThread()); // the JUnit Thread is ancestor of "run"

        //when
        _pool.beforeExecute(compareThread, run);

        //then
        assertEquals(Thread.currentThread(), compareThread.getAncestor());
    }

    /**
     * This test tests that the complementary pair of methods
     * {@link SimulationThreadPool#beforeExecute} and
     * {@link BlockingSimulationCallable#abort} form a complete
     * lifecycle with setting and deleting the thread ancestor relation.
     * <p>
     * This method is no direct testcase for the {@link SimulationThreadPool}-method,
     * but it is a joined-functionality test.
     */
    @Test
    public final void testAncestorLifecycleTest() {
        //given
        SimulationThread compareThread = new SimulationThread(
            _pool.getThreadFactory().newThread(_sampleFTask).getThreadGroup(), _sampleFTask,
            TEST_THREAD_NAME, MAX_PRIORITY);
        assertNull(compareThread.getAncestor());

        //when/then
        _pool.beforeExecute(compareThread, _sampleFTask); //sets an ancestor
        assertNotNull(compareThread.getAncestor());

        _sampleBlock.abort(compareThread); //delete the ancestor
        assertNull(compareThread.getAncestor());
    }
}