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.gui.*;
import pipe.historyActions.component.DeletePetriNetObject;
import uk.ac.imperial.pipe.exceptions.PetriNetComponentException;
import uk.ac.imperial.pipe.exceptions.PetriNetComponentNotFoundException;
import uk.ac.imperial.pipe.models.petrinet.*;
import uk.ac.imperial.pipe.visitor.TranslationVisitor;
import uk.ac.imperial.pipe.visitor.component.PetriNetComponentVisitor;

import javax.swing.event.UndoableEditListener;
import javax.swing.undo.UndoableEdit;
import java.awt.Color;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.Point2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

@RunWith(MockitoJUnitRunner.class)
public class PetriNetControllerTest {
    @Rule
    public ExpectedException expectedEx = ExpectedException.none();

    private PetriNetController controller;

    private PetriNet net;

    @Mock
    private GUIAnimator mockAnimator;

    @Mock
    private PetriNetTab mocKTab;

    @Mock
    UndoableEditListener undoListener;

    @Before
    public void setUp() {
        net = new PetriNet();
        CopyPasteManager copyPasteManager = mock(CopyPasteManager.class);
        ZoomController zoomController = mock(ZoomController.class);

        controller = new PetriNetController(net, undoListener, mockAnimator, copyPasteManager, zoomController, mocKTab);
    }

    @Test
    public void containsSelected() {
        PetriNetComponent dummyComponent = new DummyPetriNetComponent();
        controller.select(dummyComponent);
        assertTrue(controller.isSelected(dummyComponent));
    }

    @Test
    public void doesNotContainDeselected() {
        PetriNetComponent dummyComponent = new DummyPetriNetComponent();
        controller.select(dummyComponent);
        controller.deselect(dummyComponent);
        assertFalse(controller.isSelected(dummyComponent));
    }

    @Test
    public void deselectAllEmptiesSelected() {
        PetriNetComponent dummyComponent = new DummyPetriNetComponent();
        controller.select(dummyComponent);
        controller.deselectAll();
        assertFalse(controller.isSelected(dummyComponent));
    }

    @Test
    public void deletesSelectedRemovesFromNet() throws PetriNetComponentException {
        Place place = new DiscretePlace("", "");
        net.addPlace(place);

        controller.select(place);
        controller.deleteSelection();
        assertFalse(net.getPlaces().contains(place));
    }

    @Test
    public void deletingSelectionReturnsListOfAbstractUndoEdits() throws PetriNetComponentException {
        Place place = new DiscretePlace("", "");
        net.addPlace(place);

        controller.select(place);
        List<UndoableEdit> edits = controller.deleteSelection();
        DeletePetriNetObject deleteAction = new DeletePetriNetObject(place, net);
        assertEquals(1, edits.size());
        assertTrue(edits.contains(deleteAction));
    }

    @Test
    public void deleteComponentRemovesFromPetriNet() throws PetriNetComponentException {
        Place place = new DiscretePlace("", "");
        net.addPlace(place);

        controller.delete(place);

        assertFalse("Petrinet contains place after deletion", net.getPlaces().contains(place));
    }

    @Test
    public void deletingComponentAddsToHistoryManager() throws PetriNetComponentException {
        Place place = new DiscretePlace("", "");
        net.addPlace(place);

        UndoableEdit edit = controller.delete(place);

        DeletePetriNetObject deleteAction = new DeletePetriNetObject(place, net);
        assertEquals(edit, deleteAction);
    }

    @Test
    public void deletesSelectedNotifiesObserver() throws PetriNetComponentException {
        Place place = new DiscretePlace("", "");
        net.addPlace(place);

        PropertyChangeListener mockListener = mock(PropertyChangeListener.class);
        net.addPropertyChangeListener(mockListener);

        controller.select(place);
        controller.deleteSelection();
        verify(mockListener, atLeastOnce()).propertyChange(any(PropertyChangeEvent.class));
    }

    @Test
    public void selectsItemLocatedWithinSelectionArea() {
        Place place = mock(Place.class);
        when(place.getX()).thenReturn(5);
        when(place.getY()).thenReturn(10);
        when(place.getWidth()).thenReturn(5);
        when(place.getHeight()).thenReturn(20);

        net.addPlace(place);

        Rectangle selectionRectangle = new Rectangle(5, 10, 40, 40);
        controller.select(selectionRectangle);

        assertTrue(controller.isSelected(place));
    }

    /**
     * Even if top left x, y is out of the selection area if the height and width are in then select item
     */
    @Test
    public void selectsItemWithWidthAndHeightWithinSelectionArea() {
        Place place = mock(Place.class);
        when(place.getX()).thenReturn(0);
        when(place.getY()).thenReturn(0);
        when(place.getWidth()).thenReturn(10);
        when(place.getHeight()).thenReturn(10);

        net.addPlace(place);

        Rectangle selectionRectangle = new Rectangle(5, 5, 40, 40);
        controller.select(selectionRectangle);

        assertTrue(controller.isSelected(place));
    }

    @Test
    public void selectsArcIfIntersects() {
        Transition t = new DiscreteTransition("T1", "T1");
        InboundArc arc = mock(InboundArc.class);
        when(arc.getTarget()).thenReturn(t);
        Point2D.Double start = new Point2D.Double(0, 0);
        Point2D.Double end = new Point2D.Double(10, 10);
        when(arc.getArcPoints()).thenReturn(Arrays.asList(new ArcPoint(start, false), new ArcPoint(end, false)));

        net.addArc(arc);

        Rectangle selectionRectangle = new Rectangle(0, 0, 2, 2);
        controller.select(selectionRectangle);
        assertTrue(controller.isSelected(arc));
    }

    @Test
    public void doesNotSelectArcIfDoesntIntersect() {
        Transition t = new DiscreteTransition("T1", "T1");
        InboundArc arc = mock(InboundArc.class);
        when(arc.getTarget()).thenReturn(t);
        Point2D.Double start = new Point2D.Double(0, 0);
        Point2D.Double end = new Point2D.Double(10, 10);
        when(arc.getArcPoints()).thenReturn(Arrays.asList(new ArcPoint(start, false), new ArcPoint(end, false)));

        net.addArc(arc);

        Rectangle selectionRectangle = new Rectangle(30, 30, 40, 40);
        controller.select(selectionRectangle);
        assertFalse(controller.isSelected(arc));
    }

    @Test
    public void translatesSelectedItemsCorrectly() throws PetriNetComponentException {
        Transition transition = mock(Transition.class);
        when(transition.isDraggable()).thenReturn(true);
        Place place = mock(Place.class);
        when(place.isDraggable()).thenReturn(true);
        net.addPlace(place);
        net.addTransition(transition);

        controller.select(place);
        controller.select(transition);

        int x_y_value = 40;
        when(place.getX()).thenReturn(x_y_value);
        when(place.getY()).thenReturn(x_y_value);
        when(transition.getX()).thenReturn(x_y_value);
        when(transition.getY()).thenReturn(x_y_value);

        int translate_value = 50;
        controller.translateSelected(new Point(translate_value, translate_value));

        double expected_value = x_y_value + translate_value;
        verify(place).accept(any(TranslationVisitor.class));
        verify(transition).accept(any(TranslationVisitor.class));
    }

    @Test
    public void doesNotTranslateNonDraggableItems() throws PetriNetComponentException {
        PetriNetComponent petriNetComponent = mock(PetriNetComponent.class);
        when(petriNetComponent.isDraggable()).thenReturn(false);
        controller.select(petriNetComponent);
        controller.translateSelected(new Point(23, 58));
        verify(petriNetComponent, never()).accept(any(TranslationVisitor.class));
    }

    @Test
    public void updateNameIfChanged() throws PetriNetComponentNotFoundException {
        Token token = mock(Token.class);
        String id = "id";
        when(token.getId()).thenReturn(id);

        boolean enabled = true;

        Color color = Color.RED;
        when(token.getColor()).thenReturn(color);
        net.addToken(token);

        controller.updateToken(id, "new", color);
        verify(token).setId("new");
    }

    @Test
    public void doesNotUpdateNameIfNotChanged() throws PetriNetComponentNotFoundException {
        Token token = mock(Token.class);
        String id = "id";
        when(token.getId()).thenReturn(id);

        boolean enabled = true;

        Color color = new Color(255, 0, 0);
        when(token.getColor()).thenReturn(color);
        net.addToken(token);

        controller.updateToken(id, id, color);
        verify(token, never()).setId(anyString());
    }

    @Test
    public void updateTokenColorIfChanged() throws PetriNetComponentNotFoundException {
        Token token = mock(Token.class);
        String id = "id";
        when(token.getId()).thenReturn(id);

        boolean enabled = true;

        Color color = new Color(255, 0, 0);
        when(token.getColor()).thenReturn(color);
        net.addToken(token);

        Color newColor = new Color(0, 0, 0);
        controller.updateToken(id, id, newColor);
        verify(token).setColor(newColor);
    }

    @Test
    public void doesNotUpdateTokenColorIfNotChanged() throws PetriNetComponentNotFoundException {
        Token token = mock(Token.class);
        String id = "id";
        when(token.getId()).thenReturn(id);

        boolean enabled = true;

        Color color = new Color(255, 0, 0);
        when(token.getColor()).thenReturn(color);
        net.addToken(token);

        controller.updateToken(id, id, color);
        verify(token, never()).setColor(any(Color.class));
    }

    @Test
    public void createNewToken() {
        String name = "testToken";
        boolean enabled = true;
        Color color = new Color(160, 92, 240);

        controller.createNewToken(name, color);
        Collection<Token> tokens = net.getTokens();
        assertEquals(1, tokens.size());
        Token token = tokens.iterator().next();
        assertEquals(name, token.getId());
        assertEquals(color, token.getColor());
    }

    private class DummyPetriNetComponent implements PetriNetComponent {
        @Override
        public boolean isSelectable() {
            return false;
        }

        @Override
        public boolean isDraggable() {
            return false;
        }

        @Override
        public void accept(PetriNetComponentVisitor visitor) {

        }

        @Override
        public String getId() {
            return "";
        }

        @Override
        public void setId(String id) {
        }

        @Override
        public void addPropertyChangeListener(PropertyChangeListener listener) {

        }

        @Override
        public void removePropertyChangeListener(PropertyChangeListener listener) {

        }

    }
}