import platform

import matplotlib as mpl
import numpy
import pytest

mpl.use("Qt5Agg")

from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from mpl_toolkits.mplot3d import Axes3D
from PySide6 import QtCore, QtGui, QtPrintSupport, QtWidgets

# Tested module
import sas.qtgui.Plotting.Plotter2D as Plotter2D
from sas.qtgui.MainWindow.UnitTesting.DataExplorerTest import MyPerspective
from sas.qtgui.Plotting.PlotterData import Data2D
from sas.qtgui.Utilities.GuiUtils import communicator


class Plotter2DTest:
    '''Test the Plotter 2D class'''
    @pytest.fixture(autouse=True)
    def plotter(self, qapp):
        '''Create/Destroy the Plotter2D'''

        class dummy_manager:
            def communicator(self):
                return communicator
            def perspective(self):
                return MyPerspective()
            def workspace(self):
                return None

        p = Plotter2D.Plotter2D(parent=dummy_manager(), quickplot=True)

        data = Data2D(image=[[0.1]*4]*4,
                      qx_data=[[1.0, 2.0, 3.0, 4.0]]*4,
                      qy_data=[[10.0, 11.0, 12.0, 13.0]]*4,
                      dqx_data=[[0.1, 0.2, 0.3, 0.4]]*4,
                      dqy_data=[[0.1, 0.2, 0.3, 0.4]]*4,
                      q_data=[[1, 2, 3, 4]]*4,
                      xmin=1.0, xmax=5.0,
                      ymin=1.0, ymax=15.0,
                      zmin=1.0, zmax=20.0,
                      )

        data.xmin = 1.0
        data.xmax = 5.0
        data.ymin = 1.0
        data.ymax = 15.0
        data.zmin = 1.0
        data.zmax = 20.0

        data.title = "Test data"
        data.id = 1
        data.ndim = 2

        p.data = data

        yield p

        '''destroy'''
        p.figure.clf()

    def testDataProperty(self, plotter):
        """ Adding data """
        assert plotter._title == "Test data"
        assert plotter.xLabel == "$\\rm{Q_{x}}(A^{-1})$"
        assert plotter.yLabel == "$\\rm{Q_{y}}(A^{-1})$"

    def testPlot(self, plotter, mocker):
        """ Look at the plotting """
        mocker.patch.object(plotter, 'plot')
        plotter.plot()
        plotter.plot.assert_called_once()

    def testCalculateDepth(self, plotter):
        ''' Test the depth calculator '''

        # Default, log scale
        depth = plotter.calculateDepth()
        assert depth == (10.0, 1.e20)

        # Change the scale to linear
        plotter.scale = 'linear'
        depth = plotter.calculateDepth()
        assert depth[0] == 0.0
        assert depth[1] == pytest.approx(1.30103, abs=1e-5)

    def testOnColorMap(self, plotter, mocker):
        ''' Respond to the color map event '''
        plotter.plot()
        plotter.show()

        mocker.patch.object(QtWidgets.QDialog, 'exec_', return_value=QtWidgets.QDialog.Accepted)

        # Just this one plot
        plotter.onColorMap()

        # Check that exec_ got called
        QtWidgets.QDialog.exec_.assert_called()

        assert plotter.cmap == "jet"
        assert plotter.vmin == pytest.approx(10.0, abs=1e-6)
        assert plotter.vmax == pytest.approx(1e+20, abs=1e-6)

    def testOnToggleScale(self, plotter, mocker):
        """ Respond to the event by replotting """
        plotter.show()
        mocker.patch.object(FigureCanvas, 'draw_idle')

        plotter.onToggleScale(None)

        FigureCanvas.draw_idle.assert_called()

    def testOnToggleMaskedPoints(self, plotter, mocker):
        """ Respond to the masked data event by replotting """
        assert not plotter._show_masked_data
        plotter.data = Data2D(image=[[0.1]*4]*4,
                              qx_data=[[1.0, 2.0, 3.0, 4.0]]*4,
                              qy_data=[[10.0, 11.0, 12.0, 13.0]]*4,
                              dqx_data=[[0.1, 0.2, 0.3, 0.4]]*4,
                              dqy_data=[[0.1, 0.2, 0.3, 0.4]]*4,
                              q_data=[[1, 2, 3, 4]]*4,
                              mask=[[1, 1, 1, 0]]*4,
                              xmin=-1.0, xmax=5.0,
                              ymin=-1.0, ymax=15.0,
                              zmin=-1.0, zmax=20.0,
                              )
        assert len(plotter._masked_data[0].data) == 4*3

        mocker.patch.object(plotter, 'plot')
        plotter.onToggleMaskedData(None)
        assert plotter._show_masked_data
        plotter.plot.assert_called_once()

    def testOnBoxSum(self, plotter, mocker):
        """ Test the box sum display and functionality """

        # hacky way to make things work in manipulations._sum
        data = plotter.data0
        data.detector = [1]
        data.err_data = numpy.array([[1.0, 2.0, 3.0, 4.0]]*4)
        plotter.data = data
        plotter.show()

        # Mock the main window
        mocker.patch.object(plotter.manager, 'parent', create=True)

        # Call the main tested method
        plotter.onBoxSum()

        # Test various properties
        assert isinstance(plotter.slicer.model(), QtGui.QStandardItemModel)
        assert plotter.boxwidget.isVisible()
        assert isinstance(plotter.boxwidget.model, QtGui.QStandardItemModel)

    def testRemoveSlicersForPlotWindowIgnoresDeletedCanvas(self, plotter, mocker):
        """ Cleanup should not crash if the canvas is already gone """
        slicer_window = object()
        slicer = mocker.MagicMock()
        slicer._plot_window = slicer_window
        plotter.slicers = {"slicer": slicer}
        plotter.slicer = slicer
        plotter.slicer_plots_dict = {"plot_one": mocker.MagicMock()}
        mocker.patch.object(plotter.canvas, "draw", side_effect=RuntimeError("deleted"))

        plotter.removeSlicersForPlotWindow(slicer_window)

        slicer.clear.assert_called_once()
        assert plotter.slicer is None

    def testRemoveSlicersForPlotWindowRemovesMatchingEntry(self, plotter, mocker):
        """ Closing a 1D slicer plot should remove the matching slicer from the owner """
        slicer_window = mocker.MagicMock()
        slicer = mocker.MagicMock()
        slicer._plot_window = slicer_window
        plotter.slicers = {"slicer": slicer}
        plotter.slicer = slicer
        plotter.slicer_plots_dict = {"plot_one": slicer_window}
        mocker.patch.object(plotter.canvas, "draw")

        plotter.removeSlicersForPlotWindow(slicer_window)

        slicer.clear.assert_called_once()
        assert plotter.slicers == {}
        assert plotter.slicer is None
        assert plotter.slicer_plots_dict == {}

    def testContextMenuQuickPlot(self, plotter, mocker):
        """ Test the right click menu """
        plotter.createContextMenuQuick()
        actions = plotter.contextMenu.actions()
        assert len(actions) == 9

        # Trigger Print Image and make sure the method is called
        assert actions[1].text() == "Print Image"
        mocker.patch.object(QtPrintSupport.QPrintDialog, 'exec_', return_value=QtWidgets.QDialog.Rejected)
        actions[1].trigger()
        assert QtPrintSupport.QPrintDialog.exec_.called

        # Trigger Copy to Clipboard and make sure the method is called
        assert actions[2].text() == "Copy to Clipboard"

        # Trigger Toggle Grid and make sure the method is called
        assert actions[4].text() == "Toggle Grid On/Off"
        mocker.patch.object(plotter.ax, 'grid')
        actions[4].trigger()
        assert plotter.ax.grid.called

        # Trigger Change Scale and make sure the method is called
        assert actions[6].text() == "Toggle Linear/Log Scale"
        mocker.patch.object(FigureCanvas, 'draw_idle')
        actions[6].trigger()
        assert FigureCanvas.draw_idle.called

        # Spy on cliboard's dataChanged() signal
        if not platform.system == "Windows":
            return
        self.clipboard_called = False
        def done():
            self.clipboard_called = True
        QtCore.QObject.connect(QtWidgets.QApplication.clipboard(), QtCore.SIGNAL("dataChanged()"), done)
        actions[2].trigger()
        QtWidgets.QApplication.processEvents()
        # Make sure clipboard got updated.
        assert self.clipboard_called

    def testShowNoPlot(self, plotter, mocker):
        """ Test the plot rendering and generation """

        mocker.patch.object(FigureCanvas, 'draw_idle')
        mocker.patch.object(FigureCanvas, 'draw')

        # Test early return on no data
        plotter.showPlot(data=None,
                              qx_data=plotter.data0.qx_data,
                              qy_data=plotter.data0.qy_data,
                              xmin=plotter.data0.xmin,
                              xmax=plotter.data0.xmax,
                              ymin=plotter.data0.ymin,
                              ymax=plotter.data0.ymax,
                              cmap=None, zmin=0.0,
                              zmax=None)

        FigureCanvas.draw_idle.assert_not_called()
        FigureCanvas.draw.assert_not_called()

    def testShow3DPlot(self, plotter, mocker):
        """ Test the 3Dplot rendering and generation """
        # Test 3D printout
        mocker.patch.object(FigureCanvas, 'draw')
        mocker.patch.object(Axes3D, 'plot_surface')
        mocker.patch.object(plotter.figure, 'colorbar')

        data = plotter.data0
        plotter.dimension = 3
        plotter.showPlot(data=data.data,
                              qx_data=data.qx_data,
                              qy_data=data.qy_data,
                              xmin=data.xmin,
                              xmax=data.xmax,
                              ymin=data.ymin,
                              ymax=data.ymax,
                              cmap=None, zmin=0.0,
                              zmax=None)
        Axes3D.plot_surface.assert_called()
        FigureCanvas.draw.assert_called()

    def testShow2DPlot(self, plotter, mocker):
        """ Test the 2Dplot rendering and generation """
        # Test 2D printout
        mocker.patch.object(FigureCanvas, 'draw_idle')
        mocker.patch.object(plotter.figure, 'colorbar')

        plotter.dimension = 2
        plotter.showPlot(data=plotter.data0.data,
                              qx_data=plotter.data0.qx_data,
                              qy_data=plotter.data0.qy_data,
                              xmin=plotter.data0.xmin,
                              xmax=plotter.data0.xmax,
                              ymin=plotter.data0.ymin,
                              ymax=plotter.data0.ymax,
                              cmap=None, zmin=0.0,
                              zmax=None)
        FigureCanvas.draw_idle.assert_called()
