import os
import unittest
from os import environ
from unittest import mock
from platform import system

from pythonforandroid.bootstrap import Bootstrap
from pythonforandroid.distribution import Distribution
from pythonforandroid.recipe import Recipe
from pythonforandroid.build import Context
from pythonforandroid.util import BuildInterruptingException
from pythonforandroid.archs import (
    Arch,
    ArchARM,
    ArchARMv7_a,
    ArchAarch_64,
    Archx86,
    Archx86_64,
)

expected_env_gcc_keys = {
    "CFLAGS",
    "LDFLAGS",
    "CXXFLAGS",
    "TOOLCHAIN_PREFIX",
    "TOOLCHAIN_VERSION",
    "CC",
    "CXX",
    "AR",
    "RANLIB",
    "LD",
    "LDSHARED",
    "STRIP",
    "MAKE",
    "READELF",
    "NM",
    "BUILDLIB_PATH",
    "PATH",
    "ARCH",
    "NDK_API",
}


class ArchSetUpBaseClass(object):
    """
    An class object which is intended to be used as a base class to configure
    an inherited class of `unittest.TestCase`. This class will override the
    `setUp` method.
    """

    ctx = None
    expected_compiler = ""

    TEST_ARCH = 'armeabi-v7a'

    def setUp(self):
        self.ctx = Context()
        self.ctx.ndk_api = 21
        self.ctx.android_api = 27
        self.ctx._sdk_dir = "/opt/android/android-sdk"
        self.ctx._ndk_dir = "/opt/android/android-ndk"
        self.ctx.setup_dirs(os.getcwd())
        self.ctx.bootstrap = Bootstrap().get_bootstrap("sdl2", self.ctx)
        self.ctx.bootstrap.distribution = Distribution.get_distribution(
            self.ctx,
            name="sdl2",
            recipes=["python3", "kivy"],
            arch_name=self.TEST_ARCH,
        )
        self.ctx.python_recipe = Recipe.get_recipe("python3", self.ctx)
        # Here we define the expected compiler, which, as per ndk >= r19,
        # should be the same for all the tests (no more gcc compiler)
        self.expected_compiler = (
            f"/opt/android/android-ndk/toolchains/"
            f"llvm/prebuilt/{system().lower()}-x86_64/bin/clang"
        )


class TestArch(ArchSetUpBaseClass, unittest.TestCase):
    """
    An inherited class of `ArchSetUpBaseClass` and `unittest.TestCase` which
    will be used to perform tests for the base class
    :class:`~pythonforandroid.archs.Arch`.
    """

    def test_arch(self):
        arch = Arch(self.ctx)
        self.assertEqual(arch.__str__(), arch.arch)
        self.assertEqual(arch.target, "None21")
        self.assertIsNone(arch.toolchain_prefix)
        self.assertIsNone(arch.command_prefix)
        self.assertIsInstance(arch.include_dirs, list)


class TestArchARM(ArchSetUpBaseClass, unittest.TestCase):
    """
    An inherited class of `ArchSetUpBaseClass` and `unittest.TestCase` which
    will be used to perform tests for :class:`~pythonforandroid.archs.ArchARM`.
    """

    @mock.patch("pythonforandroid.archs.glob")
    @mock.patch("pythonforandroid.archs.find_executable")
    @mock.patch("pythonforandroid.build.ensure_dir")
    def test_arch_arm(self, mock_ensure_dir, mock_find_executable, mock_glob):
        """
        Test that class :class:`~pythonforandroid.archs.ArchARM` returns some
        expected attributes and environment variables.

        .. note::
            Here we mock two methods:

                - `ensure_dir` because we don't want to create any directory
                - `find_executable` because otherwise we will
                  get an error when trying to find the compiler (we are setting
                  some fake paths for our android sdk and ndk so probably will
                  not exist)

        """
        mock_find_executable.return_value = self.expected_compiler
        mock_ensure_dir.return_value = True
        mock_glob.return_value = ["llvm"]

        arch = ArchARM(self.ctx)
        self.assertEqual(arch.arch, "armeabi")
        self.assertEqual(arch.__str__(), "armeabi")
        self.assertEqual(arch.toolchain_prefix, "arm-linux-androideabi")
        self.assertEqual(arch.command_prefix, "arm-linux-androideabi")
        self.assertEqual(arch.target, "armv7a-linux-androideabi21")
        self.assertEqual(arch.platform_dir, "arch-arm")
        arch = ArchARM(self.ctx)

        # Check environment flags
        env = arch.get_env()
        self.assertIsInstance(env, dict)
        self.assertEqual(
            expected_env_gcc_keys, set(env.keys()) & expected_env_gcc_keys
        )

        # check glob and find_executable calls
        self.assertEqual(mock_glob.call_count, 4)
        for glob_call, kw in mock_glob.call_args_list:
            self.assertEqual(
                glob_call[0],
                "{ndk_dir}/toolchains/llvm*".format(ndk_dir=self.ctx._ndk_dir),
            )
        mock_find_executable.assert_called_once_with(
            self.expected_compiler, path=environ["PATH"]
        )

        # check gcc compilers
        self.assertEqual(env["CC"].split()[0], self.expected_compiler)
        self.assertEqual(env["CXX"].split()[0], self.expected_compiler + "++")
        # check android binaries
        self.assertEqual(env["AR"], "arm-linux-androideabi-ar")
        self.assertEqual(env["LD"], "arm-linux-androideabi-ld")
        self.assertEqual(env["RANLIB"], "arm-linux-androideabi-ranlib")
        self.assertEqual(
            env["STRIP"].split()[0], "arm-linux-androideabi-strip"
        )
        self.assertEqual(
            env["READELF"].split()[0], "arm-linux-androideabi-readelf"
        )
        self.assertEqual(env["NM"].split()[0], "arm-linux-androideabi-nm")

        # check that cflags are in gcc
        self.assertIn(env["CFLAGS"], env["CC"])

        # check that flags aren't in gcc and also check ccache
        self.ctx.ccache = "/usr/bin/ccache"
        env = arch.get_env(with_flags_in_cc=False)
        self.assertNotIn(env["CFLAGS"], env["CC"])
        self.assertEqual(env["USE_CCACHE"], "1")
        self.assertEqual(env["NDK_CCACHE"], "/usr/bin/ccache")

        # Check exception in case that CC is not found
        mock_find_executable.return_value = None
        with self.assertRaises(BuildInterruptingException) as e:
            arch.get_env()
        self.assertEqual(
            e.exception.args[0],
            "Couldn't find executable for CC. This indicates a problem "
            "locating the {expected_compiler} executable in the Android "
            "NDK, not that you don't have a normal compiler installed. "
            "Exiting.".format(expected_compiler=self.expected_compiler),
        )


class TestArchARMv7a(ArchSetUpBaseClass, unittest.TestCase):
    """
    An inherited class of `ArchSetUpBaseClass` and `unittest.TestCase` which
    will be used to perform tests for
    :class:`~pythonforandroid.archs.ArchARMv7_a`.
    """

    @mock.patch("pythonforandroid.archs.glob")
    @mock.patch("pythonforandroid.archs.find_executable")
    @mock.patch("pythonforandroid.build.ensure_dir")
    def test_arch_armv7a(
        self, mock_ensure_dir, mock_find_executable, mock_glob
    ):
        """
        Test that class :class:`~pythonforandroid.archs.ArchARMv7_a` returns
        some expected attributes and environment variables.

        .. note::
            Here we mock the same functions than
            :meth:`TestArchARM.test_arch_arm` plus `glob`, so we make sure that
            the glob result is the expected even if the folder doesn't exist,
            which is probably the case. This has to be done because here we
            tests the `get_env` with clang

        """
        mock_find_executable.return_value = self.expected_compiler
        mock_ensure_dir.return_value = True
        mock_glob.return_value = ["llvm"]

        arch = ArchARMv7_a(self.ctx)
        self.assertEqual(arch.arch, "armeabi-v7a")
        self.assertEqual(arch.__str__(), "armeabi-v7a")
        self.assertEqual(arch.toolchain_prefix, "arm-linux-androideabi")
        self.assertEqual(arch.command_prefix, "arm-linux-androideabi")
        self.assertEqual(arch.target, "armv7a-linux-androideabi21")
        self.assertEqual(arch.platform_dir, "arch-arm")

        env = arch.get_env()
        # check glob and find_executable calls
        self.assertEqual(mock_glob.call_count, 4)
        for glob_call, kw in mock_glob.call_args_list:
            self.assertEqual(
                glob_call[0],
                "{ndk_dir}/toolchains/llvm*".format(ndk_dir=self.ctx._ndk_dir),
            )
        mock_find_executable.assert_called_once_with(
            self.expected_compiler, path=environ["PATH"]
        )

        # check clang
        build_platform = "{system}-{machine}".format(
            system=os.uname()[0], machine=os.uname()[-1]
        ).lower()
        self.assertEqual(
            env["CC"].split()[0],
            "{ndk_dir}/toolchains/llvm/prebuilt/"
            "{build_platform}/bin/clang".format(
                ndk_dir=self.ctx._ndk_dir, build_platform=build_platform
            ),
        )
        self.assertEqual(
            env["CXX"].split()[0],
            "{ndk_dir}/toolchains/llvm/prebuilt/"
            "{build_platform}/bin/clang++".format(
                ndk_dir=self.ctx._ndk_dir, build_platform=build_platform
            ),
        )

        # For armeabi-v7a we expect some extra cflags
        self.assertIn(
            " -march=armv7-a -mfloat-abi=softfp -mfpu=vfp -mthumb",
            env["CFLAGS"],
        )


class TestArchX86(ArchSetUpBaseClass, unittest.TestCase):
    """
    An inherited class of `ArchSetUpBaseClass` and `unittest.TestCase` which
    will be used to perform tests for :class:`~pythonforandroid.archs.Archx86`.
    """

    @mock.patch("pythonforandroid.archs.glob")
    @mock.patch("pythonforandroid.archs.find_executable")
    @mock.patch("pythonforandroid.build.ensure_dir")
    def test_arch_x86(self, mock_ensure_dir, mock_find_executable, mock_glob):
        """
        Test that class :class:`~pythonforandroid.archs.Archx86` returns
        some expected attributes and environment variables.

        .. note::
            Here we mock the same functions than
            :meth:`TestArchARM.test_arch_arm` plus `glob`, so we make sure that
            the glob result is the expected even if the folder doesn't exist,
            which is probably the case. This has to be done because here we
            tests the `get_env` with clang
        """
        mock_find_executable.return_value = self.expected_compiler
        mock_ensure_dir.return_value = True
        mock_glob.return_value = ["llvm"]

        arch = Archx86(self.ctx)
        self.assertEqual(arch.arch, "x86")
        self.assertEqual(arch.__str__(), "x86")
        self.assertEqual(arch.toolchain_prefix, "x86")
        self.assertEqual(arch.command_prefix, "i686-linux-android")
        self.assertEqual(arch.target, "i686-linux-android21")
        self.assertEqual(arch.platform_dir, "arch-x86")

        env = arch.get_env()
        # check glob and find_executable calls
        self.assertEqual(mock_glob.call_count, 4)
        for glob_call, kw in mock_glob.call_args_list:
            self.assertEqual(
                glob_call[0],
                "{ndk_dir}/toolchains/llvm*".format(ndk_dir=self.ctx._ndk_dir),
            )
        mock_find_executable.assert_called_once_with(
            self.expected_compiler, path=environ["PATH"]
        )

        # For x86 we expect some extra cflags in our `environment`
        self.assertIn(
            " -march=i686 -mtune=intel -mssse3 -mfpmath=sse -m32",
            env["CFLAGS"],
        )


class TestArchX86_64(ArchSetUpBaseClass, unittest.TestCase):
    """
    An inherited class of `ArchSetUpBaseClass` and `unittest.TestCase` which
    will be used to perform tests for
    :class:`~pythonforandroid.archs.Archx86_64`.
    """

    @mock.patch("pythonforandroid.archs.glob")
    @mock.patch("pythonforandroid.archs.find_executable")
    @mock.patch("pythonforandroid.build.ensure_dir")
    def test_arch_x86_64(
        self, mock_ensure_dir, mock_find_executable, mock_glob
    ):
        """
        Test that class :class:`~pythonforandroid.archs.Archx86_64` returns
        some expected attributes and environment variables.

        .. note::
            Here we mock the same functions than
            :meth:`TestArchARM.test_arch_arm` plus `glob`, so we make sure that
            the glob result is the expected even if the folder doesn't exist,
            which is probably the case. This has to be done because here we
            tests the `get_env` with clang
        """
        mock_find_executable.return_value = self.expected_compiler
        mock_ensure_dir.return_value = True
        mock_glob.return_value = ["llvm"]

        arch = Archx86_64(self.ctx)
        self.assertEqual(arch.arch, "x86_64")
        self.assertEqual(arch.__str__(), "x86_64")
        self.assertEqual(arch.toolchain_prefix, "x86_64")
        self.assertEqual(arch.command_prefix, "x86_64-linux-android")
        self.assertEqual(arch.target, "x86_64-linux-android21")
        self.assertEqual(arch.platform_dir, "arch-x86_64")

        env = arch.get_env()
        # check glob and find_executable calls
        self.assertEqual(mock_glob.call_count, 4)
        for glob_call, kw in mock_glob.call_args_list:
            self.assertEqual(
                glob_call[0],
                "{ndk_dir}/toolchains/llvm*".format(ndk_dir=self.ctx._ndk_dir),
            )
        mock_find_executable.assert_called_once_with(
            self.expected_compiler, path=environ["PATH"]
        )

        # For x86_64 we expect some extra cflags in our `environment`
        mock_find_executable.assert_called_once()
        self.assertIn(
            " -march=x86-64 -msse4.2 -mpopcnt -m64 -mtune=intel", env["CFLAGS"]
        )


class TestArchAArch64(ArchSetUpBaseClass, unittest.TestCase):
    """
    An inherited class of `ArchSetUpBaseClass` and `unittest.TestCase` which
    will be used to perform tests for
    :class:`~pythonforandroid.archs.ArchAarch_64`.
    """

    @mock.patch("pythonforandroid.archs.glob")
    @mock.patch("pythonforandroid.archs.find_executable")
    @mock.patch("pythonforandroid.build.ensure_dir")
    def test_arch_aarch_64(
        self, mock_ensure_dir, mock_find_executable, mock_glob
    ):
        """
        Test that class :class:`~pythonforandroid.archs.ArchAarch_64` returns
        some expected attributes and environment variables.

        .. note::
            Here we mock the same functions than
            :meth:`TestArchARM.test_arch_arm` plus `glob`, so we make sure that
            the glob result is the expected even if the folder doesn't exist,
            which is probably the case. This has to be done because here we
            tests the `get_env` with clang
        """
        mock_find_executable.return_value = self.expected_compiler
        mock_ensure_dir.return_value = True
        mock_glob.return_value = ["llvm"]

        arch = ArchAarch_64(self.ctx)
        self.assertEqual(arch.arch, "arm64-v8a")
        self.assertEqual(arch.__str__(), "arm64-v8a")
        self.assertEqual(arch.toolchain_prefix, "aarch64-linux-android")
        self.assertEqual(arch.command_prefix, "aarch64-linux-android")
        self.assertEqual(arch.target, "aarch64-linux-android21")
        self.assertEqual(arch.platform_dir, "arch-arm64")

        env = arch.get_env()
        # check glob and find_executable calls
        self.assertEqual(mock_glob.call_count, 4)
        for glob_call, kw in mock_glob.call_args_list:
            self.assertEqual(
                glob_call[0],
                "{ndk_dir}/toolchains/llvm*".format(ndk_dir=self.ctx._ndk_dir),
            )
        mock_find_executable.assert_called_once_with(
            self.expected_compiler, path=environ["PATH"]
        )

        # For x86_64 we expect to find an extra key in`environment`
        for flag in {"CFLAGS", "CXXFLAGS", "CC", "CXX"}:
            self.assertIn("-march=armv8-a", env[flag])
