import inspect
import itertools

import pytest

from plover.key_combo import parse_key_combo


def generate_combo_tests(test_id, *params, key_name_to_key_code=None):
    yield ("id", test_id)
    if key_name_to_key_code is not None:
        yield ("key_name_to_key_code", key_name_to_key_code)
    for iterables in params:
        iterables = [i if isinstance(i, (tuple, list)) else (i,) for i in iterables]
        if len(iterables) < 2:
            iterables.append(("",))
        for (
            combo_string,
            parse_result,
        ) in itertools.product(*iterables):
            yield (("parse", combo_string, parse_result),)


# Test directives:
# - id TEST_IDENTIFIER
# - key_name_to_key_code KEY_NAME_TO_KEY_CODE_FUNCTION
# - parse COMBO_STRING KEY_EVENTS
KEY_COMBO_TESTS = tuple(
    itertools.chain(
        (("key_name_to_key_code", lambda k: k),),
        # No-op.
        generate_combo_tests(
            "no-op",
            (("", "   "), ""),
        ),
        # Syntax error:
        generate_combo_tests(
            "syntax_error",
            (
                (
                    # - invalid character
                    "Return,",
                    "Return&",
                    "Ret. urn <",
                    "exclam ! foo",
                    "shift[a]",
                    # - unbalanced `)`
                    ") arg",
                    "arg )",
                    "arg())",
                    "arg(x) )",
                    # - unbalanced `(`
                    "test(",
                    "( grr",
                    "foo ( bar",
                    "foo (bar ( ",
                    "foo ((",
                    # - [-+]key() is not valid
                    "+foo ()",
                    "+foo()",
                ),
                SyntaxError,
            ),
        ),
        # Pressing an already pressed key.
        generate_combo_tests(
            "already_pressed",
            (
                (
                    "foo(foo)",
                    "Foo(foO)",
                    "foo(fOo(arg))",
                    "foo(bar(Foo))",
                    "foo(bar(foo(x)))",
                ),
                ValueError,
            ),
        ),
        # Stacking.
        generate_combo_tests(
            "stacking",
            # 1 is not a valid identifier, but still a valid key name.
            ("1", "+1 -1"),
            (("Shift_l", "SHIFT_L"), "+shift_l -shift_l"),
            # Case does not matter.
            (("a", " A "), "+a -a"),
            (("a(b c)", "a ( b c   )"), "+a +b -b +c -c -a"),
            (("a(bc)", " a(  Bc )"), "+a +bc -bc -a"),
            (
                ("a(bc(d)e f(g) h())i j"),
                "+a +bc +d -d -bc +e -e +f +g -g -f +h -h -a +i -i +j -j",
            ),
            (
                ("foo () bar ( foo a b c (d))", "fOo () Bar ( FOO a B c (D))"),
                "+foo -foo +bar +foo -foo +a -a +b -b +c +d -d -c -bar",
            ),
        ),
        # Invalid key name.
        generate_combo_tests(
            "invalid_key",
            ("1 (c) 2 bad 3 (a b c)", ValueError),
            key_name_to_key_code={c: c for c in "123abc"}.get,
        ),
        # Same key code, multiple key names.
        generate_combo_tests(
            "aliasing",
            ("1 exclam", "+10 -10 +10 -10"),
            (("1 ( exclam )", "exclam(1)"), ValueError),
            key_name_to_key_code={"1": "10", "exclam": "10"}.get,
        ),
    )
)


def parametrize(tests):
    key_name_to_key_code = None
    test_id = None
    args = []
    ids = []
    for t in tests:
        if t[0] == "key_name_to_key_code":
            key_name_to_key_code = t[1]
        elif t[0] == "id":
            test_id = t[1]
        else:
            assert key_name_to_key_code is not None
            assert test_id is not None
            args.append((key_name_to_key_code, t))
            ids.append(test_id)
    return pytest.mark.parametrize(
        ("key_name_to_key_code", "instructions"), args, ids=ids
    )


@parametrize(KEY_COMBO_TESTS)
def test_key_combo(key_name_to_key_code, instructions):
    def repr_expected(result):
        assert isinstance(result, str)
        return [s.strip() for s in result.split()]

    def repr_key_events(events):
        assert isinstance(events, list)
        return ["%s%s" % ("+" if pressed else "-", key) for key, pressed in events]

    for action, *args in instructions:
        if action == "parse":
            combo_string, key_events = args
            if inspect.isclass(key_events):
                with pytest.raises(key_events):
                    parse_key_combo(
                        combo_string, key_name_to_key_code=key_name_to_key_code
                    )
            else:
                assert repr_key_events(
                    parse_key_combo(
                        combo_string, key_name_to_key_code=key_name_to_key_code
                    )
                ) == repr_expected(key_events)
        else:
            raise ValueError(args[0])
