import os
import json
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.util import BuildInterruptingException
from pythonforandroid.build import Context

dist_info_data = {
    "dist_name": None,
    "bootstrap": "sdl2",
    "archs": ["armeabi", "armeabi-v7a", "x86", "x86_64", "arm64-v8a"],
    "ndk_api": 21,
    "use_setup_py": False,
    "recipes": ["hostpython3", "python3", "sdl2", "kivy", "requests"],
    "hostpython": "/some/fake/hostpython3",
    "python_version": "3.7",
}


class TestDistribution(unittest.TestCase):
    """
    An inherited class of `unittest.TestCase`to test the module
    :mod:`~pythonforandroid.distribution`.
    """

    def setUp(self):
        """Configure a :class:`~pythonforandroid.build.Context` so we can
        perform our unittests"""
        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.recipe_build_order = [
            "hostpython3",
            "python3",
            "sdl2",
            "kivy",
        ]

    def setUp_distribution_with_bootstrap(self, bs, **kwargs):
        """Extend the setUp by configuring a distribution, because some test
        needs a distribution to be set to be properly tested"""
        self.ctx.bootstrap = bs
        self.ctx.bootstrap.distribution = Distribution.get_distribution(
            self.ctx,
            name=kwargs.pop("name", "test_prj"),
            recipes=kwargs.pop("recipes", ["python3", "kivy"]),
            **kwargs
        )

    def tearDown(self):
        """Here we make sure that we reset a possible bootstrap created in
        `setUp_distribution_with_bootstrap`"""
        self.ctx.bootstrap = None

    def test_properties(self):
        """Test that some attributes has the expected result (for now, we check
        that `__repr__` and `__str__` return the proper values"""
        self.setUp_distribution_with_bootstrap(
            Bootstrap().get_bootstrap("sdl2", self.ctx)
        )
        distribution = self.ctx.bootstrap.distribution
        self.assertEqual(self.ctx, distribution.ctx)
        expected_repr = (
            "<Distribution: name test_prj with recipes (python3, kivy)>"
        )
        self.assertEqual(distribution.__str__(), expected_repr)
        self.assertEqual(distribution.__repr__(), expected_repr)

    @mock.patch("pythonforandroid.distribution.exists")
    def test_folder_exist(self, mock_exists):
        """Test that method
        :meth:`~pythonforandroid.distribution.Distribution.folder_exist` is
        called once with the proper arguments."""

        mock_exists.return_value = False
        self.setUp_distribution_with_bootstrap(
            Bootstrap.get_bootstrap("sdl2", self.ctx)
        )
        self.ctx.bootstrap.distribution.folder_exists()
        mock_exists.assert_called_with(
            self.ctx.bootstrap.distribution.dist_dir
        )

    @mock.patch("pythonforandroid.distribution.rmtree")
    def test_delete(self, mock_rmtree):
        """Test that method
        :meth:`~pythonforandroid.distribution.Distribution.delete` is
        called once with the proper arguments."""
        self.setUp_distribution_with_bootstrap(
            Bootstrap().get_bootstrap("sdl2", self.ctx)
        )
        self.ctx.bootstrap.distribution.delete()
        mock_rmtree.assert_called_once_with(
            self.ctx.bootstrap.distribution.dist_dir
        )

    @mock.patch("pythonforandroid.distribution.exists")
    def test_get_distribution_no_name(self, mock_exists):
        """Test that method
        :meth:`~pythonforandroid.distribution.Distribution.get_distribution`
        returns the proper result which should `unnamed_dist_1`."""
        mock_exists.return_value = False
        self.ctx.bootstrap = Bootstrap().get_bootstrap("sdl2", self.ctx)
        dist = Distribution.get_distribution(self.ctx)
        self.assertEqual(dist.name, "unnamed_dist_1")

    @mock.patch("pythonforandroid.util.chdir")
    @mock.patch("pythonforandroid.distribution.open", create=True)
    def test_save_info(self, mock_open_dist_info, mock_chdir):
        """Test that method
        :meth:`~pythonforandroid.distribution.Distribution.save_info`
        is called once with the proper arguments."""
        self.setUp_distribution_with_bootstrap(
            Bootstrap().get_bootstrap("sdl2", self.ctx)
        )
        self.ctx.hostpython = "/some/fake/hostpython3"
        self.ctx.python_recipe = Recipe.get_recipe("python3", self.ctx)
        self.ctx.python_modules = ["requests"]
        mock_open_dist_info.side_effect = [
            mock.mock_open(read_data=json.dumps(dist_info_data)).return_value
        ]
        self.ctx.bootstrap.distribution.save_info("/fake_dir")
        mock_open_dist_info.assert_called_once_with("dist_info.json", "w")
        mock_open_dist_info.reset_mock()

    @mock.patch("pythonforandroid.distribution.open", create=True)
    @mock.patch("pythonforandroid.distribution.exists")
    @mock.patch("pythonforandroid.distribution.glob.glob")
    def test_get_distributions(
        self, mock_glob, mock_exists, mock_open_dist_info
    ):
        """Test that method
        :meth:`~pythonforandroid.distribution.Distribution.get_distributions`
        returns some expected values:

            - A list of instances of class
              `~pythonforandroid.distribution.Distribution
            - That one of the distributions returned in the result has the
              proper values (`name`, `ndk_api` and `recipes`)
        """
        self.setUp_distribution_with_bootstrap(
            Bootstrap().get_bootstrap("sdl2", self.ctx)
        )
        mock_glob.return_value = ["sdl2-python3"]
        mock_open_dist_info.side_effect = [
            mock.mock_open(read_data=json.dumps(dist_info_data)).return_value
        ]

        dists = self.ctx.bootstrap.distribution.get_distributions(self.ctx)
        self.assertIsInstance(dists, list)
        self.assertEqual(len(dists), 1)
        self.assertIsInstance(dists[0], Distribution)
        self.assertEqual(dists[0].name, "sdl2-python3")
        self.assertEqual(dists[0].ndk_api, 21)
        self.assertEqual(
            dists[0].recipes,
            ["hostpython3", "python3", "sdl2", "kivy", "requests"],
        )
        mock_open_dist_info.assert_called_with("sdl2-python3/dist_info.json")
        mock_open_dist_info.reset_mock()

    @mock.patch("pythonforandroid.distribution.open", create=True)
    @mock.patch("pythonforandroid.distribution.exists")
    @mock.patch("pythonforandroid.distribution.glob.glob")
    def test_get_distributions_error_ndk_api(
        self, mock_glob, mock_exists, mock_open_dist_info
    ):
        """Test method
        :meth:`~pythonforandroid.distribution.Distribution.get_distributions`
        in case that `ndk_api` is not set..which should return a `None`.
        """
        dist_info_data_no_ndk_api = dist_info_data.copy()
        dist_info_data_no_ndk_api.pop("ndk_api")
        self.setUp_distribution_with_bootstrap(
            Bootstrap().get_bootstrap("sdl2", self.ctx)
        )
        mock_glob.return_value = ["sdl2-python3"]
        mock_open_dist_info.side_effect = [
            mock.mock_open(
                read_data=json.dumps(dist_info_data_no_ndk_api)
            ).return_value
        ]

        dists = self.ctx.bootstrap.distribution.get_distributions(self.ctx)
        self.assertEqual(dists[0].ndk_api, None)
        mock_open_dist_info.assert_called_with("sdl2-python3/dist_info.json")
        mock_open_dist_info.reset_mock()

    @mock.patch("pythonforandroid.distribution.Distribution.get_distributions")
    @mock.patch("pythonforandroid.distribution.exists")
    @mock.patch("pythonforandroid.distribution.glob.glob")
    def test_get_distributions_error_ndk_api_mismatch(
        self, mock_glob, mock_exists, mock_get_dists
    ):
        """Test that method
        :meth:`~pythonforandroid.distribution.Distribution.get_distribution`
        raises an error in case that we have some distribution already build,
        with a given `name` and `ndk_api`, and we try to get another
        distribution with the same `name` but different `ndk_api`.
        """
        expected_dist = Distribution.get_distribution(
            self.ctx, name="test_prj", recipes=["python3", "kivy"]
        )
        mock_get_dists.return_value = [expected_dist]
        mock_glob.return_value = ["sdl2-python3"]

        with self.assertRaises(BuildInterruptingException) as e:
            self.setUp_distribution_with_bootstrap(
                Bootstrap().get_bootstrap("sdl2", self.ctx),
                allow_replace_dist=False,
                ndk_api=22,
            )
        self.assertEqual(
            e.exception.args[0],
            "Asked for dist with name test_prj with recipes (python3, kivy)"
            " and NDK API 22, but a dist with this name already exists and has"
            " either incompatible recipes (python3, kivy) or NDK API 21",
        )

    def test_get_distributions_error_extra_dist_dirs(self):
        """Test that method
        :meth:`~pythonforandroid.distribution.Distribution.get_distributions`
        raises an exception of
        :class:`~pythonforandroid.util.BuildInterruptingException` in case that
        we supply the kwargs `extra_dist_dirs`.
        """
        self.setUp_distribution_with_bootstrap(
            Bootstrap().get_bootstrap("sdl2", self.ctx)
        )
        with self.assertRaises(BuildInterruptingException) as e:
            self.ctx.bootstrap.distribution.get_distributions(
                self.ctx, extra_dist_dirs=["/fake/extra/dist_dirs"]
            )
        self.assertEqual(
            e.exception.args[0],
            "extra_dist_dirs argument to get"
            "_distributions is not yet implemented",
        )

    @mock.patch("pythonforandroid.distribution.Distribution.get_distributions")
    def test_get_distributions_possible_dists(self, mock_get_dists):
        """Test that method
        :meth:`~pythonforandroid.distribution.Distribution.get_distributions`
        returns the proper
        `:class:`~pythonforandroid.distribution.Distribution` in case that we
        already have it build and we request the same
        `:class:`~pythonforandroid.distribution.Distribution`.
        """
        expected_dist = Distribution.get_distribution(
            self.ctx, name="test_prj", recipes=["python3", "kivy"]
        )
        mock_get_dists.return_value = [expected_dist]
        self.setUp_distribution_with_bootstrap(
            Bootstrap().get_bootstrap("sdl2", self.ctx), name="test_prj"
        )
        dists = self.ctx.bootstrap.distribution.get_distributions(self.ctx)
        self.assertEqual(dists[0], expected_dist)
