# Copyright (C) 2023 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
from __future__ import annotations

import importlib
import os
import re
import shutil
import sys
import tempfile
import unittest
import subprocess
from pathlib import Path
from unittest import mock
from unittest.mock import patch

sys.path.append(os.fspath(Path(__file__).resolve().parents[2]))
from init_paths import init_test_paths  # noqa: E402
init_test_paths(False)


class DeployTestBase(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        cls.pyside_root = Path(__file__).parents[5].resolve()
        cls.example_root = cls.pyside_root / "examples"
        cls.temp_dir = tempfile.mkdtemp()
        cls.current_dir = Path.cwd()
        cls.pyside_wheel = Path("/tmp/PySide6-6.5.0a1-6.5.0-cp37-abi3-android_x86_64.whl")
        cls.shiboken_wheel = Path("/tmp/shiboken6-6.5.0a1-6.5.0-cp37-abi3-android_x86_64.whl")
        cls.ndk_path = Path("/tmp/android_sdk/ndk/25.2.9519653")
        cls.sdk_path = Path("/tmp/android_sdk")
        pyside_tools = cls.pyside_root / "sources" / "pyside-tools"

        # install extra python dependencies
        android_requirements_file = pyside_tools / "requirements-android.txt"
        with open(android_requirements_file, 'r', encoding='UTF-8') as file:
            while line := file.readline():
                dependent_package = line.rstrip()
                if not bool(importlib.util.find_spec(dependent_package)):
                    command = [sys.executable, "-m", "pip", "install", dependent_package]
                    subprocess.run(command)

        sys.path.append(str(pyside_tools))
        cls.deploy_lib = importlib.import_module("deploy_lib")
        cls.android_deploy = importlib.import_module("android_deploy")
        sys.modules["android_deploy"] = cls.android_deploy

        # required for comparing long strings
        cls.maxDiff = None

        # print no outputs to stdout
        sys.stdout = mock.MagicMock()

    def tearDown(self) -> None:
        super().tearDown()
        os.chdir(self.current_dir)

    @classmethod
    def tearDownClass(cls) -> None:
        shutil.rmtree(Path(cls.temp_dir))


@patch("deploy_lib.android.android_config.extract_and_copy_jar")
class TestPySide6AndroidDeployWidgets(DeployTestBase):
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        example_widget_application = cls.example_root / "gui" / "analogclock"
        cls.temp_example = Path(
            shutil.copytree(example_widget_application, Path(cls.temp_dir) / "analogclock")
        ).resolve()

    def setUp(self):
        os.chdir(self.temp_example)
        self.config_file = self.temp_example / "pysidedeploy.spec"
        self.buildozer_config = self.temp_example / "buildozer.spec"

    @patch("deploy_lib.android.android_config.AndroidConfig._find_local_libs")
    @patch("deploy_lib.android.android_config.AndroidConfig._find_dependent_qt_modules")
    @patch("deploy_lib.android.android_config.find_qtlibs_in_wheel")
    def test_dry_run(self, mock_qtlibs, mock_extraqtmodules, mock_local_libs, mock_extract_jar):
        mock_qtlibs.return_value = self.pyside_wheel / "PySide6/Qt/lib"
        mock_extraqtmodules.return_value = []
        dependent_plugins = ["platforms_qtforandroid",
                             "platforminputcontexts_qtvirtualkeyboardplugin",
                             "iconengines_qsvgicon"]
        mock_local_libs.return_value = [], dependent_plugins
        self.android_deploy.main(name="android_app", shiboken_wheel=self.shiboken_wheel,
                                 pyside_wheel=self.pyside_wheel, ndk_path=self.ndk_path,
                                 dry_run=True, force=True)

        self.assertEqual(mock_extract_jar.call_count, 0)
        self.assertEqual(mock_qtlibs.call_count, 1)
        self.assertEqual(mock_extraqtmodules.call_count, 1)
        self.assertEqual(mock_local_libs.call_count, 1)

    @patch("deploy_lib.android.buildozer.BuildozerConfig._BuildozerConfig__find_jars")
    @patch("deploy_lib.android.android_config.AndroidConfig.recipes_exist")
    @patch("deploy_lib.android.android_config.AndroidConfig._find_dependent_qt_modules")
    @patch("deploy_lib.android.android_config.find_qtlibs_in_wheel")
    def test_config(self, mock_qtlibs, mock_extraqtmodules, mock_recipes_exist, mock_find_jars,
                    mock_extract_jar):
        jar_dir = "tmp/jar/PySide6/jar"
        mock_extract_jar.return_value = Path(jar_dir)
        mock_qtlibs.return_value = self.pyside_wheel / "PySide6/Qt/lib"
        mock_extraqtmodules.return_value = []
        mock_recipes_exist.return_value = True
        jars, init_classes = ["/tmp/jar/PySide6/jar/Qt6Android.jar",
                              "/tmp/jar/PySide6/jar/Qt6AndroidBindings.jar"], []
        mock_find_jars.return_value = jars, init_classes

        self.android_deploy.main(name="android_app", shiboken_wheel=self.shiboken_wheel,
                                 pyside_wheel=self.pyside_wheel, ndk_path=self.ndk_path,
                                 init=True, force=True, keep_deployment_files=True)

        self.assertEqual(mock_extract_jar.call_count, 1)
        self.assertEqual(mock_qtlibs.call_count, 1)
        self.assertEqual(mock_extraqtmodules.call_count, 1)
        self.assertEqual(mock_recipes_exist.call_count, 1)
        self.assertEqual(mock_find_jars.call_count, 1)
        self.assertTrue(self.config_file.exists())
        self.assertTrue(self.buildozer_config.exists())

        # test config file contents
        config_obj = self.deploy_lib.BaseConfig(config_file=self.config_file)
        self.assertTrue(config_obj.get_value("app", "input_file").endswith("main.py"))
        self.assertEqual(config_obj.get_value("python", "android_packages"),
                         "buildozer==1.5.0,cython==0.29.33")
        self.assertEqual(config_obj.get_value("android", "wheel_pyside"),
                         str(self.pyside_wheel.resolve()))
        self.assertEqual(config_obj.get_value("android", "wheel_shiboken"),
                         str(self.shiboken_wheel.resolve()))
        self.assertEqual(config_obj.get_value("buildozer", "mode"), "debug")
        self.assertEqual(config_obj.get_value("buildozer", "recipe_dir"),
                         '')
        self.assertEqual(config_obj.get_value("buildozer", "jars_dir"),
                         str(self.temp_example / jar_dir))
        self.assertIn(str(self.ndk_path), config_obj.get_value("buildozer", "ndk_path"))
        self.assertEqual(config_obj.get_value("buildozer", "sdk_path"), '')
        expected_modules = {"Core", "Gui"}
        obtained_modules = set(config_obj.get_value("qt", "modules").split(","))
        self.assertEqual(obtained_modules, expected_modules)
        expected_local_libs = ""
        self.assertEqual(config_obj.get_value("buildozer", "local_libs"),
                         expected_local_libs)
        self.assertEqual(config_obj.get_value("buildozer", "arch"), "x86_64")

        # test buildozer config file contents
        buildozer_config_obj = self.deploy_lib.BaseConfig(config_file=self.buildozer_config)
        obtained_jars = set(buildozer_config_obj.get_value("app", "android.add_jars").split(','))
        expected_jars = set(jars)
        self.assertEqual(obtained_jars, expected_jars)
        obtained_extra_args = buildozer_config_obj.get_value("app", "p4a.extra_args")
        extra_args_patrn = re.compile("--qt-libs=(?P<modules>.*) --load-local-libs="
                                      "(?P<local_libs>.*) --init-classes=(?P<init_classes>.*)")
        match = extra_args_patrn.search(obtained_extra_args)
        obtained_modules = match.group("modules").split(',')
        obtained_local_libs = match.group("local_libs")
        obtained_init_classes = match.group("init_classes")
        self.assertEqual(set(obtained_modules), expected_modules)
        self.assertEqual(obtained_local_libs, expected_local_libs)
        self.assertEqual(obtained_init_classes, '')
        expected_include_exts = "py,png,jpg,kv,atlas,qml,js"
        obtained_include_exts = buildozer_config_obj.get_value("app", "source.include_exts")
        self.assertEqual(expected_include_exts, obtained_include_exts)

        self.config_file.unlink()
        self.buildozer_config.unlink()

    def test_errors(self, mock_extract_jar):
        # test when no shiboken wheel is passed
        with self.assertRaises(RuntimeError) as context:
            self.android_deploy.main(name="android_app", pyside_wheel=self.pyside_wheel,
                                     ndk_path=self.ndk_path, force=True)
        self.assertTrue("Unable to find shiboken6 Android wheel" in str(context.exception))

        # test when no PySide wheel is passed
        with self.assertRaises(RuntimeError) as context:
            self.android_deploy.main(name="android_app", shiboken_wheel=self.shiboken_wheel,
                                     ndk_path=self.ndk_path, force=True)
        self.assertTrue("Unable to find PySide6 Android wheel" in str(context.exception))

        # test if wheel name is correct
        pyside_wheel_temp = Path("/tmp/"
                                 "PySide6-6.8.0.1+commercial-6.8.0.1-cp311-cp311-android_fake.whl")
        with self.assertRaises(RuntimeError) as context:
            self.android_deploy.main(name="android_app", shiboken_wheel=self.shiboken_wheel,
                                     pyside_wheel=pyside_wheel_temp, ndk_path=self.ndk_path,
                                     force=True)
        self.assertTrue("PySide wheel corrupted" in str(context.exception))

        # test when cwd() is not project_dir
        os.chdir(self.current_dir)
        with self.assertRaises(RuntimeError) as context:
            self.android_deploy.main(name="android_app", shiboken_wheel=self.shiboken_wheel,
                                     pyside_wheel=self.pyside_wheel, init=True, force=True)
        self.assertTrue("For Android deployment to work" in str(context.exception))

    @patch("deploy_lib.android.buildozer.BuildozerConfig._BuildozerConfig__find_jars")
    @patch("deploy_lib.android.android_config.AndroidConfig.recipes_exist")
    @patch("deploy_lib.android.android_config.AndroidConfig._find_dependent_qt_modules")
    @patch("deploy_lib.android.android_config.find_qtlibs_in_wheel")
    @patch("deploy_lib.android.android_config.download_android_ndk")
    def test_no_ndk_passed(self, mock_ndk, mock_qtlibs, mock_extraqtmodules, mock_recipes_exist,
                           mock_find_jars, mock_extract_jar):
        jar_dir = "tmp/jar/PySide6/jar"
        mock_extract_jar.return_value = Path(jar_dir)
        mock_qtlibs.return_value = self.pyside_wheel / "PySide6/Qt/lib"
        mock_extraqtmodules.return_value = []
        mock_recipes_exist.return_value = True
        jars, init_classes = ["/tmp/jar/PySide6/jar/Qt6Android.jar",
                              "/tmp/jar/PySide6/jar/Qt6AndroidBindings.jar"], []
        mock_find_jars.return_value = jars, init_classes
        mock_ndk.return_value = Path("/tmp/android_ndk")

        self.android_deploy.main(name="android_app", shiboken_wheel=self.shiboken_wheel,
                                 pyside_wheel=self.pyside_wheel,
                                 init=True, force=True,
                                 keep_deployment_files=True)

        # test config file contents
        config_obj = self.deploy_lib.BaseConfig(config_file=self.config_file)
        self.assertEqual(config_obj.get_value("buildozer", "sdk_path"), '')
        self.assertEqual(config_obj.get_value("buildozer", "ndk_path"), "/tmp/android_ndk")


@patch("deploy_lib.config.run_qmlimportscanner")
@patch("deploy_lib.android.android_config.extract_and_copy_jar")
class TestPySide6AndroidDeployQml(DeployTestBase):
    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        # setting up example
        example_qml_application = cls.example_root / "quick" / "models" / "stringlistmodel"
        cls.temp_qml_example = Path(
            shutil.copytree(example_qml_application, Path(cls.temp_dir) / "stringlistmodel")
        ).resolve()

    def setUp(self):
        os.chdir(self.temp_qml_example)
        self.config_file = self.temp_qml_example / "pysidedeploy.spec"
        self.buildozer_config_file = self.temp_qml_example / "buildozer.spec"
        (self.temp_qml_example / "stringlistmodel.py").rename(self.temp_qml_example / "main.py")
        (self.temp_qml_example / "stringlistmodel.pyproject").unlink()

    @patch("deploy_lib.android.android_config.AndroidConfig._find_local_libs")
    @patch("deploy_lib.android.buildozer.BuildozerConfig._BuildozerConfig__find_jars")
    @patch("deploy_lib.android.android_config.AndroidConfig.recipes_exist")
    @patch("deploy_lib.android.android_config.AndroidConfig._find_dependent_qt_modules")
    @patch("deploy_lib.android.android_config.find_qtlibs_in_wheel")
    def test_config_with_Qml(self, mock_qtlibs, mock_extraqtmodules, mock_recipes_exist,
                             mock_find_jars, mock_local_libs, mock_extract_jar,
                             mock_qmlimportscanner):
        # setting up mocks
        jar_dir = "tmp/jar/PySide6/jar"
        mock_extract_jar.return_value = Path(jar_dir)
        mock_qtlibs.return_value = self.pyside_wheel / "PySide6/Qt/lib"
        mock_extraqtmodules.return_value = ['Qml', 'Network', 'QmlModels', 'OpenGL']
        mock_recipes_exist.return_value = True
        jars, init_classes = ["/tmp/jar/PySide6/jar/Qt6Android.jar",
                              "/tmp/jar/PySide6/jar/Qt6AndroidBindings.jar",
                              "/tmp/jar/PySide6/jar/Qt6AndroidNetworkInformationBackend.jar",
                              "/tmp/jar/PySide6/jar/Qt6AndroidNetwork.jar"], []
        mock_find_jars.return_value = jars, init_classes
        dependent_plugins = ["platforms_qtforandroid",
                             "platforminputcontexts_qtvirtualkeyboardplugin",
                             "iconengines_qsvgicon"]
        mock_local_libs.return_value = [], dependent_plugins
        mock_qmlimportscanner.return_value = ["QtQuick"]

        self.android_deploy.main(name="android_app", shiboken_wheel=self.shiboken_wheel,
                                 pyside_wheel=self.pyside_wheel, ndk_path=self.ndk_path,
                                 init=True, force=True, keep_deployment_files=True)

        self.assertEqual(mock_extract_jar.call_count, 1)
        self.assertEqual(mock_qtlibs.call_count, 1)
        self.assertEqual(mock_extraqtmodules.call_count, 1)
        self.assertEqual(mock_recipes_exist.call_count, 1)
        self.assertEqual(mock_find_jars.call_count, 1)
        self.assertEqual(mock_qmlimportscanner.call_count, 1)
        self.assertTrue(self.config_file.exists())
        self.assertTrue(self.buildozer_config_file.exists())

        config_obj = self.deploy_lib.BaseConfig(config_file=self.config_file)
        expected_modules = {"Quick", "Core", "Gui", "Network", "Qml", "QmlModels", "OpenGL"}
        obtained_modules = set(config_obj.get_value("qt", "modules").split(","))
        self.assertEqual(obtained_modules, expected_modules)
        expected_local_libs = ""
        self.assertEqual(config_obj.get_value("buildozer", "local_libs"),
                         expected_local_libs)
        expected_qt_plugins = set(dependent_plugins)
        obtained_qt_plugins = set(config_obj.get_value("android", "plugins").split(","))
        self.assertEqual(expected_qt_plugins, obtained_qt_plugins)

        # test buildozer config file contents
        buildozer_config_obj = self.deploy_lib.BaseConfig(config_file=self.buildozer_config_file)
        obtained_jars = set(buildozer_config_obj.get_value("app", "android.add_jars").split(','))
        expected_jars = set(jars)
        self.assertEqual(obtained_jars, expected_jars)
        obtained_extra_args = buildozer_config_obj.get_value("app", "p4a.extra_args")
        extra_args_patrn = re.compile("--qt-libs=(?P<modules>.*) --load-local-libs="
                                      "(?P<local_libs>.*) --init-classes=(?P<init_classes>.*)")
        match = extra_args_patrn.search(obtained_extra_args)
        obtained_modules = match.group("modules").split(',')
        obtained_local_libs = match.group("local_libs")
        obtained_init_classes = match.group("init_classes")
        self.assertEqual(set(obtained_modules), expected_modules)
        self.assertEqual(obtained_local_libs, expected_local_libs)
        self.assertEqual(obtained_init_classes, '')

        self.config_file.unlink()
        self.buildozer_config_file.unlink()


if __name__ == "__main__":
    unittest.main()
