package de.renew.net;

import java.util.Collection;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.stubbing.Answer1;

import de.renew.engine.thread.SimulationThreadPool;
import de.renew.net.loading.NetLoader;
import de.renew.simulator.api.ISimulationLockExecutor;
import de.renew.simulatorontology.loading.NetNotFoundException;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.AdditionalAnswers.answer;
import static org.mockito.AdditionalAnswers.answerVoid;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class NetLookupTest {
    private Collection<Net> _nets;
    private INetLookup _netLookup;
    @Mock
    private ISimulationLockExecutor _lock;
    private MockedStatic<SimulationThreadPool> _staticSimulationThreadPool;
    @Mock
    private SimulationThreadPool _simulationThreadPool;

    /**
     * Setup method to initialize this test environment.
     */
    @BeforeEach
    public void setUp() {
        doAnswer(answerVoid(Runnable::run)).when(_lock).runWithLock(any());
        _netLookup = new NetLookup(_lock);
        _staticSimulationThreadPool = mockStatic(SimulationThreadPool.class);
        _staticSimulationThreadPool.when(SimulationThreadPool::isSimulationThread).thenReturn(true);
        _staticSimulationThreadPool.when(SimulationThreadPool::getCurrent)
            .thenReturn(_simulationThreadPool);
        _nets = List.of(new Net("net1"), new Net("net2"), new Net("net3"));
    }

    /**
     * Teardown method to clean up this test environment.
     */
    @AfterEach
    public void tearDown() {
        _staticSimulationThreadPool.close();
        _netLookup.forgetAllNets();
        _netLookup.setNetLoader(null);
    }

    /**
     * Test method for {@link NetLookup#makeNetKnown}.
     */
    @Test
    void testMakeNetKnown() {
        //given
        Net net = new Net("net");
        //when
        _netLookup.makeNetKnown(net);
        //then
        verify(_lock).runWithLock(any());
        assertThat(_netLookup.isKnownNet("net")).isTrue();
    }

    /**
     * Test method for {@link NetLookup#makeNetsKnown}.
     */
    @Test
    void testMakeNetsKnown() {
        //when
        _netLookup.makeNetsKnown(_nets);
        //then
        _nets.forEach(net -> assertThat(_netLookup.isKnownNet(net.getName())).isTrue());
        verify(_lock).runWithLock(any());
    }

    /**
     * Test method for {@link NetLookup#forgetAllNets}.
     */
    @Test
    void testForgetAllNets() {
        //when
        _netLookup.makeNetsKnown(_nets);
        _netLookup.forgetAllNets();
        //then
        assertThat(_netLookup.getAllKnownNets()).isEmpty();
    }

    /**
     * Test method for {@link NetLookup#setNetLoader}.
     */
    @Test
    void testSetNetLoader() {
        //given
        NetLoader netLoader = mock(NetLoader.class);
        //when
        _netLookup.setNetLoader(netLoader);
        //then
        assertThat(_netLookup.getNetLoader()).isEqualTo(netLoader);
    }

    /**
     * Test method for {@link NetLookup#setNetLoader} with a known net, which
     * isn't allowed and should throw an {@link IllegalStateException}.
     */
    @Test
    void testSetNetLoaderWithKnownNets() {
        //given
        NetLoader netLoader = mock(NetLoader.class);
        //when
        _netLookup.makeNetKnown(new Net("net"));
        //when/then
        assertThatThrownBy(() -> _netLookup.setNetLoader(netLoader))
            .isInstanceOf(IllegalStateException.class);
        verify(_lock, times(2)).runWithLock(any()); // one lock for setting the net, the other for the actual invocation
    }

    /**
     * Test method for {@link NetLookup#findForName}.
     *
     * @throws NetNotFoundException if the net is not loaded and no loader was
     *                              set or the loader could not find the net
     */
    @Test
    void testFindForName() throws NetNotFoundException {
        //given
        setUpSimulationThreads();
        //when
        _netLookup.makeNetKnown(new Net("net"));
        Net net = _netLookup.findForName("net");
        //then
        assertThat(net).isNotNull();
        assertThat(net.getName()).isEqualTo("net");
    }

    /**
     * Test method for {@link NetLookup#findForName} without a known net and
     * net loader, which should throw a {@link NetNotFoundException}.
     */
    @Test
    void testFindForNameWithoutKnownNetAndNetLoader() {
        //given
        setUpSimulationThreads();
        //when/then
        assertThatThrownBy(() -> _netLookup.findForName("net"))
            .isInstanceOf(NetNotFoundException.class);
        verifyLocks(1);
    }

    /**
     * Test method for {@link NetLookup#findForName} without a known net.
     *
     * @throws NetNotFoundException if the net is not loaded and no loader was
     *                              set or the loader could not find the net
     */
    @Test
    void testFindForNameWithoutKnownNet() throws NetNotFoundException {
        //given
        setUpSimulationThreads();
        Net net = new Net("net");
        NetLoader netLoader = mock(NetLoader.class);
        when(netLoader.loadNet("net")).thenReturn(net);
        _netLookup.setNetLoader(netLoader);
        //when
        Net foundNet = _netLookup.findForName("net");
        //when/then
        assertThat(foundNet).isEqualTo(net);
        verify(_lock).runWithLock(any()); // one lock for setting the net,
        verifyLocks(1); // the other for the actual invocation
    }

    /**
     * Test method for {@link NetLookup#findForName} with a known net.
     *
     * @throws NetNotFoundException if the net is not loaded and no loader was
     *                              set or the loader could not find the net
     */
    @Test
    void testFindForNameWithKnownNet() throws NetNotFoundException {
        //given
        setUpSimulationThreads();
        Net net = new Net("net");
        _netLookup.makeNetKnown(net);
        //when
        Net foundNet = _netLookup.findForName("net");
        //when/then
        assertThat(foundNet).isEqualTo(net);
        verify(_lock).runWithLock(any()); // one lock for setting the net,
        verifyLocks(1); // the other for the actual invocation
    }

    private void setUpSimulationThreads() {
        // this mocked construction is used to ensure that all code that is run in the simulation thread is actually run in this test
        // the try catch is needed so that the behaviour of the methods under test does not change.
        // Without it, an exception thrown in the callable will immediately be thrown when the future is defined, not when it is run.
        when(_simulationThreadPool.submitAndWait(Mockito.<Callable<Net>>any()))
            .then(answer((Answer1<Future<Net>, Callable<Net>>) callable -> {
                try {
                    return CompletableFuture.completedFuture(callable.call());
                } catch (Exception e) {
                    return CompletableFuture.failedFuture(e);
                }
            }));
    }

    private void verifyLocks(int n) {
        verify(_lock, times(n)).lock();
        verify(_lock, times(n)).unlock();
    }
}
