import unittest
from os.path import join
from sys import version as py_version

from unittest import mock
from pythonforandroid.recommendations import (
    check_ndk_api,
    check_ndk_version,
    check_target_api,
    read_ndk_version,
    check_python_version,
    print_recommendations,
    MAX_NDK_VERSION,
    RECOMMENDED_NDK_VERSION,
    RECOMMENDED_TARGET_API,
    MIN_NDK_API,
    MIN_NDK_VERSION,
    NDK_DOWNLOAD_URL,
    ARMEABI_MAX_TARGET_API,
    MIN_TARGET_API,
    UNKNOWN_NDK_MESSAGE,
    PARSE_ERROR_NDK_MESSAGE,
    READ_ERROR_NDK_MESSAGE,
    ENSURE_RIGHT_NDK_MESSAGE,
    NDK_LOWER_THAN_SUPPORTED_MESSAGE,
    UNSUPPORTED_NDK_API_FOR_ARMEABI_MESSAGE,
    CURRENT_NDK_VERSION_MESSAGE,
    RECOMMENDED_NDK_VERSION_MESSAGE,
    TARGET_NDK_API_GREATER_THAN_TARGET_API_MESSAGE,
    OLD_NDK_API_MESSAGE,
    NEW_NDK_MESSAGE,
    OLD_API_MESSAGE,
    MIN_PYTHON_MAJOR_VERSION,
    MIN_PYTHON_MINOR_VERSION,
    PY2_ERROR_TEXT,
    PY_VERSION_ERROR_TEXT,
)

from pythonforandroid.util import BuildInterruptingException

running_in_py2 = int(py_version[0]) < 3


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

    def setUp(self):
        self.ndk_dir = "/opt/android/android-ndk"

    @unittest.skipIf(running_in_py2, "`assertLogs` requires Python 3.4+")
    @mock.patch("pythonforandroid.recommendations.read_ndk_version")
    def test_check_ndk_version_greater_than_recommended(self, mock_read_ndk):
        mock_read_ndk.return_value.version = [MAX_NDK_VERSION + 1, 0, 5232133]
        with self.assertLogs(level="INFO") as cm:
            check_ndk_version(self.ndk_dir)
        mock_read_ndk.assert_called_once_with(self.ndk_dir)
        self.assertEqual(
            cm.output,
            [
                "INFO:p4a:[INFO]:    {}".format(
                    CURRENT_NDK_VERSION_MESSAGE.format(
                        ndk_version=MAX_NDK_VERSION + 1
                    )
                ),
                "WARNING:p4a:[WARNING]: {}".format(
                    RECOMMENDED_NDK_VERSION_MESSAGE.format(
                        recommended_ndk_version=RECOMMENDED_NDK_VERSION
                    )
                ),
                "WARNING:p4a:[WARNING]: {}".format(NEW_NDK_MESSAGE),
            ],
        )

    @mock.patch("pythonforandroid.recommendations.read_ndk_version")
    def test_check_ndk_version_lower_than_recommended(self, mock_read_ndk):
        mock_read_ndk.return_value.version = [MIN_NDK_VERSION - 1, 0, 5232133]
        with self.assertRaises(BuildInterruptingException) as e:
            check_ndk_version(self.ndk_dir)
        self.assertEqual(
            e.exception.args[0],
            NDK_LOWER_THAN_SUPPORTED_MESSAGE.format(
                min_supported=MIN_NDK_VERSION, ndk_url=NDK_DOWNLOAD_URL
            ),
        )
        mock_read_ndk.assert_called_once_with(self.ndk_dir)

    @unittest.skipIf(running_in_py2, "`assertLogs` requires Python 3.4+")
    def test_check_ndk_version_error(self):
        """
        Test that a fake ndk dir give us two messages:
            - first should be an `INFO` log
            - second should be an `WARNING` log
        """
        with self.assertLogs(level="INFO") as cm:
            check_ndk_version(self.ndk_dir)
        self.assertEqual(
            cm.output,
            [
                "INFO:p4a:[INFO]:    {}".format(UNKNOWN_NDK_MESSAGE),
                "WARNING:p4a:[WARNING]: {}".format(
                    READ_ERROR_NDK_MESSAGE.format(ndk_dir=self.ndk_dir)
                ),
                "WARNING:p4a:[WARNING]: {}".format(
                    ENSURE_RIGHT_NDK_MESSAGE.format(
                        min_supported=MIN_NDK_VERSION,
                        rec_version=RECOMMENDED_NDK_VERSION,
                        ndk_url=NDK_DOWNLOAD_URL,
                    )
                ),
            ],
        )

    @mock.patch("pythonforandroid.recommendations.open")
    def test_read_ndk_version(self, mock_open_src_prop):
        mock_open_src_prop.side_effect = [
            mock.mock_open(
                read_data="Pkg.Revision = 17.2.4988734"
            ).return_value
        ]
        version = read_ndk_version(self.ndk_dir)
        mock_open_src_prop.assert_called_once_with(
            join(self.ndk_dir, "source.properties")
        )
        assert version == "17.2.4988734"

    @unittest.skipIf(running_in_py2, "`assertLogs` requires Python 3.4+")
    @mock.patch("pythonforandroid.recommendations.open")
    def test_read_ndk_version_error(self, mock_open_src_prop):
        mock_open_src_prop.side_effect = [
            mock.mock_open(read_data="").return_value
        ]
        with self.assertLogs(level="INFO") as cm:
            version = read_ndk_version(self.ndk_dir)
        self.assertEqual(
            cm.output,
            ["INFO:p4a:[INFO]:    {}".format(PARSE_ERROR_NDK_MESSAGE)],
        )
        mock_open_src_prop.assert_called_once_with(
            join(self.ndk_dir, "source.properties")
        )
        assert version is None

    def test_check_target_api_error_arch_armeabi(self):

        with self.assertRaises(BuildInterruptingException) as e:
            check_target_api(RECOMMENDED_TARGET_API, "armeabi")
        self.assertEqual(
            e.exception.args[0],
            UNSUPPORTED_NDK_API_FOR_ARMEABI_MESSAGE.format(
                req_ndk_api=RECOMMENDED_TARGET_API,
                max_ndk_api=ARMEABI_MAX_TARGET_API,
            ),
        )

    @unittest.skipIf(running_in_py2, "`assertLogs` requires Python 3.4+")
    def test_check_target_api_warning_target_api(self):

        with self.assertLogs(level="INFO") as cm:
            check_target_api(MIN_TARGET_API - 1, MIN_TARGET_API)
        self.assertEqual(
            cm.output,
            [
                "WARNING:p4a:[WARNING]: Target API 29 < 30",
                "WARNING:p4a:[WARNING]: {old_api_msg}".format(
                    old_api_msg=OLD_API_MESSAGE
                ),
            ],
        )

    def test_check_ndk_api_error_android_api(self):
        """
        Given an `android api` greater than an `ndk_api`, we should get an
        `BuildInterruptingException`.
        """
        ndk_api = MIN_NDK_API + 1
        android_api = MIN_NDK_API
        with self.assertRaises(BuildInterruptingException) as e:
            check_ndk_api(ndk_api, android_api)
        self.assertEqual(
            e.exception.args[0],
            TARGET_NDK_API_GREATER_THAN_TARGET_API_MESSAGE.format(
                ndk_api=ndk_api, android_api=android_api
            ),
        )

    @unittest.skipIf(running_in_py2, "`assertLogs` requires Python 3.4+")
    def test_check_ndk_api_warning_old_ndk(self):
        """
        Given an `android api` lower than the supported by p4a, we should
        get an `BuildInterruptingException`.
        """
        ndk_api = MIN_NDK_API - 1
        android_api = RECOMMENDED_TARGET_API
        with self.assertLogs(level="INFO") as cm:
            check_ndk_api(ndk_api, android_api)
        self.assertEqual(
            cm.output,
            [
                "WARNING:p4a:[WARNING]: {}".format(
                    OLD_NDK_API_MESSAGE.format(MIN_NDK_API)
                )
            ],
        )

    def test_check_python_version(self):
        """With any version info lower than the minimum, we should get a
        BuildInterruptingException with an appropriate message.
        """
        with mock.patch('sys.version_info') as fake_version_info:

            # Major version is Python 2 => exception
            fake_version_info.major = MIN_PYTHON_MAJOR_VERSION - 1
            fake_version_info.minor = MIN_PYTHON_MINOR_VERSION
            with self.assertRaises(BuildInterruptingException) as context:
                check_python_version()
            assert context.exception.message == PY2_ERROR_TEXT

            # Major version too low => exception
            # Using a float valued major version just to test the logic and avoid
            # clashing with the Python 2 check
            fake_version_info.major = MIN_PYTHON_MAJOR_VERSION - 0.1
            fake_version_info.minor = MIN_PYTHON_MINOR_VERSION
            with self.assertRaises(BuildInterruptingException) as context:
                check_python_version()
            assert context.exception.message == PY_VERSION_ERROR_TEXT

            # Minor version too low => exception
            fake_version_info.major = MIN_PYTHON_MAJOR_VERSION
            fake_version_info.minor = MIN_PYTHON_MINOR_VERSION - 1
            with self.assertRaises(BuildInterruptingException) as context:
                check_python_version()
            assert context.exception.message == PY_VERSION_ERROR_TEXT

            # Version high enough => nothing interesting happens
            fake_version_info.major = MIN_PYTHON_MAJOR_VERSION
            fake_version_info.minor = MIN_PYTHON_MINOR_VERSION
            check_python_version()

    def test_print_recommendations(self):
        """
        Simple test that the function actually runs.
        """
        # The main failure mode is if the function tries to print a variable
        # that doesn't actually exist, so simply running to check all the
        # prints work is the most important test.
        print_recommendations()
