from pythonforandroid.build import Context
from pythonforandroid.graph import (
    fix_deplist, get_dependency_tuple_list_for_recipe,
    get_recipe_order_and_bootstrap, obvious_conflict_checker,
)
from pythonforandroid.bootstrap import Bootstrap
from pythonforandroid.recipe import Recipe
from pythonforandroid.util import BuildInterruptingException
from itertools import product

import mock
import pytest

ctx = Context()

name_sets = [['python2'],
             ['kivy']]
bootstraps = [None,
              Bootstrap.get_bootstrap('sdl2', ctx)]
valid_combinations = list(product(name_sets, bootstraps))
valid_combinations.extend(
    [(['python3crystax'], Bootstrap.get_bootstrap('sdl2', ctx)),
     (['kivy', 'python3crystax'], Bootstrap.get_bootstrap('sdl2', ctx)),
     (['flask'], Bootstrap.get_bootstrap('webview', ctx)),
     (['pysdl2'], None),  # auto-detect bootstrap! important corner case
    ]
)
invalid_combinations = [
    [['python2', 'python3crystax'], None],
    [['pysdl2', 'genericndkbuild'], None],
]
invalid_combinations_simple = list(invalid_combinations)
# NOTE !! keep in mind when setting invalid_combinations_simple:
#
# This is used to test obvious_conflict_checker(), which only
# catches CERTAIN conflicts:
#
# This must be a list of conflicts where the conflict is ONLY in
# non-tuple/non-ambiguous dependencies, e.g.:
#
#     dependencies_1st = ["python2", "pillow"]
#     dependencies_2nd = ["python3crystax", "pillow"]
#
# This however won't work:
#
#     dependencies_1st = [("python2", "python3"), "pillow"]
#     dependencies_2nd = [("python2legacy", "python3crystax"), "pillow"]
#
# (This is simply because the conflict checker doesn't resolve this to
# keep the code ismple enough)


def get_fake_recipe(name, depends=None, conflicts=None):
    recipe = mock.Mock()
    recipe.name = name
    recipe.get_opt_depends_in_list = lambda: []
    recipe.get_dir_name = lambda: name
    recipe.depends = list(depends or [])
    recipe.conflicts = list(conflicts or [])
    return recipe


def register_fake_recipes_for_test(monkeypatch, recipe_list):
    _orig_get_recipe = Recipe.get_recipe

    def mock_get_recipe(name, ctx):
        for recipe in recipe_list:
            if recipe.name == name:
                return recipe
        return _orig_get_recipe(name, ctx)
    # Note: staticmethod() needed for python ONLY, don't ask me why:
    monkeypatch.setattr(Recipe, 'get_recipe', staticmethod(mock_get_recipe))


@pytest.mark.parametrize('names,bootstrap', valid_combinations)
def test_valid_recipe_order_and_bootstrap(names, bootstrap):
    get_recipe_order_and_bootstrap(ctx, names, bootstrap)


@pytest.mark.parametrize('names,bootstrap', invalid_combinations)
def test_invalid_recipe_order_and_bootstrap(names, bootstrap):
    with pytest.raises(BuildInterruptingException) as e_info:
        get_recipe_order_and_bootstrap(ctx, names, bootstrap)
    assert "conflict" in e_info.value.message.lower()


def test_blacklist():
    # First, get order without blacklist:
    build_order, python_modules, bs = get_recipe_order_and_bootstrap(
        ctx, ["python3", "kivy"], None
    )
    # Now, obtain again with blacklist:
    build_order_2, python_modules_2, bs_2 = get_recipe_order_and_bootstrap(
        ctx, ["python3", "kivy"], None, blacklist=["libffi"]
    )
    assert "libffi" not in build_order_2
    assert set(build_order_2).union({"libffi"}) == set(build_order)

    # Check that we get a conflict when using webview and kivy combined:
    wbootstrap = Bootstrap.get_bootstrap('webview', ctx)
    with pytest.raises(BuildInterruptingException) as e_info:
        get_recipe_order_and_bootstrap(ctx, ["flask", "kivy"], wbootstrap)
    assert "conflict" in e_info.value.message.lower()

    # We should no longer get a conflict blacklisting sdl2:
    get_recipe_order_and_bootstrap(
        ctx, ["flask", "kivy"], wbootstrap, blacklist=["sdl2"]
    )


def test_get_dependency_tuple_list_for_recipe(monkeypatch):
    r = get_fake_recipe("recipe1", depends=[
        "libffi",
        ("libffi", "Pillow")
    ])
    dep_list = get_dependency_tuple_list_for_recipe(
        r, blacklist={"libffi"}
    )
    assert(dep_list == [("pillow",)])


@pytest.mark.parametrize('names,bootstrap', valid_combinations)
def test_valid_obvious_conflict_checker(names, bootstrap):
    # Note: obvious_conflict_checker is stricter on input
    # (needs fix_deplist) than get_recipe_order_and_bootstrap!
    obvious_conflict_checker(ctx, fix_deplist(names))


@pytest.mark.parametrize('names,bootstrap',
                         invalid_combinations_simple  # see above for why this
                        )                             # is a separate list
def test_invalid_obvious_conflict_checker(names, bootstrap):
    # Note: obvious_conflict_checker is stricter on input
    # (needs fix_deplist) than get_recipe_order_and_bootstrap!
    with pytest.raises(BuildInterruptingException) as e_info:
        obvious_conflict_checker(ctx, fix_deplist(names))
    assert "conflict" in e_info.value.message.lower()


def test_misc_obvious_conflict_checker(monkeypatch):
    # Check that the assert about wrong input data is hit:
    with pytest.raises(AssertionError) as e_info:
        obvious_conflict_checker(
            ctx,
            ["this_is_invalid"]
            # (invalid because it isn't properly nested as tuple)
        )

    # Test that non-recipe dependencies work in overall:
    obvious_conflict_checker(
        ctx, fix_deplist(["python3", "notarecipelibrary"])
    )

    # Test that a conflict with a non-recipe dependency works:
    # This is currently not used, so we need a custom test recipe:
    # To get that, we simply modify one!
    with monkeypatch.context() as m:
        register_fake_recipes_for_test(m, [
            get_fake_recipe("recipe1", conflicts=[("fakelib")]),
        ])
        with pytest.raises(BuildInterruptingException) as e_info:
            obvious_conflict_checker(ctx, fix_deplist(["recipe1", "fakelib"]))
        assert "conflict" in e_info.value.message.lower()

    # Test a case where a recipe pulls in a conditional tuple
    # of additional dependencies. This is e.g. done for ('python3',
    # 'python2', ...) but most recipes don't depend on this anymore,
    # so we need to add a manual test for this case:
    with monkeypatch.context() as m:
        register_fake_recipes_for_test(m, [
            get_fake_recipe("recipe1", depends=[("libffi", "Pillow")]),
        ])
        obvious_conflict_checker(ctx, fix_deplist(["recipe1"]))


def test_indirectconflict_obvious_conflict_checker(monkeypatch):
    # Test a case where there's an indirect conflict, which also
    # makes sure the error message correctly blames the OUTER recipes
    # as original conflict source:
    with monkeypatch.context() as m:
        register_fake_recipes_for_test(m, [
            get_fake_recipe("outerrecipe1", depends=["innerrecipe1"]),
            get_fake_recipe("outerrecipe2", depends=["innerrecipe2"]),
            get_fake_recipe("innerrecipe1"),
            get_fake_recipe("innerrecipe2", conflicts=["innerrecipe1"]),
        ])
        with pytest.raises(BuildInterruptingException) as e_info:
            obvious_conflict_checker(
                ctx,
                fix_deplist(["outerrecipe1", "outerrecipe2"])
            )
        assert ("conflict" in e_info.value.message.lower() and
                "outerrecipe1" in e_info.value.message.lower() and
                "outerrecipe2" in e_info.value.message.lower())


def test_multichoice_obvious_conflict_checker(monkeypatch):
    # Test a case where there's a conflict with a multi-choice tuple:
    with monkeypatch.context() as m:
        register_fake_recipes_for_test(m, [
            get_fake_recipe("recipe1", conflicts=["lib1", "lib2"]),
            get_fake_recipe("recipe2", depends=[("lib1", "lib2")]),
        ])
        with pytest.raises(BuildInterruptingException) as e_info:
            obvious_conflict_checker(
                ctx,
                fix_deplist([("lib1", "lib2"), "recipe1"])
            )
        assert "conflict" in e_info.value.message.lower()


def test_bootstrap_dependency_addition():
    build_order, python_modules, bs = get_recipe_order_and_bootstrap(
        ctx, ['kivy'], None)
    assert (('hostpython2' in build_order) or ('hostpython3' in build_order))


def test_graph_deplist_transformation():
    test_pairs = [
        (["Pillow", ('python2', 'python3')],
         [('pillow',), ('python2', 'python3')]),
        (["Pillow", ('python2',)],
         [('pillow',), ('python2',)]),
    ]
    for (before_list, after_list) in test_pairs:
        assert fix_deplist(before_list) == after_list


def test_bootstrap_dependency_addition2():
    build_order, python_modules, bs = get_recipe_order_and_bootstrap(
        ctx, ['kivy', 'python2'], None)
    assert 'hostpython2' in build_order


if __name__ == "__main__":
    get_recipe_order_and_bootstrap(ctx, ['python3'],
                                   Bootstrap.get_bootstrap('sdl2', ctx))
