package de.renew.net;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.MockitoAnnotations;

import de.renew.engine.thread.SimulationThreadPool;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.when;

/**
 * Test class for {@link Net}.
 */
public class NetTest {
    private static final String TEST_NET_NAME = "TestNet";
    private static final String NET_1_NAME = "Net1";
    private static final NetElementID PLACE_ID = new NetElementID(123, 123);
    private static final NetElementID TRANSITION_ID = new NetElementID(234, 234);

    @Mock
    private Place _mockPlace;

    @Mock
    private Transition _mockTransition;
    private Net _net;

    private MockedStatic<SimulationThreadPool> _simulationThreadPoolMockedStatic;

    private AutoCloseable _closeable;

    /**
     * Setup method to initialize this test environment.
     */
    @BeforeEach
    public void setUp() {
        _closeable = MockitoAnnotations.openMocks(this);

        when(_mockPlace.getID()).thenReturn(PLACE_ID);
        when(_mockTransition.getID()).thenReturn(TRANSITION_ID);

        _simulationThreadPoolMockedStatic = mockStatic(SimulationThreadPool.class);
        _simulationThreadPoolMockedStatic.when(SimulationThreadPool::isSimulationThread)
            .thenReturn(true);

        _net = new Net(TEST_NET_NAME);
    }

    /**
     * Teardown method to clean up this test environment.
     *
     * @throws Exception if there is an error while closing the mocks
     */
    @AfterEach
    public void tearDown() throws Exception {
        _closeable.close();
        _simulationThreadPoolMockedStatic.close();
    }

    /**
     * Test method for the constructor of {@link Net}.
     * Checks that the name and {@link Place} and {@link Transition} counts
     * are initialized properly.
     */
    @Test
    public void testNetCreation() {
        //then
        assertThat(_net.getName()).isEqualTo(TEST_NET_NAME);
        assertThat(_net.placeCount()).isEqualTo(0);
        assertThat(_net.transitionCount()).isEqualTo(0);
    }

    /**
     * Test method for {@link Net#add(Place)}.
     */
    @Test
    public void testAddPlace() {
        //when
        _net.add(_mockPlace);

        //then
        assertThat(_net.placeCount()).isEqualTo(1);
        assertThat(_net.places()).contains(_mockPlace);
        assertThat(_net.getPlaceWithID(PLACE_ID)).isEqualTo(_mockPlace);
    }

    /**
     * Test method for {@link Net#add(Place)} with multiple places.
     */
    @Test
    public void testAddPlaceMultiple() {
        //given
        Place mockPlace2 = mock(Place.class);
        NetElementID place2id = new NetElementID(1, 2);
        when(mockPlace2.getID()).thenReturn(place2id);

        //when
        _net.add(_mockPlace);
        _net.add(mockPlace2);

        //then
        assertThat(_net.placeCount()).isEqualTo(2);
        assertThat(_net.places()).containsExactlyInAnyOrder(_mockPlace, mockPlace2);
        assertThat(_net.getPlaceWithID(PLACE_ID)).isEqualTo(_mockPlace);
        assertThat(_net.getPlaceWithID(place2id)).isEqualTo(mockPlace2);
    }

    /**
     * Test method for {@link Net#add(Place)}. Attempts to add the same place
     * twice, which isn't allowed and should throw an exception.
     */
    @Test
    public void testAddPlaceDuplicate() {
        //given
        _net.add(_mockPlace);

        //when / then
        assertThatThrownBy(() -> _net.add(_mockPlace)).isInstanceOf(AssertionError.class)
            .hasMessageContaining("Tried to add existing place: " + _mockPlace);
    }

    /**
     * Test method for {@link Net#add(Place)}. Attempts to add two places
     * with the same ids, which isn't allowed and should throw an exception.
     */
    @Test
    public void testAddPlacesWithSameId() {
        //given
        Place mockPlace2 = mock(Place.class);
        when(mockPlace2.getID()).thenReturn(PLACE_ID);
        _net.add(_mockPlace);

        //when / then
        assertThatThrownBy(() -> _net.add(mockPlace2)).isInstanceOf(AssertionError.class)
            .hasMessageContaining(
                "Tried to add place with existing ID: " + PLACE_ID + ", old: " + _mockPlace
                    + ", new: " + mockPlace2);
    }

    /**
     * Test method for {@link Net#add(Transition)}.
     */
    @Test
    public void testAddTransition() {
        //when
        _net.add(_mockTransition);

        //then
        assertThat(_net.transitionCount()).isEqualTo(1);
        assertThat(_net.transitions()).contains(_mockTransition);
        assertThat(_net.getTransitionWithID(TRANSITION_ID)).isEqualTo(_mockTransition);
    }

    /**
     * Test method for {@link Net#add(Transition)} with multiple transitions.
     */
    @Test
    public void testAddTransitionMultiple() {
        //given
        Transition mockTransition2 = mock(Transition.class);
        NetElementID transition2id = new NetElementID(1, 2);
        when(mockTransition2.getID()).thenReturn(transition2id);

        //when
        _net.add(_mockTransition);
        _net.add(mockTransition2);

        //then
        assertThat(_net.transitionCount()).isEqualTo(2);
        assertThat(_net.transitions()).containsExactlyInAnyOrder(_mockTransition, mockTransition2);
        assertThat(_net.getTransitionWithID(TRANSITION_ID)).isEqualTo(_mockTransition);
        assertThat(_net.getTransitionWithID(transition2id)).isEqualTo(mockTransition2);
    }

    /**
     * Test method for {@link Net#add(Transition)}. Attempts to add the same
     * transition twice, which isn't allowed and should throw an exception.
     */
    @Test
    public void testAddTransitionDuplicate() {
        //given
        _net.add(_mockTransition);

        //when / then
        assertThatThrownBy(() -> _net.add(_mockTransition)).isInstanceOf(AssertionError.class)
            .hasMessageContaining("Tried to add existing transition: " + _mockTransition);
    }

    /**
     * Test method for {@link Net#add(Transition)}. Attempts to add two transitions
     * with the same ids, which isn't allowed and should throw an exception.
     */
    @Test
    public void testAddTransitionsWithSameId() {
        //given
        Transition mockTransition2 = mock(Transition.class);
        when(mockTransition2.getID()).thenReturn(TRANSITION_ID);
        _net.add(_mockTransition);

        //when / then
        assertThatThrownBy(() -> _net.add(mockTransition2)).isInstanceOf(AssertionError.class)
            .hasMessageContaining(
                "Tried to add transition with existing ID: " + TRANSITION_ID + ", old: "
                    + _mockTransition + ", new: " + mockTransition2);
    }

    /**
     * Test method for {@link Net#remove(Place)}.
     */
    @Test
    public void testRemovePlace() {
        //given
        _net.add(_mockPlace);

        //then
        assertThat(_net.placeCount()).isEqualTo(1);

        //when
        _net.remove(_mockPlace);

        //then
        assertThat(_net.placeCount()).isEqualTo(0);
        assertThat(_net.getPlaceWithID(PLACE_ID)).isNull();
    }

    /**
     * Test method for {@link Net#remove(Place)}. Attempts to remove a place
     * that is not in the net, which shouldn't do anything.
     */
    @Test
    public void testRemovePlaceNotPresent() {
        //given
        _net.add(_mockPlace);
        Place mockPlace2 = mock(Place.class);

        //then
        assertThat(_net.placeCount()).isEqualTo(1);

        //when
        _net.remove(mockPlace2);

        //then
        assertThat(_net.placeCount()).isEqualTo(1);
        assertThat(_net.getPlaceWithID(PLACE_ID)).isEqualTo(_mockPlace);
    }

    /**
     * Test method for {@link Net#remove(Transition)}.
     */
    @Test
    public void testRemoveTransition() {
        //given
        _net.add(_mockTransition);

        //then
        assertThat(_net.transitionCount()).isEqualTo(1);

        //when
        _net.remove(_mockTransition);

        //then
        assertThat(_net.transitionCount()).isEqualTo(0);
        assertThat(_net.getTransitionWithID(TRANSITION_ID)).isNull();
    }

    /**
     * Test method for {@link Net#remove(Transition)}. Attempts to remove a
     * transition that is not in the net, which shouldn't do anything.
     */
    @Test
    public void testRemoveTransitionNotPresent() {
        //given
        _net.add(_mockTransition);
        Transition mockTransition2 = mock(Transition.class);

        //then
        assertThat(_net.transitionCount()).isEqualTo(1);

        //when
        _net.remove(mockTransition2);

        //then
        assertThat(_net.transitionCount()).isEqualTo(1);
        assertThat(_net.getTransitionWithID(TRANSITION_ID)).isEqualTo(_mockTransition);
    }

    /**
     * Test method for {@link Net#makeNetNumber}.
     */
    @Test
    public void testMakeNetNumber() {
        //when
        int num1 = _net.makeNetNumber();
        int num2 = _net.makeNetNumber();

        //then
        assertThat(num2).isEqualTo(num1 + 1);
    }


    /**
     * Test method for checking that a net can be serialized and deserialized
     * properly. This is not a test for a specific method in {@link Net}.
     *
     * @throws IOException if there is an error with the streams
     * @throws ClassNotFoundException if there is an error while reading the net
     */
    @Test
    public void testSerializeAndDeserializeNet() throws IOException, ClassNotFoundException {
        //given
        Net originalNet = new Net(NET_1_NAME);
        originalNet.add(_mockPlace);
        originalNet.add(_mockTransition);

        //when
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(originalNet);
        oos.close();

        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        Net deserializedNet = (Net) ois.readObject();
        ois.close();

        //then
        assertThat(deserializedNet.getName()).isEqualTo(NET_1_NAME);
    }
}