# coding=utf-8
# flake8: noqa E302
"""
Cmd2 unit/functional testing
"""

import builtins
import io
import os
import signal
import sys
import tempfile
from code import (
    InteractiveConsole,
)
from unittest import (
    mock,
)

import pytest

import cmd2
from cmd2 import (
    COMMAND_NAME,
    ansi,
    clipboard,
    constants,
    exceptions,
    plugin,
    utils,
)
from cmd2.rl_utils import (
    readline,  # This ensures gnureadline is used in macOS tests
)

from .conftest import (
    HELP_HISTORY,
    SET_TXT,
    SHORTCUTS_TXT,
    complete_tester,
    normalize,
    odd_file_names,
    run_cmd,
    verify_help_text,
)


def with_ansi_style(style):
    def arg_decorator(func):
        import functools

        @functools.wraps(func)
        def cmd_wrapper(*args, **kwargs):
            old = ansi.allow_style
            ansi.allow_style = style
            try:
                retval = func(*args, **kwargs)
            finally:
                ansi.allow_style = old
            return retval

        return cmd_wrapper

    return arg_decorator


def CreateOutsimApp():
    c = cmd2.Cmd()
    c.stdout = utils.StdSim(c.stdout)
    return c


@pytest.fixture
def outsim_app():
    return CreateOutsimApp()


def test_version(base_app):
    assert cmd2.__version__


def test_not_in_main_thread(base_app, capsys):
    import threading

    # Mock threading.main_thread() to return our fake thread
    saved_main_thread = threading.main_thread
    fake_main = threading.Thread()
    threading.main_thread = mock.MagicMock(name='main_thread', return_value=fake_main)

    with pytest.raises(RuntimeError) as excinfo:
        base_app.cmdloop()

    # Restore threading.main_thread()
    threading.main_thread = saved_main_thread
    assert "cmdloop must be run in the main thread" in str(excinfo.value)


def test_empty_statement(base_app):
    out, err = run_cmd(base_app, '')
    expected = normalize('')
    assert out == expected


def test_base_help(base_app):
    out, err = run_cmd(base_app, 'help')
    assert base_app.last_result is True
    verify_help_text(base_app, out)


def test_base_help_verbose(base_app):
    out, err = run_cmd(base_app, 'help -v')
    assert base_app.last_result is True
    verify_help_text(base_app, out)

    # Make sure :param type lines are filtered out of help summary
    help_doc = base_app.do_help.__func__.__doc__
    help_doc += "\n:param fake param"
    base_app.do_help.__func__.__doc__ = help_doc

    out, err = run_cmd(base_app, 'help --verbose')
    assert base_app.last_result is True
    verify_help_text(base_app, out)
    assert ':param' not in ''.join(out)


def test_base_argparse_help(base_app):
    # Verify that "set -h" gives the same output as "help set" and that it starts in a way that makes sense
    out1, err1 = run_cmd(base_app, 'set -h')
    out2, err2 = run_cmd(base_app, 'help set')

    assert out1 == out2
    assert out1[0].startswith('Usage: set')
    assert out1[1] == ''
    assert out1[2].startswith('Set a settable parameter')


def test_base_invalid_option(base_app):
    out, err = run_cmd(base_app, 'set -z')
    assert err[0] == 'Usage: set [-h] [param] [value]'
    assert 'Error: unrecognized arguments: -z' in err[1]


def test_base_shortcuts(base_app):
    out, err = run_cmd(base_app, 'shortcuts')
    expected = normalize(SHORTCUTS_TXT)
    assert out == expected
    assert base_app.last_result is True


def test_command_starts_with_shortcut():
    with pytest.raises(ValueError) as excinfo:
        cmd2.Cmd(shortcuts={'help': 'fake'})
    assert "Invalid command name 'help'" in str(excinfo.value)


def test_base_set(base_app):
    # force editor to be 'vim' so test is repeatable across platforms
    base_app.editor = 'vim'
    out, err = run_cmd(base_app, 'set')
    expected = normalize(SET_TXT)
    assert out == expected

    assert len(base_app.last_result) == len(base_app.settables)
    for param in base_app.last_result:
        assert base_app.last_result[param] == base_app.settables[param].get_value()


def test_set(base_app):
    out, err = run_cmd(base_app, 'set quiet True')
    expected = normalize(
        """
quiet - was: False
now: True
"""
    )
    assert out == expected
    assert base_app.last_result is True

    out, err = run_cmd(base_app, 'set quiet')
    expected = normalize(
        """
Name   Value                           Description
===================================================================================================
quiet  True                            Don't print nonessential feedback
"""
    )
    assert out == expected
    assert len(base_app.last_result) == 1
    assert base_app.last_result['quiet'] is True


def test_set_val_empty(base_app):
    base_app.editor = "fake"
    out, err = run_cmd(base_app, 'set editor ""')
    assert base_app.editor == ''
    assert base_app.last_result is True


def test_set_val_is_flag(base_app):
    base_app.editor = "fake"
    out, err = run_cmd(base_app, 'set editor "-h"')
    assert base_app.editor == '-h'
    assert base_app.last_result is True


def test_set_not_supported(base_app):
    out, err = run_cmd(base_app, 'set qqq True')
    expected = normalize(
        """
Parameter 'qqq' not supported (type 'set' for list of parameters).
"""
    )
    assert err == expected
    assert base_app.last_result is False


def test_set_no_settables(base_app):
    base_app._settables.clear()
    out, err = run_cmd(base_app, 'set quiet True')
    expected = normalize("There are no settable parameters")
    assert err == expected
    assert base_app.last_result is False


@pytest.mark.parametrize(
    'new_val, is_valid, expected',
    [
        (ansi.AllowStyle.NEVER, True, ansi.AllowStyle.NEVER),
        ('neVeR', True, ansi.AllowStyle.NEVER),
        (ansi.AllowStyle.TERMINAL, True, ansi.AllowStyle.TERMINAL),
        ('TeRMInal', True, ansi.AllowStyle.TERMINAL),
        (ansi.AllowStyle.ALWAYS, True, ansi.AllowStyle.ALWAYS),
        ('AlWaYs', True, ansi.AllowStyle.ALWAYS),
        ('invalid', False, ansi.AllowStyle.TERMINAL),
    ],
)
def test_set_allow_style(base_app, new_val, is_valid, expected):
    # Initialize allow_style for this test
    ansi.allow_style = ansi.AllowStyle.TERMINAL

    # Use the set command to alter it
    out, err = run_cmd(base_app, 'set allow_style {}'.format(new_val))
    assert base_app.last_result is is_valid

    # Verify the results
    assert ansi.allow_style == expected
    if is_valid:
        assert not err
        assert out

    # Reset allow_style to its default since it's an application-wide setting that can affect other unit tests
    ansi.allow_style = ansi.AllowStyle.TERMINAL


def test_set_with_choices(base_app):
    """Test choices validation of Settables"""
    fake_choices = ['valid', 'choices']
    base_app.fake = fake_choices[0]

    fake_settable = cmd2.Settable('fake', type(base_app.fake), "fake description", base_app, choices=fake_choices)
    base_app.add_settable(fake_settable)

    # Try a valid choice
    out, err = run_cmd(base_app, f'set fake {fake_choices[1]}')
    assert base_app.last_result is True
    assert not err

    # Try an invalid choice
    out, err = run_cmd(base_app, 'set fake bad_value')
    assert base_app.last_result is False
    assert err[0].startswith("Error setting fake: invalid choice")


class OnChangeHookApp(cmd2.Cmd):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.add_settable(utils.Settable('quiet', bool, "my description", self, onchange_cb=self._onchange_quiet))

    def _onchange_quiet(self, name, old, new) -> None:
        """Runs when quiet is changed via set command"""
        self.poutput("You changed " + name)


@pytest.fixture
def onchange_app():
    app = OnChangeHookApp()
    return app


def test_set_onchange_hook(onchange_app):
    out, err = run_cmd(onchange_app, 'set quiet True')
    expected = normalize(
        """
You changed quiet
quiet - was: False
now: True
"""
    )
    assert out == expected
    assert onchange_app.last_result is True


def test_base_shell(base_app, monkeypatch):
    m = mock.Mock()
    monkeypatch.setattr("{}.Popen".format('subprocess'), m)
    out, err = run_cmd(base_app, 'shell echo a')
    assert out == []
    assert m.called


def test_shell_last_result(base_app):
    base_app.last_result = None
    run_cmd(base_app, 'shell fake')
    assert base_app.last_result is not None


def test_shell_manual_call(base_app):
    # Verifies crash from Issue #986 doesn't happen
    cmds = ['echo "hi"', 'echo "there"', 'echo "cmd2!"']
    cmd = ';'.join(cmds)

    base_app.do_shell(cmd)

    cmd = '&&'.join(cmds)

    base_app.do_shell(cmd)


def test_base_error(base_app):
    out, err = run_cmd(base_app, 'meow')
    assert "is not a recognized command" in err[0]


def test_base_error_suggest_command(base_app):
    try:
        old_suggest_similar_command = base_app.suggest_similar_command
        base_app.suggest_similar_command = True
        out, err = run_cmd(base_app, 'historic')
        assert "history" in err[1]
    finally:
        base_app.suggest_similar_command = old_suggest_similar_command


def test_run_script(base_app, request):
    test_dir = os.path.dirname(request.module.__file__)
    filename = os.path.join(test_dir, 'script.txt')

    assert base_app._script_dir == []
    assert base_app._current_script_dir is None

    # Get output out the script
    script_out, script_err = run_cmd(base_app, 'run_script {}'.format(filename))
    assert base_app.last_result is True

    assert base_app._script_dir == []
    assert base_app._current_script_dir is None

    # Now run the commands manually and compare their output to script's
    with open(filename, encoding='utf-8') as file:
        script_commands = file.read().splitlines()

    manual_out = []
    manual_err = []
    for cmdline in script_commands:
        out, err = run_cmd(base_app, cmdline)
        manual_out.extend(out)
        manual_err.extend(err)

    assert script_out == manual_out
    assert script_err == manual_err


def test_run_script_with_empty_args(base_app):
    out, err = run_cmd(base_app, 'run_script')
    assert "the following arguments are required" in err[1]
    assert base_app.last_result is None


def test_run_script_with_invalid_file(base_app, request):
    # Path does not exist
    out, err = run_cmd(base_app, 'run_script does_not_exist.txt')
    assert "Problem accessing script from " in err[0]
    assert base_app.last_result is False

    # Path is a directory
    test_dir = os.path.dirname(request.module.__file__)
    out, err = run_cmd(base_app, 'run_script {}'.format(test_dir))
    assert "Problem accessing script from " in err[0]
    assert base_app.last_result is False


def test_run_script_with_empty_file(base_app, request):
    test_dir = os.path.dirname(request.module.__file__)
    filename = os.path.join(test_dir, 'scripts', 'empty.txt')
    out, err = run_cmd(base_app, 'run_script {}'.format(filename))
    assert not out and not err
    assert base_app.last_result is True


def test_run_script_with_binary_file(base_app, request):
    test_dir = os.path.dirname(request.module.__file__)
    filename = os.path.join(test_dir, 'scripts', 'binary.bin')
    out, err = run_cmd(base_app, 'run_script {}'.format(filename))
    assert "is not an ASCII or UTF-8 encoded text file" in err[0]
    assert base_app.last_result is False


def test_run_script_with_python_file(base_app, request):
    m = mock.MagicMock(name='input', return_value='2')
    builtins.input = m

    test_dir = os.path.dirname(request.module.__file__)
    filename = os.path.join(test_dir, 'pyscript', 'stop.py')
    out, err = run_cmd(base_app, 'run_script {}'.format(filename))
    assert "appears to be a Python file" in err[0]
    assert base_app.last_result is False


def test_run_script_with_utf8_file(base_app, request):
    test_dir = os.path.dirname(request.module.__file__)
    filename = os.path.join(test_dir, 'scripts', 'utf8.txt')

    assert base_app._script_dir == []
    assert base_app._current_script_dir is None

    # Get output out the script
    script_out, script_err = run_cmd(base_app, 'run_script {}'.format(filename))
    assert base_app.last_result is True

    assert base_app._script_dir == []
    assert base_app._current_script_dir is None

    # Now run the commands manually and compare their output to script's
    with open(filename, encoding='utf-8') as file:
        script_commands = file.read().splitlines()

    manual_out = []
    manual_err = []
    for cmdline in script_commands:
        out, err = run_cmd(base_app, cmdline)
        manual_out.extend(out)
        manual_err.extend(err)

    assert script_out == manual_out
    assert script_err == manual_err


def test_scripts_add_to_history(base_app, request):
    test_dir = os.path.dirname(request.module.__file__)
    filename = os.path.join(test_dir, 'scripts', 'help.txt')
    command = f'run_script {filename}'

    # Add to history
    base_app.scripts_add_to_history = True
    base_app.history.clear()
    run_cmd(base_app, command)
    assert len(base_app.history) == 2
    assert base_app.history.get(1).raw == command
    assert base_app.history.get(2).raw == 'help -v'

    # Do not add to history
    base_app.scripts_add_to_history = False
    base_app.history.clear()
    run_cmd(base_app, command)
    assert len(base_app.history) == 1
    assert base_app.history.get(1).raw == command


def test_run_script_nested_run_scripts(base_app, request):
    # Verify that running a script with nested run_script commands works correctly,
    # and runs the nested script commands in the correct order.
    test_dir = os.path.dirname(request.module.__file__)
    filename = os.path.join(test_dir, 'scripts', 'nested.txt')

    # Run the top level script
    initial_run = 'run_script ' + filename
    run_cmd(base_app, initial_run)
    assert base_app.last_result is True

    # Check that the right commands were executed.
    expected = (
        """
%s
_relative_run_script precmds.txt
set allow_style Always
help
shortcuts
_relative_run_script postcmds.txt
set allow_style Never"""
        % initial_run
    )
    out, err = run_cmd(base_app, 'history -s')
    assert out == normalize(expected)


def test_runcmds_plus_hooks(base_app, request):
    test_dir = os.path.dirname(request.module.__file__)
    prefilepath = os.path.join(test_dir, 'scripts', 'precmds.txt')
    postfilepath = os.path.join(test_dir, 'scripts', 'postcmds.txt')

    base_app.runcmds_plus_hooks(['run_script ' + prefilepath, 'help', 'shortcuts', 'run_script ' + postfilepath])
    expected = """
run_script %s
set allow_style Always
help
shortcuts
run_script %s
set allow_style Never""" % (
        prefilepath,
        postfilepath,
    )

    out, err = run_cmd(base_app, 'history -s')
    assert out == normalize(expected)


def test_runcmds_plus_hooks_ctrl_c(base_app, capsys):
    """Test Ctrl-C while in runcmds_plus_hooks"""
    import types

    def do_keyboard_interrupt(self, _):
        raise KeyboardInterrupt('Interrupting this command')

    setattr(base_app, 'do_keyboard_interrupt', types.MethodType(do_keyboard_interrupt, base_app))

    # Default behavior is to not stop runcmds_plus_hooks() on Ctrl-C
    base_app.history.clear()
    base_app.runcmds_plus_hooks(['help', 'keyboard_interrupt', 'shortcuts'])
    out, err = capsys.readouterr()
    assert not err
    assert len(base_app.history) == 3

    # Ctrl-C should stop runcmds_plus_hooks() in this case
    base_app.history.clear()
    base_app.runcmds_plus_hooks(['help', 'keyboard_interrupt', 'shortcuts'], stop_on_keyboard_interrupt=True)
    out, err = capsys.readouterr()
    assert err.startswith("Interrupting this command")
    assert len(base_app.history) == 2


def test_relative_run_script(base_app, request):
    test_dir = os.path.dirname(request.module.__file__)
    filename = os.path.join(test_dir, 'script.txt')

    assert base_app._script_dir == []
    assert base_app._current_script_dir is None

    # Get output out the script
    script_out, script_err = run_cmd(base_app, '_relative_run_script {}'.format(filename))
    assert base_app.last_result is True

    assert base_app._script_dir == []
    assert base_app._current_script_dir is None

    # Now run the commands manually and compare their output to script's
    with open(filename, encoding='utf-8') as file:
        script_commands = file.read().splitlines()

    manual_out = []
    manual_err = []
    for cmdline in script_commands:
        out, err = run_cmd(base_app, cmdline)
        manual_out.extend(out)
        manual_err.extend(err)

    assert script_out == manual_out
    assert script_err == manual_err


@pytest.mark.parametrize('file_name', odd_file_names)
def test_relative_run_script_with_odd_file_names(base_app, file_name, monkeypatch):
    """Test file names with various patterns"""
    # Mock out the do_run_script call to see what args are passed to it
    run_script_mock = mock.MagicMock(name='do_run_script')
    monkeypatch.setattr("cmd2.Cmd.do_run_script", run_script_mock)

    run_cmd(base_app, "_relative_run_script {}".format(utils.quote_string(file_name)))
    run_script_mock.assert_called_once_with(utils.quote_string(file_name))


def test_relative_run_script_requires_an_argument(base_app):
    out, err = run_cmd(base_app, '_relative_run_script')
    assert 'Error: the following arguments' in err[1]
    assert base_app.last_result is None


def test_in_script(request):
    class HookApp(cmd2.Cmd):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.register_cmdfinalization_hook(self.hook)

        def hook(self: cmd2.Cmd, data: plugin.CommandFinalizationData) -> plugin.CommandFinalizationData:
            if self.in_script():
                self.poutput("WE ARE IN SCRIPT")
            return data

    hook_app = HookApp()
    test_dir = os.path.dirname(request.module.__file__)
    filename = os.path.join(test_dir, 'script.txt')
    out, err = run_cmd(hook_app, 'run_script {}'.format(filename))

    assert "WE ARE IN SCRIPT" in out[-1]


def test_system_exit_in_command(base_app, capsys):
    """Test raising SystemExit in a command"""
    import types

    exit_code = 5

    def do_system_exit(self, _):
        raise SystemExit(exit_code)

    setattr(base_app, 'do_system_exit', types.MethodType(do_system_exit, base_app))

    stop = base_app.onecmd_plus_hooks('system_exit')
    assert stop
    assert base_app.exit_code == exit_code


def test_passthrough_exception_in_command(base_app):
    """Test raising a PassThroughException in a command"""
    import types

    def do_passthrough(self, _):
        wrapped_ex = OSError("Pass me up")
        raise exceptions.PassThroughException(wrapped_ex=wrapped_ex)

    setattr(base_app, 'do_passthrough', types.MethodType(do_passthrough, base_app))

    with pytest.raises(OSError) as excinfo:
        base_app.onecmd_plus_hooks('passthrough')
    assert 'Pass me up' in str(excinfo.value)


def test_output_redirection(base_app):
    fd, filename = tempfile.mkstemp(prefix='cmd2_test', suffix='.txt')
    os.close(fd)

    try:
        # Verify that writing to a file works
        run_cmd(base_app, 'help > {}'.format(filename))
        with open(filename) as f:
            content = f.read()
        verify_help_text(base_app, content)

        # Verify that appending to a file also works
        run_cmd(base_app, 'help history >> {}'.format(filename))
        with open(filename) as f:
            appended_content = f.read()
        assert appended_content.startswith(content)
        assert len(appended_content) > len(content)
    except Exception:
        raise
    finally:
        os.remove(filename)


def test_output_redirection_to_nonexistent_directory(base_app):
    filename = '~/fakedir/this_does_not_exist.txt'

    out, err = run_cmd(base_app, 'help > {}'.format(filename))
    assert 'Failed to redirect' in err[0]

    out, err = run_cmd(base_app, 'help >> {}'.format(filename))
    assert 'Failed to redirect' in err[0]


def test_output_redirection_to_too_long_filename(base_app):
    filename = (
        '~/sdkfhksdjfhkjdshfkjsdhfkjsdhfkjdshfkjdshfkjshdfkhdsfkjhewfuihewiufhweiufhiweufhiuewhiuewhfiuwehfia'
        'ewhfiuewhfiuewhfiuewhiuewhfiuewhfiuewfhiuwehewiufhewiuhfiweuhfiuwehfiuewfhiuwehiuewfhiuewhiewuhfiueh'
        'fiuwefhewiuhewiufhewiufhewiufhewiufhewiufhewiufhewiufhewiuhewiufhewiufhewiuheiufhiuewheiwufhewiufheu'
        'fheiufhieuwhfewiuhfeiufhiuewfhiuewheiwuhfiuewhfiuewhfeiuwfhewiufhiuewhiuewhfeiuwhfiuwehfuiwehfiuehie'
        'whfieuwfhieufhiuewhfeiuwfhiuefhueiwhfw'
    )

    out, err = run_cmd(base_app, 'help > {}'.format(filename))
    assert 'Failed to redirect' in err[0]

    out, err = run_cmd(base_app, 'help >> {}'.format(filename))
    assert 'Failed to redirect' in err[0]


def test_feedback_to_output_true(base_app):
    base_app.feedback_to_output = True
    base_app.timing = True
    f, filename = tempfile.mkstemp(prefix='cmd2_test', suffix='.txt')
    os.close(f)

    try:
        run_cmd(base_app, 'help > {}'.format(filename))
        with open(filename) as f:
            content = f.readlines()
        assert content[-1].startswith('Elapsed: ')
    except:
        raise
    finally:
        os.remove(filename)


def test_feedback_to_output_false(base_app):
    base_app.feedback_to_output = False
    base_app.timing = True
    f, filename = tempfile.mkstemp(prefix='feedback_to_output', suffix='.txt')
    os.close(f)

    try:
        out, err = run_cmd(base_app, 'help > {}'.format(filename))

        with open(filename) as f:
            content = f.readlines()
        assert not content[-1].startswith('Elapsed: ')
        assert err[0].startswith('Elapsed')
    except:
        raise
    finally:
        os.remove(filename)


def test_disallow_redirection(base_app):
    # Set allow_redirection to False
    base_app.allow_redirection = False

    filename = 'test_allow_redirect.txt'

    # Verify output wasn't redirected
    out, err = run_cmd(base_app, 'help > {}'.format(filename))
    verify_help_text(base_app, out)

    # Verify that no file got created
    assert not os.path.exists(filename)


def test_pipe_to_shell(base_app):
    if sys.platform == "win32":
        # Windows
        command = 'help | sort'
    else:
        # Mac and Linux
        # Get help on help and pipe it's output to the input of the word count shell command
        command = 'help help | wc'

    out, err = run_cmd(base_app, command)
    assert out and not err


def test_pipe_to_shell_and_redirect(base_app):
    filename = 'out.txt'
    if sys.platform == "win32":
        # Windows
        command = 'help | sort > {}'.format(filename)
    else:
        # Mac and Linux
        # Get help on help and pipe it's output to the input of the word count shell command
        command = 'help help | wc > {}'.format(filename)

    out, err = run_cmd(base_app, command)
    assert not out and not err
    assert os.path.exists(filename)
    os.remove(filename)


def test_pipe_to_shell_error(base_app):
    # Try to pipe command output to a shell command that doesn't exist in order to produce an error
    out, err = run_cmd(base_app, 'help | foobarbaz.this_does_not_exist')
    assert not out
    assert "Pipe process exited with code" in err[0]


try:
    # try getting the contents of the clipboard
    _ = clipboard.get_paste_buffer()
    # pyperclip raises at least the following types of exceptions
    #   FileNotFoundError on Windows Subsystem for Linux (WSL) when Windows paths are removed from $PATH
    #   ValueError for headless Linux systems without Gtk installed
    #   AssertionError can be raised by paste_klipper().
    #   PyperclipException for pyperclip-specific exceptions
except Exception:
    can_paste = False
else:
    can_paste = True


@pytest.mark.skipif(not can_paste, reason="Pyperclip could not find a copy/paste mechanism for your system")
def test_send_to_paste_buffer(base_app):
    # Test writing to the PasteBuffer/Clipboard
    run_cmd(base_app, 'help >')
    paste_contents = cmd2.cmd2.get_paste_buffer()
    verify_help_text(base_app, paste_contents)

    # Test appending to the PasteBuffer/Clipboard
    run_cmd(base_app, 'help history >>')
    appended_contents = cmd2.cmd2.get_paste_buffer()
    assert appended_contents.startswith(paste_contents)
    assert len(appended_contents) > len(paste_contents)


def test_get_paste_buffer_exception(base_app, mocker, capsys):
    # Force get_paste_buffer to throw an exception
    pastemock = mocker.patch('pyperclip.paste')
    pastemock.side_effect = ValueError('foo')

    # Redirect command output to the clipboard
    base_app.onecmd_plus_hooks('help > ')

    # Make sure we got the exception output
    out, err = capsys.readouterr()
    assert out == ''
    # this just checks that cmd2 is surfacing whatever error gets raised by pyperclip.paste
    assert 'ValueError' in err and 'foo' in err


def test_allow_clipboard_initializer(base_app):
    assert base_app.allow_clipboard is True
    noclipcmd = cmd2.Cmd(allow_clipboard=False)
    assert noclipcmd.allow_clipboard is False


# if clipboard access is not allowed, cmd2 should check that first
# before it tries to do anything with pyperclip, that's why we can
# safely run this test without skipping it if pyperclip doesn't
# work in the test environment, like we do for test_send_to_paste_buffer()
def test_allow_clipboard(base_app):
    base_app.allow_clipboard = False
    out, err = run_cmd(base_app, 'help >')
    assert not out
    assert "Clipboard access not allowed" in err


def test_base_timing(base_app):
    base_app.feedback_to_output = False
    out, err = run_cmd(base_app, 'set timing True')
    expected = normalize(
        """timing - was: False
now: True
"""
    )
    assert out == expected

    if sys.platform == 'win32':
        assert err[0].startswith('Elapsed: 0:00:00')
    else:
        assert err[0].startswith('Elapsed: 0:00:00.0')


def _expected_no_editor_error():
    expected_exception = 'OSError'
    # If PyPy, expect a different exception than with Python 3
    if hasattr(sys, "pypy_translation_info"):
        expected_exception = 'EnvironmentError'

    expected_text = normalize(
        """
EXCEPTION of type '{}' occurred with message: Please use 'set editor' to specify your text editing program of choice.
To enable full traceback, run the following command: 'set debug true'
""".format(expected_exception)
    )

    return expected_text


def test_base_debug(base_app):
    # Purposely set the editor to None
    base_app.editor = None

    # Make sure we get an exception, but cmd2 handles it
    out, err = run_cmd(base_app, 'edit')

    expected = _expected_no_editor_error()
    assert err == expected

    # Set debug true
    out, err = run_cmd(base_app, 'set debug True')
    expected = normalize(
        """
debug - was: False
now: True
"""
    )
    assert out == expected

    # Verify that we now see the exception traceback
    out, err = run_cmd(base_app, 'edit')
    assert err[0].startswith('Traceback (most recent call last):')


def test_debug_not_settable(base_app):
    # Set debug to False and make it unsettable
    base_app.debug = False
    base_app.remove_settable('debug')

    # Cause an exception
    out, err = run_cmd(base_app, 'bad "quote')

    # Since debug is unsettable, the user will not be given the option to enable a full traceback
    assert err == ['Invalid syntax: No closing quotation']


def test_remove_settable_keyerror(base_app):
    with pytest.raises(KeyError):
        base_app.remove_settable('fake')


def test_edit_file(base_app, request, monkeypatch):
    # Set a fake editor just to make sure we have one.  We aren't really going to call it due to the mock
    base_app.editor = 'fooedit'

    # Mock out the subprocess.Popen call so we don't actually open an editor
    m = mock.MagicMock(name='Popen')
    monkeypatch.setattr("subprocess.Popen", m)

    test_dir = os.path.dirname(request.module.__file__)
    filename = os.path.join(test_dir, 'script.txt')

    run_cmd(base_app, 'edit {}'.format(filename))

    # We think we have an editor, so should expect a Popen call
    m.assert_called_once()


@pytest.mark.parametrize('file_name', odd_file_names)
def test_edit_file_with_odd_file_names(base_app, file_name, monkeypatch):
    """Test editor and file names with various patterns"""
    # Mock out the do_shell call to see what args are passed to it
    shell_mock = mock.MagicMock(name='do_shell')
    monkeypatch.setattr("cmd2.Cmd.do_shell", shell_mock)

    base_app.editor = 'fooedit'
    file_name = utils.quote_string('nothingweird.py')
    run_cmd(base_app, "edit {}".format(utils.quote_string(file_name)))
    shell_mock.assert_called_once_with('"fooedit" {}'.format(utils.quote_string(file_name)))


def test_edit_file_with_spaces(base_app, request, monkeypatch):
    # Set a fake editor just to make sure we have one.  We aren't really going to call it due to the mock
    base_app.editor = 'fooedit'

    # Mock out the subprocess.Popen call so we don't actually open an editor
    m = mock.MagicMock(name='Popen')
    monkeypatch.setattr("subprocess.Popen", m)

    test_dir = os.path.dirname(request.module.__file__)
    filename = os.path.join(test_dir, 'my commands.txt')

    run_cmd(base_app, 'edit "{}"'.format(filename))

    # We think we have an editor, so should expect a Popen call
    m.assert_called_once()


def test_edit_blank(base_app, monkeypatch):
    # Set a fake editor just to make sure we have one.  We aren't really going to call it due to the mock
    base_app.editor = 'fooedit'

    # Mock out the subprocess.Popen call so we don't actually open an editor
    m = mock.MagicMock(name='Popen')
    monkeypatch.setattr("subprocess.Popen", m)

    run_cmd(base_app, 'edit')

    # We have an editor, so should expect a Popen call
    m.assert_called_once()


def test_base_py_interactive(base_app):
    # Mock out the InteractiveConsole.interact() call so we don't actually wait for a user's response on stdin
    m = mock.MagicMock(name='interact')
    InteractiveConsole.interact = m

    run_cmd(base_app, "py")

    # Make sure our mock was called once and only once
    m.assert_called_once()


def test_base_cmdloop_with_startup_commands():
    intro = 'Hello World, this is an intro ...'

    # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args
    testargs = ["prog", 'quit']
    expected = intro + '\n'

    with mock.patch.object(sys, 'argv', testargs):
        app = CreateOutsimApp()

    app.use_rawinput = True

    # Run the command loop with custom intro
    app.cmdloop(intro=intro)

    out = app.stdout.getvalue()
    assert out == expected


def test_base_cmdloop_without_startup_commands():
    # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args
    testargs = ["prog"]
    with mock.patch.object(sys, 'argv', testargs):
        app = CreateOutsimApp()

    app.use_rawinput = True
    app.intro = 'Hello World, this is an intro ...'

    # Mock out the input call so we don't actually wait for a user's response on stdin
    m = mock.MagicMock(name='input', return_value='quit')
    builtins.input = m

    expected = app.intro + '\n'

    # Run the command loop
    app.cmdloop()
    out = app.stdout.getvalue()
    assert out == expected


def test_cmdloop_without_rawinput():
    # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args
    testargs = ["prog"]
    with mock.patch.object(sys, 'argv', testargs):
        app = CreateOutsimApp()

    app.use_rawinput = False
    app.echo = False
    app.intro = 'Hello World, this is an intro ...'

    # Mock out the input call so we don't actually wait for a user's response on stdin
    m = mock.MagicMock(name='input', return_value='quit')
    builtins.input = m

    expected = app.intro + '\n'

    with pytest.raises(OSError):
        app.cmdloop()
    out = app.stdout.getvalue()
    assert out == expected


@pytest.mark.skipif(sys.platform.startswith('win'), reason="stty sane only run on Linux/Mac")
def test_stty_sane(base_app, monkeypatch):
    """Make sure stty sane is run on Linux/Mac after each command if stdin is a terminal"""
    with mock.patch('sys.stdin.isatty', mock.MagicMock(name='isatty', return_value=True)):
        # Mock out the subprocess.Popen call so we don't actually run stty sane
        m = mock.MagicMock(name='Popen')
        monkeypatch.setattr("subprocess.Popen", m)

        base_app.onecmd_plus_hooks('help')
        m.assert_called_once_with(['stty', 'sane'])


def test_sigint_handler(base_app):
    # No KeyboardInterrupt should be raised when using sigint_protection
    with base_app.sigint_protection:
        base_app.sigint_handler(signal.SIGINT, 1)

    # Without sigint_protection, a KeyboardInterrupt is raised
    with pytest.raises(KeyboardInterrupt):
        base_app.sigint_handler(signal.SIGINT, 1)


def test_raise_keyboard_interrupt(base_app):
    with pytest.raises(KeyboardInterrupt) as excinfo:
        base_app._raise_keyboard_interrupt()
    assert 'Got a keyboard interrupt' in str(excinfo.value)


@pytest.mark.skipif(sys.platform.startswith('win'), reason="SIGTERM only handled on Linux/Mac")
def test_termination_signal_handler(base_app):
    with pytest.raises(SystemExit) as excinfo:
        base_app.termination_signal_handler(signal.SIGHUP, 1)
    assert excinfo.value.code == signal.SIGHUP + 128

    with pytest.raises(SystemExit) as excinfo:
        base_app.termination_signal_handler(signal.SIGTERM, 1)
    assert excinfo.value.code == signal.SIGTERM + 128


class HookFailureApp(cmd2.Cmd):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # register a postparsing hook method
        self.register_postparsing_hook(self.postparsing_precmd)

    def postparsing_precmd(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData:
        """Simulate precmd hook failure."""
        data.stop = True
        return data


@pytest.fixture
def hook_failure():
    app = HookFailureApp()
    return app


def test_precmd_hook_success(base_app):
    out = base_app.onecmd_plus_hooks('help')
    assert out is False


def test_precmd_hook_failure(hook_failure):
    out = hook_failure.onecmd_plus_hooks('help')
    assert out is True


class SayApp(cmd2.Cmd):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def do_say(self, arg):
        self.poutput(arg)


@pytest.fixture
def say_app():
    app = SayApp(allow_cli_args=False)
    app.stdout = utils.StdSim(app.stdout)
    return app


def test_ctrl_c_at_prompt(say_app):
    # Mock out the input call so we don't actually wait for a user's response on stdin
    m = mock.MagicMock(name='input')
    m.side_effect = ['say hello', KeyboardInterrupt(), 'say goodbye', 'eof']
    builtins.input = m

    say_app.cmdloop()

    # And verify the expected output to stdout
    out = say_app.stdout.getvalue()
    assert out == 'hello\n^C\ngoodbye\n\n'


class ShellApp(cmd2.Cmd):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.default_to_shell = True


def test_default_to_shell(base_app, monkeypatch):
    if sys.platform.startswith('win'):
        line = 'dir'
    else:
        line = 'ls'

    base_app.default_to_shell = True
    m = mock.Mock()
    monkeypatch.setattr("{}.Popen".format('subprocess'), m)
    out, err = run_cmd(base_app, line)
    assert out == []
    assert m.called


def test_escaping_prompt():
    from cmd2.rl_utils import (
        rl_escape_prompt,
        rl_unescape_prompt,
    )

    # This prompt has nothing which needs to be escaped
    prompt = '(Cmd) '
    assert rl_escape_prompt(prompt) == prompt

    # This prompt has color which needs to be escaped
    color = ansi.Fg.CYAN
    prompt = ansi.style('InColor', fg=color)

    escape_start = "\x01"
    escape_end = "\x02"

    escaped_prompt = rl_escape_prompt(prompt)
    if sys.platform.startswith('win'):
        # PyReadline on Windows doesn't need to escape invisible characters
        assert escaped_prompt == prompt
    else:
        assert escaped_prompt.startswith(escape_start + color + escape_end)
        assert escaped_prompt.endswith(escape_start + ansi.Fg.RESET + escape_end)

    assert rl_unescape_prompt(escaped_prompt) == prompt


class HelpApp(cmd2.Cmd):
    """Class for testing custom help_* methods which override docstring help."""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def do_squat(self, arg):
        """This docstring help will never be shown because the help_squat method overrides it."""
        pass

    def help_squat(self):
        self.stdout.write('This command does diddly squat...\n')

    def do_edit(self, arg):
        """This overrides the edit command and does nothing."""
        pass

    # This command will be in the "undocumented" section of the help menu
    def do_undoc(self, arg):
        pass

    def do_multiline_docstr(self, arg):
        """
        This documentation
        is multiple lines
        and there are no
        tabs
        """
        pass

    parser_cmd_parser = cmd2.Cmd2ArgumentParser(description="This is the description.")

    @cmd2.with_argparser(parser_cmd_parser)
    def do_parser_cmd(self, args):
        """This is the docstring."""
        pass


@pytest.fixture
def help_app():
    app = HelpApp()
    return app


def test_custom_command_help(help_app):
    out, err = run_cmd(help_app, 'help squat')
    expected = normalize('This command does diddly squat...')
    assert out == expected
    assert help_app.last_result is True


def test_custom_help_menu(help_app):
    out, err = run_cmd(help_app, 'help')
    verify_help_text(help_app, out)


def test_help_undocumented(help_app):
    out, err = run_cmd(help_app, 'help undoc')
    assert err[0].startswith("No help on undoc")
    assert help_app.last_result is False


def test_help_overridden_method(help_app):
    out, err = run_cmd(help_app, 'help edit')
    expected = normalize('This overrides the edit command and does nothing.')
    assert out == expected
    assert help_app.last_result is True


def test_help_multiline_docstring(help_app):
    out, err = run_cmd(help_app, 'help multiline_docstr')
    expected = normalize('This documentation\nis multiple lines\nand there are no\ntabs')
    assert out == expected
    assert help_app.last_result is True


def test_help_verbose_uses_parser_description(help_app: HelpApp):
    out, err = run_cmd(help_app, 'help --verbose')
    verify_help_text(help_app, out, verbose_strings=[help_app.parser_cmd_parser.description])


class HelpCategoriesApp(cmd2.Cmd):
    """Class for testing custom help_* methods which override docstring help."""

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    @cmd2.with_category('Some Category')
    def do_diddly(self, arg):
        """This command does diddly"""
        pass

    # This command will be in the "Some Category" section of the help menu even though it has no docstring
    @cmd2.with_category("Some Category")
    def do_cat_nodoc(self, arg):
        pass

    def do_squat(self, arg):
        """This docstring help will never be shown because the help_squat method overrides it."""
        pass

    def help_squat(self):
        self.stdout.write('This command does diddly squat...\n')

    def do_edit(self, arg):
        """This overrides the edit command and does nothing."""
        pass

    cmd2.categorize((do_squat, do_edit), 'Custom Category')

    # This command will be in the "undocumented" section of the help menu
    def do_undoc(self, arg):
        pass


@pytest.fixture
def helpcat_app():
    app = HelpCategoriesApp()
    return app


def test_help_cat_base(helpcat_app):
    out, err = run_cmd(helpcat_app, 'help')
    assert helpcat_app.last_result is True
    verify_help_text(helpcat_app, out)


def test_help_cat_verbose(helpcat_app):
    out, err = run_cmd(helpcat_app, 'help --verbose')
    assert helpcat_app.last_result is True
    verify_help_text(helpcat_app, out)


class SelectApp(cmd2.Cmd):
    def do_eat(self, arg):
        """Eat something, with a selection of sauces to choose from."""
        # Pass in a single string of space-separated selections
        sauce = self.select('sweet salty', 'Sauce? ')
        result = '{food} with {sauce} sauce, yum!'
        result = result.format(food=arg, sauce=sauce)
        self.stdout.write(result + '\n')

    def do_study(self, arg):
        """Learn something, with a selection of subjects to choose from."""
        # Pass in a list of strings for selections
        subject = self.select(['math', 'science'], 'Subject? ')
        result = 'Good luck learning {}!\n'.format(subject)
        self.stdout.write(result)

    def do_procrastinate(self, arg):
        """Waste time in your manner of choice."""
        # Pass in a list of tuples for selections
        leisure_activity = self.select(
            [('Netflix and chill', 'Netflix'), ('YouTube', 'WebSurfing')], 'How would you like to procrastinate? '
        )
        result = 'Have fun procrasinating with {}!\n'.format(leisure_activity)
        self.stdout.write(result)

    def do_play(self, arg):
        """Play your favorite musical instrument."""
        # Pass in an uneven list of tuples for selections
        instrument = self.select([('Guitar', 'Electric Guitar'), ('Drums',)], 'Instrument? ')
        result = 'Charm us with the {}...\n'.format(instrument)
        self.stdout.write(result)

    def do_return_type(self, arg):
        """Test that return values can be non-strings"""
        choice = self.select([(1, 'Integer'), ("test_str", 'String'), (self.do_play, 'Method')], 'Choice? ')
        result = f'The return type is {type(choice)}\n'
        self.stdout.write(result)


@pytest.fixture
def select_app():
    app = SelectApp()
    return app


def test_select_options(select_app, monkeypatch):
    # Mock out the read_input call so we don't actually wait for a user's response on stdin
    read_input_mock = mock.MagicMock(name='read_input', return_value='2')
    monkeypatch.setattr("cmd2.Cmd.read_input", read_input_mock)

    food = 'bacon'
    out, err = run_cmd(select_app, "eat {}".format(food))
    expected = normalize(
        """
   1. sweet
   2. salty
{} with salty sauce, yum!
""".format(food)
    )

    # Make sure our mock was called with the expected arguments
    read_input_mock.assert_called_once_with('Sauce? ')

    # And verify the expected output to stdout
    assert out == expected


def test_select_invalid_option_too_big(select_app, monkeypatch):
    # Mock out the input call so we don't actually wait for a user's response on stdin
    read_input_mock = mock.MagicMock(name='read_input')

    # If side_effect is an iterable then each call to the mock will return the next value from the iterable.
    read_input_mock.side_effect = ['3', '1']  # First pass an invalid selection, then pass a valid one
    monkeypatch.setattr("cmd2.Cmd.read_input", read_input_mock)

    food = 'fish'
    out, err = run_cmd(select_app, "eat {}".format(food))
    expected = normalize(
        """
   1. sweet
   2. salty
'3' isn't a valid choice. Pick a number between 1 and 2:
{} with sweet sauce, yum!
""".format(food)
    )

    # Make sure our mock was called exactly twice with the expected arguments
    arg = 'Sauce? '
    calls = [mock.call(arg), mock.call(arg)]
    read_input_mock.assert_has_calls(calls)
    assert read_input_mock.call_count == 2

    # And verify the expected output to stdout
    assert out == expected


def test_select_invalid_option_too_small(select_app, monkeypatch):
    # Mock out the input call so we don't actually wait for a user's response on stdin
    read_input_mock = mock.MagicMock(name='read_input')

    # If side_effect is an iterable then each call to the mock will return the next value from the iterable.
    read_input_mock.side_effect = ['0', '1']  # First pass an invalid selection, then pass a valid one
    monkeypatch.setattr("cmd2.Cmd.read_input", read_input_mock)

    food = 'fish'
    out, err = run_cmd(select_app, "eat {}".format(food))
    expected = normalize(
        """
   1. sweet
   2. salty
'0' isn't a valid choice. Pick a number between 1 and 2:
{} with sweet sauce, yum!
""".format(food)
    )

    # Make sure our mock was called exactly twice with the expected arguments
    arg = 'Sauce? '
    calls = [mock.call(arg), mock.call(arg)]
    read_input_mock.assert_has_calls(calls)
    assert read_input_mock.call_count == 2

    # And verify the expected output to stdout
    assert out == expected


def test_select_list_of_strings(select_app, monkeypatch):
    # Mock out the input call so we don't actually wait for a user's response on stdin
    read_input_mock = mock.MagicMock(name='read_input', return_value='2')
    monkeypatch.setattr("cmd2.Cmd.read_input", read_input_mock)

    out, err = run_cmd(select_app, "study")
    expected = normalize(
        """
   1. math
   2. science
Good luck learning {}!
""".format('science')
    )

    # Make sure our mock was called with the expected arguments
    read_input_mock.assert_called_once_with('Subject? ')

    # And verify the expected output to stdout
    assert out == expected


def test_select_list_of_tuples(select_app, monkeypatch):
    # Mock out the input call so we don't actually wait for a user's response on stdin
    read_input_mock = mock.MagicMock(name='read_input', return_value='2')
    monkeypatch.setattr("cmd2.Cmd.read_input", read_input_mock)

    out, err = run_cmd(select_app, "procrastinate")
    expected = normalize(
        """
   1. Netflix
   2. WebSurfing
Have fun procrasinating with {}!
""".format('YouTube')
    )

    # Make sure our mock was called with the expected arguments
    read_input_mock.assert_called_once_with('How would you like to procrastinate? ')

    # And verify the expected output to stdout
    assert out == expected


def test_select_uneven_list_of_tuples(select_app, monkeypatch):
    # Mock out the input call so we don't actually wait for a user's response on stdin
    read_input_mock = mock.MagicMock(name='read_input', return_value='2')
    monkeypatch.setattr("cmd2.Cmd.read_input", read_input_mock)

    out, err = run_cmd(select_app, "play")
    expected = normalize(
        """
   1. Electric Guitar
   2. Drums
Charm us with the {}...
""".format('Drums')
    )

    # Make sure our mock was called with the expected arguments
    read_input_mock.assert_called_once_with('Instrument? ')

    # And verify the expected output to stdout
    assert out == expected


@pytest.mark.parametrize(
    'selection, type_str',
    [
        ('1', "<class 'int'>"),
        ('2', "<class 'str'>"),
        ('3', "<class 'method'>"),
    ],
)
def test_select_return_type(select_app, monkeypatch, selection, type_str):
    # Mock out the input call so we don't actually wait for a user's response on stdin
    read_input_mock = mock.MagicMock(name='read_input', return_value=selection)
    monkeypatch.setattr("cmd2.Cmd.read_input", read_input_mock)

    out, err = run_cmd(select_app, "return_type")
    expected = normalize(
        """
   1. Integer
   2. String
   3. Method
The return type is {}
""".format(type_str)
    )

    # Make sure our mock was called with the expected arguments
    read_input_mock.assert_called_once_with('Choice? ')

    # And verify the expected output to stdout
    assert out == expected


def test_select_eof(select_app, monkeypatch):
    # Ctrl-D during select causes an EOFError that just reprompts the user
    read_input_mock = mock.MagicMock(name='read_input', side_effect=[EOFError, 2])
    monkeypatch.setattr("cmd2.Cmd.read_input", read_input_mock)

    food = 'fish'
    out, err = run_cmd(select_app, "eat {}".format(food))

    # Make sure our mock was called exactly twice with the expected arguments
    arg = 'Sauce? '
    calls = [mock.call(arg), mock.call(arg)]
    read_input_mock.assert_has_calls(calls)
    assert read_input_mock.call_count == 2


def test_select_ctrl_c(outsim_app, monkeypatch):
    # Ctrl-C during select prints ^C and raises a KeyboardInterrupt
    read_input_mock = mock.MagicMock(name='read_input', side_effect=KeyboardInterrupt)
    monkeypatch.setattr("cmd2.Cmd.read_input", read_input_mock)

    with pytest.raises(KeyboardInterrupt):
        outsim_app.select([('Guitar', 'Electric Guitar'), ('Drums',)], 'Instrument? ')

    out = outsim_app.stdout.getvalue()
    assert out.rstrip().endswith('^C')


class HelpNoDocstringApp(cmd2.Cmd):
    greet_parser = cmd2.Cmd2ArgumentParser()
    greet_parser.add_argument('-s', '--shout', action="store_true", help="N00B EMULATION MODE")

    @cmd2.with_argparser(greet_parser, with_unknown_args=True)
    def do_greet(self, opts, arg):
        arg = ''.join(arg)
        if opts.shout:
            arg = arg.upper()
        self.stdout.write(arg + '\n')


def test_help_with_no_docstring(capsys):
    app = HelpNoDocstringApp()
    app.onecmd_plus_hooks('greet -h')
    out, err = capsys.readouterr()
    assert err == ''
    assert (
        out
        == """Usage: greet [-h] [-s]

optional arguments:
  -h, --help   show this help message and exit
  -s, --shout  N00B EMULATION MODE

"""
    )


class MultilineApp(cmd2.Cmd):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, multiline_commands=['orate'], **kwargs)

    orate_parser = cmd2.Cmd2ArgumentParser()
    orate_parser.add_argument('-s', '--shout', action="store_true", help="N00B EMULATION MODE")

    @cmd2.with_argparser(orate_parser, with_unknown_args=True)
    def do_orate(self, opts, arg):
        arg = ''.join(arg)
        if opts.shout:
            arg = arg.upper()
        self.stdout.write(arg + '\n')


@pytest.fixture
def multiline_app():
    app = MultilineApp()
    return app


def test_multiline_complete_empty_statement_raises_exception(multiline_app):
    with pytest.raises(exceptions.EmptyStatement):
        multiline_app._complete_statement('')


def test_multiline_complete_statement_without_terminator(multiline_app):
    # Mock out the input call so we don't actually wait for a user's response
    # on stdin when it looks for more input
    m = mock.MagicMock(name='input', return_value='\n')
    builtins.input = m

    command = 'orate'
    args = 'hello world'
    line = '{} {}'.format(command, args)
    statement = multiline_app._complete_statement(line)
    assert statement == args
    assert statement.command == command
    assert statement.multiline_command == command


def test_multiline_complete_statement_with_unclosed_quotes(multiline_app):
    # Mock out the input call so we don't actually wait for a user's response
    # on stdin when it looks for more input
    m = mock.MagicMock(name='input', side_effect=['quotes', '" now closed;'])
    builtins.input = m

    line = 'orate hi "partially open'
    statement = multiline_app._complete_statement(line)
    assert statement == 'hi "partially open\nquotes\n" now closed'
    assert statement.command == 'orate'
    assert statement.multiline_command == 'orate'
    assert statement.terminator == ';'


def test_multiline_input_line_to_statement(multiline_app):
    # Verify _input_line_to_statement saves the fully entered input line for multiline commands

    # Mock out the input call so we don't actually wait for a user's response
    # on stdin when it looks for more input
    m = mock.MagicMock(name='input', side_effect=['person', '\n'])
    builtins.input = m

    line = 'orate hi'
    statement = multiline_app._input_line_to_statement(line)
    assert statement.raw == 'orate hi\nperson\n'
    assert statement == 'hi person'
    assert statement.command == 'orate'
    assert statement.multiline_command == 'orate'


def test_multiline_history_no_prior_history(multiline_app):
    # Test no existing history prior to typing the command
    m = mock.MagicMock(name='input', side_effect=['person', '\n'])
    builtins.input = m

    # Set orig_rl_history_length to 0 before the first line is typed.
    readline.clear_history()
    orig_rl_history_length = readline.get_current_history_length()

    line = "orate hi"
    readline.add_history(line)
    multiline_app._complete_statement(line, orig_rl_history_length=orig_rl_history_length)

    assert readline.get_current_history_length() == orig_rl_history_length + 1
    assert readline.get_history_item(1) == "orate hi person"


def test_multiline_history_first_line_matches_prev_entry(multiline_app):
    # Test when first line of multiline command matches previous history entry
    m = mock.MagicMock(name='input', side_effect=['person', '\n'])
    builtins.input = m

    # Since the first line of our command matches the previous entry,
    # orig_rl_history_length is set before the first line is typed.
    line = "orate hi"
    readline.clear_history()
    readline.add_history(line)
    orig_rl_history_length = readline.get_current_history_length()

    multiline_app._complete_statement(line, orig_rl_history_length=orig_rl_history_length)

    assert readline.get_current_history_length() == orig_rl_history_length + 1
    assert readline.get_history_item(1) == line
    assert readline.get_history_item(2) == "orate hi person"


def test_multiline_history_matches_prev_entry(multiline_app):
    # Test combined multiline command that matches previous history entry
    m = mock.MagicMock(name='input', side_effect=['person', '\n'])
    builtins.input = m

    readline.clear_history()
    readline.add_history("orate hi person")
    orig_rl_history_length = readline.get_current_history_length()

    line = "orate hi"
    readline.add_history(line)
    multiline_app._complete_statement(line, orig_rl_history_length=orig_rl_history_length)

    # Since it matches the previous history item, nothing was added to readline history
    assert readline.get_current_history_length() == orig_rl_history_length
    assert readline.get_history_item(1) == "orate hi person"


def test_multiline_history_does_not_match_prev_entry(multiline_app):
    # Test combined multiline command that does not match previous history entry
    m = mock.MagicMock(name='input', side_effect=['person', '\n'])
    builtins.input = m

    readline.clear_history()
    readline.add_history("no match")
    orig_rl_history_length = readline.get_current_history_length()

    line = "orate hi"
    readline.add_history(line)
    multiline_app._complete_statement(line, orig_rl_history_length=orig_rl_history_length)

    # Since it doesn't match the previous history item, it was added to readline history
    assert readline.get_current_history_length() == orig_rl_history_length + 1
    assert readline.get_history_item(1) == "no match"
    assert readline.get_history_item(2) == "orate hi person"


def test_multiline_history_with_quotes(multiline_app):
    # Test combined multiline command with quotes
    m = mock.MagicMock(name='input', side_effect=['  and spaces  ', ' "', ' in', 'quotes.', ';'])
    builtins.input = m

    readline.clear_history()
    orig_rl_history_length = readline.get_current_history_length()

    line = 'orate Look, "There are newlines'
    readline.add_history(line)
    multiline_app._complete_statement(line, orig_rl_history_length=orig_rl_history_length)

    # Since spaces and newlines in quotes are preserved, this history entry spans multiple lines.
    assert readline.get_current_history_length() == orig_rl_history_length + 1

    history_lines = readline.get_history_item(1).splitlines()
    assert history_lines[0] == 'orate Look, "There are newlines'
    assert history_lines[1] == '  and spaces  '
    assert history_lines[2] == ' " in quotes.;'


class CommandResultApp(cmd2.Cmd):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def do_affirmative(self, arg):
        self.last_result = cmd2.CommandResult(arg, data=True)

    def do_negative(self, arg):
        self.last_result = cmd2.CommandResult(arg, data=False)

    def do_affirmative_no_data(self, arg):
        self.last_result = cmd2.CommandResult(arg)

    def do_negative_no_data(self, arg):
        self.last_result = cmd2.CommandResult('', arg)


@pytest.fixture
def commandresult_app():
    app = CommandResultApp()
    return app


def test_commandresult_truthy(commandresult_app):
    arg = 'foo'
    run_cmd(commandresult_app, 'affirmative {}'.format(arg))
    assert commandresult_app.last_result
    assert commandresult_app.last_result == cmd2.CommandResult(arg, data=True)

    run_cmd(commandresult_app, 'affirmative_no_data {}'.format(arg))
    assert commandresult_app.last_result
    assert commandresult_app.last_result == cmd2.CommandResult(arg)


def test_commandresult_falsy(commandresult_app):
    arg = 'bar'
    run_cmd(commandresult_app, 'negative {}'.format(arg))
    assert not commandresult_app.last_result
    assert commandresult_app.last_result == cmd2.CommandResult(arg, data=False)

    run_cmd(commandresult_app, 'negative_no_data {}'.format(arg))
    assert not commandresult_app.last_result
    assert commandresult_app.last_result == cmd2.CommandResult('', arg)


def test_is_text_file_bad_input(base_app):
    # Test with a non-existent file
    with pytest.raises(OSError):
        utils.is_text_file('does_not_exist.txt')

    # Test with a directory
    with pytest.raises(OSError):
        utils.is_text_file('.')


def test_eof(base_app):
    # Only thing to verify is that it returns True
    assert base_app.do_eof('')
    assert base_app.last_result is True


def test_quit(base_app):
    # Only thing to verify is that it returns True
    assert base_app.do_quit('')
    assert base_app.last_result is True


def test_echo(capsys):
    app = cmd2.Cmd()
    app.echo = True
    commands = ['help history']

    app.runcmds_plus_hooks(commands)

    out, err = capsys.readouterr()
    assert out.startswith('{}{}\n'.format(app.prompt, commands[0]) + HELP_HISTORY.split()[0])


def test_read_input_rawinput_true(capsys, monkeypatch):
    prompt_str = 'the_prompt'
    input_str = 'some input'

    app = cmd2.Cmd()
    app.use_rawinput = True

    # Mock out input() to return input_str
    monkeypatch.setattr("builtins.input", lambda *args: input_str)

    # isatty is True
    with mock.patch('sys.stdin.isatty', mock.MagicMock(name='isatty', return_value=True)):
        line = app.read_input(prompt_str)
        assert line == input_str

        # Run custom history code
        readline.add_history('old_history')
        custom_history = ['cmd1', 'cmd2']
        line = app.read_input(prompt_str, history=custom_history, completion_mode=cmd2.CompletionMode.NONE)
        assert line == input_str
        readline.clear_history()

        # Run all completion modes
        line = app.read_input(prompt_str, completion_mode=cmd2.CompletionMode.NONE)
        assert line == input_str

        line = app.read_input(prompt_str, completion_mode=cmd2.CompletionMode.COMMANDS)
        assert line == input_str

        # custom choices
        custom_choices = ['choice1', 'choice2']
        line = app.read_input(prompt_str, completion_mode=cmd2.CompletionMode.CUSTOM, choices=custom_choices)
        assert line == input_str

        # custom choices_provider
        line = app.read_input(
            prompt_str, completion_mode=cmd2.CompletionMode.CUSTOM, choices_provider=cmd2.Cmd.get_all_commands
        )
        assert line == input_str

        # custom completer
        line = app.read_input(prompt_str, completion_mode=cmd2.CompletionMode.CUSTOM, completer=cmd2.Cmd.path_complete)
        assert line == input_str

        # custom parser
        line = app.read_input(prompt_str, completion_mode=cmd2.CompletionMode.CUSTOM, parser=cmd2.Cmd2ArgumentParser())
        assert line == input_str

    # isatty is False
    with mock.patch('sys.stdin.isatty', mock.MagicMock(name='isatty', return_value=False)):
        # echo True
        app.echo = True
        line = app.read_input(prompt_str)
        out, err = capsys.readouterr()
        assert line == input_str
        assert out == "{}{}\n".format(prompt_str, input_str)

        # echo False
        app.echo = False
        line = app.read_input(prompt_str)
        out, err = capsys.readouterr()
        assert line == input_str
        assert not out


def test_read_input_rawinput_false(capsys, monkeypatch):
    prompt_str = 'the_prompt'
    input_str = 'some input'

    def make_app(isatty: bool, empty_input: bool = False):
        """Make a cmd2 app with a custom stdin"""
        app_input_str = '' if empty_input else input_str

        fakein = io.StringIO('{}'.format(app_input_str))
        fakein.isatty = mock.MagicMock(name='isatty', return_value=isatty)

        new_app = cmd2.Cmd(stdin=fakein)
        new_app.use_rawinput = False
        return new_app

    # isatty True
    app = make_app(isatty=True)
    line = app.read_input(prompt_str)
    out, err = capsys.readouterr()
    assert line == input_str
    assert out == prompt_str

    # isatty True, empty input
    app = make_app(isatty=True, empty_input=True)
    line = app.read_input(prompt_str)
    out, err = capsys.readouterr()
    assert line == 'eof'
    assert out == prompt_str

    # isatty is False, echo is True
    app = make_app(isatty=False)
    app.echo = True
    line = app.read_input(prompt_str)
    out, err = capsys.readouterr()
    assert line == input_str
    assert out == "{}{}\n".format(prompt_str, input_str)

    # isatty is False, echo is False
    app = make_app(isatty=False)
    app.echo = False
    line = app.read_input(prompt_str)
    out, err = capsys.readouterr()
    assert line == input_str
    assert not out

    # isatty is False, empty input
    app = make_app(isatty=False, empty_input=True)
    line = app.read_input(prompt_str)
    out, err = capsys.readouterr()
    assert line == 'eof'
    assert not out


def test_read_command_line_eof(base_app, monkeypatch):
    read_input_mock = mock.MagicMock(name='read_input', side_effect=EOFError)
    monkeypatch.setattr("cmd2.Cmd.read_input", read_input_mock)

    line = base_app._read_command_line("Prompt> ")
    assert line == 'eof'


def test_poutput_string(outsim_app):
    msg = 'This is a test'
    outsim_app.poutput(msg)
    out = outsim_app.stdout.getvalue()
    expected = msg + '\n'
    assert out == expected


def test_poutput_zero(outsim_app):
    msg = 0
    outsim_app.poutput(msg)
    out = outsim_app.stdout.getvalue()
    expected = str(msg) + '\n'
    assert out == expected


def test_poutput_empty_string(outsim_app):
    msg = ''
    outsim_app.poutput(msg)
    out = outsim_app.stdout.getvalue()
    expected = '\n'
    assert out == expected


def test_poutput_none(outsim_app):
    msg = None
    outsim_app.poutput(msg)
    out = outsim_app.stdout.getvalue()
    expected = 'None\n'
    assert out == expected


@with_ansi_style(ansi.AllowStyle.ALWAYS)
def test_poutput_ansi_always(outsim_app):
    msg = 'Hello World'
    colored_msg = ansi.style(msg, fg=ansi.Fg.CYAN)
    outsim_app.poutput(colored_msg)
    out = outsim_app.stdout.getvalue()
    expected = colored_msg + '\n'
    assert colored_msg != msg
    assert out == expected


@with_ansi_style(ansi.AllowStyle.NEVER)
def test_poutput_ansi_never(outsim_app):
    msg = 'Hello World'
    colored_msg = ansi.style(msg, fg=ansi.Fg.CYAN)
    outsim_app.poutput(colored_msg)
    out = outsim_app.stdout.getvalue()
    expected = msg + '\n'
    assert colored_msg != msg
    assert out == expected


# These are invalid names for aliases and macros
invalid_command_name = [
    '""',  # Blank name
    constants.COMMENT_CHAR,
    '!no_shortcut',
    '">"',
    '"no>pe"',
    '"no spaces"',
    '"nopipe|"',
    '"noterm;"',
    'noembedded"quotes',
]


def test_get_alias_completion_items(base_app):
    run_cmd(base_app, 'alias create fake run_pyscript')
    run_cmd(base_app, 'alias create ls !ls -hal')

    results = base_app._get_alias_completion_items()
    assert len(results) == len(base_app.aliases)

    for cur_res in results:
        assert cur_res in base_app.aliases
        # Strip trailing spaces from table output
        assert cur_res.description.rstrip() == base_app.aliases[cur_res]


def test_get_macro_completion_items(base_app):
    run_cmd(base_app, 'macro create foo !echo foo')
    run_cmd(base_app, 'macro create bar !echo bar')

    results = base_app._get_macro_completion_items()
    assert len(results) == len(base_app.macros)

    for cur_res in results:
        assert cur_res in base_app.macros
        # Strip trailing spaces from table output
        assert cur_res.description.rstrip() == base_app.macros[cur_res].value


def test_get_settable_completion_items(base_app):
    results = base_app._get_settable_completion_items()
    assert len(results) == len(base_app.settables)

    for cur_res in results:
        cur_settable = base_app.settables.get(cur_res)
        assert cur_settable is not None

        # These CompletionItem descriptions are a two column table (Settable Value and Settable Description)
        # First check if the description text starts with the value
        str_value = str(cur_settable.get_value())
        assert cur_res.description.startswith(str_value)

        # The second column is likely to have wrapped long text. So we will just examine the
        # first couple characters to look for the Settable's description.
        assert cur_settable.description[0:10] in cur_res.description


def test_alias_no_subcommand(base_app):
    out, err = run_cmd(base_app, 'alias')
    assert "Usage: alias [-h]" in err[0]
    assert "Error: the following arguments are required: SUBCOMMAND" in err[1]


def test_alias_create(base_app):
    # Create the alias
    out, err = run_cmd(base_app, 'alias create fake run_pyscript')
    assert out == normalize("Alias 'fake' created")
    assert base_app.last_result is True

    # Use the alias
    out, err = run_cmd(base_app, 'fake')
    assert "the following arguments are required: script_path" in err[1]

    # See a list of aliases
    out, err = run_cmd(base_app, 'alias list')
    assert out == normalize('alias create fake run_pyscript')
    assert len(base_app.last_result) == len(base_app.aliases)
    assert base_app.last_result['fake'] == "run_pyscript"

    # Look up the new alias
    out, err = run_cmd(base_app, 'alias list fake')
    assert out == normalize('alias create fake run_pyscript')
    assert len(base_app.last_result) == 1
    assert base_app.last_result['fake'] == "run_pyscript"

    # Overwrite alias
    out, err = run_cmd(base_app, 'alias create fake help')
    assert out == normalize("Alias 'fake' overwritten")
    assert base_app.last_result is True

    # Look up the updated alias
    out, err = run_cmd(base_app, 'alias list fake')
    assert out == normalize('alias create fake help')
    assert len(base_app.last_result) == 1
    assert base_app.last_result['fake'] == "help"


def test_alias_create_with_quoted_tokens(base_app):
    """Demonstrate that quotes in alias value will be preserved"""
    alias_name = "fake"
    alias_command = 'help ">" "out file.txt" ";"'
    create_command = f"alias create {alias_name} {alias_command}"

    # Create the alias
    out, err = run_cmd(base_app, create_command)
    assert out == normalize("Alias 'fake' created")

    # Look up the new alias and verify all quotes are preserved
    out, err = run_cmd(base_app, 'alias list fake')
    assert out == normalize(create_command)
    assert len(base_app.last_result) == 1
    assert base_app.last_result[alias_name] == alias_command


@pytest.mark.parametrize('alias_name', invalid_command_name)
def test_alias_create_invalid_name(base_app, alias_name, capsys):
    out, err = run_cmd(base_app, 'alias create {} help'.format(alias_name))
    assert "Invalid alias name" in err[0]
    assert base_app.last_result is False


def test_alias_create_with_command_name(base_app):
    out, err = run_cmd(base_app, 'alias create help stuff')
    assert "Alias cannot have the same name as a command" in err[0]
    assert base_app.last_result is False


def test_alias_create_with_macro_name(base_app):
    macro = "my_macro"
    run_cmd(base_app, 'macro create {} help'.format(macro))
    out, err = run_cmd(base_app, 'alias create {} help'.format(macro))
    assert "Alias cannot have the same name as a macro" in err[0]
    assert base_app.last_result is False


def test_alias_that_resolves_into_comment(base_app):
    # Create the alias
    out, err = run_cmd(base_app, 'alias create fake ' + constants.COMMENT_CHAR + ' blah blah')
    assert out == normalize("Alias 'fake' created")

    # Use the alias
    out, err = run_cmd(base_app, 'fake')
    assert not out
    assert not err


def test_alias_list_invalid_alias(base_app):
    # Look up invalid alias
    out, err = run_cmd(base_app, 'alias list invalid')
    assert "Alias 'invalid' not found" in err[0]
    assert base_app.last_result == {}


def test_alias_delete(base_app):
    # Create an alias
    run_cmd(base_app, 'alias create fake run_pyscript')

    # Delete the alias
    out, err = run_cmd(base_app, 'alias delete fake')
    assert out == normalize("Alias 'fake' deleted")
    assert base_app.last_result is True


def test_alias_delete_all(base_app):
    out, err = run_cmd(base_app, 'alias delete --all')
    assert out == normalize("All aliases deleted")
    assert base_app.last_result is True


def test_alias_delete_non_existing(base_app):
    out, err = run_cmd(base_app, 'alias delete fake')
    assert "Alias 'fake' does not exist" in err[0]
    assert base_app.last_result is True


def test_alias_delete_no_name(base_app):
    out, err = run_cmd(base_app, 'alias delete')
    assert "Either --all or alias name(s)" in err[0]
    assert base_app.last_result is False


def test_multiple_aliases(base_app):
    alias1 = 'h1'
    alias2 = 'h2'
    run_cmd(base_app, 'alias create {} help'.format(alias1))
    run_cmd(base_app, 'alias create {} help -v'.format(alias2))
    out, err = run_cmd(base_app, alias1)
    verify_help_text(base_app, out)

    out, err = run_cmd(base_app, alias2)
    verify_help_text(base_app, out)


def test_macro_no_subcommand(base_app):
    out, err = run_cmd(base_app, 'macro')
    assert "Usage: macro [-h]" in err[0]
    assert "Error: the following arguments are required: SUBCOMMAND" in err[1]


def test_macro_create(base_app):
    # Create the macro
    out, err = run_cmd(base_app, 'macro create fake run_pyscript')
    assert out == normalize("Macro 'fake' created")
    assert base_app.last_result is True

    # Use the macro
    out, err = run_cmd(base_app, 'fake')
    assert "the following arguments are required: script_path" in err[1]

    # See a list of macros
    out, err = run_cmd(base_app, 'macro list')
    assert out == normalize('macro create fake run_pyscript')
    assert len(base_app.last_result) == len(base_app.macros)
    assert base_app.last_result['fake'] == "run_pyscript"

    # Look up the new macro
    out, err = run_cmd(base_app, 'macro list fake')
    assert out == normalize('macro create fake run_pyscript')
    assert len(base_app.last_result) == 1
    assert base_app.last_result['fake'] == "run_pyscript"

    # Overwrite macro
    out, err = run_cmd(base_app, 'macro create fake help')
    assert out == normalize("Macro 'fake' overwritten")
    assert base_app.last_result is True

    # Look up the updated macro
    out, err = run_cmd(base_app, 'macro list fake')
    assert out == normalize('macro create fake help')
    assert len(base_app.last_result) == 1
    assert base_app.last_result['fake'] == "help"


def test_macro_create_with_quoted_tokens(base_app):
    """Demonstrate that quotes in macro value will be preserved"""
    macro_name = "fake"
    macro_command = 'help ">" "out file.txt" ";"'
    create_command = f"macro create {macro_name} {macro_command}"

    # Create the macro
    out, err = run_cmd(base_app, create_command)
    assert out == normalize("Macro 'fake' created")

    # Look up the new macro and verify all quotes are preserved
    out, err = run_cmd(base_app, 'macro list fake')
    assert out == normalize(create_command)
    assert len(base_app.last_result) == 1
    assert base_app.last_result[macro_name] == macro_command


@pytest.mark.parametrize('macro_name', invalid_command_name)
def test_macro_create_invalid_name(base_app, macro_name):
    out, err = run_cmd(base_app, 'macro create {} help'.format(macro_name))
    assert "Invalid macro name" in err[0]
    assert base_app.last_result is False


def test_macro_create_with_command_name(base_app):
    out, err = run_cmd(base_app, 'macro create help stuff')
    assert "Macro cannot have the same name as a command" in err[0]
    assert base_app.last_result is False


def test_macro_create_with_alias_name(base_app):
    macro = "my_macro"
    run_cmd(base_app, 'alias create {} help'.format(macro))
    out, err = run_cmd(base_app, 'macro create {} help'.format(macro))
    assert "Macro cannot have the same name as an alias" in err[0]
    assert base_app.last_result is False


def test_macro_create_with_args(base_app):
    # Create the macro
    out, err = run_cmd(base_app, 'macro create fake {1} {2}')
    assert out == normalize("Macro 'fake' created")

    # Run the macro
    out, err = run_cmd(base_app, 'fake help -v')
    verify_help_text(base_app, out)


def test_macro_create_with_escaped_args(base_app):
    # Create the macro
    out, err = run_cmd(base_app, 'macro create fake help {{1}}')
    assert out == normalize("Macro 'fake' created")

    # Run the macro
    out, err = run_cmd(base_app, 'fake')
    assert err[0].startswith('No help on {1}')


def test_macro_usage_with_missing_args(base_app):
    # Create the macro
    out, err = run_cmd(base_app, 'macro create fake help {1} {2}')
    assert out == normalize("Macro 'fake' created")

    # Run the macro
    out, err = run_cmd(base_app, 'fake arg1')
    assert "expects at least 2 arguments" in err[0]


def test_macro_usage_with_exta_args(base_app):
    # Create the macro
    out, err = run_cmd(base_app, 'macro create fake help {1}')
    assert out == normalize("Macro 'fake' created")

    # Run the macro
    out, err = run_cmd(base_app, 'fake alias create')
    assert "Usage: alias create" in out[0]


def test_macro_create_with_missing_arg_nums(base_app):
    # Create the macro
    out, err = run_cmd(base_app, 'macro create fake help {1} {3}')
    assert "Not all numbers between 1 and 3" in err[0]
    assert base_app.last_result is False


def test_macro_create_with_invalid_arg_num(base_app):
    # Create the macro
    out, err = run_cmd(base_app, 'macro create fake help {1} {-1} {0}')
    assert "Argument numbers must be greater than 0" in err[0]
    assert base_app.last_result is False


def test_macro_create_with_unicode_numbered_arg(base_app):
    # Create the macro expecting 1 argument
    out, err = run_cmd(base_app, 'macro create fake help {\N{ARABIC-INDIC DIGIT ONE}}')
    assert out == normalize("Macro 'fake' created")

    # Run the macro
    out, err = run_cmd(base_app, 'fake')
    assert "expects at least 1 argument" in err[0]


def test_macro_create_with_missing_unicode_arg_nums(base_app):
    out, err = run_cmd(base_app, 'macro create fake help {1} {\N{ARABIC-INDIC DIGIT THREE}}')
    assert "Not all numbers between 1 and 3" in err[0]
    assert base_app.last_result is False


def test_macro_that_resolves_into_comment(base_app):
    # Create the macro
    out, err = run_cmd(base_app, 'macro create fake {1} blah blah')
    assert out == normalize("Macro 'fake' created")

    # Use the macro
    out, err = run_cmd(base_app, 'fake ' + constants.COMMENT_CHAR)
    assert not out
    assert not err


def test_macro_list_invalid_macro(base_app):
    # Look up invalid macro
    out, err = run_cmd(base_app, 'macro list invalid')
    assert "Macro 'invalid' not found" in err[0]
    assert base_app.last_result == {}


def test_macro_delete(base_app):
    # Create an macro
    run_cmd(base_app, 'macro create fake run_pyscript')

    # Delete the macro
    out, err = run_cmd(base_app, 'macro delete fake')
    assert out == normalize("Macro 'fake' deleted")
    assert base_app.last_result is True


def test_macro_delete_all(base_app):
    out, err = run_cmd(base_app, 'macro delete --all')
    assert out == normalize("All macros deleted")
    assert base_app.last_result is True


def test_macro_delete_non_existing(base_app):
    out, err = run_cmd(base_app, 'macro delete fake')
    assert "Macro 'fake' does not exist" in err[0]
    assert base_app.last_result is True


def test_macro_delete_no_name(base_app):
    out, err = run_cmd(base_app, 'macro delete')
    assert "Either --all or macro name(s)" in err[0]
    assert base_app.last_result is False


def test_multiple_macros(base_app):
    macro1 = 'h1'
    macro2 = 'h2'
    run_cmd(base_app, 'macro create {} help'.format(macro1))
    run_cmd(base_app, 'macro create {} help -v'.format(macro2))
    out, err = run_cmd(base_app, macro1)
    verify_help_text(base_app, out)

    out2, err2 = run_cmd(base_app, macro2)
    verify_help_text(base_app, out2)
    assert len(out2) > len(out)


def test_nonexistent_macro(base_app):
    from cmd2.parsing import (
        StatementParser,
    )

    exception = None

    try:
        base_app._resolve_macro(StatementParser().parse('fake'))
    except KeyError as e:
        exception = e

    assert exception is not None


@with_ansi_style(ansi.AllowStyle.ALWAYS)
def test_perror_style(base_app, capsys):
    msg = 'testing...'
    end = '\n'
    base_app.perror(msg)
    out, err = capsys.readouterr()
    assert err == ansi.style_error(msg) + end


@with_ansi_style(ansi.AllowStyle.ALWAYS)
def test_perror_no_style(base_app, capsys):
    msg = 'testing...'
    end = '\n'
    base_app.perror(msg, apply_style=False)
    out, err = capsys.readouterr()
    assert err == msg + end


@with_ansi_style(ansi.AllowStyle.ALWAYS)
def test_pexcept_style(base_app, capsys):
    msg = Exception('testing...')

    base_app.pexcept(msg)
    out, err = capsys.readouterr()
    assert err.startswith(ansi.style_error("EXCEPTION of type 'Exception' occurred with message: testing..."))


@with_ansi_style(ansi.AllowStyle.ALWAYS)
def test_pexcept_no_style(base_app, capsys):
    msg = Exception('testing...')

    base_app.pexcept(msg, apply_style=False)
    out, err = capsys.readouterr()
    assert err.startswith("EXCEPTION of type 'Exception' occurred with message: testing...")


@with_ansi_style(ansi.AllowStyle.ALWAYS)
def test_pexcept_not_exception(base_app, capsys):
    # Pass in a msg that is not an Exception object
    msg = False

    base_app.pexcept(msg)
    out, err = capsys.readouterr()
    assert err.startswith(ansi.style_error(msg))


def test_ppaged(outsim_app):
    msg = 'testing...'
    end = '\n'
    outsim_app.ppaged(msg)
    out = outsim_app.stdout.getvalue()
    assert out == msg + end


@with_ansi_style(ansi.AllowStyle.TERMINAL)
def test_ppaged_strips_ansi_when_redirecting(outsim_app):
    msg = 'testing...'
    end = '\n'
    outsim_app._redirecting = True
    outsim_app.ppaged(ansi.style(msg, fg=ansi.Fg.RED))
    out = outsim_app.stdout.getvalue()
    assert out == msg + end


@with_ansi_style(ansi.AllowStyle.ALWAYS)
def test_ppaged_strips_ansi_when_redirecting_if_always(outsim_app):
    msg = 'testing...'
    end = '\n'
    outsim_app._redirecting = True
    colored_msg = ansi.style(msg, fg=ansi.Fg.RED)
    outsim_app.ppaged(colored_msg)
    out = outsim_app.stdout.getvalue()
    assert out == colored_msg + end


# we override cmd.parseline() so we always get consistent
# command parsing by parent methods we don't override
# don't need to test all the parsing logic here, because
# parseline just calls StatementParser.parse_command_only()
def test_parseline_empty(base_app):
    statement = ''
    command, args, line = base_app.parseline(statement)
    assert not command
    assert not args
    assert not line


def test_parseline_quoted(base_app):
    statement = " command with 'partially completed quotes  "
    command, args, line = base_app.parseline(statement)
    assert command == 'command'
    assert args == "with 'partially completed quotes  "
    assert line == statement.lstrip()


def test_onecmd_raw_str_continue(outsim_app):
    line = "help"
    stop = outsim_app.onecmd(line)
    out = outsim_app.stdout.getvalue()
    assert not stop
    verify_help_text(outsim_app, out)


def test_onecmd_raw_str_quit(outsim_app):
    line = "quit"
    stop = outsim_app.onecmd(line)
    out = outsim_app.stdout.getvalue()
    assert stop
    assert out == ''


def test_onecmd_add_to_history(outsim_app):
    line = "help"
    saved_hist_len = len(outsim_app.history)

    # Allow command to be added to history
    outsim_app.onecmd(line, add_to_history=True)
    new_hist_len = len(outsim_app.history)
    assert new_hist_len == saved_hist_len + 1

    saved_hist_len = new_hist_len

    # Prevent command from being added to history
    outsim_app.onecmd(line, add_to_history=False)
    new_hist_len = len(outsim_app.history)
    assert new_hist_len == saved_hist_len


def test_get_all_commands(base_app):
    # Verify that the base app has the expected commands
    commands = base_app.get_all_commands()
    expected_commands = [
        '_relative_run_script',
        'alias',
        'edit',
        'eof',
        'help',
        'history',
        'ipy',
        'macro',
        'py',
        'quit',
        'run_pyscript',
        'run_script',
        'set',
        'shell',
        'shortcuts',
    ]
    assert commands == expected_commands


def test_get_help_topics(base_app):
    # Verify that the base app has no additional help_foo methods
    custom_help = base_app.get_help_topics()
    assert len(custom_help) == 0


def test_get_help_topics_hidden():
    # Verify get_help_topics() filters out hidden commands
    class TestApp(cmd2.Cmd):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)

        def do_my_cmd(self, args):
            pass

        def help_my_cmd(self, args):
            pass

    app = TestApp()
    assert 'my_cmd' in app.get_help_topics()

    app.hidden_commands.append('my_cmd')
    assert 'my_cmd' not in app.get_help_topics()


class ReplWithExitCode(cmd2.Cmd):
    """Example cmd2 application where we can specify an exit code when existing."""

    def __init__(self):
        super().__init__(allow_cli_args=False)

    @cmd2.with_argument_list
    def do_exit(self, arg_list) -> bool:
        """Exit the application with an optional exit code.

        Usage:  exit [exit_code]
            Where:
                * exit_code - integer exit code to return to the shell"""
        # If an argument was provided
        if arg_list:
            try:
                self.exit_code = int(arg_list[0])
            except ValueError:
                self.perror("{} isn't a valid integer exit code".format(arg_list[0]))
                self.exit_code = 1

        # Return True to stop the command loop
        return True

    def postloop(self) -> None:
        """Hook method executed once when the cmdloop() method is about to return."""
        self.poutput('exiting with code: {}'.format(self.exit_code))


@pytest.fixture
def exit_code_repl():
    app = ReplWithExitCode()
    app.stdout = utils.StdSim(app.stdout)
    return app


def test_exit_code_default(exit_code_repl):
    app = exit_code_repl
    app.use_rawinput = True

    # Mock out the input call so we don't actually wait for a user's response on stdin
    m = mock.MagicMock(name='input', return_value='exit')
    builtins.input = m

    expected = 'exiting with code: 0\n'

    # Run the command loop
    app.cmdloop()
    out = app.stdout.getvalue()
    assert out == expected


def test_exit_code_nonzero(exit_code_repl):
    app = exit_code_repl
    app.use_rawinput = True

    # Mock out the input call so we don't actually wait for a user's response on stdin
    m = mock.MagicMock(name='input', return_value='exit 23')
    builtins.input = m

    expected = 'exiting with code: 23\n'

    # Run the command loop
    app.cmdloop()
    out = app.stdout.getvalue()
    assert out == expected


class AnsiApp(cmd2.Cmd):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def do_echo(self, args):
        self.poutput(args)
        self.perror(args)

    def do_echo_error(self, args):
        self.poutput(ansi.style(args, fg=ansi.Fg.RED))
        # perror uses colors by default
        self.perror(args)


@with_ansi_style(ansi.AllowStyle.ALWAYS)
def test_ansi_pouterr_always_tty(mocker, capsys):
    app = AnsiApp()
    mocker.patch.object(app.stdout, 'isatty', return_value=True)
    mocker.patch.object(sys.stderr, 'isatty', return_value=True)

    app.onecmd_plus_hooks('echo_error oopsie')
    out, err = capsys.readouterr()
    # if colors are on, the output should have some ANSI style sequences in it
    assert len(out) > len('oopsie\n')
    assert 'oopsie' in out
    assert len(err) > len('oopsie\n')
    assert 'oopsie' in err

    # but this one shouldn't
    app.onecmd_plus_hooks('echo oopsie')
    out, err = capsys.readouterr()
    assert out == 'oopsie\n'
    # errors always have colors
    assert len(err) > len('oopsie\n')
    assert 'oopsie' in err


@with_ansi_style(ansi.AllowStyle.ALWAYS)
def test_ansi_pouterr_always_notty(mocker, capsys):
    app = AnsiApp()
    mocker.patch.object(app.stdout, 'isatty', return_value=False)
    mocker.patch.object(sys.stderr, 'isatty', return_value=False)

    app.onecmd_plus_hooks('echo_error oopsie')
    out, err = capsys.readouterr()
    # if colors are on, the output should have some ANSI style sequences in it
    assert len(out) > len('oopsie\n')
    assert 'oopsie' in out
    assert len(err) > len('oopsie\n')
    assert 'oopsie' in err

    # but this one shouldn't
    app.onecmd_plus_hooks('echo oopsie')
    out, err = capsys.readouterr()
    assert out == 'oopsie\n'
    # errors always have colors
    assert len(err) > len('oopsie\n')
    assert 'oopsie' in err


@with_ansi_style(ansi.AllowStyle.TERMINAL)
def test_ansi_terminal_tty(mocker, capsys):
    app = AnsiApp()
    mocker.patch.object(app.stdout, 'isatty', return_value=True)
    mocker.patch.object(sys.stderr, 'isatty', return_value=True)

    app.onecmd_plus_hooks('echo_error oopsie')
    # if colors are on, the output should have some ANSI style sequences in it
    out, err = capsys.readouterr()
    assert len(out) > len('oopsie\n')
    assert 'oopsie' in out
    assert len(err) > len('oopsie\n')
    assert 'oopsie' in err

    # but this one shouldn't
    app.onecmd_plus_hooks('echo oopsie')
    out, err = capsys.readouterr()
    assert out == 'oopsie\n'
    assert len(err) > len('oopsie\n')
    assert 'oopsie' in err


@with_ansi_style(ansi.AllowStyle.TERMINAL)
def test_ansi_terminal_notty(mocker, capsys):
    app = AnsiApp()
    mocker.patch.object(app.stdout, 'isatty', return_value=False)
    mocker.patch.object(sys.stderr, 'isatty', return_value=False)

    app.onecmd_plus_hooks('echo_error oopsie')
    out, err = capsys.readouterr()
    assert out == err == 'oopsie\n'

    app.onecmd_plus_hooks('echo oopsie')
    out, err = capsys.readouterr()
    assert out == err == 'oopsie\n'


@with_ansi_style(ansi.AllowStyle.NEVER)
def test_ansi_never_tty(mocker, capsys):
    app = AnsiApp()
    mocker.patch.object(app.stdout, 'isatty', return_value=True)
    mocker.patch.object(sys.stderr, 'isatty', return_value=True)

    app.onecmd_plus_hooks('echo_error oopsie')
    out, err = capsys.readouterr()
    assert out == err == 'oopsie\n'

    app.onecmd_plus_hooks('echo oopsie')
    out, err = capsys.readouterr()
    assert out == err == 'oopsie\n'


@with_ansi_style(ansi.AllowStyle.NEVER)
def test_ansi_never_notty(mocker, capsys):
    app = AnsiApp()
    mocker.patch.object(app.stdout, 'isatty', return_value=False)
    mocker.patch.object(sys.stderr, 'isatty', return_value=False)

    app.onecmd_plus_hooks('echo_error oopsie')
    out, err = capsys.readouterr()
    assert out == err == 'oopsie\n'

    app.onecmd_plus_hooks('echo oopsie')
    out, err = capsys.readouterr()
    assert out == err == 'oopsie\n'


class DisableCommandsApp(cmd2.Cmd):
    """Class for disabling commands"""

    category_name = "Test Category"

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    @cmd2.with_category(category_name)
    def do_has_helper_funcs(self, arg):
        self.poutput("The real has_helper_funcs")

    def help_has_helper_funcs(self):
        self.poutput('Help for has_helper_funcs')

    def complete_has_helper_funcs(self, *args):
        return ['result']

    @cmd2.with_category(category_name)
    def do_has_no_helper_funcs(self, arg):
        """Help for has_no_helper_funcs"""
        self.poutput("The real has_no_helper_funcs")


@pytest.fixture
def disable_commands_app():
    app = DisableCommandsApp()
    return app


def test_disable_and_enable_category(disable_commands_app):
    ##########################################################################
    # Disable the category
    ##########################################################################
    message_to_print = 'These commands are currently disabled'
    disable_commands_app.disable_category(disable_commands_app.category_name, message_to_print)

    # Make sure all the commands and help on those commands displays the message
    out, err = run_cmd(disable_commands_app, 'has_helper_funcs')
    assert err[0].startswith(message_to_print)

    out, err = run_cmd(disable_commands_app, 'help has_helper_funcs')
    assert err[0].startswith(message_to_print)

    out, err = run_cmd(disable_commands_app, 'has_no_helper_funcs')
    assert err[0].startswith(message_to_print)

    out, err = run_cmd(disable_commands_app, 'help has_no_helper_funcs')
    assert err[0].startswith(message_to_print)

    # Make sure neither function completes
    text = ''
    line = 'has_helper_funcs {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    first_match = complete_tester(text, line, begidx, endidx, disable_commands_app)
    assert first_match is None

    text = ''
    line = 'has_no_helper_funcs {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    first_match = complete_tester(text, line, begidx, endidx, disable_commands_app)
    assert first_match is None

    # Make sure both commands are invisible
    visible_commands = disable_commands_app.get_visible_commands()
    assert 'has_helper_funcs' not in visible_commands
    assert 'has_no_helper_funcs' not in visible_commands

    # Make sure get_help_topics() filters out disabled commands
    help_topics = disable_commands_app.get_help_topics()
    assert 'has_helper_funcs' not in help_topics

    ##########################################################################
    # Enable the category
    ##########################################################################
    disable_commands_app.enable_category(disable_commands_app.category_name)

    # Make sure all the commands and help on those commands are restored
    out, err = run_cmd(disable_commands_app, 'has_helper_funcs')
    assert out[0] == "The real has_helper_funcs"

    out, err = run_cmd(disable_commands_app, 'help has_helper_funcs')
    assert out[0] == "Help for has_helper_funcs"

    out, err = run_cmd(disable_commands_app, 'has_no_helper_funcs')
    assert out[0] == "The real has_no_helper_funcs"

    out, err = run_cmd(disable_commands_app, 'help has_no_helper_funcs')
    assert out[0] == "Help for has_no_helper_funcs"

    # has_helper_funcs should complete now
    text = ''
    line = 'has_helper_funcs {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    first_match = complete_tester(text, line, begidx, endidx, disable_commands_app)
    assert first_match is not None and disable_commands_app.completion_matches == ['result ']

    # has_no_helper_funcs had no completer originally, so there should be no results
    text = ''
    line = 'has_no_helper_funcs {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    first_match = complete_tester(text, line, begidx, endidx, disable_commands_app)
    assert first_match is None

    # Make sure both commands are visible
    visible_commands = disable_commands_app.get_visible_commands()
    assert 'has_helper_funcs' in visible_commands
    assert 'has_no_helper_funcs' in visible_commands

    # Make sure get_help_topics() contains our help function
    help_topics = disable_commands_app.get_help_topics()
    assert 'has_helper_funcs' in help_topics


def test_enable_enabled_command(disable_commands_app):
    # Test enabling a command that is not disabled
    saved_len = len(disable_commands_app.disabled_commands)
    disable_commands_app.enable_command('has_helper_funcs')

    # The number of disabled_commands should not have changed
    assert saved_len == len(disable_commands_app.disabled_commands)


def test_disable_fake_command(disable_commands_app):
    with pytest.raises(AttributeError):
        disable_commands_app.disable_command('fake', 'fake message')


def test_disable_command_twice(disable_commands_app):
    saved_len = len(disable_commands_app.disabled_commands)
    message_to_print = 'These commands are currently disabled'
    disable_commands_app.disable_command('has_helper_funcs', message_to_print)

    # The length of disabled_commands should have increased one
    new_len = len(disable_commands_app.disabled_commands)
    assert saved_len == new_len - 1
    saved_len = new_len

    # Disable again and the length should not change
    disable_commands_app.disable_command('has_helper_funcs', message_to_print)
    new_len = len(disable_commands_app.disabled_commands)
    assert saved_len == new_len


def test_disabled_command_not_in_history(disable_commands_app):
    message_to_print = 'These commands are currently disabled'
    disable_commands_app.disable_command('has_helper_funcs', message_to_print)

    saved_len = len(disable_commands_app.history)
    run_cmd(disable_commands_app, 'has_helper_funcs')
    assert saved_len == len(disable_commands_app.history)


def test_disabled_message_command_name(disable_commands_app):
    message_to_print = '{} is currently disabled'.format(COMMAND_NAME)
    disable_commands_app.disable_command('has_helper_funcs', message_to_print)

    out, err = run_cmd(disable_commands_app, 'has_helper_funcs')
    assert err[0].startswith('has_helper_funcs is currently disabled')


@pytest.mark.parametrize('silence_startup_script', [True, False])
def test_startup_script(request, capsys, silence_startup_script):
    test_dir = os.path.dirname(request.module.__file__)
    startup_script = os.path.join(test_dir, '.cmd2rc')
    app = cmd2.Cmd(allow_cli_args=False, startup_script=startup_script, silence_startup_script=silence_startup_script)
    assert len(app._startup_commands) == 1
    app._startup_commands.append('quit')
    app.cmdloop()

    out, err = capsys.readouterr()
    if silence_startup_script:
        assert not out
    else:
        assert out

    out, err = run_cmd(app, 'alias list')
    assert len(out) > 1
    assert 'alias create ls' in out[0]


@pytest.mark.parametrize('startup_script', odd_file_names)
def test_startup_script_with_odd_file_names(startup_script):
    """Test file names with various patterns"""
    # Mock os.path.exists to trick cmd2 into adding this script to its startup commands
    saved_exists = os.path.exists
    os.path.exists = mock.MagicMock(name='exists', return_value=True)

    app = cmd2.Cmd(allow_cli_args=False, startup_script=startup_script)
    assert len(app._startup_commands) == 1
    assert app._startup_commands[0] == "run_script {}".format(utils.quote_string(os.path.abspath(startup_script)))

    # Restore os.path.exists
    os.path.exists = saved_exists


def test_transcripts_at_init():
    transcript_files = ['foo', 'bar']
    app = cmd2.Cmd(allow_cli_args=False, transcript_files=transcript_files)
    assert app._transcript_files == transcript_files


def test_columnize_too_wide(outsim_app):
    """Test calling columnize with output that wider than display_width"""
    str_list = ["way too wide", "much wider than the first"]
    outsim_app.columnize(str_list, display_width=5)

    expected = "\n".join(str_list) + "\n"
    assert outsim_app.stdout.getvalue() == expected


def test_command_parser_retrieval(outsim_app: cmd2.Cmd):
    # Pass something that isn't a method
    not_a_method = "just a string"
    assert outsim_app._command_parsers.get(not_a_method) is None

    # Pass a non-command method
    assert outsim_app._command_parsers.get(outsim_app.__init__) is None


def test_command_synonym_parser():
    # Make sure a command synonym returns the same parser as what it aliases
    class SynonymApp(cmd2.cmd2.Cmd):
        do_synonym = cmd2.cmd2.Cmd.do_help

    app = SynonymApp()

    synonym_parser = app._command_parsers.get(app.do_synonym)
    help_parser = app._command_parsers.get(app.do_help)

    assert synonym_parser is not None
    assert synonym_parser is help_parser
