import sys
from unittest import TestCase
from contextlib import contextmanager

from IPython.display import Markdown, Image
from ipywidgets import widget_output


class TestOutputWidget(TestCase):

    @contextmanager
    def _mocked_ipython(self, get_ipython, clear_output):
        """ Context manager that monkeypatches get_ipython and clear_output """
        original_clear_output = widget_output.clear_output
        original_get_ipython = widget_output.get_ipython
        widget_output.get_ipython = get_ipython
        widget_output.clear_output = clear_output
        try:
            yield
        finally:
            widget_output.clear_output = original_clear_output
            widget_output.get_ipython = original_get_ipython

    def _mock_get_ipython(self, msg_id):
        """ Returns a mock IPython application with a mocked kernel """
        kernel = type(
            'mock_kernel',
            (object, ),
            {'_parent_header': {'header': {'msg_id': msg_id}}}
        )

        # Specifically override this so the traceback
        # is still printed to screen
        def showtraceback(self_, exc_tuple, *args, **kwargs):
            etype, evalue, tb = exc_tuple
            raise etype(evalue)

        ipython = type(
            'mock_ipython',
            (object, ),
            {'kernel': kernel, 'showtraceback': showtraceback}
        )
        return ipython

    def _mock_clear_output(self):
        """ Mock function that records calls to it """
        calls = []

        def clear_output(*args, **kwargs):
            calls.append((args, kwargs))
        clear_output.calls = calls

        return clear_output

    def test_set_msg_id_when_capturing(self):
        msg_id = 'msg-id'
        get_ipython = self._mock_get_ipython(msg_id)
        clear_output = self._mock_clear_output()

        with self._mocked_ipython(get_ipython, clear_output):
            widget = widget_output.Output()
            assert widget.msg_id == ''
            with widget:
                assert widget.msg_id == msg_id
            assert widget.msg_id == ''

    def test_clear_output(self):
        msg_id = 'msg-id'
        get_ipython = self._mock_get_ipython(msg_id)
        clear_output = self._mock_clear_output()

        with self._mocked_ipython(get_ipython, clear_output):
            widget = widget_output.Output()
            widget.clear_output(wait=True)

        assert len(clear_output.calls) == 1
        assert clear_output.calls[0] == ((), {'wait': True})

    def test_capture_decorator(self):
        msg_id = 'msg-id'
        get_ipython = self._mock_get_ipython(msg_id)
        clear_output = self._mock_clear_output()
        expected_argument = 'arg'
        expected_keyword_argument = True
        captee_calls = []

        with self._mocked_ipython(get_ipython, clear_output):
            widget = widget_output.Output()
            assert widget.msg_id == ''

            @widget.capture()
            def captee(*args, **kwargs):
                # Check that we are capturing output
                assert widget.msg_id == msg_id

                # Check that arguments are passed correctly
                captee_calls.append((args, kwargs))

            captee(
                expected_argument, keyword_argument=expected_keyword_argument)
            assert widget.msg_id == ''
            captee()

        assert len(captee_calls) == 2
        assert captee_calls[0] == (
            (expected_argument, ),
            {'keyword_argument': expected_keyword_argument}
        )
        assert captee_calls[1] == ((), {})

    def test_capture_decorator_clear_output(self):
        msg_id = 'msg-id'
        get_ipython = self._mock_get_ipython(msg_id)
        clear_output = self._mock_clear_output()

        with self._mocked_ipython(get_ipython, clear_output):
            widget = widget_output.Output()

            @widget.capture(clear_output=True, wait=True)
            def captee(*args, **kwargs):
                # Check that we are capturing output
                assert widget.msg_id == msg_id

            captee()
            captee()

        assert len(clear_output.calls) == 2
        assert clear_output.calls[0] == clear_output.calls[1] == \
            ((), {'wait': True})

    def test_capture_decorator_no_clear_output(self):
        msg_id = 'msg-id'
        get_ipython = self._mock_get_ipython(msg_id)
        clear_output = self._mock_clear_output()

        with self._mocked_ipython(get_ipython, clear_output):
            widget = widget_output.Output()

            @widget.capture(clear_output=False)
            def captee(*args, **kwargs):
                # Check that we are capturing output
                assert widget.msg_id == msg_id

            captee()
            captee()

        assert len(clear_output.calls) == 0


def _make_stream_output(text, name):
    return {
        'output_type': 'stream',
        'name': name,
        'text': text
    }


def test_append_stdout():
    widget = widget_output.Output()

    # Try appending a message to stdout.
    widget.append_stdout("snakes!")
    expected = (_make_stream_output("snakes!", "stdout"),)
    assert widget.outputs == expected, repr(widget.outputs)

    # Try appending a second message.
    widget.append_stdout("more snakes!")
    expected += (_make_stream_output("more snakes!", "stdout"),)
    assert widget.outputs == expected, repr(widget.outputs)


def test_append_stderr():
    widget = widget_output.Output()

    # Try appending a message to stderr.
    widget.append_stderr("snakes!")
    expected = (_make_stream_output("snakes!", "stderr"),)
    assert widget.outputs == expected, repr(widget.outputs)

    # Try appending a second message.
    widget.append_stderr("more snakes!")
    expected += (_make_stream_output("more snakes!", "stderr"),)
    assert widget.outputs == expected, repr(widget.outputs)


def test_append_display_data():
    widget = widget_output.Output()

    # Try appending a Markdown object.
    widget.append_display_data(Markdown("# snakes!"))
    expected = (
        {
            'output_type': 'display_data',
            'data': {
                'text/plain': '<IPython.core.display.Markdown object>',
                'text/markdown': '# snakes!'
            },
            'metadata': {}
        },
    )
    assert widget.outputs == expected, repr(widget.outputs)

    # Now try appending an Image.
    image_data = b"foobar"
    image_data_b64 = image_data if sys.version_info[0] < 3 else 'Zm9vYmFy\n'

    widget.append_display_data(Image(image_data, width=123, height=456))
    expected += (
        {
            'output_type': 'display_data',
            'data': {
                'image/png': image_data_b64,
                'text/plain': '<IPython.core.display.Image object>'
            },
            'metadata': {
                'image/png': {
                    'width': 123,
                    'height': 456
                }
            }
        },
    )
    assert widget.outputs == expected, repr(widget.outputs)
