from contextlib import contextmanager

import pytest

from cltoolbox import Program

program = Program("example.py", "1.0.10")


def NoopCompleter(prefix, **kwd):
    return []


program.option(
    "-f",
    "--foo",
    dest="foo",
    default="bar",
    completer=NoopCompleter,
    help="Real programmers don't comment their code. \
          If it was hard to write, it should be hard to read.",
)

program.add_subprog("sub")
program.sub.option(
    "-i", "--inc", dest="inc", type=int, default=0, help="Some help text."
)


@program.command
def getopt(name):
    """:param name: Name of option to return."""
    # also allows for: Script.foo
    return getattr(program, name)


@program.sub.command
def powOfSub(b, e):
    """
    :param b: Base.
    :param e: Exponent.
    """
    return int(b) ** int(e) + program.inc


@program.sub.command("powOfSub2")
def powOfSub2_impl(b, e):
    """
    :param b: Base.
    :param e: Exponent.
    """
    return int(b) ** int(e) - program.inc


@program.command
def goo(pos, verbose=False, bar=None):
    """
    Do nothing function for testing.
    :param pos: Positional argument.
    :param verbose: Verbose flag.
    :param bar: Bar option.
    """


@program.command
def vara(pos, foo, *varargs, spam=24):
    """:param varargs: Yeah, you got it right, the variable arguments."""


@program.command
def another(baw, owl=42, json=False, tomawk=None):
    """This yet another example showcasing the power of CLtoolbox!

    :param baw: That's the positional argument, obviously.
    :param --owl: Yeah, I know, this is too much.
    :param --json: In case you want to pipe it through something.
    :param --tomawk: Well, in this case -t isn't for time.
    """


@program.command("alias")
def analiased(a, b=4):
    """
    Do nothing function for testing.
    :param a: A.
    :param b: B.
    """


@program.command
def power(x, y=2):
    return int(x) ** y


@program.command("more-power")
def more_power(x, y=2):
    """This one really shows off complete power.

    :param x: Well, the base.
    :param -y: You got it, the exponent.
    """
    return int(x) ** int(y)


@program.command
def repeat(what, times=10):
    """Getting types from annotations.

    :param what: what to repeat.
    :param --times: how many times to repeat.
    """
    return what * times


# version-agnostic way of setting annotations.
# Equivalent to 'repeat(what: str, times: int=10)'
repeat.__annotations__ = {"what": str, "times": int}


@program.command("more-powerful")
@program.arg("x", type=int, completer=NoopCompleter)
@program.arg("y", "-y", "--epsilon", type=int)
def more_power_2(x, y=2):
    return x**y


@program.command
@program.arg("x", type=int)
@program.arg("y", type=int)
def overriding(x, y=4):
    """Yoo an override test.

    :param x <str>: This is so wroong!!! Let's hope it gets overridden by @arg.
    :param -y <metavar>: This too!!
    """
    return x - y


@program.command
def dashes(a, b=5):
    """Usual command help.

    :param a: A help obviously.
    :type a: int
    :param b: Yooo.
    :type b: int
    """
    return int(a) ** int(b)


@program.command
def append(acc=[]):
    """Have to use a list as default value for append tests to pass."""
    return acc


GENERIC_COMMANDS_CASES = [
    ("goo 2", [["2", False, None]]),
    ("goo 2 --verbose", [["2", True, None]]),
    ("goo 2 --bar 9", [["2", False, "9"]]),
    ("goo 2 --verbose --bar 8", [["2", True, "8"]]),
    ("vara 2 3", [["2", "3", 24]]),
    ("vara 2 3 --spam 8", [["2", "3", 8]]),
    # Unfortunately this is an argparse "bug". See:
    # http://bugs.python.org/issue15112
    # You cannot intermix positional and optional arguments for now.
    # ('vara 1 2 --spam 8 9 8', ['1', '2', 8, '9', '8']),
    ("vara 1 2 4 5 --spam 8", [["1", "2", "4", "5", 8]]),
    ("vara --spam 8 1 2 4 5", [["1", "2", "4", "5", 8]]),
    ("vara 9 8 1 2 3 4", [["9", "8", "1", "2", "3", "4", 24]]),
    ("another 2", [["2", 42, False, None]]),
    ("another 2 --json", [["2", 42, True, None]]),
    ("another 2 --tomawk 1 --owl 3", [["2", 3, False, "1"]]),
    ("another 2 --owl 89 --tomawk 98", [["2", 89, False, "98"]]),
    ("another 2 --json --owl 1", [["2", 1, True, None]]),
    ("another 3 --owl 8 --json --tomawk 8", [["3", 8, True, "8"]]),
    ("alias 5 -b 9", [["5", 9], "analiased"]),
    ("more-power 9 -y 2", [["9", 2], "more_power"]),
    ("more-powerful 9 -y 3", [[9, 3], "more_power_2"]),
    ("more-powerful 9 --epsilon 3", [[9, 3], "more_power_2"]),
    ("overriding 2", [[2, 4]]),
    ("overriding 2 -y 7", [[2, 7]]),
    ("dashes 2", [["2", 5]]),
    ("dashes 8 -b 7", [["8", 7]]),
    ("append", [[[]]]),
    ("append --acc 2", [[["2"]]]),
    ("append --acc 2 --acc 3", [[["2", "3"]]]),
]


@pytest.mark.parametrize("args,rest", GENERIC_COMMANDS_CASES)
def test_generic_commands(args, rest):
    args = args.split()
    to_args = rest[0]
    real_name = args[0] if len(rest) == 1 else rest[1]
    parsed = program.parse(args)
    assert real_name == parsed[0].__name__
    assert to_args == parsed[1]


PROGRAM_EXECUTE_CASES = [
    ("power 2", 4),
    ("power 2 -y 4", 16),
    ("more-power 3", 9),
    ("more-power 3 -y 4", 81),
    ("more-powerful 4 -y 2", 16),
    ("more-powerful 4 --epsilon 2", 16),
    ("overriding 2", -2),
    ("overriding 2 -y 7", -5),
    ("dashes 2", 32),
    ("dashes 7 -b 3", 343),
    ("repeat a", "aaaaaaaaaa"),
    ("repeat a --times 5", "aaaaa"),
]


@pytest.mark.parametrize("args,result", PROGRAM_EXECUTE_CASES)
def test_program_execute(args, result):
    args = args.split()
    assert result == program.execute(args)
    assert program.parse(args)[0].__name__ == program._current_command


@contextmanager
def does_not_raise():
    yield


PROGRAM_EXCEPT_CASES = [
    ("repeat a", does_not_raise()),
    ("repeat a -t blah", pytest.raises(SystemExit)),
]


@pytest.mark.parametrize("args,expectation", PROGRAM_EXCEPT_CASES)
def test_program_except(args, expectation):
    args = args.split()

    with expectation:
        program.execute(args)

        assert program.parse(args)[0].__name__ == program._current_command


PROGRAM_OPTIONS_CASES = [
    ("          getopt foo", "bar"),
    ("   -f xyz getopt foo", "xyz"),
    ("--foo xyz getopt foo", "xyz"),
    ("          sub         powOfSub  2 3", 8),
    ("   -f xyz sub    -i 1 powOfSub  2 3", 9),
    ("--foo xyz sub --inc 2 powOfSub  2 3", 10),
    ("          sub         powOfSub2 2 3", 8),
    ("   -f xyz sub    -i 1 powOfSub2 2 3", 7),
    ("--foo xyz sub --inc 2 powOfSub2 2 3", 6),
]


@pytest.mark.parametrize("args,result", PROGRAM_OPTIONS_CASES)
def test_program_options(args, result):
    args = args.split()
    assert "example.py" == program.name
    assert result == program.execute(args)
