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"])
        assert len(data) == 1
        assert 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]
        assert event.obj is plotly_pane
        assert event.new is 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],
        }

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

        plotly_pane.viewport = None
        assert rangexy1.x_range is None
        assert rangexy1.y_range is None
        assert rangexy3.x_range is None
        assert rangexy3.y_range is None

        # 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]}}

        assert boundsxy2a.bounds == (10, 11, 20, 22)
        assert 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]},
        }

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

        # bounds streams on scatter 2 are None
        assert boundsxy2a.bounds is None
        assert boundsxy2b.bounds is None

        # Clear selection
        plotly_pane.selected_data = None
        assert boundsxy3.bounds is None
        assert boundsxy2a.bounds is None
        assert boundsxy2b.bounds is None
        assert selection1d3.index == []
