package de.renew.draw.ui.impl.services;

import java.awt.Dimension;
import java.awt.Point;
import java.awt.event.MouseListener;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Enumeration;
import java.util.Vector;
import java.util.stream.Stream;

import org.junit.jupiter.api.AfterEach;
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.MockedStatic;
import org.mockito.Mockito;

import CH.ifa.draw.DrawPlugin;
import CH.ifa.draw.IOHelper;
import CH.ifa.draw.application.DrawApplication;
import CH.ifa.draw.figures.TextFigure;
import CH.ifa.draw.io.PositionedDrawing;
import CH.ifa.draw.standard.NullDrawingEditor;
import CH.ifa.draw.standard.NullDrawingView;
import CH.ifa.draw.standard.StandardDrawingView;
import de.renew.draw.storables.ontology.Drawing;
import de.renew.draw.storables.ontology.DrawingListener;
import de.renew.draw.storables.ontology.Figure;
import de.renew.draw.ui.ontology.DrawingEditor;
import de.renew.draw.ui.ontology.DrawingView;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

public class EditorServiceImplTest {

    private EditorServiceImpl _service;
    private Drawing _mockedDrawing;
    private DrawApplication _mockedDrawApplication;
    private MockedStatic<DrawPlugin> _mockedDrawPluginStatic;


    @BeforeEach
    void setUp() {
        _mockedDrawing = mock(Drawing.class);
        _mockedDrawApplication = mock(DrawApplication.class);
        _mockedDrawPluginStatic = Mockito.mockStatic(DrawPlugin.class);
        _mockedDrawPluginStatic.when(DrawPlugin::getGui).thenReturn(_mockedDrawApplication);

        _service = new EditorServiceImpl();
    }

    @AfterEach
    void tearDown() {
        _service = null;
        _mockedDrawing = null;
        _mockedDrawApplication = null;
        _mockedDrawPluginStatic.close();
        _mockedDrawPluginStatic = null;
    }

    @Test
    void testGetCurrentEditorWithoutDrawPlugin() {
        //given
        _mockedDrawPluginStatic.when(DrawPlugin::getCurrent).thenReturn(null);

        //when
        DrawingEditor editor = _service.getCurrentEditor();

        //then
        assertThat(editor).isSameAs(NullDrawingEditor.INSTANCE);
    }

    @Test
    void testGetCurrentEditorWithDrawPlugin() {
        //given
        DrawPlugin mockedDrawPlugin = mock(DrawPlugin.class);
        _mockedDrawPluginStatic.when(DrawPlugin::getCurrent).thenReturn(mockedDrawPlugin);
        DrawingEditor expectedEditor = mock(DrawingEditor.class);
        when(mockedDrawPlugin.getDrawingEditor()).thenReturn(expectedEditor);

        //when
        DrawingEditor actualEditor = _service.getCurrentEditor();

        //then
        assertThat(actualEditor).isSameAs(expectedEditor);
        verify(mockedDrawPlugin).getDrawingEditor();
    }

    @Test
    void testGetNullDrawingEditor() {
        //given
        NullDrawingEditor expectedEditor = NullDrawingEditor.INSTANCE;

        //when
        DrawingEditor actualEditor = _service.getNullDrawingEditor();

        //then
        assertThat(actualEditor).isSameAs(expectedEditor);
    }

    @Test
    void testGetNullDrawingView() {
        //given
        DrawingView expectedView = NullDrawingView.INSTANCE;

        //when
        DrawingView actualView = _service.getNullDrawingView();

        //then
        assertThat(actualView).isSameAs(expectedView);
    }


    @Test
    void testGetCurrentDrawing() {
        when(_service.getCurrentDrawing()).thenReturn(_mockedDrawing);

        //when
        Drawing res = _service.getCurrentDrawing();

        //then
        assertThat(res).isEqualTo(_mockedDrawing);
        verify(_mockedDrawApplication).drawing();
    }

    @Test
    void testGetCurrentDrawings() {
        //given
        Vector<Drawing> drawings = new Vector<>();
        drawings.add(_mockedDrawing);
        Enumeration enumeration = drawings.elements();
        when(_service.getCurrentDrawings()).thenReturn(enumeration);

        //when
        Enumeration res = _service.getCurrentDrawings();

        //then
        assertThat(res).isEqualTo(enumeration);
        verify(_mockedDrawApplication).drawings();
    }

    @Test
    void testOpenDrawing() {
        //given
        when(_service.openDrawing(_mockedDrawing)).thenReturn(_mockedDrawing);

        //when
        Drawing res = _service.openDrawing(_mockedDrawing);

        //then
        assertThat(res).isEqualTo(_mockedDrawing);
        verify(_mockedDrawApplication).openDrawing(_mockedDrawing);
    }

    @Test
    void testOpenDrawingWithDimensionAndPoint() {
        //given
        Point mockedPoint = Mockito.mock(Point.class);
        Dimension mockedDimension = Mockito.mock(Dimension.class);

        //when
        _service.openDrawing(_mockedDrawing, mockedPoint, mockedDimension);

        //then
        verify(_mockedDrawApplication).openDrawing(any(PositionedDrawing.class));
    }

    @Test
    void testCloseDrawing() {
        // given/when
        _service.closeDrawing(_mockedDrawing);

        //then
        verify(_mockedDrawApplication).closeDrawing(_mockedDrawing);
    }

    @Test
    void testAddDrawingListener() {
        //given
        DrawingListener mockedListener = Mockito.mock(DrawingListener.class);

        //when
        _service.addDrawingListener(mockedListener);

        //then
        verify(_mockedDrawApplication).addDrawingListener(mockedListener);
    }

    @Test
    void testAddRemoveListener() {
        //given
        DrawingListener mockedListener = Mockito.mock(DrawingListener.class);

        //when
        _service.removeDrawingListener(mockedListener);

        //then
        verify(_mockedDrawApplication).removeDrawingListener(mockedListener);
    }

    @Test
    void testLoadAndOpeDrawingsURL() throws IOException {
        //given
        try (MockedStatic<IOHelper> mocked = mockStatic(IOHelper.class)) {
            URL testUrl = File.createTempFile("test", ".drw").toURI().toURL();
            IOHelper ioHelper = Mockito.mock(IOHelper.class);
            doNothing().when(ioHelper).loadAndOpenDrawings(testUrl);
            mocked.when(IOHelper::getInstance).thenReturn(ioHelper);
            //when
            _service.loadAndOpenDrawings(testUrl);
            //then
            verify(ioHelper).loadAndOpenDrawings(testUrl);
        }
    }

    @Test
    void testLoadAndOpeDrawingsInput() throws IOException {
        //given
        try (MockedStatic<IOHelper> mocked = mockStatic(IOHelper.class)) {
            InputStream inputStream = mock(InputStream.class);
            File file = new File("test", ".drw");
            IOHelper ioHelper = Mockito.mock(IOHelper.class);
            doNothing().when(ioHelper).loadAndOpenDrawings(file);
            mocked.when(IOHelper::getInstance).thenReturn(ioHelper);
            //when
            _service.loadAndOpenDrawings(inputStream, file.getName());
            //then
            verify(ioHelper).loadAndOpenDrawings(inputStream, file.getName());
        }
    }

    @Test
    void testOpenOrLoadDrawing() throws IOException {
        //given
        try (MockedStatic<IOHelper> mocked = mockStatic(IOHelper.class)) {
            File file = new File("test", ".drw");
            doNothing().when(_mockedDrawApplication).openOrLoadDrawing(file);
            mocked.when(DrawPlugin::getGui).thenReturn(_mockedDrawApplication);
            //when
            _service.openOrLoadDrawing(file);
            //then
            verify(_mockedDrawApplication).openOrLoadDrawing(file);
        }
    }


    @Test
    void testCheckDamage() {
        //given
        DrawingView drawingView = mock(DrawingView.class);
        when(_mockedDrawApplication.view()).thenReturn(drawingView);
        //when
        _service.checkDamage();
        //then
        verify(drawingView).checkDamage();
    }

    @Test
    void testRepairDamage() {
        //given
        DrawingView drawingView = mock(DrawingView.class);
        when(_mockedDrawApplication.view()).thenReturn(drawingView);
        //when
        _service.repairDamage();
        //then
        verify(drawingView).repairDamage();
    }

    @Test
    void testToolDone() {
        //given/when
        _service.toolDone();
        //then
        verify(_mockedDrawApplication).toolDone();
    }

    @Test
    void testAddMouseListener() {
        //given
        MouseListener mockedListener = Mockito.mock(MouseListener.class);
        StandardDrawingView drawingView = mock(StandardDrawingView.class);
        when(_mockedDrawApplication.view()).thenReturn(drawingView);
        //when
        _service.addMouseListener(mockedListener);
        //then
        verify(drawingView).addMouseListener(mockedListener);
    }

    @Test
    void testRemoveMouseListener() {
        //given
        MouseListener mockedListener = Mockito.mock(MouseListener.class);
        StandardDrawingView drawingView = mock(StandardDrawingView.class);
        when(_mockedDrawApplication.view()).thenReturn(drawingView);
        //when
        _service.removeMouseListener(mockedListener);
        //then
        verify(drawingView).removeMouseListener(mockedListener);
    }


    @Test
    void testTextEdit() {
        //given
        TextFigure figure = mock(TextFigure.class);
        Figure figureThrowsException = mock(Figure.class);
        //when
        _service.doTextEdit(figure);
        //then
        verify(_mockedDrawApplication).doTextEdit(figure);
    }

    @ParameterizedTest
    @MethodSource("provideLineAndColumn")
    void testTextEditSingleLineAndColumn(int line, int column) {
        //given
        TextFigure figure = mock(TextFigure.class);
        Figure figureThrowsException = mock(Figure.class);
        //when
        _service.doTextEdit(figure, line, column);
        //then
        verify(_mockedDrawApplication).doTextEdit(figure, line, column);
    }

    @Test
    void testTextEditSingleLineAndColumnNonTextFigure() {
        //given
        Figure figureThrowsException = mock(Figure.class);
        //when / then
        assertThrows(IllegalArgumentException.class, () -> {
            _service.doTextEdit(figureThrowsException);
        });
    }

    @ParameterizedTest
    @MethodSource("provideInputLineAndColumnTextSection")
    void testTextEditTextSection(int startLine, int startColumn, int endLine, int endColumn) {
        //given
        TextFigure figure = mock(TextFigure.class);
        //when
        _service.doTextEdit(figure, startLine, startColumn, endLine, endColumn);
        //then
        verify(_mockedDrawApplication)
            .doTextEditSelected(figure, startLine, startColumn, endLine, endColumn);
    }

    @Test
    void testTextEditTextSectionNonTextFigure() {
        //given
        Figure throwsExceptionFigure = mock(Figure.class);
        //when /then
        assertThrows(IllegalArgumentException.class, () -> {
            _service.doTextEdit(throwsExceptionFigure);
        });
    }

    private static Stream<Arguments> provideLineAndColumn() {
        return Stream.of(Arguments.of(1, 1), Arguments.of(1, -2), Arguments.of(-300, -20));
    }

    private static Stream<Arguments> provideInputLineAndColumnTextSection() {
        return Stream.of(
            Arguments.of(1, 1, 1, 1), Arguments.of(1, 2, 200, 300), Arguments.of(300, 20, 400, 40));
    }
}
