package de.renew.engine.thread;

import java.util.concurrent.Callable;

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.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;


/**
 * Test class for the {@link BlockingSimulationCallable}.
 *
 * @author Felix Ortmann
 */
public class BlockingSimulationCallableTest {
    private static final String TEST_THREAD_NAME = "testThread";
    private static final String TEST_STRING = "a String to experiment with";

    private BlockingSimulationCallable<Object> _callable1;
    private BlockingSimulationCallable<String> _callable2;
    private BlockingSimulationCallable<?> _callable3;
    private SimulationThreadPool _pool;
    private Object _returnValueObject;
    private String _returnValueString;
    private Semaphor _lock;
    private SimulationThread _thread;

    /**
     * Setup method to initialize this test environment.
     * Resets the return objects and initializes the variables that will be used
     * in the tests.
     */
    @BeforeEach
    public void setUp() {
        _returnValueObject = new Object();
        _returnValueString = TEST_STRING;

        _pool = SimulationThreadPool.getNew();

        _lock = mock(Semaphor.class);
        Runnable run = () -> {
            try {
                assertEquals(_returnValueObject, _callable1.call());
            } catch (Exception e) {
                e.printStackTrace();
            }
        };

        _thread =
            new SimulationThread(Thread.currentThread().getThreadGroup(), run, TEST_THREAD_NAME, 5);

        //here we do compute something, what we want to return via the call method
        Callable<Object> innerCallable1 = () -> {
            //here we do compute something, what we want to return via the call method
            return _returnValueObject;
        };

        //here we do compute something, what we want to return via the call method
        Callable<String> innerCallable2 = () -> {
            //here we do compute something, what we want to return via the call method
            return _returnValueString;
        };

        _callable1 = new BlockingSimulationCallable<>(innerCallable1, _lock, _thread);

        _callable2 = new BlockingSimulationCallable<>(innerCallable2, _lock, _thread);

        _callable3 = new BlockingSimulationCallable<>(innerCallable1, _lock, null);
    }

    /**
     * Test method for {@link BlockingSimulationCallable#getAncestor}.
     */
    @Test
    public final void testGetAncestor() {
        //when/then
        assertEquals(_thread, _callable1.getAncestor());
        assertEquals(_thread, _callable2.getAncestor());
        assertNull(_callable3.getAncestor());
    }

    /**
     * Test method for {@link BlockingSimulationCallable#call}.
     */
    @Test
    public final synchronized void testCall() {
        System.out.println(
            "Trying to get the values by calling the inner callables call() methods. No Exception should be thrown here.");

        //given
        SimulationThread thread2 = new SimulationThread(
            Thread.currentThread().getThreadGroup(), getRunnable(_callable2, _returnValueString),
            TEST_THREAD_NAME, 5);
        SimulationThread thread3 = new SimulationThread(
            Thread.currentThread().getThreadGroup(), getRunnable(_callable3, _returnValueObject),
            TEST_THREAD_NAME, 5);

        //when/then
        _pool.execute(_thread);
        _pool.execute(thread2);
        _pool.execute(thread3);

        //since the pool executes the threads concurrently we want it to have enough time
        try {
            System.out.println("Waiting for SimulationThreadPool to execute.");
            wait(1500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //then
        //even in the case of an exception, the finally part of the callables call method
        //should have been executed at least three times (this is the abort in the semaphore)
        verify(_lock, atLeast(3)).V();
    }

    /**
     * Helper method to create a {@code Runnable} instance to be used in {@link #testCall}.
     * It compares the result of the {@link BlockingSimulationCallable#call}-method
     * with the expected return objects.
     *
     * @param callable the {@code Callable} that should be used
     * @return the {@code Runnable}
     */
    private Runnable getRunnable(BlockingSimulationCallable<?> callable, Object expectedValue) {
        return () -> {
            try {
                assertEquals(expectedValue, callable.call());
            } catch (Exception e) {
                e.printStackTrace();
            }
        };
    }

    /**
     * Test method for {@link BlockingSimulationCallable#abort}.
     * Checks that the abort method unlocks the semaphore.
     */
    @Test
    public final void testAbort() {
        //given
        _thread.setAncestor(Thread.currentThread());
        assertNotNull(_thread.getAncestor());

        //when
        _callable1.abort(_thread);

        //then
        verify(_lock, atLeastOnce()).V();
        assertNull(_thread.getAncestor());
    }
}