# flake8: noqa E302
"""
Unit/functional testing for argparse customizations in cmd2
"""
import argparse

import pytest

import cmd2
from cmd2 import (
    Cmd2ArgumentParser,
    constants,
)
from cmd2.argparse_custom import (
    generate_range_error,
)

from .conftest import (
    run_cmd,
)


class ApCustomTestApp(cmd2.Cmd):
    """Test app for cmd2's argparse customization"""

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

    range_parser = Cmd2ArgumentParser()
    range_parser.add_argument('--arg0', nargs=1)
    range_parser.add_argument('--arg1', nargs=2)
    range_parser.add_argument('--arg2', nargs=(3,))
    range_parser.add_argument('--arg3', nargs=(2, 3))
    range_parser.add_argument('--arg4', nargs=argparse.ZERO_OR_MORE)
    range_parser.add_argument('--arg5', nargs=argparse.ONE_OR_MORE)

    @cmd2.with_argparser(range_parser)
    def do_range(self, _):
        pass


@pytest.fixture
def cust_app():
    return ApCustomTestApp()


def fake_func():
    pass


@pytest.mark.parametrize(
    'kwargs, is_valid',
    [
        ({'choices_provider': fake_func}, True),
        ({'completer': fake_func}, True),
        ({'choices_provider': fake_func, 'completer': fake_func}, False),
    ],
)
def test_apcustom_choices_callable_count(kwargs, is_valid):
    parser = Cmd2ArgumentParser()
    try:
        parser.add_argument('name', **kwargs)
        assert is_valid
    except ValueError as ex:
        assert not is_valid
        assert 'Only one of the following parameters' in str(ex)


@pytest.mark.parametrize('kwargs', [({'choices_provider': fake_func}), ({'completer': fake_func})])
def test_apcustom_no_choices_callables_alongside_choices(kwargs):
    with pytest.raises(TypeError) as excinfo:
        parser = Cmd2ArgumentParser()
        parser.add_argument('name', choices=['my', 'choices', 'list'], **kwargs)
    assert 'None of the following parameters can be used alongside a choices parameter' in str(excinfo.value)


@pytest.mark.parametrize('kwargs', [({'choices_provider': fake_func}), ({'completer': fake_func})])
def test_apcustom_no_choices_callables_when_nargs_is_0(kwargs):
    with pytest.raises(TypeError) as excinfo:
        parser = Cmd2ArgumentParser()
        parser.add_argument('name', action='store_true', **kwargs)
    assert 'None of the following parameters can be used on an action that takes no arguments' in str(excinfo.value)


def test_apcustom_usage():
    usage = "A custom usage statement"
    parser = Cmd2ArgumentParser(usage=usage)
    assert usage in parser.format_help()


def test_apcustom_nargs_help_format(cust_app):
    out, err = run_cmd(cust_app, 'help range')
    assert 'Usage: range [-h] [--arg0 ARG0] [--arg1 ARG1{2}] [--arg2 ARG2{3+}]' in out[0]
    assert '             [--arg3 ARG3{2..3}] [--arg4 [ARG4 [...]]] [--arg5 ARG5 [...]]' in out[1]


def test_apcustom_nargs_range_validation(cust_app):
    # nargs = (3,)
    out, err = run_cmd(cust_app, 'range --arg2 one two')
    assert 'Error: argument --arg2: expected at least 3 arguments' in err[2]

    out, err = run_cmd(cust_app, 'range --arg2 one two three')
    assert not err

    out, err = run_cmd(cust_app, 'range --arg2 one two three four')
    assert not err

    # nargs = (2,3)
    out, err = run_cmd(cust_app, 'range --arg3 one')
    assert 'Error: argument --arg3: expected 2 to 3 arguments' in err[2]

    out, err = run_cmd(cust_app, 'range --arg3 one two')
    assert not err

    out, err = run_cmd(cust_app, 'range --arg2 one two three')
    assert not err


@pytest.mark.parametrize(
    'nargs_tuple',
    [
        (),
        ('f', 5),
        (5, 'f'),
        (1, 2, 3),
    ],
)
def test_apcustom_narg_invalid_tuples(nargs_tuple):
    with pytest.raises(ValueError) as excinfo:
        parser = Cmd2ArgumentParser()
        parser.add_argument('invalid_tuple', nargs=nargs_tuple)
    assert 'Ranged values for nargs must be a tuple of 1 or 2 integers' in str(excinfo.value)


def test_apcustom_narg_tuple_order():
    with pytest.raises(ValueError) as excinfo:
        parser = Cmd2ArgumentParser()
        parser.add_argument('invalid_tuple', nargs=(2, 1))
    assert 'Invalid nargs range. The first value must be less than the second' in str(excinfo.value)


def test_apcustom_narg_tuple_negative():
    with pytest.raises(ValueError) as excinfo:
        parser = Cmd2ArgumentParser()
        parser.add_argument('invalid_tuple', nargs=(-1, 1))
    assert 'Negative numbers are invalid for nargs range' in str(excinfo.value)


# noinspection PyUnresolvedReferences
def test_apcustom_narg_tuple_zero_base():
    parser = Cmd2ArgumentParser()
    arg = parser.add_argument('arg', nargs=(0,))
    assert arg.nargs == argparse.ZERO_OR_MORE
    assert arg.nargs_range is None
    assert "[arg [...]]" in parser.format_help()

    parser = Cmd2ArgumentParser()
    arg = parser.add_argument('arg', nargs=(0, 1))
    assert arg.nargs == argparse.OPTIONAL
    assert arg.nargs_range is None
    assert "[arg]" in parser.format_help()

    parser = Cmd2ArgumentParser()
    arg = parser.add_argument('arg', nargs=(0, 3))
    assert arg.nargs == argparse.ZERO_OR_MORE
    assert arg.nargs_range == (0, 3)
    assert "arg{0..3}" in parser.format_help()


# noinspection PyUnresolvedReferences
def test_apcustom_narg_tuple_one_base():
    parser = Cmd2ArgumentParser()
    arg = parser.add_argument('arg', nargs=(1,))
    assert arg.nargs == argparse.ONE_OR_MORE
    assert arg.nargs_range is None
    assert "arg [...]" in parser.format_help()

    parser = Cmd2ArgumentParser()
    arg = parser.add_argument('arg', nargs=(1, 5))
    assert arg.nargs == argparse.ONE_OR_MORE
    assert arg.nargs_range == (1, 5)
    assert "arg{1..5}" in parser.format_help()


# noinspection PyUnresolvedReferences
def test_apcustom_narg_tuple_other_ranges():

    # Test range with no upper bound on max
    parser = Cmd2ArgumentParser()
    arg = parser.add_argument('arg', nargs=(2,))
    assert arg.nargs == argparse.ONE_OR_MORE
    assert arg.nargs_range == (2, constants.INFINITY)

    # Test finite range
    parser = Cmd2ArgumentParser()
    arg = parser.add_argument('arg', nargs=(2, 5))
    assert arg.nargs == argparse.ONE_OR_MORE
    assert arg.nargs_range == (2, 5)


def test_apcustom_print_message(capsys):
    import sys

    test_message = 'The test message'

    # Specify the file
    parser = Cmd2ArgumentParser()
    parser._print_message(test_message, file=sys.stdout)
    out, err = capsys.readouterr()
    assert test_message in out

    # Make sure file defaults to sys.stderr
    parser = Cmd2ArgumentParser()
    parser._print_message(test_message)
    out, err = capsys.readouterr()
    assert test_message in err


def test_generate_range_error():
    # max is INFINITY
    err_str = generate_range_error(1, constants.INFINITY)
    assert err_str == "expected at least 1 argument"

    err_str = generate_range_error(2, constants.INFINITY)
    assert err_str == "expected at least 2 arguments"

    # min and max are equal
    err_str = generate_range_error(1, 1)
    assert err_str == "expected 1 argument"

    err_str = generate_range_error(2, 2)
    assert err_str == "expected 2 arguments"

    # min and max are not equal
    err_str = generate_range_error(0, 1)
    assert err_str == "expected 0 to 1 argument"

    err_str = generate_range_error(0, 2)
    assert err_str == "expected 0 to 2 arguments"


def test_apcustom_required_options():
    # Make sure a 'required arguments' section shows when a flag is marked required
    parser = Cmd2ArgumentParser()
    parser.add_argument('--required_flag', required=True)
    assert 'required arguments' in parser.format_help()


def test_override_parser():
    """Test overriding argparse_custom.DEFAULT_ARGUMENT_PARSER"""
    import importlib

    from cmd2 import (
        argparse_custom,
    )

    # The standard parser is Cmd2ArgumentParser
    assert argparse_custom.DEFAULT_ARGUMENT_PARSER == Cmd2ArgumentParser

    # Set our parser module and force a reload of cmd2 so it loads the module
    argparse.cmd2_parser_module = 'examples.custom_parser'
    importlib.reload(cmd2)

    # Verify DEFAULT_ARGUMENT_PARSER is now our CustomParser
    from examples.custom_parser import (
        CustomParser,
    )

    assert argparse_custom.DEFAULT_ARGUMENT_PARSER == CustomParser


def test_apcustom_metavar_tuple():
    # Test the case when a tuple metavar is used with nargs an integer > 1
    parser = Cmd2ArgumentParser()
    parser.add_argument('--aflag', nargs=2, metavar=('foo', 'bar'), help='This is a test')
    assert '[--aflag foo bar]' in parser.format_help()


def test_cmd2_attribute_wrapper():
    initial_val = 5
    wrapper = cmd2.Cmd2AttributeWrapper(initial_val)
    assert wrapper.get() == initial_val

    new_val = 22
    wrapper.set(new_val)
    assert wrapper.get() == new_val


def test_completion_items_as_choices(capsys):
    """
    Test cmd2's patch to Argparse._check_value() which supports CompletionItems as choices.
    Choices are compared to CompletionItems.orig_value instead of the CompletionItem instance.
    """
    from cmd2.argparse_custom import (
        CompletionItem,
    )

    ##############################################################
    # Test CompletionItems with str values
    ##############################################################
    choices = [CompletionItem("1", "Description One"), CompletionItem("2", "Two")]
    parser = Cmd2ArgumentParser()
    parser.add_argument("choices_arg", type=str, choices=choices)

    # First test valid choices. Confirm the parsed data matches the correct type of str.
    args = parser.parse_args(['1'])
    assert args.choices_arg == '1'

    args = parser.parse_args(['2'])
    assert args.choices_arg == '2'

    # Next test invalid choice
    with pytest.raises(SystemExit):
        args = parser.parse_args(['3'])

    # Confirm error text contains correct value type of str
    out, err = capsys.readouterr()
    assert "invalid choice: '3' (choose from '1', '2')" in err

    ##############################################################
    # Test CompletionItems with int values
    ##############################################################
    choices = [CompletionItem(1, "Description One"), CompletionItem(2, "Two")]
    parser = Cmd2ArgumentParser()
    # noinspection PyTypeChecker
    parser.add_argument("choices_arg", type=int, choices=choices)

    # First test valid choices. Confirm the parsed data matches the correct type of int.
    args = parser.parse_args(['1'])
    assert args.choices_arg == 1

    args = parser.parse_args(['2'])
    assert args.choices_arg == 2

    # Next test invalid choice
    with pytest.raises(SystemExit):
        args = parser.parse_args(['3'])

    # Confirm error text contains correct value type of int
    out, err = capsys.readouterr()
    assert 'invalid choice: 3 (choose from 1, 2)' in err
