#-----------------------------------------------------------------------------
# Copyright (c) 2012 - 2017, Anaconda, Inc. All rights reserved.
#
# Powered by the Bokeh Development Team.
#
# The full license is in the file LICENSE.txt, distributed with this software.
#-----------------------------------------------------------------------------

#-----------------------------------------------------------------------------
# Boilerplate
#-----------------------------------------------------------------------------
from __future__ import absolute_import, division, print_function, unicode_literals

import pytest ; pytest

#-----------------------------------------------------------------------------
# Imports
#-----------------------------------------------------------------------------

# Standard library imports
from mock import patch

# External imports
import bs4
from jinja2 import Template
from six import string_types

# Bokeh imports
from bokeh.core.properties import Instance
from bokeh.document import Document
from bokeh.io import curdoc
from bokeh.model import Model
from bokeh.plotting import figure
from bokeh.resources import CDN, JSResources, CSSResources
from bokeh.util.string import encode_utf8

# Module under test
import bokeh.embed.standalone as bes
from bokeh.embed.util import RenderRoot

#-----------------------------------------------------------------------------
# Setup
#-----------------------------------------------------------------------------

class SomeModelInTestObjects(Model):
    child = Instance(Model)

def stable_id():
    return 'ID'

@pytest.fixture
def test_plot():
    from bokeh.plotting import figure
    test_plot = figure()
    test_plot.circle([1, 2], [2, 3])
    return test_plot

#-----------------------------------------------------------------------------
# General API
#-----------------------------------------------------------------------------

class Test_autoload_static(object):

    def test_return_type(self, test_plot):
        r = bes.autoload_static(test_plot, CDN, "some/path")
        assert len(r) == 2

    @patch('bokeh.embed.util.make_id', new_callable=lambda: stable_id)
    def test_script_attrs(self, mock_make_id, test_plot):
        js, tag = bes.autoload_static(test_plot, CDN, "some/path")
        html = bs4.BeautifulSoup(tag, "lxml")
        scripts = html.findAll(name='script')
        assert len(scripts) == 1
        attrs = scripts[0].attrs
        assert set(attrs) == set(['src', 'id'])
        assert attrs['src'] == 'some/path'


class Test_components(object):

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

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

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

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

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

    @patch('bokeh.embed.util.make_id', new_callable=lambda: stable_id)
    def test_plot_dict_returned_when_wrap_plot_info_is_false(self, mock_make_id):
        doc = Document()
        plot1 = figure()
        plot1.circle([], [])
        doc.add_root(plot1)

        plot2 = figure()
        plot2.circle([], [])
        doc.add_root(plot2)

        expected_plotdict_1 = RenderRoot(elementid="ID", id="ID")
        expected_plotdict_2 = RenderRoot(elementid="ID", id="ID")

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

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

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

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

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

        divs = html.findAll(name='div')
        assert len(divs) == 1

        div = divs[0]
        assert set(div.attrs) == set(['class', 'id'])
        assert div.attrs['class'] == ['bk-root']
        assert div.text == ''

    def test_script_is_utf8_encoded(self, test_plot):
        script, div = bes.components(test_plot)
        assert isinstance(script, str)

    @patch('bokeh.embed.util.make_id', new_callable=lambda: stable_id)
    def test_output_is_without_script_tag_when_wrap_script_is_false(self, mock_make_id, test_plot):
        script, div = bes.components(test_plot)
        html = bs4.BeautifulSoup(script, "lxml")
        scripts = html.findAll(name='script')
        assert len(scripts) == 1

        # XXX: this needs to account for indentation
        #script_content = scripts[0].getText()

        #rawscript, div = bes.components(test_plot, wrap_script=False)
        #self.maxDiff = None
        #assert rawscript.strip() == script_content.strip()

class Test_file_html(object):

    def test_return_type(self, test_plot):

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

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

        r = bes.file_html(test_plot, CDN, "title")
        assert isinstance(r, str)

        r = bes.file_html(test_plot, CDN, "title", fake_template(self))
        assert isinstance(r, str)

        r = bes.file_html(test_plot, CDN, "title",
                            fake_template(self, {"test_var"}),
                            {"test_var": "test"})
        assert isinstance(r, str)

    @patch('bokeh.embed.bundle.warn')
    def test_file_html_handles_js_only_resources(self, mock_warn, test_plot):
        js_resources = JSResources(mode="relative", components=["bokeh"])
        template = Template("<head>{{ bokeh_js }}</head><body></body>")
        output = bes.file_html(test_plot, (js_resources, None), "title", template=template)
        html = encode_utf8("<head>%s</head><body></body>" % js_resources.render_js())
        assert output == html

    @patch('bokeh.embed.bundle.warn')
    def test_file_html_provides_warning_if_no_css(self, mock_warn, test_plot):
        js_resources = JSResources()
        bes.file_html(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.'
        )

    @patch('bokeh.embed.bundle.warn')
    def test_file_html_handles_css_only_resources(self, mock_warn, test_plot):
        css_resources = CSSResources(mode="relative", components=["bokeh"])
        template = Template("<head>{{ bokeh_css }}</head><body></body>")
        output = bes.file_html(test_plot, (None, css_resources), "title", template=template)
        html = encode_utf8("<head>%s</head><body></body>" % css_resources.render_css())
        assert output == html

    @patch('bokeh.embed.bundle.warn')
    def test_file_html_provides_warning_if_no_js(self, mock_warn, test_plot):
        css_resources = CSSResources()
        bes.file_html(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(self, test_plot):
        r = bes.file_html(test_plot, CDN, "&<")
        assert "<title>&amp;&lt;</title>" in r

    def test_entire_doc_is_used(self):
        from bokeh.document import Document
        from bokeh.models import Button

        fig = figure()
        fig.x([0], [0])

        button = Button(label="Button")

        d = Document()
        d.add_root(fig)
        d.add_root(button)
        out = bes.file_html([fig], CDN)

        # this is a very coarse test but it will do
        assert "bokeh-widgets" in out

#-----------------------------------------------------------------------------
# Dev API
#-----------------------------------------------------------------------------

#-----------------------------------------------------------------------------
# Private API
#-----------------------------------------------------------------------------

class Test__ModelInDocument(object):

    def test_single_model(self):
        p = Model()
        assert p.document is None
        with bes._ModelInDocument([p]):
            assert p.document is not None
        assert p.document is None

    def test_list_of_model(self):
        p1 = Model()
        p2 = Model()
        assert p1.document is None
        assert p2.document is None
        with bes._ModelInDocument([p1, p2]):
            assert p1.document is not None
            assert p2.document is not None
        assert p1.document is None
        assert p2.document is None

    def test_uses_precedent(self):
        # it's deliberate that the doc is on p2, so _ModelInDocument
        # has to be smart about looking for a doc anywhere in the list
        # before it starts inventing new documents
        doc = Document()
        p1 = Model()
        p2 = Model()
        doc.add_root(p2)
        assert p1.document is None
        assert p2.document is not None
        with bes._ModelInDocument([p1, p2]):
            assert p1.document is not None
            assert p2.document is not None
            assert p1.document is doc
            assert p2.document is doc
        assert p1.document is None
        assert p2.document is not None

    def test_uses_doc_precedent(self):
        doc = Document()
        p1 = Model()
        p2 = Model()
        assert p1.document is None
        assert p2.document is None
        with bes._ModelInDocument([p1, p2, doc]):
            assert p1.document is not None
            assert p2.document is not None
            assert p1.document is doc
            assert p2.document is doc
        assert p1.document is None
        assert p2.document is None

    def test_with_doc_in_child_raises_error(self):
        doc = Document()
        p1 = Model()
        p2 = SomeModelInTestObjects(child=Model())
        doc.add_root(p2.child)
        assert p1.document is None
        assert p2.document is None
        assert p2.child.document is doc
        with pytest.raises(RuntimeError):
            with bes._ModelInDocument([p1, p2]):
                assert p1.document is not None
                assert p2.document is not None
                assert p1.document is doc
                assert p2.document is doc

    @patch('bokeh.document.document.check_integrity')
    def test_validates_document_by_default(self, check_integrity, test_plot):
        with bes._ModelInDocument([test_plot]):
            pass
        assert check_integrity.called

    @patch('bokeh.document.document.check_integrity')
    def test_doesnt_validate_doc_due_to_env_var(self, check_integrity, monkeypatch, test_plot):
        monkeypatch.setenv("BOKEH_VALIDATE_DOC", "false")
        with bes._ModelInDocument([test_plot]):
            pass
        assert not check_integrity.called

class Test__add_doc_to_models(object):
    pass

class Test__title_from_models(object):
    pass