package de.renew.ptchannel.single;

import java.util.Collection;
import java.util.Collections;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockedConstruction;
import org.mockito.Mockito;

import de.renew.expression.TupleExpression;
import de.renew.formalism.java.ParseException;
import de.renew.net.Place;
import de.renew.net.Transition;
import de.renew.net.inscription.TransitionInscription;
import de.renew.net.inscription.transition.UplinkInscription;
import de.renew.net.loading.NetLoader;
import de.renew.shadowcompiler.ShadowLookup;
import de.renew.simulatorontology.shadow.ShadowArc;
import de.renew.simulatorontology.shadow.ShadowDeclarationNode;
import de.renew.simulatorontology.shadow.ShadowInscription;
import de.renew.simulatorontology.shadow.ShadowNet;
import de.renew.simulatorontology.shadow.ShadowTransition;
import de.renew.simulatorontology.shadow.SyntaxException;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

/**
 * @author 1lopes
 */
public class SinglePTNetWithChannelCompilerIntegrationTest {

    private ShadowNet _mockShadowNet;
    private SinglePTNetWithChannelCompiler _compiler;
    private ShadowLookup _mockLookup;

    @BeforeEach
    void setUp() {
        _mockShadowNet = mock(ShadowNet.class);
        _mockLookup = mock(ShadowLookup.class);
        NetLoader mockLoader = mock(NetLoader.class);

        _compiler = new SinglePTNetWithChannelCompiler();
        _compiler.setShadowLookup(_mockLookup);
        _compiler.setLoopbackNetLoader(mockLoader);
    }

    @Test
    void testConstructorInitializesPTCTransitionCompiler() {
        // given / when
        SinglePTNetWithChannelCompiler realCompiler = new SinglePTNetWithChannelCompiler();

        // then
        assertNotNull(realCompiler.getTransitionCompiler());
        assertEquals(PTCTransitionCompiler.class, realCompiler.getTransitionCompiler().getClass());
    }

    @Test
    void testCheckTransitionInscriptionDelegates() throws SyntaxException {
        // given
        _compiler.makeDeclarationNode(_mockShadowNet);

        String inscription = ":channel()";
        String result;

        // when
        try (MockedConstruction<UplinkInscription> ignored =
            Mockito.mockConstruction(UplinkInscription.class)) {

            result = _compiler.checkTransitionInscription(inscription, false, _mockShadowNet);
        }

        // then
        assertEquals("uplink", result);
    }

    @Test
    void testCheckArcInscriptionDelegates() throws SyntaxException {
        // given
        _compiler.makeDeclarationNode(_mockShadowNet);

        // when
        String result = _compiler.checkArcInscription("x", false, _mockShadowNet);

        // then
        assertEquals("inscription", result);
    }

    @Test
    void testCompileTransitionInscriptionDelegates() throws SyntaxException {
        // given
        _compiler.makeDeclarationNode(_mockShadowNet);

        ShadowInscription mockInsc = mock(ShadowInscription.class);
        ShadowTransition mockShadowTrans = mock(ShadowTransition.class);
        Transition mockTrans = mock(Transition.class);

        when(mockInsc.getInscription()).thenReturn(":channel(1)");
        when(mockInsc.getInscribable()).thenReturn(mockShadowTrans);
        when(_mockLookup.get(mockShadowTrans)).thenReturn(mockTrans);

        Collection<TransitionInscription> result;

        // when
        try (MockedConstruction<UplinkInscription> ignored = Mockito.mockConstruction(
            UplinkInscription.class, (mock, context) -> when(mock.getParams())
                .thenReturn((TupleExpression) context.arguments().get(1)))) {

            result = _compiler.compileTransitionInscription(mockInsc);
        }

        // then
        assertNotNull(result);
        assertEquals(1, result.size());
    }

    @Test
    void testCompileArcHandlesUninscribedArc() throws SyntaxException {
        // given
        _compiler.makeDeclarationNode(_mockShadowNet);

        ShadowArc mockArc = mock(ShadowArc.class);
        ShadowTransition mockShadowTransition = mock(ShadowTransition.class);
        Transition realTransition = mock(Transition.class);
        Place realPlace = mock(Place.class);
        de.renew.simulatorontology.shadow.ShadowPlace mockShadowPlace =
            mock(de.renew.simulatorontology.shadow.ShadowPlace.class);

        when(mockArc.elements()).thenReturn(Collections.emptySet());
        when(mockArc.getShadowArcType()).thenReturn(ShadowArc.ORDINARY);
        when(mockArc.getTransition()).thenReturn(mockShadowTransition);
        when(mockArc.getPlace()).thenReturn(mockShadowPlace);
        when(_mockLookup.get(mockShadowTransition)).thenReturn(realTransition);
        when(_mockLookup.get(mockShadowPlace)).thenReturn(realPlace);

        // when / then
        assertDoesNotThrow(() -> _compiler.compileArc(mockArc));
    }

    @Test
    void testCompileArcDelegatesToTCWhenSuperFails() throws SyntaxException {
        // given
        _compiler.makeDeclarationNode(_mockShadowNet);

        ShadowArc mockArc = mock(ShadowArc.class);
        when(mockArc.getShadowArcType()).thenReturn(ShadowArc.ORDINARY);

        ShadowTransition mockShadowTransition = mock(ShadowTransition.class);
        Transition realTransition = mock(Transition.class);
        de.renew.simulatorontology.shadow.ShadowPlace mockShadowPlace =
            mock(de.renew.simulatorontology.shadow.ShadowPlace.class);
        Place realPlace = mock(Place.class);
        when(mockArc.getTransition()).thenReturn(mockShadowTransition);
        when(mockArc.getPlace()).thenReturn(mockShadowPlace);
        when(_mockLookup.get(mockShadowTransition)).thenReturn(realTransition);
        when(_mockLookup.get(mockShadowPlace)).thenReturn(realPlace);

        ShadowInscription mockArcInscription = mock(ShadowInscription.class);
        when(mockArcInscription.getInscription()).thenReturn("x");
        when(mockArc.elements()).thenReturn(Collections.singleton(mockArcInscription));

        // when / then
        assertDoesNotThrow(() -> _compiler.compileArc(mockArc));
    }

    @Test
    void testCompileArcThrowsExceptionWhenSuperFailsAndArcNotOrdinary() throws SyntaxException {
        // given
        _compiler.makeDeclarationNode(_mockShadowNet);
        ShadowArc mockArc = mock(ShadowArc.class);
        when(mockArc.getShadowArcType()).thenReturn(ShadowArc.INHIBITOR);
        when(mockArc.getTransition()).thenReturn(mock(ShadowTransition.class));
        when(mockArc.getPlace())
            .thenReturn(mock(de.renew.simulatorontology.shadow.ShadowPlace.class));

        ShadowInscription mockArcInscription = mock(ShadowInscription.class);
        when(mockArcInscription.getInscription()).thenReturn("x");
        when(mockArc.elements()).thenReturn(Collections.singleton(mockArcInscription));

        // when
        SyntaxException thrown =
            assertThrows(SyntaxException.class, () -> _compiler.compileArc(mockArc));

        // then
        assertEquals("Only ordinary arcs are allowed.", thrown.getMessage());
    }

    @Test
    void testCheckDeclarationNodeReturnsDeclarationForNullInscription() throws SyntaxException {
        // given / when
        String result = _compiler.checkDeclarationNode(null, false, _mockShadowNet);

        // then
        assertEquals("declaration", result);
        assertNotNull(_compiler.parseDeclarationNode(null));
    }

    @Test
    void testParseDeclarationNodeThrowsExceptionOnParseException() {
        // given
        String invalidInscription = "int x = ;";

        // when
        SyntaxException thrown = assertThrows(
            SyntaxException.class, () -> _compiler.parseDeclarationNode(invalidInscription));

        // then
        assertNotNull(thrown.getCause());
        assertInstanceOf(ParseException.class, thrown.getCause());
    }

    @Test
    void testCompileThrowsExceptionWhenParseFails() {
        // given
        ShadowDeclarationNode mockNode = mock(ShadowDeclarationNode.class);
        when(mockNode.getInscription()).thenReturn("not valid {");

        // when / then
        assertThrows(SyntaxException.class, () -> _compiler.compile(mockNode));
    }
}
