from __future__ import absolute_import

import mock
import unittest

import bs4

from bokeh.core.templates import DOC_JS, PLOT_DIV, NOTEBOOK_DIV
from bokeh.core.json_encoder import serialize_json

import bokeh.embed as embed
from bokeh.plotting import figure, curdoc
from bokeh.resources import CDN, JSResources, CSSResources
from bokeh.util.string import encode_utf8
from jinja2 import Template
from six import string_types

_embed_test_plot = None


def setUpModule():
    global _embed_test_plot
    _embed_test_plot = figure()
    _embed_test_plot.circle([1, 2], [2, 3])


def _stable_id():
    return 'ID'


def test_components_return_type():
    plot1 = figure()
    plot2 = figure()
    # This is a testing artefact, users dont' have to do this in practice
    curdoc().add_root(plot1)
    curdoc().add_root(plot2)

    r = embed.components(plot1)
    assert len(r) == 2

    _, divs = embed.components((plot1, plot2))
    assert isinstance(divs, tuple)

    _, divs = embed.components([plot1, plot2])
    assert isinstance(divs, tuple)

    _, divs = embed.components({"Plot 1": plot1, "Plot 2": plot2})
    assert isinstance(divs, dict)
    assert all(isinstance(x, string_types) for x in divs.keys())


@mock.patch('bokeh.embed.make_id', new_callable=lambda: _stable_id)
def test_plot_dict_returned_when_wrap_plot_info_is_false(mock_make_id):
    plot1 = figure()
    plot2 = figure()
    # This is a testing artefact, users dont' have to do this in practice
    curdoc().add_root(plot1)
    curdoc().add_root(plot2)

    expected_plotdict_1 = {"modelid": plot1.ref["id"], "elementid": "ID", "docid": "ID"}
    expected_plotdict_2 = {"modelid": plot2.ref["id"], "elementid": "ID", "docid": "ID"}

    _, plotdict = embed.components(plot1, wrap_plot_info=False)
    assert plotdict == expected_plotdict_1

    _, plotids = embed.components((plot1, plot2), wrap_plot_info=False)
    assert plotids == (expected_plotdict_1, expected_plotdict_2)

    _, plotiddict = embed.components({'p1': plot1, 'p2': plot2}, wrap_plot_info=False)
    assert plotiddict == {'p1': expected_plotdict_1, 'p2': expected_plotdict_2}



class TestComponents(unittest.TestCase):


    def test_result_attrs(self):
        script, div = embed.components(_embed_test_plot)
        html = bs4.BeautifulSoup(script, "lxml")
        scripts = html.findAll(name='script')
        self.assertEqual(len(scripts), 1)
        self.assertTrue(scripts[0].attrs, {'type': 'text/javascript'})

    def test_div_attrs(self):
        script, div = embed.components(_embed_test_plot)
        html = bs4.BeautifulSoup(div, "lxml")

        divs = html.findAll(name='div')
        self.assertEqual(len(divs), 2)

        div = divs[0]
        self.assertEqual(set(div.attrs), set(['class']))
        self.assertEqual(div.attrs['class'], ['bk-root'])
        self.assertEqual(div.text, '\n\n')

        div = divs[1]
        self.assertEqual(set(div.attrs), set(['id', 'class']))
        self.assertEqual(div.attrs['class'], ['bk-plotdiv'])
        self.assertEqual(div.text, '')

    def test_script_is_utf8_encoded(self):
        script, div = embed.components(_embed_test_plot)
        self.assertTrue(isinstance(script, str))

    @mock.patch('bokeh.embed.make_id', new_callable=lambda: _stable_id)
    def test_output_is_without_script_tag_when_wrap_script_is_false(self, mock_make_id):
        script, div = embed.components(_embed_test_plot)
        html = bs4.BeautifulSoup(script, "lxml")
        scripts = html.findAll(name='script')
        self.assertEqual(len(scripts), 1)
        script_content = scripts[0].getText()

        rawscript, div = embed.components(_embed_test_plot, wrap_script=False)
        self.maxDiff = None
        self.assertEqual(rawscript.strip(), script_content.strip())

class TestNotebookContent(unittest.TestCase):

    @mock.patch('bokeh.embed._standalone_docs_json_and_render_items')
    def test_notebook_content(self, mock_standalone_docs_json_and_render_items):
        (docs_json, render_items) = ("DOC_JSON", [dict(docid="foo", elementid="bar", modelid="bat")])
        mock_standalone_docs_json_and_render_items.return_value = (docs_json, render_items)

        expected_script = DOC_JS.render(docs_json=serialize_json(docs_json),
                                        render_items=serialize_json(render_items))
        expected_div = PLOT_DIV.render(elementid=render_items[0]['elementid'])

        (script, div) = embed.notebook_content(_embed_test_plot)

        self.assertEqual(script, expected_script)
        self.assertEqual(div, expected_div)

    @mock.patch('bokeh.embed._standalone_docs_json_and_render_items')
    def test_notebook_content_with_notebook_comms_target(self, mock_standalone_docs_json_and_render_items):
        (docs_json, render_items) = ("DOC_JSON", [dict(docid="foo", elementid="bar", modelid="bat")])
        mock_standalone_docs_json_and_render_items.return_value = (docs_json, render_items)
        comms_target = "NOTEBOOK_COMMS_TARGET"

        ## assert that NOTEBOOK_COMMS_TARGET is added to render_items bundle
        self.assertTrue('notebook_comms_target' not in render_items[0])
        (script, _) = embed.notebook_content(_embed_test_plot, notebook_comms_target=comms_target)
        self.assertTrue('notebook_comms_target' in render_items[0])

        ## assert that NOTEBOOK_COMMS_TARGET ends up in generated script
        expected_script = DOC_JS.render(docs_json=serialize_json(docs_json),
                                        render_items=serialize_json(render_items))

        self.assertEqual(script, expected_script)

class TestNotebookDiv(unittest.TestCase):

    @mock.patch('bokeh.embed.notebook_content')
    def test_notebook_div(self, mock_notebook_content):
        (script, div) = ("(function(){})()", "<div></div>")
        mock_notebook_content.return_value = (script, div)

        notebook_div = embed.notebook_div(_embed_test_plot)

        expected_notebook_div = NOTEBOOK_DIV.render(plot_script=script, plot_div=div)

        self.assertEqual(notebook_div, expected_notebook_div)

class TestFileHTML(unittest.TestCase):

    def test_return_type(self):

        class fake_template:
            def __init__(self, tester, user_template_variables=None):
                self.tester = tester
                self.template_variables = {
                    "title",
                    "bokeh_js",
                    "bokeh_css",
                    "plot_script",
                    "plot_div"
                }
                if user_template_variables is not None:
                    self.template_variables.update(user_template_variables)

            def render(self, template_variables):
                self.tester.assertTrue(
                    self.template_variables.issubset(
                        set(template_variables.keys())
                    )
                )
                return "template result"

        r = embed.file_html(_embed_test_plot, CDN, "title")
        self.assertTrue(isinstance(r, str))

        r = embed.file_html(_embed_test_plot, CDN, "title", fake_template(self))
        self.assertTrue(isinstance(r, str))

        r = embed.file_html(_embed_test_plot, CDN, "title",
                            fake_template(self, {"test_var"}),
                            {"test_var": "test"})
        self.assertTrue(isinstance(r, str))


def test_file_html_handles_js_only_resources():
    js_resources = JSResources(mode="relative", components=["bokeh"])
    template = Template("<head>{{ bokeh_js }}</head><body></body>")
    output = embed.file_html(_embed_test_plot, (js_resources, None), "title", template=template)
    html = encode_utf8("<head>%s</head><body></body>" % js_resources.render_js())
    assert output == html


@mock.patch('bokeh.embed.warn')
def test_file_html_provides_warning_if_no_css(mock_warn):
    js_resources = JSResources()
    embed.file_html(_embed_test_plot, (js_resources, None), "title")
    mock_warn.assert_called_once_with(
        'No Bokeh CSS Resources provided to template. If required you will need to provide them manually.'
    )


def test_file_html_handles_css_only_resources():
    css_resources = CSSResources(mode="relative", components=["bokeh"])
    template = Template("<head>{{ bokeh_css }}</head><body></body>")
    output = embed.file_html(_embed_test_plot, (None, css_resources), "title", template=template)
    html = encode_utf8("<head>%s</head><body></body>" % css_resources.render_css())
    assert output == html


@mock.patch('bokeh.embed.warn')
def test_file_html_provides_warning_if_no_js(mock_warn):
    css_resources = CSSResources()
    embed.file_html(_embed_test_plot, (None, css_resources), "title")
    mock_warn.assert_called_once_with(
        'No Bokeh JS Resources provided to template. If required you will need to provide them manually.'
    )


def test_file_html_title_is_escaped():
    r = embed.file_html(_embed_test_plot, CDN, "&<")
    assert "<title>&amp;&lt;</title>" in r


class TestAutoloadStatic(unittest.TestCase):

    def test_return_type(self):
        r = embed.autoload_static(_embed_test_plot, CDN, "some/path")
        self.assertEqual(len(r), 2)

    @mock.patch('bokeh.embed.make_id', new_callable=lambda: _stable_id)
    def test_script_attrs(self, mock_make_id):
        js, tag = embed.autoload_static(_embed_test_plot, CDN, "some/path")
        html = bs4.BeautifulSoup(tag, "lxml")
        scripts = html.findAll(name='script')
        self.assertEqual(len(scripts), 1)
        attrs = scripts[0].attrs
        self.assertTrue(set(attrs), set(['src',
            'data-bokeh-model-id',
            'id',
            'data-bokeh-doc-id']))
        self.assertEqual(attrs['data-bokeh-doc-id'], 'ID')
        self.assertEqual(attrs['data-bokeh-model-id'], str(_embed_test_plot._id))
        self.assertEqual(attrs['src'], 'some/path')


class TestConnectSessionOrDocument(unittest.TestCase):

    def test_invalid_resources_param(self):
        with self.assertRaises(ValueError):
            embed._connect_session_or_document(url="http://localhost:8081/foo/bar/sliders", resources=123)
        with self.assertRaises(ValueError):
            embed._connect_session_or_document(url="http://localhost:8081/foo/bar/sliders", resources="whatever")

    def test_resources_default_is_implicit(self):
        r = embed._connect_session_or_document(url="http://localhost:8081/foo/bar/sliders", resources="default")
        self.assertFalse('resources=' in r)

    def test_resources_none(self):
        r = embed._connect_session_or_document(url="http://localhost:8081/foo/bar/sliders", resources=None)
        self.assertTrue('resources=none' in r)


class TestServerDocument(unittest.TestCase):

    def test_ensure_no_session_do_model(self):
        r = embed.server_document(url="http://localhost:8081/foo/bar/sliders")
        self.assertTrue('bokeh-app-path=/foo/bar/sliders' in r)
        self.assertTrue('bokeh-absolute-url=http://localhost:8081/foo/bar/sliders' in r)
        html = bs4.BeautifulSoup(r, "lxml")
        scripts = html.findAll(name='script')
        self.assertEqual(len(scripts), 1)
        attrs = scripts[0].attrs
        self.assertTrue(set(attrs), set([
            'src',
            'data-bokeh-doc-id',
            'data-bokeh-model-id',
            'id'
        ]))
        divid = attrs['id']
        src = "%s/autoload.js?bokeh-autoload-element=%s&bokeh-app-path=/foo/bar/sliders&bokeh-absolute-url=%s" % \
              ("http://localhost:8081/foo/bar/sliders", divid, "http://localhost:8081/foo/bar/sliders")
        self.assertDictEqual({ 'data-bokeh-doc-id' : '',
                               'data-bokeh-model-id' : '',
                               'id' : divid,
                               'src' : src },
                             attrs)

class TestServerSession(unittest.TestCase):

    def test_model_and_session_both_required(self):
        with self.assertRaises(TypeError):
            embed.server_session()
        with self.assertRaises(TypeError):
            embed.server_session(_embed_test_plot)
        with self.assertRaises(TypeError):
            embed.server_session(session_id='fakesession')

    def test_ensure_session_and_model(self):
        r = embed.server_session(_embed_test_plot, session_id='fakesession')
        self.assertTrue('bokeh-session-id=fakesession' in r)
        html = bs4.BeautifulSoup(r, "lxml")
        scripts = html.findAll(name='script')
        self.assertEqual(len(scripts), 1)
        attrs = scripts[0].attrs
        self.assertTrue(set(attrs), set([
            'src',
            'data-bokeh-doc-id',
            'data-bokeh-model-id',
            'id'
        ]))
        divid = attrs['id']
        src = "%s/autoload.js?bokeh-autoload-element=%s&bokeh-absolute-url=%s&bokeh-session-id=fakesession" % \
              ("http://localhost:5006", divid, "http://localhost:5006")
        self.assertDictEqual({ 'data-bokeh-doc-id' : '',
                               'data-bokeh-model-id' : str(_embed_test_plot._id),
                               'id' : divid,
                               'src' : src },
                             attrs)

class TestAutoloadServer(unittest.TestCase):

    def test_return_type(self):
        r = embed.autoload_server(_embed_test_plot, session_id='fakesession')
        self.assertTrue(isinstance(r, str))

    def test_script_attrs_session_id_provided(self):
        r = embed.autoload_server(_embed_test_plot, session_id='fakesession')
        self.assertTrue('bokeh-session-id=fakesession' in r)
        html = bs4.BeautifulSoup(r, "lxml")
        scripts = html.findAll(name='script')
        self.assertEqual(len(scripts), 1)
        attrs = scripts[0].attrs
        self.assertTrue(set(attrs), set([
            'src',
            'data-bokeh-doc-id',
            'data-bokeh-model-id',
            'id'
        ]))
        divid = attrs['id']
        src = "%s/autoload.js?bokeh-autoload-element=%s&bokeh-absolute-url=%s&bokeh-session-id=fakesession" % \
              ("http://localhost:5006", divid, "http://localhost:5006")
        self.assertDictEqual({ 'data-bokeh-doc-id' : '',
                               'data-bokeh-model-id' : str(_embed_test_plot._id),
                               'id' : divid,
                               'src' : src },
                             attrs)

    def test_script_attrs_no_session_id_provided(self):
        r = embed.autoload_server(None)
        self.assertFalse('bokeh-session-id' in r)
        html = bs4.BeautifulSoup(r, "lxml")
        scripts = html.findAll(name='script')
        self.assertEqual(len(scripts), 1)
        attrs = scripts[0].attrs
        self.assertTrue(set(attrs), set([
            'src',
            'data-bokeh-doc-id',
            'data-bokeh-model-id',
            'id'
        ]))
        divid = attrs['id']
        src = "%s/autoload.js?bokeh-autoload-element=%s&bokeh-absolute-url=%s" % \
              ("http://localhost:5006", divid, "http://localhost:5006")
        self.assertDictEqual({ 'data-bokeh-doc-id' : '',
                               'data-bokeh-model-id' : '',
                               'id' : divid,
                               'src' : src },
                             attrs)

    def test_script_attrs_url_provided(self):
        r = embed.autoload_server(url="http://localhost:8081/foo/bar/sliders", relative_urls=True)
        self.assertTrue('bokeh-app-path=/foo/bar/sliders' in r)
        html = bs4.BeautifulSoup(r, "lxml")
        scripts = html.findAll(name='script')
        self.assertEqual(len(scripts), 1)
        attrs = scripts[0].attrs
        self.assertTrue(set(attrs), set([
            'src',
            'data-bokeh-doc-id',
            'data-bokeh-model-id',
            'id'
        ]))
        divid = attrs['id']
        src = "%s/autoload.js?bokeh-autoload-element=%s&bokeh-app-path=/foo/bar/sliders" % \
              ("http://localhost:8081/foo/bar/sliders", divid)
        self.assertDictEqual({ 'data-bokeh-doc-id' : '',
                               'data-bokeh-model-id' : '',
                               'id' : divid,
                               'src' : src },
                             attrs)

    def test_script_attrs_url_provided_absolute_resources(self):
        r = embed.autoload_server(url="http://localhost:8081/foo/bar/sliders")
        self.assertTrue('bokeh-app-path=/foo/bar/sliders' in r)
        self.assertTrue('bokeh-absolute-url=http://localhost:8081/foo/bar/sliders' in r)
        html = bs4.BeautifulSoup(r, "lxml")
        scripts = html.findAll(name='script')
        self.assertEqual(len(scripts), 1)
        attrs = scripts[0].attrs
        self.assertTrue(set(attrs), set([
            'src',
            'data-bokeh-doc-id',
            'data-bokeh-model-id',
            'id'
        ]))
        divid = attrs['id']
        src = "%s/autoload.js?bokeh-autoload-element=%s&bokeh-app-path=/foo/bar/sliders&bokeh-absolute-url=%s" % \
              ("http://localhost:8081/foo/bar/sliders", divid, "http://localhost:8081/foo/bar/sliders")
        self.assertDictEqual({ 'data-bokeh-doc-id' : '',
                               'data-bokeh-model-id' : '',
                               'id' : divid,
                               'src' : src },
                             attrs)

    def test_script_attrs_url_and_app_path_provided(self):
        for path in ("/foo/bar/sliders", "/foo/bar/sliders/", "foo/bar/sliders", "foo/bar/sliders"):
            r = embed.autoload_server(url="http://localhost:8081", app_path=path, relative_urls=True)
            self.assertTrue('bokeh-app-path=/foo/bar/sliders' in r)
            html = bs4.BeautifulSoup(r, "lxml")
            scripts = html.findAll(name='script')
            self.assertEqual(len(scripts), 1)
            attrs = scripts[0].attrs
            self.assertTrue(set(attrs), set([
                'src',
                'data-bokeh-doc-id',
                'data-bokeh-model-id',
                'id'
            ]))
            divid = attrs['id']
            src = "%s/autoload.js?bokeh-autoload-element=%s&bokeh-app-path=/foo/bar/sliders" % \
                  ("http://localhost:8081/foo/bar/sliders", divid)
            self.assertDictEqual({ 'data-bokeh-doc-id' : '',
                                   'data-bokeh-model-id' : '',
                                   'id' : divid,
                                   'src' : src },
                                 attrs)

    def test_script_attrs_arguments_provided(self):
        r = embed.server_document(arguments=dict(foo=10))
        self.assertTrue('foo=10' in r)
        html = bs4.BeautifulSoup(r, "lxml")
        scripts = html.findAll(name='script')
        self.assertEqual(len(scripts), 1)
        attrs = scripts[0].attrs
        self.assertTrue(set(attrs), set([
            'src',
            'data-bokeh-doc-id',
            'data-bokeh-model-id',
            'id'
        ]))
        divid = attrs['id']
        src = "%s/autoload.js?bokeh-autoload-element=%s&bokeh-absolute-url=%s&foo=10" % \
              ("http://localhost:5006", divid, "http://localhost:5006")
        self.assertDictEqual({ 'data-bokeh-doc-id' : '',
                               'data-bokeh-model-id' : '',
                               'id' : divid,
                               'src' : src },
                             attrs)


@mock.patch('bokeh.document.document.check_integrity')
def test_modelindocument_validates_document_by_default(check_integrity):
    p = figure()
    with embed._ModelInDocument([p]):
        pass
    assert check_integrity.called

@mock.patch('bokeh.document.document.check_integrity')
def test_modelindocument_doesnt_validate_doc_due_to_env_var(check_integrity, monkeypatch):
    monkeypatch.setenv("BOKEH_VALIDATE_DOC", "false")
    p = figure()
    with embed._ModelInDocument([p]):
        pass
    assert not check_integrity.called

@mock.patch('bokeh.document.document.check_integrity')
def test_modelinemptydocument_validates_document_by_default(check_integrity):
    p = figure()
    with embed._ModelInEmptyDocument(p):
        pass
    assert check_integrity.called

@mock.patch('bokeh.document.document.check_integrity')
def test_modelinemptydocument_doesnt_validate_document_due_to_env_var(check_integrity, monkeypatch):
    monkeypatch.setenv("BOKEH_VALIDATE_DOC", "false")
    p = figure()
    with embed._ModelInEmptyDocument(p):
        pass
    assert not check_integrity.called
