package de.renew.net;

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

import de.renew.engine.thread.SimulationThreadPool;
import de.renew.simulatorontology.simulation.SimulationRunningException;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;

/**
 * This class is a test class for the interaction between the classes {@link Place}
 * and {@link Net} in the module {@link de.renew.util}.
 */
public class PlaceTest {
    private static final String NET_NAME = "Test net";
    private static final String PLACE_NAME = "Test place";
    private static final String OTHER_PLACE_NAME = "Other place";

    private Place _place;
    private Place _placeCopy;
    private NetElementID _placeNetElementID;
    private int _placeCount;
    private boolean _done;
    private AssertionError _thrown;

    /**
     * Setup method to initialize this test environment.
     */
    @BeforeEach
    void setUp() {
        _place = null;
        _placeNetElementID = null;
        _done = false;
        _thrown = null;
        _placeCount = -1;
    }

    /**
     * Tests the interaction between the constructor of the class {@link Place}
     * and the method {@link Net#add(Place)}.
     * Tests the case of adding two places to the same net.
     *
     * @throws InterruptedException if any thread interrupts the current thread
     *                              while it is sleeping
     */
    @Test
    void testAddNewPlace() throws InterruptedException {
        Runnable runnable = () -> {
            try {
                //given
                Net net = new Net(NET_NAME);
                NetElementID placeNetElementID = new NetElementID(1);

                //when
                _place = new Place(net, PLACE_NAME, placeNetElementID);
                new Place(net, OTHER_PLACE_NAME, new NetElementID(2));

                //given
                _placeCount = net.placeCount();
                _done = true;
            } catch (SimulationRunningException e) {
                SimulationThreadPool.discardNew();
                throw e;
            }
        };
        //when
        runOnSimulationThread(runnable);

        //then
        assertEquals(2, _placeCount);
    }

    /**
     * Tests that the {@link Place} added to a {@link Net} via the constructor of that
     * class is the same as the one returned by {@link Net#getPlaceWithID(NetElementID)}.
     *
     * @throws InterruptedException if any thread interrupts the current thread
     *                              while it is sleeping
     */
    @Test
    void testGetPlaceWithIdIsTheSame() throws InterruptedException {
        Runnable runnable = () -> {
            try {
                //given
                Net net = new Net(NET_NAME);
                NetElementID placeNetElementID = new NetElementID(1);
                _place = new Place(net, PLACE_NAME, placeNetElementID);

                //when
                _placeCopy = net.getPlaceWithID(placeNetElementID);

                _done = true;
            } catch (SimulationRunningException e) {
                SimulationThreadPool.discardNew();
                throw e;
            }
        };

        //when
        runOnSimulationThread(runnable);

        //then
        assertSame(_place, _placeCopy);
    }

    /**
     * Tests the interaction between the constructor of the class {@link Place}
     * and the method {@link Net#add(Place)} from the Net class.
     * Tests the case of adding two places with the same {@link NetElementID} to the same net.
     * In this case an {@link AssertionError} should be thrown.
     *
     * @throws InterruptedException if any thread interrupts the current thread
     *                              while it is sleeping
     */
    @Test
    void testAddNewPlaceWithExistingID() throws InterruptedException {
        Runnable runnable = () -> {
            try {
                //given
                Net net = new Net(NET_NAME);
                _placeNetElementID = new NetElementID();
                _place = new Place(net, PLACE_NAME, _placeNetElementID);

                //when/then
                _thrown = assertThrows(
                    AssertionError.class,
                    () -> new Place(net, OTHER_PLACE_NAME, _placeNetElementID));

                //given
                _placeCount = net.placeCount();
                _done = true;
            } catch (SimulationRunningException e) {
                SimulationThreadPool.discardNew();
                throw e;
            }
        };

        //when
        runOnSimulationThread(runnable);

        //given
        String expectedMessage = "Tried to add place with existing ID: " + _placeNetElementID
            + ", old: " + _place + ", new: " + OTHER_PLACE_NAME;

        //then
        assertEquals(expectedMessage, _thrown.getMessage());
        assertEquals(1, _placeCount);
    }

    /**
     * Tests the interaction between the constructor of the class {@link Place}
     * and the method {@link Net#add(Place)} from the Net class.
     * Tests the case of adding the same place twice to the same net.
     * In this case an {@link AssertionError} should be thrown.
     *
     * @throws InterruptedException if any thread interrupts the current thread
     *                              while it is sleeping
     */
    @Test
    void testAddExistingPlace() throws InterruptedException {
        Runnable runnable = () -> {
            try {
                //given
                Net net = new Net(NET_NAME);
                _placeNetElementID = new NetElementID();
                _place = new Place(net, PLACE_NAME, _placeNetElementID);

                //when
                _thrown = assertThrows(AssertionError.class, () -> net.add(_place));

                //given
                _placeCount = net.placeCount();
                _done = true;
            } catch (SimulationRunningException e) {
                SimulationThreadPool.discardNew();
                throw e;
            }
        };

        //when
        runOnSimulationThread(runnable);

        //given
        String expectedMessage = "Tried to add existing place: " + _place;

        //then
        assertEquals(expectedMessage, _thrown.getMessage());
        assertEquals(1, _placeCount);
    }

    /**
     * Runs the given {@code Runnable} on a simulation thread and waits until
     * the execution is finished.
     *
     * @param runnable the {@code Runnable} to run
     * @throws InterruptedException if any thread interrupts the current thread
     *                              while it is sleeping
     */
    private void runOnSimulationThread(Runnable runnable) throws InterruptedException {
        _done = false;
        SimulationThreadPool.getNew();
        SimulationThreadPool simulationThreadPool = SimulationThreadPool.getSimulationThreadPool();
        Thread simulationThread = simulationThreadPool.getThreadFactory().newThread(runnable);
        simulationThread.start();
        while (!_done) {
            System.out.println("Simulation thread is running");
            Thread.sleep(2000);
        }
    }
}