from unittest import mock
from platform import system
from tests.recipes.recipe_ctx import RecipeCtx


class BaseTestForMakeRecipe(RecipeCtx):
    """
    An unittest for testing any recipe using the standard build commands
    (`configure/make`).

    .. note:: Note that Some cmake recipe may need some more specific testing
        ...but this should cover the basics.
    """

    recipe_name = None
    expected_compiler = (
        "{android_ndk}/toolchains/llvm/prebuilt/{system}-x86_64/bin/clang"
    )

    sh_command_calls = ["./configure"]
    """The expected commands that the recipe runs via `sh.command`."""

    extra_env_flags = {}
    """
    This must be a dictionary containing pairs of key (env var) and value.
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.recipes = ["python3", "kivy", self.recipe_name]
        self.recipe_build_order = [
            "hostpython3", self.recipe_name, "python3", "sdl2", "kivy"
        ]
        print(f"We are testing recipe: {self.recipe_name}")

    @mock.patch("pythonforandroid.recipe.Recipe.check_recipe_choices")
    @mock.patch("pythonforandroid.build.ensure_dir")
    @mock.patch("pythonforandroid.archs.glob")
    @mock.patch("pythonforandroid.archs.find_executable")
    def test_get_recipe_env(
        self,
        mock_find_executable,
        mock_glob,
        mock_ensure_dir,
        mock_check_recipe_choices,
    ):
        """
        Test that get_recipe_env contains some expected arch flags and that
        some internal methods has been called.
        """
        mock_find_executable.return_value = self.expected_compiler.format(
                android_ndk=self.ctx._ndk_dir, system=system().lower()
        )
        mock_glob.return_value = ["llvm"]
        mock_check_recipe_choices.return_value = sorted(
            self.ctx.recipe_build_order
        )

        # make sure the arch flags are in env
        env = self.recipe.get_recipe_env(self.arch)
        for flag in self.arch.arch_cflags:
            self.assertIn(flag, env["CFLAGS"])
        self.assertIn(
            f"-target {self.arch.target}",
            env["CFLAGS"],
        )

        for flag, value in self.extra_env_flags.items():
            self.assertIn(value, env[flag])

        # make sure that the mocked methods are actually called
        mock_glob.assert_called()
        mock_ensure_dir.assert_called()
        mock_find_executable.assert_called()
        mock_check_recipe_choices.assert_called()

    @mock.patch("pythonforandroid.util.chdir")
    @mock.patch("pythonforandroid.build.ensure_dir")
    @mock.patch("pythonforandroid.archs.glob")
    @mock.patch("pythonforandroid.archs.find_executable")
    def test_build_arch(
        self,
        mock_find_executable,
        mock_glob,
        mock_ensure_dir,
        mock_current_directory,
    ):
        mock_find_executable.return_value = self.expected_compiler.format(
                android_ndk=self.ctx._ndk_dir, system=system().lower()
        )
        mock_glob.return_value = ["llvm"]

        # Since the following mocks are dynamic,
        # we mock it inside a Context Manager
        with mock.patch(
            f"pythonforandroid.recipes.{self.recipe_name}.sh.Command"
        ) as mock_sh_command, mock.patch(
            f"pythonforandroid.recipes.{self.recipe_name}.sh.make"
        ) as mock_make:
            self.recipe.build_arch(self.arch)

        # make sure that the mocked methods are actually called
        for command in self.sh_command_calls:
            self.assertIn(
                mock.call(command),
                mock_sh_command.mock_calls,
            )
        mock_make.assert_called()
        mock_glob.assert_called()
        mock_ensure_dir.assert_called()
        mock_current_directory.assert_called()
        mock_find_executable.assert_called()


class BaseTestForCmakeRecipe(BaseTestForMakeRecipe):
    """
    An unittest for testing any recipe using `cmake`. It inherits from
    `BaseTestForMakeRecipe` but we override the build method to match the cmake
    build method.

    .. note:: Note that Some cmake recipe may need some more specific testing
        ...but this should cover the basics.
    """

    @mock.patch("pythonforandroid.util.chdir")
    @mock.patch("pythonforandroid.build.ensure_dir")
    @mock.patch("pythonforandroid.archs.glob")
    @mock.patch("pythonforandroid.archs.find_executable")
    def test_build_arch(
        self,
        mock_find_executable,
        mock_glob,
        mock_ensure_dir,
        mock_current_directory,
    ):
        mock_find_executable.return_value = self.expected_compiler.format(
                android_ndk=self.ctx._ndk_dir, system=system().lower()
        )
        mock_glob.return_value = ["llvm"]

        # Since the following mocks are dynamic,
        # we mock it inside a Context Manager
        with mock.patch(
            f"pythonforandroid.recipes.{self.recipe_name}.sh.make"
        ) as mock_make, mock.patch(
            f"pythonforandroid.recipes.{self.recipe_name}.sh.cmake"
        ) as mock_cmake:
            self.recipe.build_arch(self.arch)

        # make sure that the mocked methods are actually called
        mock_cmake.assert_called()
        mock_make.assert_called()
        mock_glob.assert_called()
        mock_ensure_dir.assert_called()
        mock_current_directory.assert_called()
        mock_find_executable.assert_called()
