from unittest import TestCase
from unittest.mock import call, patch, sentinel, mock_open, Mock
from mistletoe import cli


class TestCLI(TestCase):
    @patch('mistletoe.cli.parse', return_value=Mock(filenames=[], renderer=sentinel.Renderer))
    @patch('mistletoe.cli.interactive')
    def test_main_to_interactive(self, mock_interactive, mock_parse):
        cli.main(None)
        mock_interactive.assert_called_with(sentinel.Renderer)

    @patch('mistletoe.cli.parse', return_value=Mock(filenames=['foo.md'], renderer=sentinel.Renderer))
    @patch('mistletoe.cli.convert')
    def test_main_to_convert(self, mock_convert, mock_parse):
        cli.main(None)
        mock_convert.assert_called_with(['foo.md'], sentinel.Renderer)


    @patch('importlib.import_module', return_value=Mock(Renderer=sentinel.RendererCls))
    def test_parse_renderer(self, mock_import_module):
        namespace = cli.parse(['-r', 'foo.Renderer'])
        mock_import_module.assert_called_with('foo')
        self.assertEqual(namespace.renderer, sentinel.RendererCls)

    def test_parse_filenames(self):
        filenames = ['foo.md', 'bar.md']
        namespace = cli.parse(filenames)
        self.assertEqual(namespace.filenames, filenames)

    @patch('mistletoe.cli.convert_file')
    def test_convert(self, mock_convert_file):
        filenames = ['foo', 'bar']
        cli.convert(filenames, sentinel.RendererCls)
        calls = [call(filename, sentinel.RendererCls) for filename in filenames]
        mock_convert_file.assert_has_calls(calls)

    @patch('mistletoe.markdown', return_value='rendered text')
    @patch('builtins.print')
    @patch('builtins.open', new_callable=mock_open)
    def test_convert_file_success(self, mock_open_, mock_print, mock_markdown):
        filename = 'foo'
        cli.convert_file(filename, sentinel.RendererCls)
        mock_open_.assert_called_with(filename, 'r')
        mock_print.assert_called_with('rendered text', end='')

    @patch('builtins.open', side_effect=OSError)
    @patch('sys.exit')
    def test_convert_file_fail(self, mock_exit, mock_open_):
        filename = 'foo'
        cli.convert_file(filename, sentinel.RendererCls)
        mock_open_.assert_called_with(filename, 'r')
        mock_exit.assert_called_with('Cannot open file "foo".')

    @patch('mistletoe.cli._import_readline')
    @patch('mistletoe.cli._print_heading')
    @patch('mistletoe.markdown', return_value='rendered text')
    @patch('builtins.print')
    def test_interactive(self, mock_print, mock_markdown,
                         mock_print_heading, mock_import_readline):
        def MockInputFactory(return_values):
            _counter = -1
            def mock_input(prompt=''):
                nonlocal _counter
                _counter += 1
                if _counter < len(return_values):
                    return return_values[_counter]
                elif _counter == len(return_values):
                    raise EOFError
                else:
                    raise KeyboardInterrupt
            return mock_input

        return_values = ['foo', 'bar', 'baz']
        with patch('builtins.input', MockInputFactory(return_values)):
            cli.interactive(sentinel.RendererCls)

        mock_import_readline.assert_called_with()
        mock_print_heading.assert_called_with(sentinel.RendererCls)
        mock_markdown.assert_called_with(['foo\n', 'bar\n', 'baz\n'],
                sentinel.RendererCls)
        calls = [call('\nrendered text', end=''), call('\nExiting.')]
        mock_print.assert_has_calls(calls)

    @patch('importlib.import_module', return_value=Mock(Renderer=sentinel.RendererCls))
    def test_import_success(self, mock_import_module):
        self.assertEqual(sentinel.RendererCls, cli._import('foo.Renderer'))

    @patch('sys.exit')
    def test_import_incomplete_path(self, mock_exit):
        cli._import('foo')
        error_msg = '[error] please supply full path to your custom renderer.'
        mock_exit.assert_called_with(error_msg)

    @patch('importlib.import_module', side_effect=ImportError)
    @patch('sys.exit')
    def test_import_module_error(self, mock_exit, mock_import_module):
        cli._import('foo.Renderer')
        mock_exit.assert_called_with('[error] cannot import module "foo".')

    @patch('importlib.import_module', return_value=Mock(spec=[]))
    @patch('sys.exit')
    def test_import_class_error(self, mock_exit, mock_import_module):
        cli._import('foo.Renderer')
        error_msg = '[error] cannot find renderer "Renderer" from module "foo".'
        mock_exit.assert_called_with(error_msg)

    @patch('builtins.__import__')
    @patch('builtins.print')
    def test_import_readline_success(self, mock_print, mock_import):
        cli._import_readline()
        mock_print.assert_not_called()

    @patch('builtins.__import__', side_effect=ImportError)
    @patch('builtins.print')
    def test_import_readline_fail(self, mock_print, mock_import):
        cli._import_readline()
        mock_print.assert_called_with('[warning] readline library not available.')

    @patch('builtins.print')
    def test_print_heading(self, mock_print):
        cli._print_heading(Mock(__name__='Renderer'))
        version = cli.mistletoe.__version__
        msgs = ['mistletoe [version {}] (interactive)'.format(version),
                'Type Ctrl-D to complete input, or Ctrl-C to exit.', 
                'Using renderer: Renderer']
        calls = [call(msg) for msg in msgs]
        mock_print.assert_has_calls(calls)

