# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.

import json
import os
import sys
import warnings

from unittest import mock

import pytest

from IPython import display
from IPython.core.display import ImageFormat
from IPython.core.getipython import get_ipython
from IPython.utils.io import capture_output
from IPython.utils.tempdir import NamedFileInTemporaryDirectory
from IPython import paths as ipath
from IPython.testing.tools import AssertNotPrints

import IPython.testing.decorators as dec


def test_image_size():
    """Simple test for display.Image(args, width=x,height=y)"""
    thisurl = "http://www.google.fr/images/srpr/logo3w.png"
    img = display.Image(url=thisurl, width=200, height=200)
    assert '<img src="%s" width="200" height="200"/>' % (thisurl) == img._repr_html_()
    img = display.Image(url=thisurl, metadata={"width": 200, "height": 200})
    assert '<img src="%s" width="200" height="200"/>' % (thisurl) == img._repr_html_()
    img = display.Image(url=thisurl, width=200)
    assert '<img src="%s" width="200"/>' % (thisurl) == img._repr_html_()
    img = display.Image(url=thisurl)
    assert '<img src="%s"/>' % (thisurl) == img._repr_html_()
    img = display.Image(url=thisurl, unconfined=True)
    assert '<img src="%s" class="unconfined"/>' % (thisurl) == img._repr_html_()


def test_image_mimes():
    fmt = get_ipython().display_formatter.format
    for name, format in ImageFormat.__members__.items():
        img = display.Image(b"garbage", format=name)
        data, metadata = fmt(img)
        assert sorted(data) == sorted([format.mime_type, "text/plain"])


def test_geojson():
    gj = display.GeoJSON(
        data={
            "type": "Feature",
            "geometry": {"type": "Point", "coordinates": [-81.327, 296.038]},
            "properties": {"name": "Inca City"},
        },
        url_template="http://s3-eu-west-1.amazonaws.com/whereonmars.cartodb.net/{basemap_id}/{z}/{x}/{y}.png",
        layer_options={
            "basemap_id": "celestia_mars-shaded-16k_global",
            "attribution": "Celestia/praesepe",
            "minZoom": 0,
            "maxZoom": 18,
        },
    )
    assert "<IPython.core.display.GeoJSON object>" == str(gj)


def test_retina_png():
    here = os.path.dirname(__file__)
    img = display.Image(os.path.join(here, "2x2.png"), retina=True)
    assert img.height == 1
    assert img.width == 1
    data, md = img._repr_png_()
    assert md["width"] == 1
    assert md["height"] == 1


def test_embed_svg_url():
    import gzip
    from io import BytesIO

    svg_data = b'<svg><circle x="0" y="0" r="1"/></svg>'
    url = "http://test.com/circle.svg"

    gzip_svg = BytesIO()
    with gzip.open(gzip_svg, "wb") as fp:
        fp.write(svg_data)
    gzip_svg = gzip_svg.getvalue()

    def mocked_urlopen(*args, **kwargs):
        class MockResponse:
            def __init__(self, svg):
                self._svg_data = svg
                self.headers = {"content-type": "image/svg+xml"}

            def read(self):
                return self._svg_data

        if args[0] == url:
            return MockResponse(svg_data)
        elif args[0] == url + "z":
            ret = MockResponse(gzip_svg)
            ret.headers["content-encoding"] = "gzip"
            return ret
        return MockResponse(None)

    with mock.patch("urllib.request.urlopen", side_effect=mocked_urlopen):
        svg = display.SVG(url=url)
        assert svg._repr_svg_().startswith("<svg") is True
        svg = display.SVG(url=url + "z")
        assert svg._repr_svg_().startswith("<svg") is True


def test_retina_jpeg():
    here = os.path.dirname(__file__)
    img = display.Image(os.path.join(here, "2x2.jpg"), retina=True)
    assert img.height == 1
    assert img.width == 1
    data, md = img._repr_jpeg_()
    assert md["width"] == 1
    assert md["height"] == 1


def test_base64image():
    display.Image(
        "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB94BCRQnOqNu0b4AAAAKSURBVAjXY2AAAAACAAHiIbwzAAAAAElFTkSuQmCC"
    )


def test_image_filename_defaults():
    """test format constraint, and validity of jpeg and png"""
    tpath = ipath.get_ipython_package_dir()
    pytest.raises(
        ValueError,
        display.Image,
        filename=os.path.join(tpath, "testing/tests/badformat.zip"),
        embed=True,
    )
    pytest.raises(ValueError, display.Image)
    pytest.raises(
        ValueError,
        display.Image,
        data="this is not an image",
        format="badformat",
        embed=True,
    )


def _get_inline_config():
    from matplotlib_inline.config import InlineBackend

    return InlineBackend.instance()


@dec.skip_without("matplotlib")
def test_set_matplotlib_close():
    from matplotlib_inline.backend_inline import set_matplotlib_close

    cfg = _get_inline_config()
    cfg.close_figures = False

    set_matplotlib_close()
    assert cfg.close_figures
    set_matplotlib_close(False)
    assert not cfg.close_figures


_fmt_mime_map = {
    "png": "image/png",
    "jpeg": "image/jpeg",
    "pdf": "application/pdf",
    "retina": "image/png",
    "svg": "image/svg+xml",
}


@dec.skip_without("matplotlib")
def test_set_matplotlib_formats():
    from matplotlib.figure import Figure
    from matplotlib_inline.backend_inline import set_matplotlib_formats

    formatters = get_ipython().display_formatter.formatters
    for formats in [
        ("png",),
        ("pdf", "svg"),
        ("jpeg", "retina", "png"),
        (),
    ]:
        active_mimes = {_fmt_mime_map[fmt] for fmt in formats}
        set_matplotlib_formats(*formats)
        for mime, f in formatters.items():
            if mime in active_mimes:
                assert Figure in f
            else:
                assert Figure not in f


@dec.skip_without("matplotlib")
def test_set_matplotlib_formats_kwargs():
    from matplotlib.figure import Figure
    from matplotlib_inline.backend_inline import set_matplotlib_formats

    ip = get_ipython()
    cfg = _get_inline_config()
    cfg.print_figure_kwargs.update(dict(foo="bar"))
    kwargs = dict(dpi=150)
    try:
        set_matplotlib_formats("png", **kwargs)
        formatter = ip.display_formatter.formatters["image/png"]
        f = formatter.lookup_by_type(Figure)
        formatter_kwargs = f.keywords
        expected = kwargs
        expected["base64"] = True
        expected["fmt"] = "png"
        expected.update(cfg.print_figure_kwargs)
        assert formatter_kwargs == expected
    finally:
        cfg.print_figure_kwargs.clear()


@dec.skip_without("matplotlib")
@pytest.mark.skipif(
    sys.platform == "linux" and sys.version_info[:2] == (3, 11),
    reason="matplotlib marker style regression on Python 3.11 CI causes plots to silently fail to render",
)
def test_matplotlib_positioning():
    _ip = get_ipython()

    prev_active_types = _ip.display_formatter.active_types.copy()
    prev_execution_count = _ip.execution_count
    prev_user_ns_underscore = _ip.user_ns.get("_", None)

    _ip.history_manager.reset()
    _ip.display_formatter.active_types = ["text/plain", "image/png"]

    _ip.run_cell("import matplotlib")
    prev_mpl_backend = _ip.run_cell("matplotlib.get_backend()").result

    try:
        _ip.run_line_magic("matplotlib", "inline")
        _ip.execution_count = 1
        _ip.run_cell("'no plot here'", store_history=True)

        # Cell 2: No manual flush
        _ip.run_cell(
            "import matplotlib.pyplot as plt;plt.plot([0, 1])", store_history=True
        )

        _ip.run_cell("'no plot here'", store_history=True)

        # Cell 4: Manual flush
        _ip.run_cell("plt.plot([1, 0])\nplt.show()", store_history=True)

        _ip.run_cell("'no plot here'", store_history=True)

        outputs = _ip.history_manager.outputs

        # Only cells 2 and 4 should have plots
        for cell_num in [1, 3, 5]:
            assert not any(
                "image/png" in out.bundle for out in outputs.get(cell_num, [])
            ), f"Cell {cell_num} should not have plot"

        cell_2_has_plot = any("image/png" in out.bundle for out in outputs.get(2, []))
        cell_4_has_plot = any("image/png" in out.bundle for out in outputs.get(4, []))

        assert cell_2_has_plot, "Cell 2 should have plot (auto-flush)"
        assert cell_4_has_plot, "Cell 4 should have plot (manual flush)"

    finally:
        _ip.run_cell("plt.close('all')")
        _ip.run_line_magic("matplotlib", prev_mpl_backend)
        _ip.history_manager.reset()
        _ip.display_formatter.active_types = prev_active_types
        _ip.displayhook.flush()

        _ip.execution_count = prev_execution_count
        if prev_user_ns_underscore is not None:
            _ip.user_ns["_"] = prev_user_ns_underscore
        elif "_" in _ip.user_ns:
            del _ip.user_ns["_"]


def test_display_available():
    """
    Test that display is available without import

    We don't really care if it's in builtin or anything else, but it should
    always be available.
    """
    ip = get_ipython()
    with AssertNotPrints("NameError"):
        ip.run_cell("display")
    try:
        ip.run_cell("del display")
    except NameError:
        pass  # it's ok, it might be in builtins
    # even if deleted it should be back
    with AssertNotPrints("NameError"):
        ip.run_cell("display")


def test_textdisplayobj_pretty_repr():
    p = display.Pretty("This is a simple test")
    assert repr(p) == "<IPython.core.display.Pretty object>"
    assert p.data == "This is a simple test"

    p._show_mem_addr = True
    assert repr(p) == object.__repr__(p)


def test_displayobject_repr():
    h = display.HTML("<br />")
    assert repr(h) == "<IPython.core.display.HTML object>"
    h._show_mem_addr = True
    assert repr(h) == object.__repr__(h)
    h._show_mem_addr = False
    assert repr(h) == "<IPython.core.display.HTML object>"

    j = display.Javascript("")
    assert repr(j) == "<IPython.core.display.Javascript object>"
    j._show_mem_addr = True
    assert repr(j) == object.__repr__(j)
    j._show_mem_addr = False
    assert repr(j) == "<IPython.core.display.Javascript object>"


@mock.patch("warnings.warn")
def test_encourage_iframe_over_html(m_warn):
    display.HTML()
    m_warn.assert_not_called()

    display.HTML("<br />")
    m_warn.assert_not_called()

    display.HTML(
        '<html><p>Lots of content here</p><iframe src="http://a.com"></iframe>'
    )
    m_warn.assert_not_called()

    display.HTML('<iframe src="http://a.com"></iframe>')
    m_warn.assert_called_with("Consider using IPython.display.IFrame instead")

    m_warn.reset_mock()
    display.HTML('<IFRAME SRC="http://a.com"></IFRAME>')
    m_warn.assert_called_with("Consider using IPython.display.IFrame instead")


def test_progress():
    p = display.ProgressBar(10)
    assert "0/10" in repr(p)
    p.html_width = "100%"
    p.progress = 5
    assert (
        p._repr_html_() == "<progress style='width:100%' max='10' value='5'></progress>"
    )


def test_progress_iter():
    with capture_output(display=False) as captured:
        for i in display.ProgressBar(5):
            out = captured.stdout
            assert "{0}/5".format(i) in out
    out = captured.stdout
    assert "5/5" in out


def test_json():
    d = {"a": 5}
    lis = [d]
    metadata = [
        {"expanded": False, "root": "root"},
        {"expanded": True, "root": "root"},
        {"expanded": False, "root": "custom"},
        {"expanded": True, "root": "custom"},
    ]
    json_objs = [
        display.JSON(d),
        display.JSON(d, expanded=True),
        display.JSON(d, root="custom"),
        display.JSON(d, expanded=True, root="custom"),
    ]
    for j, md in zip(json_objs, metadata):
        assert j._repr_json_() == (d, md)

    with warnings.catch_warnings(record=True) as w:
        warnings.simplefilter("always")
        j = display.JSON(json.dumps(d))
        assert len(w) == 1
        assert j._repr_json_() == (d, metadata[0])

    json_objs = [
        display.JSON(lis),
        display.JSON(lis, expanded=True),
        display.JSON(lis, root="custom"),
        display.JSON(lis, expanded=True, root="custom"),
    ]
    for j, md in zip(json_objs, metadata):
        assert j._repr_json_() == (lis, md)

    with warnings.catch_warnings(record=True) as w:
        warnings.simplefilter("always")
        j = display.JSON(json.dumps(lis))
        assert len(w) == 1
        assert j._repr_json_() == (lis, metadata[0])


def test_video_embedding():
    """use a tempfile, with dummy-data, to ensure that video embedding doesn't crash"""
    v = display.Video("http://ignored")
    assert not v.embed
    html = v._repr_html_()
    assert 'src="data:' not in html
    assert 'src="http://ignored"' in html

    with pytest.raises(ValueError):
        v = display.Video(b"abc")

    with NamedFileInTemporaryDirectory("test.mp4", "wb") as f:
        f.write(b"abc")
        f.close()

        v = display.Video(f.name)
        assert not v.embed
        html = v._repr_html_()
        assert 'src="data:' not in html

        v = display.Video(f.name, embed=True)
        html = v._repr_html_()
        assert 'src="data:video/mp4;base64,YWJj"' in html

        v = display.Video(f.name, embed=True, mimetype="video/other")
        html = v._repr_html_()
        assert 'src="data:video/other;base64,YWJj"' in html

        v = display.Video(b"abc", embed=True, mimetype="video/mp4")
        html = v._repr_html_()
        assert 'src="data:video/mp4;base64,YWJj"' in html

        v = display.Video("YWJj", embed=True, mimetype="video/xyz")
        html = v._repr_html_()
        assert 'src="data:video/xyz;base64,YWJj"' in html


def test_html_metadata():
    s = "<h1>Test</h1>"
    h = display.HTML(s, metadata={"isolated": True})
    assert h._repr_html_() == (s, {"isolated": True})


def test_display_id():
    ip = get_ipython()
    with mock.patch.object(ip.display_pub, "publish") as pub:
        handle = display.display("x")
        assert handle is None
        handle = display.display("y", display_id="secret")
        assert isinstance(handle, display.DisplayHandle)
        handle2 = display.display("z", display_id=True)
        assert isinstance(handle2, display.DisplayHandle)
    assert handle.display_id != handle2.display_id

    assert pub.call_count == 3
    args, kwargs = pub.call_args_list[0]
    assert args == ()
    assert kwargs == {
        "data": {"text/plain": repr("x")},
        "metadata": {},
    }
    args, kwargs = pub.call_args_list[1]
    assert args == ()
    assert kwargs == {
        "data": {"text/plain": repr("y")},
        "metadata": {},
        "transient": {
            "display_id": handle.display_id,
        },
    }
    args, kwargs = pub.call_args_list[2]
    assert args == ()
    assert kwargs == {
        "data": {"text/plain": repr("z")},
        "metadata": {},
        "transient": {
            "display_id": handle2.display_id,
        },
    }


def test_update_display():
    ip = get_ipython()
    with mock.patch.object(ip.display_pub, "publish") as pub:
        with pytest.raises(TypeError):
            display.update_display("x")
        display.update_display("x", display_id="1")
        display.update_display("y", display_id="2")
    args, kwargs = pub.call_args_list[0]
    assert args == ()
    assert kwargs == {
        "data": {"text/plain": repr("x")},
        "metadata": {},
        "transient": {
            "display_id": "1",
        },
        "update": True,
    }
    args, kwargs = pub.call_args_list[1]
    assert args == ()
    assert kwargs == {
        "data": {"text/plain": repr("y")},
        "metadata": {},
        "transient": {
            "display_id": "2",
        },
        "update": True,
    }


def test_display_handle():
    ip = get_ipython()
    handle = display.DisplayHandle()
    assert isinstance(handle.display_id, str)
    handle = display.DisplayHandle("my-id")
    assert handle.display_id == "my-id"
    with mock.patch.object(ip.display_pub, "publish") as pub:
        handle.display("x")
        handle.update("y")

    args, kwargs = pub.call_args_list[0]
    assert args == ()
    assert kwargs == {
        "data": {"text/plain": repr("x")},
        "metadata": {},
        "transient": {
            "display_id": handle.display_id,
        },
    }
    args, kwargs = pub.call_args_list[1]
    assert args == ()
    assert kwargs == {
        "data": {"text/plain": repr("y")},
        "metadata": {},
        "transient": {
            "display_id": handle.display_id,
        },
        "update": True,
    }


def test_image_alt_tag():
    """Simple test for display.Image(args, alt=x,)"""
    thisurl = "http://example.com/image.png"
    img = display.Image(url=thisurl, alt="an image")
    assert '<img src="%s" alt="an image"/>' % (thisurl) == img._repr_html_()
    img = display.Image(url=thisurl, unconfined=True, alt="an image")
    assert (
        '<img src="%s" class="unconfined" alt="an image"/>' % (thisurl)
        == img._repr_html_()
    )
    img = display.Image(url=thisurl, alt='>"& <')
    assert '<img src="%s" alt="&gt;&quot;&amp; &lt;"/>' % (thisurl) == img._repr_html_()

    img = display.Image(url=thisurl, metadata={"alt": "an image"})
    assert img.alt == "an image"
    here = os.path.dirname(__file__)
    img = display.Image(os.path.join(here, "2x2.png"), alt="an image")
    assert img.alt == "an image"
    _, md = img._repr_png_()
    assert md["alt"] == "an image"


def test_image_url_escaping():
    """Image: a quote in the url does not break out of the src attribute"""
    img = display.Image(url='http://example.com/i.png" onerror="alert(1)')
    assert (
        '<img src="http://example.com/i.png&quot; onerror=&quot;alert(1)"/>'
        == img._repr_html_()
    )


def test_video_url_escaping():
    """Video: a quote in the url does not break out of the src attribute"""
    v = display.Video('http://example.com/v.mp4" onerror="alert(1)')
    assert (
        'src="http://example.com/v.mp4&quot; onerror=&quot;alert(1)"'
        in v._repr_html_()
    )


def test_iframe_escaping():
    """IFrame: quotes in src, width and height stay inside their attributes"""
    html = display.IFrame('http://example.com/?a=1"><script>', 400, 300)._repr_html_()
    assert 'src="http://example.com/?a=1&quot;&gt;&lt;script&gt;"' in html

    html = display.YouTubeVideo('abc"><script>')._repr_html_()
    assert '"><script>' not in html

    html = display.IFrame("http://example.com", '400" onload="alert(1)', 300)
    assert 'width="400&quot; onload=&quot;alert(1)"' in html._repr_html_()


def test_image_bad_filename_raises_proper_exception():
    with pytest.raises(FileNotFoundError):
        display.Image("/this/file/does/not/exist/")._repr_png_()
