from unittest import SkipTest

from holoviews.core.spaces import DynamicMap
from holoviews.core.options import Store
from holoviews.element import Curve, Polygons, Path, HLine
from holoviews.element.comparison import ComparisonTestCase
from holoviews.streams import RangeXY, PlotReset

try:
    from bokeh.application.handlers import FunctionHandler
    from bokeh.application import Application
    from bokeh.client import pull_session
    from bokeh.document import Document
    from bokeh.io import curdoc
    from bokeh.server.server import Server

    from holoviews.plotting.bokeh.callbacks import (
        Callback, RangeXYCallback, ResetCallback
    )
    from holoviews.plotting.bokeh.renderer import BokehRenderer
    bokeh_renderer = BokehRenderer.instance(mode='server')
except:
    bokeh_renderer = None


class TestBokehServerSetup(ComparisonTestCase):

    def setUp(self):
        self.previous_backend = Store.current_backend
        if not bokeh_renderer:
            raise SkipTest("Bokeh required to test plot instantiation")
        Store.current_backend = 'bokeh'

    def tearDown(self):
        Store.current_backend = self.previous_backend
        bokeh_renderer.last_plot = None
        Callback._callbacks = {}

    def test_render_server_doc_element(self):
        obj = Curve([])
        doc = bokeh_renderer.server_doc(obj)
        self.assertIs(doc, curdoc())
        self.assertIs(bokeh_renderer.last_plot.document, curdoc())

    def test_render_explicit_server_doc_element(self):
        obj = Curve([])
        doc = Document()
        server_doc = bokeh_renderer.server_doc(obj, doc)
        self.assertIs(server_doc, doc)
        self.assertIs(bokeh_renderer.last_plot.document, doc)

    def test_set_up_linked_change_stream_on_server_doc(self):
        obj = Curve([])
        stream = RangeXY(source=obj)
        server_doc = bokeh_renderer.server_doc(obj)
        self.assertIsInstance(server_doc, Document)
        self.assertEqual(len(bokeh_renderer.last_plot.callbacks), 1)
        cb = bokeh_renderer.last_plot.callbacks[0]
        self.assertIsInstance(cb, RangeXYCallback)
        self.assertEqual(cb.streams, [stream])
        x_range = bokeh_renderer.last_plot.handles['x_range']
        self.assertIn(cb.on_change, x_range._callbacks['start'])
        self.assertIn(cb.on_change, x_range._callbacks['end'])
        y_range = bokeh_renderer.last_plot.handles['y_range']
        self.assertIn(cb.on_change, y_range._callbacks['start'])
        self.assertIn(cb.on_change, y_range._callbacks['end'])

    def test_set_up_linked_event_stream_on_server_doc(self):
        obj = Curve([])
        stream = PlotReset(source=obj)
        server_doc = bokeh_renderer.server_doc(obj)
        self.assertIsInstance(server_doc, Document)
        cb = bokeh_renderer.last_plot.callbacks[0]
        self.assertIsInstance(cb, ResetCallback)
        self.assertEqual(cb.streams, [stream])
        plot = bokeh_renderer.last_plot.state
        self.assertIn(cb.on_event, plot._event_callbacks['reset'])



class TestBokehServerRun(ComparisonTestCase):

    def setUp(self):
        self.previous_backend = Store.current_backend
        if not bokeh_renderer:
            raise SkipTest("Bokeh required to test plot instantiation")
        Store.current_backend = 'bokeh'

    def tearDown(self):
        Store.current_backend = self.previous_backend
        Callback._callbacks = {}

    def test_launch_simple_server(self):
        obj = Curve([])
        launched = []
        def modify_doc(doc):
            bokeh_renderer(obj, doc=doc)
            launched.append(True)
            server.stop()
        handler = FunctionHandler(modify_doc)
        app = Application(handler)
        server = Server({'/': app}, port=0)
        server.start()
        url = "http://localhost:" + str(server.port) + "/"
        pull_session(session_id='Test', url=url, io_loop=server.io_loop)
        self.assertTrue(len(launched)==1)

    def test_launch_server_with_stream(self):
        obj = Curve([])
        stream = RangeXY(source=obj)

        launched = []
        def modify_doc(doc):
            bokeh_renderer(obj, doc=doc)
            launched.append(True)
            server.stop()
        handler = FunctionHandler(modify_doc)
        app = Application(handler)
        server = Server({'/': app}, port=0)
        server.start()
        url = "http://localhost:" + str(server.port) + "/"
        pull_session(session_id='Test', url=url, io_loop=server.io_loop)

        self.assertTrue(len(launched)==1)
        cb = bokeh_renderer.last_plot.callbacks[0]
        self.assertIsInstance(cb, RangeXYCallback)
        self.assertEqual(cb.streams, [stream])
        x_range = bokeh_renderer.last_plot.handles['x_range']
        self.assertIn(cb.on_change, x_range._callbacks['start'])
        self.assertIn(cb.on_change, x_range._callbacks['end'])
        y_range = bokeh_renderer.last_plot.handles['y_range']
        self.assertIn(cb.on_change, y_range._callbacks['start'])
        self.assertIn(cb.on_change, y_range._callbacks['end'])


    def test_launch_server_with_complex_plot(self):
        dmap = DynamicMap(lambda x_range, y_range: Curve([]), streams=[RangeXY()])
        overlay = dmap * HLine(0)
        static = Polygons([]) * Path([]) * Curve([])
        layout = overlay + static

        launched = []
        def modify_doc(doc):
            bokeh_renderer(layout, doc=doc)
            launched.append(True)
            server.stop()
        handler = FunctionHandler(modify_doc)
        app = Application(handler)
        server = Server({'/': app}, port=0)
        server.start()
        url = "http://localhost:" + str(server.port) + "/"
        pull_session(session_id='Test', url=url, io_loop=server.io_loop)
        self.assertTrue(len(launched)==1)
