from unittest.mock import Mock

import numpy as np
import panel as pn
from bokeh.document import Document
from pyviz_comms import Comm

import holoviews as hv
from holoviews.plotting.plotly.util import _convert_numpy_in_fig_dict
from holoviews.streams import (
    BoundsXY,
    RangeXY,
    Selection1D,
    Stream,
)

from .test_plot import TestPlotlyPlot


class TestDynamicMap(TestPlotlyPlot):

    def test_update_dynamic_map_with_stream(self):
        ys = np.arange(10)

        # Build stream
        Scale = Stream.define('Scale', scale=1.0)
        scale_stream = Scale()

        # Build DynamicMap
        def build_scatter(scale):
            return hv.Scatter(ys * scale)

        dmap = hv.DynamicMap(build_scatter, streams=[scale_stream])

        # Create HoloViews Pane using panel so that we can access the plotly pane
        # used to display the plotly figure
        dmap_pane = pn.pane.HoloViews(dmap, backend='plotly')

        # Call get_root to force instantiation of internal plots/models
        doc = Document()
        comm = Comm()
        dmap_pane.get_root(doc, comm)

        # Get reference to the plotly pane
        _, plotly_pane = next(iter(dmap_pane._plots.values()))

        # Check initial data
        data = _convert_numpy_in_fig_dict(plotly_pane.object['data'])
        self.assertEqual(len(data), 1)
        self.assertEqual(data[0]['type'], 'scatter')
        np.testing.assert_equal(data[0]['y'], ys)

        # Watch object for changes
        fn = Mock()
        plotly_pane.param.watch(fn, 'object')

        # Update stream
        scale_stream.event(scale=2.0)

        # Check that figure object was updated
        data = _convert_numpy_in_fig_dict(plotly_pane.object['data'])
        np.testing.assert_equal(data[0]['y'], ys * 2.0)

        # Check that object callback was triggered
        fn.assert_called_once()
        args, kwargs = fn.call_args_list[0]
        event = args[0]
        self.assertIs(event.obj, plotly_pane)
        self.assertIs(event.new, plotly_pane.object)


class TestInteractiveStream(TestPlotlyPlot):
    # Note: Testing the core logic of each interactive stream should take place in
    # testcallbacks.py. Here we are testing that that callbacks are properly
    # routed to streams

    def test_interactive_streams(self):
        ys = np.arange(10)
        scatter1 = hv.Scatter(ys)
        scatter2 = hv.Scatter(ys)
        scatter3 = hv.Scatter(ys)

        # Single stream on the first scatter
        rangexy1 = RangeXY(source=scatter1)

        # Multiple streams of the same type on second scatter
        boundsxy2a = BoundsXY(source=scatter2)
        boundsxy2b = BoundsXY(source=scatter2)

        # Multiple streams of different types on third scatter
        rangexy3 = RangeXY(source=scatter3)
        boundsxy3 = BoundsXY(source=scatter3)
        selection1d3 = Selection1D(source=scatter3)

        # Build layout and layout Pane
        layout = scatter1 + scatter2 + scatter3
        layout_pane = pn.pane.HoloViews(layout, backend='plotly')

        # Get plotly pane reference
        doc = Document()
        comm = Comm()
        layout_pane.get_root(doc, comm)
        _, plotly_pane = next(iter(layout_pane._plots.values()))

        # Simulate zoom and check that RangeXY streams updated accordingly
        plotly_pane.viewport = {
            'xaxis.range': [1, 3],
            'yaxis.range': [2, 4],
            'xaxis2.range': [3, 5],
            'yaxis2.range': [4, 6],
            'xaxis3.range': [5, 7],
            'yaxis3.range': [6, 8],
        }

        self.assertEqual(rangexy1.x_range, (1, 3))
        self.assertEqual(rangexy1.y_range, (2, 4))
        self.assertEqual(rangexy3.x_range, (5, 7))
        self.assertEqual(rangexy3.y_range, (6, 8))

        plotly_pane.viewport = None
        self.assertIsNone(rangexy1.x_range)
        self.assertIsNone(rangexy1.y_range)
        self.assertIsNone(rangexy3.x_range)
        self.assertIsNone(rangexy3.y_range)

        # Simulate box selection and check that BoundsXY and Selection1D streams
        # update accordingly

        # Box select on second subplot
        plotly_pane.selected_data = {
            'points': [],
            'range': {
                'x2': [10, 20],
                'y2': [11, 22]
            }
        }

        self.assertEqual(boundsxy2a.bounds, (10, 11, 20, 22))
        self.assertEqual(boundsxy2b.bounds, (10, 11, 20, 22))

        # Box select on third subplot
        plotly_pane.selected_data = {
            'points': [
                {'curveNumber': 2, 'pointNumber': 0},
                {'curveNumber': 2, 'pointNumber': 3},
                {'curveNumber': 2, 'pointNumber': 7},
            ],
            'range': {
                'x3': [0, 5],
                'y3': [1, 6]
            }
        }

        self.assertEqual(boundsxy3.bounds, (0, 1, 5, 6))
        self.assertEqual(selection1d3.index, [0, 3, 7])

        # bounds streams on scatter 2 are None
        self.assertIsNone(boundsxy2a.bounds)
        self.assertIsNone(boundsxy2b.bounds)

        # Clear selection
        plotly_pane.selected_data = None
        self.assertIsNone(boundsxy3.bounds)
        self.assertIsNone(boundsxy2a.bounds)
        self.assertIsNone(boundsxy2b.bounds)
        self.assertEqual(selection1d3.index, [])
