import os

import unittest

try:
    from unittest import mock
except ImportError:
    # `Python 2` or lower than `Python 3.3` does not
    # have the `unittest.mock` module built-in
    import mock
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

    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"]
        )
        self.ctx.python_recipe = Recipe.get_recipe("python3", self.ctx)


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)
        with self.assertRaises(AttributeError) as e1:
            arch.__str__()
        self.assertEqual(
            e1.exception.args[0], "'Arch' object has no attribute 'arch'"
        )
        with self.assertRaises(AttributeError) as e2:
            getattr(arch, "target")
        self.assertEqual(
            e2.exception.args[0], "'NoneType' object has no attribute 'split'"
        )
        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.find_executable")
    @mock.patch("pythonforandroid.build.ensure_dir")
    def test_arch_arm(self, mock_ensure_dir, mock_find_executable):
        """
        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 = "arm-linux-androideabi-gcc"
        mock_ensure_dir.return_value = True

        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-none-linux-androideabi")
        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 gcc compilers
        self.assertEqual(env["CC"].split()[0], "arm-linux-androideabi-gcc")
        self.assertEqual(env["CXX"].split()[0], "arm-linux-androideabi-g++")
        # 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 arm-linux-androideabi-gcc executable in the Android "
            "NDK, not that you don't have a normal compiler installed. "
            "Exiting.",
        )


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 = "arm-linux-androideabi-gcc"
        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-none-linux-androideabi")
        self.assertEqual(arch.platform_dir, "arch-arm")

        env = arch.get_env(clang=True)
        # 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.find_executable")
    @mock.patch("pythonforandroid.build.ensure_dir")
    def test_arch_x86(self, mock_ensure_dir, mock_find_executable):
        """
        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`
        """
        mock_find_executable.return_value = "arm-linux-androideabi-gcc"
        mock_ensure_dir.return_value = True

        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-none-linux-android")
        self.assertEqual(arch.platform_dir, "arch-x86")

        # For x86 we expect some extra cflags in our `environment`
        env = arch.get_env()
        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.find_executable")
    @mock.patch("pythonforandroid.build.ensure_dir")
    def test_arch_x86_64(self, mock_ensure_dir, mock_find_executable):
        """
        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`
        """
        mock_find_executable.return_value = "arm-linux-androideabi-gcc"
        mock_ensure_dir.return_value = True

        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-none-linux-android")
        self.assertEqual(arch.platform_dir, "arch-x86_64")

        # For x86_64 we expect some extra cflags in our `environment`
        env = arch.get_env()
        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.find_executable")
    @mock.patch("pythonforandroid.build.ensure_dir")
    def test_arch_aarch_64(self, mock_ensure_dir, mock_find_executable):
        """
        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`
        """
        mock_find_executable.return_value = "arm-linux-androideabi-gcc"
        mock_ensure_dir.return_value = True

        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-none-linux-android")
        self.assertEqual(arch.platform_dir, "arch-arm64")

        # For x86_64 we expect to find an extra key in`environment`
        env = arch.get_env()
        self.assertIn("EXTRA_CFLAGS", env.keys())
