package de.renew.shadowcompiler;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
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.Mock;
import org.mockito.MockitoAnnotations;

import de.renew.net.INetLookup;
import de.renew.net.Net;
import de.renew.net.NetLookup;
import de.renew.simulatorontology.shadow.ShadowNet;
import de.renew.simulatorontology.shadow.ShadowNetSystem;
import de.renew.simulatorontology.shadow.SyntaxException;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.params.provider.Arguments.arguments;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * Test class for {@link ShadowNetSystemCompiler}.
 * Tests only the relevant methods and exceptions.
 */
public class ShadowNetSystemCompilerTest {
    private static final String NET_NAME_1 = "net1";
    private static final String NET_NAME_2 = "net2";
    private static final String NET_NAME_3 = "net3";

    private IShadowNetSystemCompiler _shadowNetSystemCompiler;
    @Mock
    private ShadowCompiler _mockShadowCompiler;
    @Mock
    private ShadowPreprocessor _mockPreprocessor;
    @Mock
    private ShadowCompilerFactory _shadowCompilerFactoryMock;
    private INetLookup _netLookup;
    private AutoCloseable _closeable;

    /**
     * Setup method to initialize this test environment.
     */
    @BeforeEach
    public void setup() {
        _shadowNetSystemCompiler = ShadowNetSystemCompiler.getInstance();
        _closeable = MockitoAnnotations.openMocks(this);
        _netLookup = new NetLookup();
        when(_shadowCompilerFactoryMock.createCompiler()).thenReturn(_mockShadowCompiler);
    }

    /**
     * TearDown method to clean up test environment.
     */
    @AfterEach
    public void tearDown() throws Exception {
        _netLookup.forgetAllNets();
        _closeable.close();
    }

    private ShadowNetSystem createNetSystemFromNets(String... netNames) {
        ShadowNetSystem shadowNetSystem = new ShadowNetSystem();
        Arrays.stream(netNames).forEach(name -> new ShadowNet(name, shadowNetSystem));
        return shadowNetSystem;
    }

    private void mockCompiledNets(List<String> netNames) {
        netNames
            .forEach(name -> when(_mockShadowCompiler.createNet(name)).thenReturn(mock(Net.class)));
    }

    private void assertCompilation(List<ShadowNet> shadowNets, ShadowLookup lookup)
        throws SyntaxException
    {
        for (ShadowNet net : shadowNets) {
            verify(_mockShadowCompiler).compile(net);
        }
        assertThat(Collections.list(lookup.allNetNames()))
            .containsExactlyInAnyOrder(NET_NAME_1, NET_NAME_2);
    }

    /**
     * Tests that the {@link ShadowNetSystemCompiler} is a Singleton.
     */
    @Test
    public void testShadowNetSystemCompilerSingleton() {
        // given
        IShadowNetSystemCompiler shadowNetSystemCompiler1 = ShadowNetSystemCompiler.getInstance();
        IShadowNetSystemCompiler shadowNetSystemCompiler2 = ShadowNetSystemCompiler.getInstance();

        // when and then
        assertThat(shadowNetSystemCompiler1).isSameAs(shadowNetSystemCompiler2);
    }

    /**
     * Verify that the compiled nets have to be recompiled when the compiler is changed.
     */
    @Test
    public void testSetDefaultCompileFactoryRecompileNetSystem() {
        //given
        ShadowNetSystem shadowNetSystem = mock(ShadowNetSystem.class);
        //when
        _shadowNetSystemCompiler
            .setDefaultCompilerFactory(_shadowCompilerFactoryMock, shadowNetSystem);
        //then
        verify(shadowNetSystem).recompile();
    }

    /**
     * Verify that an exception is thrown when there is no compiler set for a net.
     */
    @Test
    public void testCompileWhenNoCompilerIsSet() {
        //given
        ShadowNetSystem shadowNetSystem = createNetSystemFromNets("net");

        //when/then
        assertThatThrownBy(() -> _shadowNetSystemCompiler.compile(shadowNetSystem))
            .isInstanceOf(SyntaxException.class)
            .hasMessageContaining("No compiler or default compiler set for net net");
    }

    /**
     * Tests, that the correct {@link ShadowCompiler} is generated.
     */
    @Test
    public void testCreateShadowNetCompiler() throws SyntaxException {
        //given
        ShadowNetSystem shadowNetSystem = new ShadowNetSystem();
        ShadowNet shadowNet = new ShadowNet("net", shadowNetSystem);
        shadowNetSystem.add(shadowNet);
        ShadowLookup lookup = new ShadowLookup();

        //when/then
        _shadowNetSystemCompiler
            .setDefaultCompilerFactory(_shadowCompilerFactoryMock, shadowNetSystem);
        assertThat(_shadowNetSystemCompiler.createShadowNetCompiler(lookup, shadowNet))
            .isSameAs(_mockShadowCompiler);
        verify(_mockShadowCompiler).setShadowLookup(lookup);
    }

    /**
     * Tests the successful compilation of the nets.
     */
    @Test
    public void testCompileSuccessful() throws SyntaxException {
        //given
        ShadowNetSystem shadowNetSystem = new ShadowNetSystem();
        ShadowNet shadowNet1 = new ShadowNet(NET_NAME_1, shadowNetSystem);
        ShadowNet shadowNet2 = new ShadowNet(NET_NAME_2, shadowNetSystem);

        _shadowNetSystemCompiler
            .setDefaultCompilerFactory(_shadowCompilerFactoryMock, shadowNetSystem);
        mockCompiledNets(List.of(NET_NAME_1, NET_NAME_2));

        //when
        ShadowLookup lookup = _shadowNetSystemCompiler.compile(shadowNetSystem);

        //then
        assertCompilation(List.of(shadowNet1, shadowNet2), lookup);
    }

    /**
     * Tests the method {@link IShadowNetSystemCompiler#setCompilerFactory(ShadowCompilerFactory, ShadowNet)} and the correct compilation afterwards.
     */
    @Test
    public void testCompileWhenNetHasOwnCompiler() throws SyntaxException {
        //given
        ShadowNetSystem shadowNetSystem = new ShadowNetSystem();
        ShadowNet shadowNet1 = new ShadowNet(NET_NAME_1, shadowNetSystem);
        ShadowNet shadowNet2 = new ShadowNet(NET_NAME_2, shadowNetSystem);

        ShadowCompilerFactory compilerFactory = mock(ShadowCompilerFactory.class);
        ShadowCompiler compiler = mock(ShadowCompiler.class);
        when(compilerFactory.createCompiler()).thenReturn(compiler);

        _shadowNetSystemCompiler.setCompilerFactory(compilerFactory, shadowNet1);
        _shadowNetSystemCompiler
            .setDefaultCompilerFactory(_shadowCompilerFactoryMock, shadowNetSystem);
        when(compiler.createNet(NET_NAME_1)).thenReturn(mock(Net.class));
        mockCompiledNets(List.of(NET_NAME_2));

        //when
        ShadowLookup lookup = _shadowNetSystemCompiler.compile(shadowNetSystem);

        //then
        verify(compiler).compile(shadowNet1);
        assertCompilation(List.of(shadowNet2), lookup);
    }

    /**
     * Tests that only the uncompiled nets get compiled.
     */
    @Test
    public void testCompileOnlyUncompiledNets() throws SyntaxException {
        //given
        ShadowNetSystem shadowNetSystem = new ShadowNetSystem();
        ShadowNet shadowNet1 = new ShadowNet(NET_NAME_1, shadowNetSystem);
        ShadowNet shadowNet2 = new ShadowNet(NET_NAME_2, shadowNetSystem);

        _shadowNetSystemCompiler
            .setDefaultCompilerFactory(_shadowCompilerFactoryMock, shadowNetSystem);
        shadowNetSystem.markAsCompiled(shadowNet1);

        mockCompiledNets(List.of(NET_NAME_2));

        //when
        _shadowNetSystemCompiler.compile(shadowNetSystem);
        //then
        verify(_mockShadowCompiler, never()).compile(shadowNet1);
        verify(_mockShadowCompiler).compile(shadowNet2);
    }

    /**
     * Tests the successful compilation with preprocessors.
     */
    @Test
    public void testCompileSuccessfulWithPreprocessors() throws SyntaxException {
        //given
        ShadowNetSystem shadowNetSystem = new ShadowNetSystem();
        ShadowNet shadowNet1 = new ShadowNet(NET_NAME_1, shadowNetSystem);
        ShadowNet shadowNet2 = new ShadowNet(NET_NAME_2, shadowNetSystem);

        _shadowNetSystemCompiler
            .setDefaultCompilerFactory(_shadowCompilerFactoryMock, shadowNetSystem);
        when(_mockShadowCompiler.getRequiredPreprocessors())
            .thenReturn(new ShadowPreprocessor[] { _mockPreprocessor });

        mockCompiledNets(List.of(NET_NAME_1, NET_NAME_2));;

        //when
        ShadowLookup lookup = _shadowNetSystemCompiler.compile(shadowNetSystem);
        //then
        verify(_mockPreprocessor).setShadowLookup(lookup);
        verify(_mockPreprocessor).preprocess(shadowNetSystem);

        assertCompilation(List.of(shadowNet1, shadowNet2), lookup);
    }

    /**
     * Tests that {@link IShadowNetSystemCompiler#compile(ShadowNetSystem)} throws an exception when there is already a net known with the same net as the net to be compiled.
     */
    @Test
    public void testCompileThrowsExceptionWithKnownNet() {
        // given
        ShadowNetSystem shadowNetSystem = createNetSystemFromNets(NET_NAME_1);

        _shadowNetSystemCompiler
            .setDefaultCompilerFactory(_shadowCompilerFactoryMock, shadowNetSystem);

        Net net = mock(Net.class);
        when(net.getName()).thenReturn(NET_NAME_1);
        _netLookup.makeNetKnown(net);

        // when and then
        assertSyntaxException(shadowNetSystem);
    }

    /**
     * Tests that {@link IShadowNetSystemCompiler#compile(ShadowNetSystem)} throws an exception when two nets with the same name are to be compiled.
     */
    @Test
    public void testCompileThrowsExceptionOnTwoSameNetNames() {
        // given
        ShadowNetSystem shadowNetSystem = createNetSystemFromNets(NET_NAME_1, NET_NAME_1);

        _shadowNetSystemCompiler
            .setDefaultCompilerFactory(_shadowCompilerFactoryMock, shadowNetSystem);

        mockCompiledNets(List.of(NET_NAME_1));

        Net net = mock(Net.class);
        when(net.getName()).thenReturn(NET_NAME_1);

        _netLookup.makeNetKnown(net);
        // when and then
        assertSyntaxException(shadowNetSystem);
    }

    private void assertSyntaxException(ShadowNetSystem shadowNetSystem) {
        assertThatThrownBy(() -> _shadowNetSystemCompiler.compile(shadowNetSystem))
            .isInstanceOf(SyntaxException.class)
            .hasMessageContaining("Detected two nets with the same name");
    }

    /**
     * Test method for {@link IShadowNetSystemCompiler#compileMore(ShadowNetSystem)}.
     */
    @Test
    public void testCompileMore() throws SyntaxException {
        //given
        ShadowNetSystem shadowNetSystem = createNetSystemFromNets(NET_NAME_1, NET_NAME_2);
        Net compiledNet = mock(Net.class);
        when(compiledNet.getName()).thenReturn(NET_NAME_3);
        _shadowNetSystemCompiler
            .setDefaultCompilerFactory(_shadowCompilerFactoryMock, shadowNetSystem);
        mockCompiledNets(List.of(NET_NAME_1, NET_NAME_2));

        _netLookup.makeNetKnown(compiledNet);
        //when
        ShadowLookup lookup = _shadowNetSystemCompiler.compileMore(shadowNetSystem);
        //then
        assertThat(Collections.list(lookup.allNetNames()))
            .containsExactlyInAnyOrder(NET_NAME_1, NET_NAME_2, NET_NAME_3);
    }

    /**
     * Test method for {@link IShadowNetSystemCompiler#compileMore(ShadowNetSystem)} when an uncompiled net has preprocessors.
     */
    @ParameterizedTest
    @MethodSource("provideNets")
    public void testCompileMoreThrowsExceptionForDynamicNetsWithPreprocessors(
        List<String> netNames, String expectedErrorMessage)
    {
        // given
        ShadowNetSystem shadowNetSystem = createNetSystemFromNets(netNames.toArray(new String[0]));

        _shadowNetSystemCompiler
            .setDefaultCompilerFactory(_shadowCompilerFactoryMock, shadowNetSystem);

        when(_mockShadowCompiler.getRequiredPreprocessors())
            .thenReturn(new ShadowPreprocessor[] { _mockPreprocessor });

        mockCompiledNets(List.of(NET_NAME_1, NET_NAME_2));

        //when/then
        assertThatThrownBy(() -> _shadowNetSystemCompiler.compileMore(shadowNetSystem))
            .isInstanceOf(SyntaxException.class).hasMessage(expectedErrorMessage);
    }

    public static Stream<Arguments> provideNets() {
        return Stream.of(
            arguments(
                List.of(NET_NAME_1),
                "The dynamically loaded net net1 requires a preprocessing phase. This is not allowed."),
            arguments(
                List.of(NET_NAME_1, NET_NAME_2),
                "Some dynamically loaded nets (net2, net1) require a preprocessing phase. This is not allowed."));
    }

    /**
     * Test method for {@link IShadowNetSystemCompiler#compile(ShadowNetSystem)} when the system does not contain any nets.
     */
    @Test
    void testCompileEmpty() throws SyntaxException {
        // given
        ShadowNetSystem shadowNetSystem = new ShadowNetSystem();
        // when
        ShadowLookup result = _shadowNetSystemCompiler.compile(shadowNetSystem);
        // then
        Assertions.assertNotNull(result);
    }

    /**
     * Test method for {@link IShadowNetSystemCompiler#switchNetSystem(ShadowNet, ShadowNetSystem)}.
     */
    @Test
    public void testSwitchNetSystem() throws SyntaxException {
        //given
        ShadowNetSystem originalSystem = new ShadowNetSystem();
        ShadowNetSystem newSystem = new ShadowNetSystem();
        ShadowCompilerFactory newSystemCompilerFactory = mock(ShadowCompilerFactory.class);
        ShadowCompiler newCompiler = mock(ShadowCompiler.class);
        when(newSystemCompilerFactory.createCompiler()).thenReturn(newCompiler);
        ShadowNet shadowNet = new ShadowNet(NET_NAME_1, originalSystem);
        _shadowNetSystemCompiler
            .setDefaultCompilerFactory(_shadowCompilerFactoryMock, originalSystem);
        _shadowNetSystemCompiler.setDefaultCompilerFactory(newSystemCompilerFactory, newSystem);

        //when
        _shadowNetSystemCompiler.switchNetSystem(shadowNet, newSystem);

        //then
        ShadowCompiler compiler =
            _shadowNetSystemCompiler.createShadowNetCompiler(new ShadowLookup(), shadowNet);
        assertThat(compiler).isSameAs(_mockShadowCompiler);
    }
}
