package de.renew.ptchannel.single;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

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

import de.renew.expression.ConstantExpression;
import de.renew.expression.Expression;
import de.renew.expression.ListExpression;
import de.renew.expression.LocalVariable;
import de.renew.expression.TupleExpression;
import de.renew.expression.TypeCheckingExpression;
import de.renew.expression.VariableExpression;
import de.renew.formalism.java.ParsedDeclarationNode;
import de.renew.net.Transition;
import de.renew.net.inscription.TransitionInscription;
import de.renew.net.inscription.transition.DownlinkInscription;
import de.renew.net.inscription.transition.UplinkInscription;
import de.renew.shadowcompiler.ShadowLookup;
import de.renew.simulatorontology.shadow.ShadowDeclarationNode;
import de.renew.simulatorontology.shadow.ShadowInscription;
import de.renew.simulatorontology.shadow.ShadowNet;
import de.renew.simulatorontology.shadow.ShadowNetElement;
import de.renew.simulatorontology.shadow.ShadowTransition;
import de.renew.simulatorontology.shadow.SyntaxException;
import de.renew.util.Value;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.anyString;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;

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

    static class TestablePTCTransitionCompiler extends PTCTransitionCompiler {

        public TestablePTCTransitionCompiler(
            boolean allowDangerousArcs, boolean allowTimeInscriptions, boolean wantEarlyTokens)
        {
            super(allowDangerousArcs, allowTimeInscriptions, wantEarlyTokens);
        }

        @Override
        public Collection<TransitionInscription> makeInscriptions(String s, Transition t, boolean b)
            throws SyntaxException
        {
            return super.makeInscriptions(s, t, b);
        }
    }

    private TestablePTCTransitionCompiler _compiler;
    private ShadowLookup _mockLookup;
    private ShadowNet _mockShadowNet;
    private ParsedDeclarationNode _mockDeclaration;

    @BeforeEach
    void setUp() throws SyntaxException {
        _compiler = spy(new TestablePTCTransitionCompiler(true, true, true));
        _mockLookup = mock(ShadowLookup.class);
        _mockDeclaration = mock(ParsedDeclarationNode.class);
        _mockShadowNet = mock(ShadowNet.class);

        doReturn(mock(ShadowDeclarationNode.class)).when(_compiler)
            .findDeclarationNode(any(ShadowNet.class));
    }

    @Test
    void testCheckTransitionInscriptionThrowsExceptionWhenMultipleInscriptions() throws Exception {
        //given
        Collection<TransitionInscription> multiple =
            Arrays.asList(mock(TransitionInscription.class), mock(TransitionInscription.class));
        doReturn(multiple).when(_compiler).makeInscriptions(anyString(), isNull(), eq(false));

        //when
        SyntaxException e = assertThrows(
            SyntaxException.class,
            () -> _compiler.checkTransitionInscription("dummy", false, _mockShadowNet));

        //then
        assertEquals("Multiple inscriptions not supported", e.getMessage());
    }

    @Test
    void testCheckTransitionInscriptionThrowsExceptionWhenEmptyInscriptions() throws Exception {
        //given
        doReturn(Collections.emptyList()).when(_compiler)
            .makeInscriptions(anyString(), isNull(), eq(false));

        //when
        SyntaxException e = assertThrows(
            SyntaxException.class,
            () -> _compiler.checkTransitionInscription("", false, _mockShadowNet));

        //then
        assertEquals("Invalid inscription", e.getMessage());
    }

    @Test
    void testCheckTransitionInscriptionThrowsExceptionWhenInscriptionIsNotChannel()
        throws Exception
    {
        //given
        Collection<TransitionInscription> notChannel =
            Collections.singletonList(mock(TransitionInscription.class));
        doReturn(notChannel).when(_compiler).makeInscriptions(anyString(), isNull(), eq(false));

        //when
        SyntaxException e = assertThrows(
            SyntaxException.class,
            () -> _compiler.checkTransitionInscription(":dummy()", false, _mockShadowNet));

        //then
        assertEquals("Only up- and downlinks are supported", e.getMessage());
    }

    @Test
    void testCheckTransitionInscriptionRecognizesUplink() throws Exception {
        //given
        Collection<TransitionInscription> uplink =
            Collections.singletonList(mock(UplinkInscription.class));
        doReturn(uplink).when(_compiler).makeInscriptions(eq(":channel()"), isNull(), eq(false));

        //when
        String result = _compiler.checkTransitionInscription(":channel()", false, _mockShadowNet);

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

    @Test
    void testCheckTransitionInscriptionRecognizesDownlinkWithThis() throws Exception {
        //given
        DownlinkInscription mockDownlink = mock(DownlinkInscription.class);
        VariableExpression mockCallee = mock(VariableExpression.class);
        LocalVariable mockVar = mock(LocalVariable.class);
        when(mockVar.getName()).thenReturn("this");
        when(mockCallee.getVariable()).thenReturn(mockVar);
        when(mockDownlink.getCallee()).thenReturn(mockCallee);

        Collection<TransitionInscription> downlink = Collections.singletonList(mockDownlink);
        doReturn(downlink).when(_compiler).makeInscriptions(eq("x:channel()"), isNull(), eq(false));

        //when
        String result = _compiler.checkTransitionInscription("x:channel()", false, _mockShadowNet);

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

    @Test
    void testCheckTransitionInscriptionThrowsExceptionWhenDownlinkCalleeNotThis() throws Exception {
        //given
        DownlinkInscription mockDownlink = mock(DownlinkInscription.class);
        VariableExpression mockCallee = mock(VariableExpression.class);
        LocalVariable mockVar = mock(LocalVariable.class);
        when(mockVar.getName()).thenReturn("other");
        when(mockCallee.getVariable()).thenReturn(mockVar);
        when(mockDownlink.getCallee()).thenReturn(mockCallee);

        Collection<TransitionInscription> downlink = Collections.singletonList(mockDownlink);
        doReturn(downlink).when(_compiler).makeInscriptions(eq("x:channel()"), isNull(), eq(false));

        //when
        SyntaxException e = assertThrows(
            SyntaxException.class,
            () -> _compiler.checkTransitionInscription("x:channel()", false, _mockShadowNet));

        //then
        assertEquals("Callee must be this.", e.getMessage());
    }

    @Test
    void testMakeInscriptionsTransformsIntegerToBlackTokensUplink() throws Exception {
        //given
        ShadowInscription mockShadowInsc = mock(ShadowInscription.class);
        ShadowTransition mockShadowTrans = mock(ShadowTransition.class);
        Transition mockTrans = mock(Transition.class);
        when(mockShadowInsc.getInscribable()).thenReturn(mockShadowTrans);
        when(_mockLookup.get(mockShadowTrans)).thenReturn(mockTrans);
        Set<ShadowNetElement> elementsSet = new HashSet<>();
        elementsSet.add(mockShadowInsc);
        when(mockShadowTrans.elements()).thenReturn(elementsSet);

        when(mockShadowInsc.getInscription()).thenReturn(":channel(3)");

        Value intValue = new Value(3);
        ConstantExpression constExpr = mock(ConstantExpression.class);
        when(constExpr.getConstant()).thenReturn(intValue);
        TypeCheckingExpression typeCheckExpr = mock(TypeCheckingExpression.class);
        when(typeCheckExpr.getArgument()).thenReturn(constExpr);
        TupleExpression params = mock(TupleExpression.class);
        when(params.getExpressions()).thenReturn(new Expression[] { typeCheckExpr });
        UplinkInscription stubUplink = mock(UplinkInscription.class);
        when(stubUplink.getParams()).thenReturn(params);
        when(stubUplink.getName()).thenReturn("channel");

        Collection<TransitionInscription> baseResult = Collections.singletonList(stubUplink);
        doReturn(baseResult).when(_compiler)
            .makeInscriptions(eq(":channel(3)"), eq(mockTrans), eq(false));

        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
                .makeInscriptions(mockShadowInsc, _mockLookup, false, _mockDeclaration, null);
        }

        //then
        assertEquals(1, result.size());
        UplinkInscription resultUplink = (UplinkInscription) result.iterator().next();
        TupleExpression resultParams = (TupleExpression) resultUplink.getParams();
        TypeCheckingExpression resultTCE =
            (TypeCheckingExpression) resultParams.getExpressions()[0];
        ListExpression listExpr = (ListExpression) resultTCE.getArgument();

        assertEquals(3, listExpr.getExpressions().length);
    }

    @Test
    void testMakeInscriptionsTransformsIntegerToBlackTokensDownlink() throws Exception {
        //given
        ShadowInscription mockShadowInsc = mock(ShadowInscription.class);
        ShadowTransition mockShadowTrans = mock(ShadowTransition.class);
        Transition mockTrans = mock(Transition.class);
        when(mockShadowInsc.getInscribable()).thenReturn(mockShadowTrans);
        when(_mockLookup.get(mockShadowTrans)).thenReturn(mockTrans);
        Set<ShadowNetElement> elementsSet = new HashSet<>();
        elementsSet.add(mockShadowInsc);
        when(mockShadowTrans.elements()).thenReturn(elementsSet);
        VariableExpression mockCallee = mock(VariableExpression.class);
        LocalVariable mockVar = mock(LocalVariable.class);
        when(mockVar.getName()).thenReturn("this");
        when(mockCallee.getVariable()).thenReturn(mockVar);

        when(mockShadowInsc.getInscription()).thenReturn("this:channel(2)");

        Value intValue = new Value(2);
        ConstantExpression constExpr = mock(ConstantExpression.class);
        when(constExpr.getConstant()).thenReturn(intValue);
        TypeCheckingExpression typeCheckExpr = mock(TypeCheckingExpression.class);
        when(typeCheckExpr.getArgument()).thenReturn(constExpr);
        TupleExpression params = mock(TupleExpression.class);
        when(params.getExpressions()).thenReturn(new Expression[] { typeCheckExpr });
        DownlinkInscription stubDownlink = mock(DownlinkInscription.class);
        when(stubDownlink.getParameterExpression()).thenReturn(params);
        when(stubDownlink.getName()).thenReturn("channel");
        when(stubDownlink.getCallee()).thenReturn(mockCallee);
        when(stubDownlink.getTransition()).thenReturn(mockTrans);

        Collection<TransitionInscription> baseResult = Collections.singletonList(stubDownlink);
        doReturn(baseResult).when(_compiler)
            .makeInscriptions(eq("this:channel(2)"), eq(mockTrans), eq(false));

        //when
        Collection<TransitionInscription> result =
            _compiler.makeInscriptions(mockShadowInsc, _mockLookup, false, _mockDeclaration, null);

        //then
        assertEquals(1, result.size());
        DownlinkInscription resultDownlink = (DownlinkInscription) result.iterator().next();
        assertEquals(mockCallee, resultDownlink.getCallee());
        TupleExpression resultParams = (TupleExpression) resultDownlink.getParameterExpression();
        TypeCheckingExpression resultTCE =
            (TypeCheckingExpression) resultParams.getExpressions()[0];
        ListExpression listExpr = (ListExpression) resultTCE.getArgument();

        assertEquals(2, listExpr.getExpressions().length);
    }

    @Test
    void testMakeInscriptionsLeavesNonIntegerParamUnchanged() throws Exception {
        //given
        ShadowInscription mockShadowInsc = mock(ShadowInscription.class);
        ShadowTransition mockShadowTrans = mock(ShadowTransition.class);
        Transition mockTrans = mock(Transition.class);
        when(mockShadowInsc.getInscribable()).thenReturn(mockShadowTrans);
        when(_mockLookup.get(mockShadowTrans)).thenReturn(mockTrans);
        Set<ShadowNetElement> elementsSet = new HashSet<>();
        elementsSet.add(mockShadowInsc);
        when(mockShadowTrans.elements()).thenReturn(elementsSet);

        when(mockShadowInsc.getInscription()).thenReturn(":channel(x)");

        LocalVariable xVar = new LocalVariable("x", false);
        VariableExpression varExpr = new VariableExpression(null, xVar);

        TupleExpression params = mock(TupleExpression.class);
        when(params.getExpressions()).thenReturn(new Expression[] { varExpr });

        UplinkInscription stubUplink = mock(UplinkInscription.class);
        when(stubUplink.getParams()).thenReturn(params);
        when(stubUplink.getName()).thenReturn("channel");

        Collection<TransitionInscription> baseResult = Collections.singletonList(stubUplink);
        doReturn(baseResult).when(_compiler)
            .makeInscriptions(eq(":channel(x)"), eq(mockTrans), eq(false));

        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
                .makeInscriptions(mockShadowInsc, _mockLookup, false, _mockDeclaration, null);
        }

        //then
        assertEquals(1, result.size());
        UplinkInscription resultUplink = (UplinkInscription) result.iterator().next();
        TupleExpression resultParams = (TupleExpression) resultUplink.getParams();
        assertSame(varExpr, resultParams.getExpressions()[0]);
    }
}