# coding=utf-8
# flake8: noqa E302
"""
Unit/functional testing for readline tab completion functions in the cmd2.py module.

These are primarily tests related to readline completer functions which handle tab completion of cmd2/cmd commands,
file system paths, and shell commands.
"""

import enum
import os
import sys
from unittest import (
    mock,
)

import pytest

import cmd2
from cmd2 import (
    utils,
)
from examples.subcommands import (
    SubcommandsExample,
)

from .conftest import (
    complete_tester,
    normalize,
    run_cmd,
)

# List of strings used with completion functions
food_item_strs = ['Pizza', 'Ham', 'Ham Sandwich', 'Potato', 'Cheese "Pizza"']
sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball']
delimited_strs = [
    '/home/user/file.txt',
    '/home/user/file space.txt',
    '/home/user/prog.c',
    '/home/other user/maps',
    '/home/other user/tests',
]

# Dictionary used with flag based completion functions
flag_dict = {
    # Tab complete food items after -f and --food flag in command line
    '-f': food_item_strs,
    '--food': food_item_strs,
    # Tab complete sport items after -s and --sport flag in command line
    '-s': sport_item_strs,
    '--sport': sport_item_strs,
}

# Dictionary used with index based completion functions
index_dict = {
    1: food_item_strs,  # Tab complete food items at index 1 in command line
    2: sport_item_strs,  # Tab complete sport items at index 2 in command line
}


class CompletionsExample(cmd2.Cmd):
    """
    Example cmd2 application used to exercise tab completion tests
    """

    def __init__(self):
        cmd2.Cmd.__init__(self, multiline_commands=['test_multiline'])
        self.foo = 'bar'
        self.add_settable(
            utils.Settable(
                'foo',
                str,
                description="a settable param",
                settable_object=self,
                completer=CompletionsExample.complete_foo_val,
            )
        )

    def do_test_basic(self, args):
        pass

    def complete_test_basic(self, text, line, begidx, endidx):
        return self.basic_complete(text, line, begidx, endidx, food_item_strs)

    def do_test_delimited(self, args):
        pass

    def complete_test_delimited(self, text, line, begidx, endidx):
        return self.delimiter_complete(text, line, begidx, endidx, delimited_strs, '/')

    def do_test_sort_key(self, args):
        pass

    def complete_test_sort_key(self, text, line, begidx, endidx):
        num_strs = ['2', '11', '1']
        return self.basic_complete(text, line, begidx, endidx, num_strs)

    def do_test_raise_exception(self, args):
        pass

    def complete_test_raise_exception(self, text, line, begidx, endidx):
        raise IndexError("You are out of bounds!!")

    def do_test_multiline(self, args):
        pass

    def complete_test_multiline(self, text, line, begidx, endidx):
        return self.basic_complete(text, line, begidx, endidx, sport_item_strs)

    def do_test_no_completer(self, args):
        """Completing this should result in completedefault() being called"""
        pass

    def complete_foo_val(self, text, line, begidx, endidx, arg_tokens):
        """Supports unit testing cmd2.Cmd2.complete_set_val to confirm it passes all tokens in the set command"""
        if 'param' in arg_tokens:
            return ["SUCCESS"]
        else:
            return ["FAIL"]

    def completedefault(self, *ignored):
        """Method called to complete an input line when no command-specific
        complete_*() method is available.

        """
        return ['default']


@pytest.fixture
def cmd2_app():
    c = CompletionsExample()
    return c


def test_cmd2_command_completion_single(cmd2_app):
    text = 'he'
    line = text
    endidx = len(line)
    begidx = endidx - len(text)
    assert cmd2_app.completenames(text, line, begidx, endidx) == ['help']


def test_complete_command_single(cmd2_app):
    text = 'he'
    line = text
    endidx = len(line)
    begidx = endidx - len(text)

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


def test_complete_empty_arg(cmd2_app):
    text = ''
    line = 'help {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    expected = sorted(cmd2_app.get_visible_commands(), key=cmd2_app.default_sort_key)
    first_match = complete_tester(text, line, begidx, endidx, cmd2_app)

    assert first_match is not None and cmd2_app.completion_matches == expected


def test_complete_bogus_command(cmd2_app):
    text = ''
    line = 'fizbuzz {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

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


def test_complete_exception(cmd2_app, capsys):
    text = ''
    line = 'test_raise_exception {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
    out, err = capsys.readouterr()

    assert first_match is None
    assert "IndexError" in err


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

    # Macros do path completion
    test_dir = os.path.dirname(request.module.__file__)

    text = os.path.join(test_dir, 's')
    line = 'fake {}'.format(text)

    endidx = len(line)
    begidx = endidx - len(text)

    expected = [text + 'cript.py', text + 'cript.txt', text + 'cripts' + os.path.sep]
    first_match = complete_tester(text, line, begidx, endidx, base_app)
    assert first_match is not None and base_app.completion_matches == expected


def test_default_sort_key(cmd2_app):
    text = ''
    line = 'test_sort_key {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    # First do alphabetical sorting
    cmd2_app.default_sort_key = cmd2.Cmd.ALPHABETICAL_SORT_KEY
    expected = ['1', '11', '2']
    first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
    assert first_match is not None and cmd2_app.completion_matches == expected

    # Now switch to natural sorting
    cmd2_app.default_sort_key = cmd2.Cmd.NATURAL_SORT_KEY
    expected = ['1', '2', '11']
    first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
    assert first_match is not None and cmd2_app.completion_matches == expected


def test_cmd2_command_completion_multiple(cmd2_app):
    text = 'h'
    line = text
    endidx = len(line)
    begidx = endidx - len(text)
    assert cmd2_app.completenames(text, line, begidx, endidx) == ['help', 'history']


def test_cmd2_command_completion_nomatch(cmd2_app):
    text = 'fakecommand'
    line = text
    endidx = len(line)
    begidx = endidx - len(text)
    assert cmd2_app.completenames(text, line, begidx, endidx) == []


def test_cmd2_help_completion_single(cmd2_app):
    text = 'he'
    line = 'help {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    first_match = complete_tester(text, line, begidx, endidx, cmd2_app)

    # It is at end of line, so extra space is present
    assert first_match is not None and cmd2_app.completion_matches == ['help ']


def test_cmd2_help_completion_multiple(cmd2_app):
    text = 'h'
    line = 'help {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

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


def test_cmd2_help_completion_nomatch(cmd2_app):
    text = 'fakecommand'
    line = 'help {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

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


def test_set_allow_style_completion(cmd2_app):
    """Confirm that completing allow_style presents AllowStyle strings"""
    text = ''
    line = 'set allow_style'
    endidx = len(line)
    begidx = endidx - len(text)

    expected = [val.name.lower() for val in cmd2.ansi.AllowStyle]

    first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
    assert first_match
    assert cmd2_app.completion_matches == sorted(expected, key=cmd2_app.default_sort_key)


def test_set_bool_completion(cmd2_app):
    """Confirm that completing a boolean Settable presents true and false strings"""
    text = ''
    line = 'set debug'
    endidx = len(line)
    begidx = endidx - len(text)

    expected = ['false', 'true']

    first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
    assert first_match
    assert cmd2_app.completion_matches == sorted(expected, key=cmd2_app.default_sort_key)


def test_shell_command_completion_shortcut(cmd2_app):
    # Made sure ! runs a shell command and all matches start with ! since there
    # isn't a space between ! and the shell command. Display matches won't
    # begin with the !.
    if sys.platform == "win32":
        text = '!calc'
        expected = ['!calc.exe ']
        expected_display = ['calc.exe']
    else:
        text = '!egr'
        expected = ['!egrep ']
        expected_display = ['egrep']

    line = text
    endidx = len(line)
    begidx = 0

    first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
    assert first_match is not None and cmd2_app.completion_matches == expected and cmd2_app.display_matches == expected_display


def test_shell_command_completion_doesnt_match_wildcards(cmd2_app):
    if sys.platform == "win32":
        text = 'c*'
    else:
        text = 'e*'

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

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


def test_shell_command_completion_multiple(cmd2_app):
    if sys.platform == "win32":
        text = 'c'
        expected = 'calc.exe'
    else:
        text = 'l'
        expected = 'ls'

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

    first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
    assert first_match is not None and expected in cmd2_app.completion_matches


def test_shell_command_completion_nomatch(cmd2_app):
    text = 'zzzz'
    line = 'shell {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

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


def test_shell_command_completion_doesnt_complete_when_just_shell(cmd2_app):
    text = ''
    line = 'shell {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

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


def test_shell_command_completion_does_path_completion_when_after_command(cmd2_app, request):
    test_dir = os.path.dirname(request.module.__file__)

    text = os.path.join(test_dir, 'conftest')
    line = 'shell cat {}'.format(text)

    endidx = len(line)
    begidx = endidx - len(text)

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


def test_shell_command_complete_in_path(cmd2_app, request):
    test_dir = os.path.dirname(request.module.__file__)

    text = os.path.join(test_dir, 's')
    line = 'shell {}'.format(text)

    endidx = len(line)
    begidx = endidx - len(text)

    # Since this will look for directories and executables in the given path,
    # we expect to see the scripts dir among the results
    expected = os.path.join(test_dir, 'scripts' + os.path.sep)
    first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
    assert first_match is not None and expected in cmd2_app.completion_matches


def test_path_completion_single_end(cmd2_app, request):
    test_dir = os.path.dirname(request.module.__file__)

    text = os.path.join(test_dir, 'conftest')
    line = 'shell cat {}'.format(text)

    endidx = len(line)
    begidx = endidx - len(text)

    assert cmd2_app.path_complete(text, line, begidx, endidx) == [text + '.py']


def test_path_completion_multiple(cmd2_app, request):
    test_dir = os.path.dirname(request.module.__file__)

    text = os.path.join(test_dir, 's')
    line = 'shell cat {}'.format(text)

    endidx = len(line)
    begidx = endidx - len(text)

    matches = cmd2_app.path_complete(text, line, begidx, endidx)
    expected = [text + 'cript.py', text + 'cript.txt', text + 'cripts' + os.path.sep]
    assert matches == expected


def test_path_completion_nomatch(cmd2_app, request):
    test_dir = os.path.dirname(request.module.__file__)

    text = os.path.join(test_dir, 'fakepath')
    line = 'shell cat {}'.format(text)

    endidx = len(line)
    begidx = endidx - len(text)

    assert cmd2_app.path_complete(text, line, begidx, endidx) == []


def test_default_to_shell_completion(cmd2_app, request):
    cmd2_app.default_to_shell = True
    test_dir = os.path.dirname(request.module.__file__)

    text = os.path.join(test_dir, 'conftest')

    if sys.platform == "win32":
        command = 'calc.exe'
    else:
        command = 'egrep'

    # Make sure the command is on the testing system
    assert command in utils.get_exes_in_path(command)
    line = '{} {}'.format(command, text)

    endidx = len(line)
    begidx = endidx - len(text)

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


def test_path_completion_no_text(cmd2_app):
    # Run path complete with no search text which should show what's in cwd
    text = ''
    line = 'shell ls {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)
    completions_no_text = cmd2_app.path_complete(text, line, begidx, endidx)

    # Run path complete with path set to the CWD
    text = os.getcwd() + os.path.sep
    line = 'shell ls {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    # We have to strip off the path from the beginning since the matches are entire paths
    completions_cwd = [match.replace(text, '', 1) for match in cmd2_app.path_complete(text, line, begidx, endidx)]

    # Verify that the first test gave results for entries in the cwd
    assert completions_no_text == completions_cwd
    assert completions_cwd


def test_path_completion_no_path(cmd2_app):
    # Run path complete with search text that isn't preceded by a path. This should use CWD as the path.
    text = 'p'
    line = 'shell ls {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)
    completions_no_text = cmd2_app.path_complete(text, line, begidx, endidx)

    # Run path complete with path set to the CWD
    text = os.getcwd() + os.path.sep + text
    line = 'shell ls {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    # We have to strip off the path from the beginning since the matches are entire paths (Leave the 's')
    completions_cwd = [match.replace(text[:-1], '', 1) for match in cmd2_app.path_complete(text, line, begidx, endidx)]

    # Verify that the first test gave results for entries in the cwd
    assert completions_no_text == completions_cwd
    assert completions_cwd


@pytest.mark.skipif(sys.platform == 'win32', reason="this only applies on systems where the root directory is a slash")
def test_path_completion_cwd_is_root_dir(cmd2_app):
    # Change our CWD to root dir
    cwd = os.getcwd()
    os.chdir(os.path.sep)

    text = ''
    line = 'shell ls {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)
    completions = cmd2_app.path_complete(text, line, begidx, endidx)

    # No match should start with a slash
    assert not any(match.startswith(os.path.sep) for match in completions)

    # Restore CWD
    os.chdir(cwd)


def test_path_completion_doesnt_match_wildcards(cmd2_app, request):
    test_dir = os.path.dirname(request.module.__file__)

    text = os.path.join(test_dir, 'c*')
    line = 'shell cat {}'.format(text)

    endidx = len(line)
    begidx = endidx - len(text)

    # Currently path completion doesn't accept wildcards, so will always return empty results
    assert cmd2_app.path_complete(text, line, begidx, endidx) == []


def test_path_completion_complete_user(cmd2_app):
    import getpass

    user = getpass.getuser()

    text = '~{}'.format(user)
    line = 'shell fake {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)
    completions = cmd2_app.path_complete(text, line, begidx, endidx)

    expected = text + os.path.sep
    assert expected in completions


def test_path_completion_user_path_expansion(cmd2_app):
    # Run path with a tilde and a slash
    if sys.platform.startswith('win'):
        cmd = 'dir'
    else:
        cmd = 'ls'

    # Use a ~ which will be expanded into the user's home directory
    text = '~{}'.format(os.path.sep)
    line = 'shell {} {}'.format(cmd, text)
    endidx = len(line)
    begidx = endidx - len(text)
    completions_tilde_slash = [match.replace(text, '', 1) for match in cmd2_app.path_complete(text, line, begidx, endidx)]

    # Run path complete on the user's home directory
    text = os.path.expanduser('~') + os.path.sep
    line = 'shell {} {}'.format(cmd, text)
    endidx = len(line)
    begidx = endidx - len(text)
    completions_home = [match.replace(text, '', 1) for match in cmd2_app.path_complete(text, line, begidx, endidx)]

    assert completions_tilde_slash == completions_home


def test_path_completion_directories_only(cmd2_app, request):
    test_dir = os.path.dirname(request.module.__file__)

    text = os.path.join(test_dir, 's')
    line = 'shell cat {}'.format(text)

    endidx = len(line)
    begidx = endidx - len(text)

    expected = [text + 'cripts' + os.path.sep]

    assert cmd2_app.path_complete(text, line, begidx, endidx, path_filter=os.path.isdir) == expected


def test_basic_completion_single(cmd2_app):
    text = 'Pi'
    line = 'list_food -f {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    assert cmd2_app.basic_complete(text, line, begidx, endidx, food_item_strs) == ['Pizza']


def test_basic_completion_multiple(cmd2_app):
    text = ''
    line = 'list_food -f {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    matches = sorted(cmd2_app.basic_complete(text, line, begidx, endidx, food_item_strs))
    assert matches == sorted(food_item_strs)


def test_basic_completion_nomatch(cmd2_app):
    text = 'q'
    line = 'list_food -f {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    assert cmd2_app.basic_complete(text, line, begidx, endidx, food_item_strs) == []


def test_delimiter_completion(cmd2_app):
    text = '/home/'
    line = 'run_script {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    cmd2_app.delimiter_complete(text, line, begidx, endidx, delimited_strs, '/')

    # Remove duplicates from display_matches and sort it. This is typically done in complete().
    display_list = utils.remove_duplicates(cmd2_app.display_matches)
    display_list = utils.alphabetical_sort(display_list)

    assert display_list == ['other user', 'user']


def test_flag_based_completion_single(cmd2_app):
    text = 'Pi'
    line = 'list_food -f {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    assert cmd2_app.flag_based_complete(text, line, begidx, endidx, flag_dict) == ['Pizza']


def test_flag_based_completion_multiple(cmd2_app):
    text = ''
    line = 'list_food -f {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    matches = sorted(cmd2_app.flag_based_complete(text, line, begidx, endidx, flag_dict))
    assert matches == sorted(food_item_strs)


def test_flag_based_completion_nomatch(cmd2_app):
    text = 'q'
    line = 'list_food -f {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    assert cmd2_app.flag_based_complete(text, line, begidx, endidx, flag_dict) == []


def test_flag_based_default_completer(cmd2_app, request):
    test_dir = os.path.dirname(request.module.__file__)

    text = os.path.join(test_dir, 'c')
    line = 'list_food {}'.format(text)

    endidx = len(line)
    begidx = endidx - len(text)

    assert cmd2_app.flag_based_complete(text, line, begidx, endidx, flag_dict, all_else=cmd2_app.path_complete) == [
        text + 'onftest.py'
    ]


def test_flag_based_callable_completer(cmd2_app, request):
    test_dir = os.path.dirname(request.module.__file__)

    text = os.path.join(test_dir, 'c')
    line = 'list_food -o {}'.format(text)

    endidx = len(line)
    begidx = endidx - len(text)

    flag_dict['-o'] = cmd2_app.path_complete
    assert cmd2_app.flag_based_complete(text, line, begidx, endidx, flag_dict) == [text + 'onftest.py']


def test_index_based_completion_single(cmd2_app):
    text = 'Foo'
    line = 'command Pizza {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    assert cmd2_app.index_based_complete(text, line, begidx, endidx, index_dict) == ['Football']


def test_index_based_completion_multiple(cmd2_app):
    text = ''
    line = 'command Pizza {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    matches = sorted(cmd2_app.index_based_complete(text, line, begidx, endidx, index_dict))
    assert matches == sorted(sport_item_strs)


def test_index_based_completion_nomatch(cmd2_app):
    text = 'q'
    line = 'command {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)
    assert cmd2_app.index_based_complete(text, line, begidx, endidx, index_dict) == []


def test_index_based_default_completer(cmd2_app, request):
    test_dir = os.path.dirname(request.module.__file__)

    text = os.path.join(test_dir, 'c')
    line = 'command Pizza Bat Computer {}'.format(text)

    endidx = len(line)
    begidx = endidx - len(text)

    assert cmd2_app.index_based_complete(text, line, begidx, endidx, index_dict, all_else=cmd2_app.path_complete) == [
        text + 'onftest.py'
    ]


def test_index_based_callable_completer(cmd2_app, request):
    test_dir = os.path.dirname(request.module.__file__)

    text = os.path.join(test_dir, 'c')
    line = 'command Pizza Bat {}'.format(text)

    endidx = len(line)
    begidx = endidx - len(text)

    index_dict[3] = cmd2_app.path_complete
    assert cmd2_app.index_based_complete(text, line, begidx, endidx, index_dict) == [text + 'onftest.py']


def test_tokens_for_completion_quoted(cmd2_app):
    text = 'Pi'
    line = 'list_food "{}"'.format(text)
    endidx = len(line)
    begidx = endidx

    expected_tokens = ['list_food', 'Pi', '']
    expected_raw_tokens = ['list_food', '"Pi"', '']

    tokens, raw_tokens = cmd2_app.tokens_for_completion(line, begidx, endidx)
    assert expected_tokens == tokens
    assert expected_raw_tokens == raw_tokens


def test_tokens_for_completion_unclosed_quote(cmd2_app):
    text = 'Pi'
    line = 'list_food "{}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    expected_tokens = ['list_food', 'Pi']
    expected_raw_tokens = ['list_food', '"Pi']

    tokens, raw_tokens = cmd2_app.tokens_for_completion(line, begidx, endidx)
    assert expected_tokens == tokens
    assert expected_raw_tokens == raw_tokens


def test_tokens_for_completion_punctuation(cmd2_app):
    """Test that redirectors and terminators are word delimiters"""
    text = 'file'
    line = 'command | < ;>>{}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    expected_tokens = ['command', '|', '<', ';', '>>', 'file']
    expected_raw_tokens = ['command', '|', '<', ';', '>>', 'file']

    tokens, raw_tokens = cmd2_app.tokens_for_completion(line, begidx, endidx)
    assert expected_tokens == tokens
    assert expected_raw_tokens == raw_tokens


def test_tokens_for_completion_quoted_punctuation(cmd2_app):
    """Test that quoted punctuation characters are not word delimiters"""
    text = '>file'
    line = 'command "{}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    expected_tokens = ['command', '>file']
    expected_raw_tokens = ['command', '">file']

    tokens, raw_tokens = cmd2_app.tokens_for_completion(line, begidx, endidx)
    assert expected_tokens == tokens
    assert expected_raw_tokens == raw_tokens


def test_add_opening_quote_basic_no_text(cmd2_app):
    text = ''
    line = 'test_basic {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    # The whole list will be returned with no opening quotes added
    first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
    assert first_match is not None and cmd2_app.completion_matches == sorted(food_item_strs, key=cmd2_app.default_sort_key)


def test_add_opening_quote_basic_nothing_added(cmd2_app):
    text = 'P'
    line = 'test_basic {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

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


def test_add_opening_quote_basic_quote_added(cmd2_app):
    text = 'Ha'
    line = 'test_basic {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    expected = sorted(['"Ham', '"Ham Sandwich'], key=cmd2_app.default_sort_key)
    first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
    assert first_match is not None and cmd2_app.completion_matches == expected


def test_add_opening_quote_basic_single_quote_added(cmd2_app):
    text = 'Ch'
    line = 'test_basic {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    expected = ["'Cheese \"Pizza\"' "]
    first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
    assert first_match is not None and cmd2_app.completion_matches == expected


def test_add_opening_quote_basic_text_is_common_prefix(cmd2_app):
    # This tests when the text entered is the same as the common prefix of the matches
    text = 'Ham'
    line = 'test_basic {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    expected = sorted(['"Ham', '"Ham Sandwich'], key=cmd2_app.default_sort_key)
    first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
    assert first_match is not None and cmd2_app.completion_matches == expected


def test_add_opening_quote_delimited_no_text(cmd2_app):
    text = ''
    line = 'test_delimited {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    # The whole list will be returned with no opening quotes added
    first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
    assert first_match is not None and cmd2_app.completion_matches == sorted(delimited_strs, key=cmd2_app.default_sort_key)


def test_add_opening_quote_delimited_nothing_added(cmd2_app):
    text = '/ho'
    line = 'test_delimited {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    expected_matches = sorted(delimited_strs, key=cmd2_app.default_sort_key)
    expected_display = sorted(['other user', 'user'], key=cmd2_app.default_sort_key)

    first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
    assert (
        first_match is not None
        and cmd2_app.completion_matches == expected_matches
        and cmd2_app.display_matches == expected_display
    )


def test_add_opening_quote_delimited_quote_added(cmd2_app):
    text = '/home/user/fi'
    line = 'test_delimited {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    expected_common_prefix = '"/home/user/file'
    expected_display = sorted(['file.txt', 'file space.txt'], key=cmd2_app.default_sort_key)

    first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
    assert (
        first_match is not None
        and os.path.commonprefix(cmd2_app.completion_matches) == expected_common_prefix
        and cmd2_app.display_matches == expected_display
    )


def test_add_opening_quote_delimited_text_is_common_prefix(cmd2_app):
    # This tests when the text entered is the same as the common prefix of the matches
    text = '/home/user/file'
    line = 'test_delimited {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    expected_common_prefix = '"/home/user/file'
    expected_display = sorted(['file.txt', 'file space.txt'], key=cmd2_app.default_sort_key)

    first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
    assert (
        first_match is not None
        and os.path.commonprefix(cmd2_app.completion_matches) == expected_common_prefix
        and cmd2_app.display_matches == expected_display
    )


def test_add_opening_quote_delimited_space_in_prefix(cmd2_app):
    # This test when a space appears before the part of the string that is the display match
    text = '/home/oth'
    line = 'test_delimited {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    expected_common_prefix = '"/home/other user/'
    expected_display = ['maps', 'tests']

    first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
    assert (
        first_match is not None
        and os.path.commonprefix(cmd2_app.completion_matches) == expected_common_prefix
        and cmd2_app.display_matches == expected_display
    )


def test_no_completer(cmd2_app):
    text = ''
    line = 'test_no_completer {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

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


def test_wordbreak_in_command(cmd2_app):
    text = ''
    line = '"{}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
    assert first_match is None and not cmd2_app.completion_matches


def test_complete_multiline_on_single_line(cmd2_app):
    text = ''
    line = 'test_multiline {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    expected = sorted(sport_item_strs, key=cmd2_app.default_sort_key)
    first_match = complete_tester(text, line, begidx, endidx, cmd2_app)

    assert first_match is not None and cmd2_app.completion_matches == expected


def test_complete_multiline_on_multiple_lines(cmd2_app):
    # Set the same variables _complete_statement() sets when a user is entering data at a continuation prompt
    cmd2_app._at_continuation_prompt = True
    cmd2_app._multiline_in_progress = "test_multiline\n"

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

    expected = sorted(['Bat', 'Basket', 'Basketball'], key=cmd2_app.default_sort_key)
    first_match = complete_tester(text, line, begidx, endidx, cmd2_app)

    assert first_match is not None and cmd2_app.completion_matches == expected


# Used by redirect_complete tests
class RedirCompType(enum.Enum):
    SHELL_CMD = (1,)
    PATH = (2,)
    DEFAULT = (3,)
    NONE = 4


@pytest.mark.parametrize(
    'line, comp_type',
    [
        ('fake', RedirCompType.DEFAULT),
        ('fake arg', RedirCompType.DEFAULT),
        ('fake |', RedirCompType.SHELL_CMD),
        ('fake | grep', RedirCompType.PATH),
        ('fake | grep arg', RedirCompType.PATH),
        ('fake | grep >', RedirCompType.PATH),
        ('fake | grep > >', RedirCompType.NONE),
        ('fake | grep > file', RedirCompType.NONE),
        ('fake | grep > file >', RedirCompType.NONE),
        ('fake | grep > file |', RedirCompType.SHELL_CMD),
        ('fake | grep > file | grep', RedirCompType.PATH),
        ('fake | |', RedirCompType.NONE),
        ('fake | >', RedirCompType.NONE),
        ('fake >', RedirCompType.PATH),
        ('fake >>', RedirCompType.PATH),
        ('fake > >', RedirCompType.NONE),
        ('fake > |', RedirCompType.SHELL_CMD),
        ('fake >> file |', RedirCompType.SHELL_CMD),
        ('fake >> file | grep', RedirCompType.PATH),
        ('fake > file', RedirCompType.NONE),
        ('fake > file >', RedirCompType.NONE),
        ('fake > file >>', RedirCompType.NONE),
    ],
)
def test_redirect_complete(cmd2_app, monkeypatch, line, comp_type):
    # Test both cases of allow_redirection
    cmd2_app.allow_redirection = True
    for count in range(2):
        shell_cmd_complete_mock = mock.MagicMock(name='shell_cmd_complete')
        monkeypatch.setattr("cmd2.Cmd.shell_cmd_complete", shell_cmd_complete_mock)

        path_complete_mock = mock.MagicMock(name='path_complete')
        monkeypatch.setattr("cmd2.Cmd.path_complete", path_complete_mock)

        default_complete_mock = mock.MagicMock(name='fake_completer')

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

        cmd2_app._redirect_complete(text, line, begidx, endidx, default_complete_mock)

        if comp_type == RedirCompType.SHELL_CMD:
            shell_cmd_complete_mock.assert_called_once()
        elif comp_type == RedirCompType.PATH:
            path_complete_mock.assert_called_once()
        elif comp_type == RedirCompType.DEFAULT:
            default_complete_mock.assert_called_once()
        else:
            shell_cmd_complete_mock.assert_not_called()
            path_complete_mock.assert_not_called()
            default_complete_mock.assert_not_called()

        # Do the next test with allow_redirection as False
        cmd2_app.allow_redirection = False
        if comp_type != RedirCompType.DEFAULT:
            comp_type = RedirCompType.NONE


def test_complete_set_value(cmd2_app):
    text = ''
    line = 'set foo {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
    assert first_match == "SUCCESS "
    assert cmd2_app.completion_hint == "Hint:\n  value  a settable param\n"


def test_complete_set_value_invalid_settable(cmd2_app, capsys):
    text = ''
    line = 'set fake {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

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

    out, err = capsys.readouterr()
    assert "fake is not a settable parameter" in out


@pytest.fixture
def sc_app():
    c = SubcommandsExample()
    c.stdout = utils.StdSim(c.stdout)
    return c


def test_cmd2_subcommand_completion_single_end(sc_app):
    text = 'f'
    line = 'base {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    first_match = complete_tester(text, line, begidx, endidx, sc_app)

    # It is at end of line, so extra space is present
    assert first_match is not None and sc_app.completion_matches == ['foo ']


def test_cmd2_subcommand_completion_multiple(sc_app):
    text = ''
    line = 'base {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    first_match = complete_tester(text, line, begidx, endidx, sc_app)
    assert first_match is not None and sc_app.completion_matches == ['bar', 'foo', 'sport']


def test_cmd2_subcommand_completion_nomatch(sc_app):
    text = 'z'
    line = 'base {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

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


def test_help_subcommand_completion_single(sc_app):
    text = 'base'
    line = 'help {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    first_match = complete_tester(text, line, begidx, endidx, sc_app)

    # It is at end of line, so extra space is present
    assert first_match is not None and sc_app.completion_matches == ['base ']


def test_help_subcommand_completion_multiple(sc_app):
    text = ''
    line = 'help base {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    first_match = complete_tester(text, line, begidx, endidx, sc_app)
    assert first_match is not None and sc_app.completion_matches == ['bar', 'foo', 'sport']


def test_help_subcommand_completion_nomatch(sc_app):
    text = 'z'
    line = 'help base {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

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


def test_subcommand_tab_completion(sc_app):
    # This makes sure the correct completer for the sport subcommand is called
    text = 'Foot'
    line = 'base sport {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    first_match = complete_tester(text, line, begidx, endidx, sc_app)

    # It is at end of line, so extra space is present
    assert first_match is not None and sc_app.completion_matches == ['Football ']


def test_subcommand_tab_completion_with_no_completer(sc_app):
    # This tests what happens when a subcommand has no completer
    # In this case, the foo subcommand has no completer defined
    text = 'Foot'
    line = 'base foo {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

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


def test_subcommand_tab_completion_space_in_text(sc_app):
    text = 'B'
    line = 'base sport "Space {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    first_match = complete_tester(text, line, begidx, endidx, sc_app)

    assert first_match is not None and sc_app.completion_matches == ['Ball" '] and sc_app.display_matches == ['Space Ball']


####################################################


class SubcommandsWithUnknownExample(cmd2.Cmd):
    """
    Example cmd2 application where we a base command which has a couple subcommands
    and the "sport" subcommand has tab completion enabled.
    """

    def __init__(self):
        cmd2.Cmd.__init__(self)

    # subcommand functions for the base command
    def base_foo(self, args):
        """foo subcommand of base command"""
        self.poutput(args.x * args.y)

    def base_bar(self, args):
        """bar subcommand of base command"""
        self.poutput('((%s))' % args.z)

    def base_sport(self, args):
        """sport subcommand of base command"""
        self.poutput('Sport is {}'.format(args.sport))

    # create the top-level parser for the base command
    base_parser = cmd2.Cmd2ArgumentParser()
    base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help')

    # create the parser for the "foo" subcommand
    parser_foo = base_subparsers.add_parser('foo', help='foo help')
    parser_foo.add_argument('-x', type=int, default=1, help='integer')
    parser_foo.add_argument('y', type=float, help='float')
    parser_foo.set_defaults(func=base_foo)

    # create the parser for the "bar" subcommand
    parser_bar = base_subparsers.add_parser('bar', help='bar help')
    parser_bar.add_argument('z', help='string')
    parser_bar.set_defaults(func=base_bar)

    # create the parser for the "sport" subcommand
    parser_sport = base_subparsers.add_parser('sport', help='sport help')
    sport_arg = parser_sport.add_argument('sport', help='Enter name of a sport', choices=sport_item_strs)

    @cmd2.with_argparser(base_parser, with_unknown_args=True)
    def do_base(self, args):
        """Base command help"""
        func = getattr(args, 'func', None)
        if func is not None:
            # Call whatever subcommand function was selected
            func(self, args)
        else:
            # No subcommand was provided, so call help
            self.do_help('base')


@pytest.fixture
def scu_app():
    """Declare test fixture for with_argparser decorator"""
    app = SubcommandsWithUnknownExample()
    return app


def test_subcmd_with_unknown_completion_single_end(scu_app):
    text = 'f'
    line = 'base {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    first_match = complete_tester(text, line, begidx, endidx, scu_app)

    print('first_match: {}'.format(first_match))

    # It is at end of line, so extra space is present
    assert first_match is not None and scu_app.completion_matches == ['foo ']


def test_subcmd_with_unknown_completion_multiple(scu_app):
    text = ''
    line = 'base {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    first_match = complete_tester(text, line, begidx, endidx, scu_app)
    assert first_match is not None and scu_app.completion_matches == ['bar', 'foo', 'sport']


def test_subcmd_with_unknown_completion_nomatch(scu_app):
    text = 'z'
    line = 'base {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

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


def test_help_subcommand_completion_single_scu(scu_app):
    text = 'base'
    line = 'help {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    first_match = complete_tester(text, line, begidx, endidx, scu_app)

    # It is at end of line, so extra space is present
    assert first_match is not None and scu_app.completion_matches == ['base ']


def test_help_subcommand_completion_multiple_scu(scu_app):
    text = ''
    line = 'help base {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    first_match = complete_tester(text, line, begidx, endidx, scu_app)
    assert first_match is not None and scu_app.completion_matches == ['bar', 'foo', 'sport']


def test_help_subcommand_completion_with_flags_before_command(scu_app):
    text = ''
    line = 'help -h -v base {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    first_match = complete_tester(text, line, begidx, endidx, scu_app)
    assert first_match is not None and scu_app.completion_matches == ['bar', 'foo', 'sport']


def test_complete_help_subcommands_with_blank_command(scu_app):
    text = ''
    line = 'help "" {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    first_match = complete_tester(text, line, begidx, endidx, scu_app)
    assert first_match is None and not scu_app.completion_matches


def test_help_subcommand_completion_nomatch_scu(scu_app):
    text = 'z'
    line = 'help base {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

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


def test_subcommand_tab_completion_scu(scu_app):
    # This makes sure the correct completer for the sport subcommand is called
    text = 'Foot'
    line = 'base sport {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    first_match = complete_tester(text, line, begidx, endidx, scu_app)

    # It is at end of line, so extra space is present
    assert first_match is not None and scu_app.completion_matches == ['Football ']


def test_subcommand_tab_completion_with_no_completer_scu(scu_app):
    # This tests what happens when a subcommand has no completer
    # In this case, the foo subcommand has no completer defined
    text = 'Foot'
    line = 'base foo {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

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


def test_subcommand_tab_completion_space_in_text_scu(scu_app):
    text = 'B'
    line = 'base sport "Space {}'.format(text)
    endidx = len(line)
    begidx = endidx - len(text)

    first_match = complete_tester(text, line, begidx, endidx, scu_app)

    assert first_match is not None and scu_app.completion_matches == ['Ball" '] and scu_app.display_matches == ['Space Ball']
