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 java.util.stream.Stream;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;

import de.renew.engine.thread.SimulationThreadPool;
import de.renew.net.event.PlaceEventListener;
import de.renew.unify.Impossible;

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

/**
 * Test class for {@link Place}.
 */
public class PlaceTest {
    private static final NetElementID NET_ELEMENT_ID = new NetElementID(123, 123);
    private static final String PLACE_NAME = "TestPlace";

    private MockedStatic<SimulationThreadPool> _simulationThreadPoolMockedStatic;
    private Place _place;
    private Net _net;

    private AutoCloseable _closeable;

    /**
     * Setup method to initialize this test environment.
     */
    @BeforeEach
    public void setUp() {
        _closeable = MockitoAnnotations.openMocks(this);
        _simulationThreadPoolMockedStatic = mockStatic(SimulationThreadPool.class);
        _simulationThreadPoolMockedStatic.when(SimulationThreadPool::isSimulationThread)
            .thenReturn(true);

        _net = Mockito.spy(new Net("cool net"));
        _place = new Place(_net, PLACE_NAME, NET_ELEMENT_ID);
    }

    /**
     * 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 Place}.
     * Checks that all variables are initialized correctly.
     */
    @Test
    public void testConstructor() {
        //then
        verify(_net).add(_place);

        assertThat(_place.getID()).isEqualTo(NET_ELEMENT_ID);
        assertThat(_place.getName()).isEqualTo(PLACE_NAME);
        assertThat(_place.getTrace()).isTrue();
        assertThat(_place.getTokenSources()).isEmpty();
    }

    /**
     * Test method for {@link Place#toString}.
     */
    @Test
    public void testToString() {
        //then
        assertThat(_place.toString()).isEqualTo(PLACE_NAME);
    }

    /**
     * Test method for {@link Place#setBehaviour}.
     */
    @Test
    public void testSetBehaviour() {
        //given
        NetInstance mockNetInstance = mock(NetInstance.class);
        Net mockNetForInstance = mock(Net.class);

        when(mockNetInstance.getNet()).thenReturn(mockNetForInstance);

        //when
        _place.setBehaviour(Place.MULTISETPLACE);

        _place.setBehaviour(Place.FIFOPLACE);

        //then
        assertThat(_place.getPlacetype()).isEqualTo(Place.FIFOPLACE);

        try {
            //when
            _place.setBehaviour(999);
            _place.makeInstance(mockNetInstance, false);
            fail("Should throw RuntimeException for invalid behavior");
        } catch (RuntimeException e) {
            //then
            assertThat(e.getMessage()).contains("Illegal place behaviour");
        } catch (Impossible e) {
            fail(e.getMessage());
        }
    }

    private static Stream<Arguments> providePlaceBehaviors() {
        return Stream.of(
            Arguments.of(Place.FIFOPLACE, FIFOPlaceInstance.class),
            Arguments.of(Place.MULTISETPLACE, MultisetPlaceInstance.class));
    }

    /**
     * Test method for {@link Place#makeInstance}.
     */
    @ParameterizedTest
    @MethodSource("providePlaceBehaviors")
    public void testMakeInstance(
        int behavior, Class<? extends SimulatablePlaceInstance> expectedClass) throws Impossible
    {
        //given
        NetInstance mockNetInstance = mock(NetInstance.class);
        Net mockNetForInstance = mock(Net.class);

        when(mockNetInstance.getNet()).thenReturn(mockNetForInstance);

        _place.setBehaviour(behavior);

        //when
        PlaceInstance instance = _place.makeInstance(mockNetInstance, false);

        //then
        assertThat(instance).isNotNull();
        assertThat(instance).isInstanceOf(expectedClass);
    }

    /**
     * Test method for {@link Place#makeInstance} with invalid behaviour.
     */
    @Test
    public void testMakeInstanceInvalidBehaviour() {
        //given
        NetInstance mockNetInstance = mock(NetInstance.class);
        Net mockNetForInstance = mock(Net.class);

        when(mockNetInstance.getNet()).thenReturn(mockNetForInstance);

        //when
        _place.setBehaviour(999);

        //then
        assertThatThrownBy(() -> _place.makeInstance(mockNetInstance, false))
            .isInstanceOf(RuntimeException.class).hasMessageContaining("Illegal place behaviour");
    }

    /**
     * Test method for {@link Place#add(TokenSource)} and {@link Place#remove(TokenSource)}.
     */
    @Test
    public void testAddRemoveTokenSource() {
        //given
        TokenSource mockTokenSource1 = mock(TokenSource.class);
        TokenSource mockTokenSource2 = mock(TokenSource.class);

        //then
        assertThat(_place.getTokenSources()).isEmpty();
        assertThat(_place.hasInitialTokens()).isFalse();

        //when
        _place.add(mockTokenSource1);

        //then
        assertThat(_place.getTokenSources()).containsExactly(mockTokenSource1);
        assertThat(_place.hasInitialTokens()).isTrue();

        //when
        _place.add(mockTokenSource2);

        //then
        assertThat(_place.getTokenSources())
            .containsExactlyInAnyOrder(mockTokenSource1, mockTokenSource2);

        //when
        _place.remove(mockTokenSource1);

        //then
        assertThat(_place.getTokenSources()).containsExactly(mockTokenSource2);

        //when
        _place.remove(mockTokenSource2);

        //then
        assertThat(_place.getTokenSources()).isEmpty();
        assertThat(_place.hasInitialTokens()).isFalse();
    }

    /**
     * Test for {@link Place#add(TokenSource)}. Attempts to add the same {@link TokenSource}
     * twice. The second time shouldn't do anything.
     */
    @Test
    public void testAddDuplicateTokenSource() {
        //given
        TokenSource mockTokenSource = mock(TokenSource.class);

        //when
        _place.add(mockTokenSource);
        _place.add(mockTokenSource);

        //then
        assertThat(_place.getTokenSources()).hasSize(1);
    }

    /**
     * Test method for {@link Place#hasInitialTokens}.
     */
    @Test
    public void testHasInitialTokens() {
        //given
        TokenSource mockTokenSource = mock(TokenSource.class);

        //when/then
        assertThat(_place.hasInitialTokens()).isFalse();

        //given
        _place.add(mockTokenSource);

        //when/then
        assertThat(_place.hasInitialTokens()).isTrue();

        //given
        _place.remove(mockTokenSource);

        //when/then
        assertThat(_place.hasInitialTokens()).isFalse();
    }

    /**
     * Test method for checking that a place can be serialized and deserialized
     * properly. This is not a test for a specific method in {@link Place}.
     *
     * @throws IOException if there is an error with the streams
     * @throws ClassNotFoundException if there is an error while reading the place
     */
    @Test
    public void testSerializationDeserialization() throws IOException, ClassNotFoundException {
        //given
        TokenSource mockTokenSource = mock(TokenSource.class);
        _place.add(mockTokenSource);

        PlaceEventListener mockListener = mock(PlaceEventListener.class);
        _place.addPlaceEventListener(mockListener);

        String comment = "cool comment";
        _place.setComment(comment);

        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectStream = new ObjectOutputStream(outputStream);

        //when
        objectStream.writeObject(_place);
        objectStream.close();

        ByteArrayInputStream bais = new ByteArrayInputStream(outputStream.toByteArray());
        ObjectInputStream inputStream = new ObjectInputStream(bais);
        Place deserializedPlace = (Place) inputStream.readObject();
        inputStream.close();

        //then
        assertThat(deserializedPlace.getID()).isEqualTo(_place.getID());
        assertThat(deserializedPlace.getName()).isEqualTo(_place.getName());
        assertThat(deserializedPlace.getTrace()).isEqualTo(_place.getTrace());
        assertThat(deserializedPlace.getComment()).isEqualTo(_place.getComment());

        assertThat(deserializedPlace.getListenerSet()).isNotNull();
    }
}