package pipe.controllers;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import pipe.historyActions.MultipleEdit;
import pipe.historyActions.arc.AddArcPathPoint;
import pipe.historyActions.arc.ArcPathPointType;
import pipe.historyActions.arc.SetArcWeightAction;
import pipe.historyActions.component.ChangePetriNetComponentName;
import pipe.utilities.transformers.Contains;
import uk.ac.imperial.pipe.models.petrinet.Arc;
import uk.ac.imperial.pipe.models.petrinet.ArcPoint;
import uk.ac.imperial.pipe.models.petrinet.Place;
import uk.ac.imperial.pipe.models.petrinet.Transition;
import uk.ac.imperial.pipe.parsers.FunctionalResults;
import uk.ac.imperial.pipe.parsers.UnparsableException;

import javax.swing.event.UndoableEditListener;
import javax.swing.undo.UndoableEdit;
import java.awt.geom.Point2D;
import java.util.*;

import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class ArcControllerTest {
    @Rule
    public ExpectedException exception = ExpectedException.none();

    @Mock
    Arc<Place, Transition> mockArc;

    @Mock
    PetriNetController mockPetriNetController;

    ArcController<Place, Transition> controller;

    private static final String DEFAULT_TOKEN_ID = "Default";

    @Mock
    UndoableEditListener listener;

    @Before
    public void setUp() {
        controller = new ArcController<>(mockArc, mockPetriNetController, listener);

        FunctionalResults<Double> results = new FunctionalResults<>(1.0, new HashSet<String>());
        when(mockPetriNetController.parseFunctionalExpression(anyString())).thenReturn(results);
    }

    @Test
    public void setWeightCreatesHistoryItem() throws UnparsableException {
        String oldWeight = "5";
        when(mockArc.getWeightForToken(DEFAULT_TOKEN_ID)).thenReturn(oldWeight);


        String newWeight = "51";
        controller.setWeight(DEFAULT_TOKEN_ID, newWeight);

        SetArcWeightAction<Place, Transition> weightAction =
                new SetArcWeightAction<>(mockArc, DEFAULT_TOKEN_ID, oldWeight, newWeight);

        verify(listener).undoableEditHappened(argThat(Contains.thisAction(weightAction)));
    }

    @Test
    public void setWeightUpdatesArcWeight() throws UnparsableException {
        String oldWeight = "5";
        when(mockArc.getWeightForToken(DEFAULT_TOKEN_ID)).thenReturn(oldWeight);


        String newWeight = "51";
        controller.setWeight(DEFAULT_TOKEN_ID, newWeight);
        verify(mockArc).setWeight(DEFAULT_TOKEN_ID, newWeight);
    }

    @Test
    public void setWeightsCreatesHistoryItem() throws UnparsableException {
        Map<String, String> tokenWeights = new HashMap<>();
        String oldWeight = "5";
        when(mockArc.getWeightForToken(DEFAULT_TOKEN_ID)).thenReturn(oldWeight);


        String newWeight = "51";
        tokenWeights.put(DEFAULT_TOKEN_ID, newWeight);
        controller.setWeights(tokenWeights);

        UndoableEdit weightAction = new SetArcWeightAction<>(mockArc, DEFAULT_TOKEN_ID, oldWeight, newWeight);
        UndoableEdit edit = new MultipleEdit(Arrays.asList(weightAction));
        verify(listener).undoableEditHappened(argThat(Contains.thisAction(edit)));

    }

    @Test
    public void setWeightsUpdatesArc() throws UnparsableException {
        Map<String, String> tokenWeights = new HashMap<>();
        String oldWeight = "5";
        when(mockArc.getWeightForToken(DEFAULT_TOKEN_ID)).thenReturn(oldWeight);


        String newWeight = "51";
        tokenWeights.put(DEFAULT_TOKEN_ID, newWeight);
        controller.setWeights(tokenWeights);
        verify(mockArc).setWeight(DEFAULT_TOKEN_ID, newWeight);
    }

    @Test
    public void throwsUnparsableExceptionForNonIntegerWeight() throws UnparsableException {
        FunctionalResults<Double> result = new FunctionalResults<>(5.2, new HashSet<String>());
        when(mockPetriNetController.parseFunctionalExpression(anyString())).thenReturn(result);
        exception.expect(UnparsableException.class);
        exception.expectMessage("Value is not an integer, please surround expression with floor or ceil");

        controller.setWeight(DEFAULT_TOKEN_ID, "1.2");
    }


    @Test
    public void throwsUnparsableExceptionForNonIntegerWeights() throws UnparsableException {

        Map<String, String> tokenWeights = new HashMap<>();
        FunctionalResults<Double> result = new FunctionalResults<>(5.2, new HashSet<String>());
        when(mockPetriNetController.parseFunctionalExpression(anyString())).thenReturn(result);
        exception.expect(UnparsableException.class);
        exception.expectMessage("Value is not an integer, please surround expression with floor or ceil");

        tokenWeights.put(DEFAULT_TOKEN_ID, "5.2");
        controller.setWeights(tokenWeights);
    }

    @Test
    public void throwsUnparsableExceptionForErrorInWeights() throws UnparsableException {

        Map<String, String> tokenWeights = new HashMap<>();
        List<String> errors = new LinkedList<>();
        errors.add("test error");

        FunctionalResults<Double> result = new FunctionalResults<>(5.0, errors, new HashSet<String>());
        when(mockPetriNetController.parseFunctionalExpression(anyString())).thenReturn(result);
        exception.expect(UnparsableException.class);
        exception.expectMessage("test error");

        tokenWeights.put(DEFAULT_TOKEN_ID, "5.2");
        controller.setWeights(tokenWeights);
    }

    @Test
    public void throwsUnparsableExceptionForErrorInWeight() throws UnparsableException {
        List<String> errors = new LinkedList<>();
        errors.add("test error");

        FunctionalResults<Double> result = new FunctionalResults<>(5.0, errors, new HashSet<String>());
        when(mockPetriNetController.parseFunctionalExpression(anyString())).thenReturn(result);
        exception.expect(UnparsableException.class);
        exception.expectMessage("test error");

        controller.setWeight(DEFAULT_TOKEN_ID, "5.0");
    }

    @Test
    public void splittingArcPointSplitsHalfWay() {
        ArcPoint nextPoint = new ArcPoint(new Point2D.Double(10, 10), false);
        ArcPoint splitPoint = new ArcPoint(new Point2D.Double(0, 0), false);

        when(mockArc.getNextPoint(splitPoint)).thenReturn(nextPoint);

        controller.splitArcPoint(splitPoint);
        ArcPoint expected = new ArcPoint(new Point2D.Double(5, 5), false);
        verify(mockArc).addIntermediatePoint(expected);
    }

    @Test
    public void splittingArcPointKeepsArcUncurved() {
        ArcPoint nextPoint = new ArcPoint(new Point2D.Double(10, 10), false);
        ArcPoint splitPoint = new ArcPoint(new Point2D.Double(0, 0), false);

        when(mockArc.getNextPoint(splitPoint)).thenReturn(nextPoint);

        controller.splitArcPoint(splitPoint);
        ArcPoint expected = new ArcPoint(new Point2D.Double(5, 5), false);
        verify(mockArc).addIntermediatePoint(expected);
    }

    @Test
    public void splittingArcPointKeepsArcCurved() {
        ArcPoint nextPoint = new ArcPoint(new Point2D.Double(10, 10), false);
        ArcPoint splitPoint = new ArcPoint(new Point2D.Double(0, 0), true);

        when(mockArc.getNextPoint(splitPoint)).thenReturn(nextPoint);

        controller.splitArcPoint(splitPoint);
        ArcPoint expected = new ArcPoint(new Point2D.Double(5, 5), true);
        verify(mockArc).addIntermediatePoint(expected);
    }

    @Test
    public void splittingCreatesHistoryItem() {
        ArcPoint nextPoint = new ArcPoint(new Point2D.Double(10, 10), false);
        ArcPoint splitPoint = new ArcPoint(new Point2D.Double(0, 0), true);

        when(mockArc.getNextPoint(splitPoint)).thenReturn(nextPoint);

        controller.splitArcPoint(splitPoint);
        ArcPoint expected = new ArcPoint(new Point2D.Double(5, 5), true);
        UndoableEdit addArcPointAction = new AddArcPathPoint<>(mockArc, expected);

        verify(listener).undoableEditHappened(argThat(Contains.thisAction(addArcPointAction)));
    }

    @Test
    public void toggleCreatesHistoryItem() {
        ArcPoint point = new ArcPoint(new Point2D.Double(0, 0), false);
        controller.toggleArcPointType(point);
        ArcPathPointType arcPathPointType = new ArcPathPointType(point);
        verify(listener).undoableEditHappened(argThat(Contains.thisAction(arcPathPointType)));
    }

    @Test
    public void arcPointCreatesAtSpecifiedPoint() {
        Point2D.Double point = new Point2D.Double(4, 1);
        controller.addPoint(point);

        ArcPoint expected = new ArcPoint(point, false);
        verify(mockArc).addIntermediatePoint(expected);
    }

    @Test
    public void addPointCreatesHistoryItem() {
        ArcPoint point = new ArcPoint(new Point2D.Double(0, 0), false);
        controller.addPoint(point.getPoint());
        UndoableEdit addArcPointAction = new AddArcPathPoint<>(mockArc, point);
        verify(listener).undoableEditHappened(argThat(Contains.thisAction(addArcPointAction)));
    }


    @Test
    public void setNameChangesName() {
        String newName = "newName";
        when(mockArc.getId()).thenReturn("oldId");
        controller.setId(newName);
        verify(mockArc).setId(newName);
    }


    @Test
    public void setNameCreatesUndoItem() {
        String oldName = "oldName";
        String newName = "newName";
        when(mockArc.getId()).thenReturn(oldName);
        controller.setId(newName);

        UndoableEdit nameEdit = new ChangePetriNetComponentName(mockArc, oldName, newName);
        verify(listener).undoableEditHappened(argThat(Contains.thisAction(nameEdit)));
    }
}