# SPDX-License-Identifier: LGPL-2.1-or-later
# SPDX-FileNotice: Part of the AddonManager.

################################################################################
#                                                                              #
#   This addon is free software: you can redistribute it and/or modify         #
#   it under the terms of the GNU Lesser General Public License as             #
#   published by the Free Software Foundation, either version 2.1              #
#   of the License, or (at your option) any later version.                     #
#                                                                              #
#   This addon is distributed in the hope that it will be useful,              #
#   but WITHOUT ANY WARRANTY; without even the implied warranty                #
#   of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.                    #
#   See the GNU Lesser General Public License for more details.                #
#                                                                              #
#   You should have received a copy of the GNU Lesser General Public           #
#   License along with this addon. If not, see https://www.gnu.org/licenses    #
#                                                                              #
################################################################################

import os
import tempfile
import unittest

from Addon import Addon, INTERNAL_WORKBENCHES
from addonmanager_macro import Macro


class TestAddon(unittest.TestCase):

    def setUp(self):
        self.test_dir = os.path.join(os.path.dirname(__file__), "..", "data")

    def test_display_name(self):
        # Case 1: No display name set elsewhere: name == display_name
        addon = Addon(
            "FreeCAD",
            "https://github.com/FreeCAD/FreeCAD",
            Addon.Status.NOT_INSTALLED,
            "master",
        )
        self.assertEqual(addon.name, "FreeCAD")
        self.assertEqual(addon.display_name, "FreeCAD")

        # Case 2: Package.xml metadata file sets a display name:
        addon.load_metadata_file(os.path.join(self.test_dir, "good_package.xml"))
        self.assertEqual(addon.name, "FreeCAD")
        self.assertEqual(addon.display_name, "Test Workbench")

    def test_git_url_cleanup(self):
        base_url = "https://github.com/FreeCAD/FreeCAD"
        test_urls = [f"  {base_url}  ", f"{base_url}.git", f"  {base_url}.git  "]
        for url in test_urls:
            addon = Addon("FreeCAD", url, Addon.Status.NOT_INSTALLED, "master")
            self.assertEqual(addon.url, base_url)

    def test_tag_extraction(self):
        addon = Addon(
            "FreeCAD",
            "https://github.com/FreeCAD/FreeCAD",
            Addon.Status.NOT_INSTALLED,
            "master",
        )
        addon.load_metadata_file(os.path.join(self.test_dir, "good_package.xml"))

        tags = addon.tags
        self.assertEqual(len(tags), 5)
        expected_tags = set()
        expected_tags.add("Tag0")
        expected_tags.add("Tag1")
        expected_tags.add("TagA")
        expected_tags.add("TagB")
        expected_tags.add("TagC")
        self.assertEqual(expected_tags, tags)

    def test_contains_functions(self):
        # Test package.xml combinations:

        # Workbenches
        addon_with_workbench = Addon(
            "FreeCAD",
            "https://github.com/FreeCAD/FreeCAD",
            Addon.Status.NOT_INSTALLED,
            "master",
        )
        addon_with_workbench.load_metadata_file(os.path.join(self.test_dir, "workbench_only.xml"))
        self.assertTrue(addon_with_workbench.contains_workbench())
        self.assertFalse(addon_with_workbench.contains_macro())
        self.assertFalse(addon_with_workbench.contains_preference_pack())
        self.assertFalse(addon_with_workbench.contains_bundle())
        self.assertFalse(addon_with_workbench.contains_other())

        # Macros
        addon_with_macro = Addon(
            "FreeCAD",
            "https://github.com/FreeCAD/FreeCAD",
            Addon.Status.NOT_INSTALLED,
            "master",
        )
        addon_with_macro.load_metadata_file(os.path.join(self.test_dir, "macro_only.xml"))
        self.assertFalse(addon_with_macro.contains_workbench())
        self.assertTrue(addon_with_macro.contains_macro())
        self.assertFalse(addon_with_macro.contains_preference_pack())
        self.assertFalse(addon_with_workbench.contains_bundle())
        self.assertFalse(addon_with_workbench.contains_other())

        # Preference Packs
        addon_with_prefpack = Addon(
            "FreeCAD",
            "https://github.com/FreeCAD/FreeCAD",
            Addon.Status.NOT_INSTALLED,
            "master",
        )
        addon_with_prefpack.load_metadata_file(os.path.join(self.test_dir, "prefpack_only.xml"))
        self.assertFalse(addon_with_prefpack.contains_workbench())
        self.assertFalse(addon_with_prefpack.contains_macro())
        self.assertTrue(addon_with_prefpack.contains_preference_pack())
        self.assertFalse(addon_with_workbench.contains_bundle())
        self.assertFalse(addon_with_workbench.contains_other())

        # Combination
        addon_with_all = Addon(
            "FreeCAD",
            "https://github.com/FreeCAD/FreeCAD",
            Addon.Status.NOT_INSTALLED,
            "master",
        )
        addon_with_all.load_metadata_file(os.path.join(self.test_dir, "combination.xml"))
        self.assertTrue(addon_with_all.contains_workbench())
        self.assertTrue(addon_with_all.contains_macro())
        self.assertTrue(addon_with_all.contains_preference_pack())
        self.assertTrue(addon_with_all.contains_bundle())
        self.assertTrue(addon_with_all.contains_other())

        # Now do the simple, explicitly-set cases
        addon_wb = Addon(
            "FreeCAD",
            "https://github.com/FreeCAD/FreeCAD",
            Addon.Status.NOT_INSTALLED,
            "master",
        )
        addon_wb.repo_type = Addon.Kind.WORKBENCH
        self.assertTrue(addon_wb.contains_workbench())
        self.assertFalse(addon_wb.contains_macro())
        self.assertFalse(addon_wb.contains_preference_pack())

        addon_m = Addon(
            "FreeCAD",
            "https://github.com/FreeCAD/FreeCAD",
            Addon.Status.NOT_INSTALLED,
            "master",
        )
        addon_m.repo_type = Addon.Kind.MACRO
        self.assertFalse(addon_m.contains_workbench())
        self.assertTrue(addon_m.contains_macro())
        self.assertFalse(addon_m.contains_preference_pack())

        # There is no equivalent for preference packs, they are always accompanied by a
        # metadata file

    def test_create_from_macro(self):
        macro_file = os.path.join(self.test_dir, "DoNothing.FCMacro")
        macro = Macro("DoNothing")
        macro.fill_details_from_file(macro_file)
        addon = Addon.from_macro(macro)

        self.assertEqual(addon.repo_type, Addon.Kind.MACRO)
        self.assertEqual(addon.name, "DoNothing")
        self.assertEqual(
            addon.macro.comment,
            "Do absolutely nothing. For Addon Manager integration tests.",
        )
        self.assertEqual(addon.url, "https://github.com/FreeCAD/FreeCAD")
        self.assertEqual(addon.macro.version, "1.0")
        self.assertEqual(len(addon.macro.other_files), 3)
        self.assertEqual(addon.macro.author, "Chris Hennes")
        self.assertEqual(addon.macro.date, "2022-02-28")
        self.assertEqual(addon.macro.icon, "not_real.png")
        self.assertNotEqual(addon.macro.xpm, "")

    def test_dependency_resolution(self):
        addonA = Addon(
            "AddonA",
            "https://github.com/FreeCAD/FakeAddonA",
            Addon.Status.NOT_INSTALLED,
            "master",
        )
        addonB = Addon(
            "AddonB",
            "https://github.com/FreeCAD/FakeAddonB",
            Addon.Status.NOT_INSTALLED,
            "master",
        )
        addonC = Addon(
            "AddonC",
            "https://github.com/FreeCAD/FakeAddonC",
            Addon.Status.NOT_INSTALLED,
            "master",
        )
        addonD = Addon(
            "AddonD",
            "https://github.com/FreeCAD/FakeAddonD",
            Addon.Status.NOT_INSTALLED,
            "master",
        )

        addonA.requires.add("AddonB")
        addonB.requires.add("AddonC")
        addonB.requires.add("AddonD")
        addonD.requires.add("CAM")

        all_addons = {
            addonA.name: addonA,
            addonB.name: addonB,
            addonC.name: addonC,
            addonD.name: addonD,
        }

        deps = Addon.Dependencies()
        addonA.walk_dependency_tree(all_addons, deps)

        self.assertEqual(len(deps.required_external_addons), 3)
        addon_strings = [addon.name for addon in deps.required_external_addons]
        self.assertTrue(
            "AddonB" in addon_strings,
            "AddonB not in required dependencies, and it should be.",
        )
        self.assertTrue(
            "AddonC" in addon_strings,
            "AddonC not in required dependencies, and it should be.",
        )
        self.assertTrue(
            "AddonD" in addon_strings,
            "AddonD not in required dependencies, and it should be.",
        )
        self.assertTrue(
            "CAM" in deps.internal_workbenches,
            "CAM not in workbench dependencies, and it should be.",
        )

    def test_internal_workbench_list(self):
        addon = Addon(
            "FreeCAD",
            "https://github.com/FreeCAD/FreeCAD",
            Addon.Status.NOT_INSTALLED,
            "master",
        )
        addon.load_metadata_file(os.path.join(self.test_dir, "depends_on_all_workbenches.xml"))
        deps = Addon.Dependencies()
        addon.walk_dependency_tree({}, deps)
        self.assertEqual(len(deps.internal_workbenches), len(INTERNAL_WORKBENCHES))

    def test_version_check(self):
        addon = Addon(
            "FreeCAD",
            "https://github.com/FreeCAD/FreeCAD",
            Addon.Status.NOT_INSTALLED,
            "master",
        )
        addon.load_metadata_file(os.path.join(self.test_dir, "test_version_detection.xml"))

        self.assertEqual(
            len(addon.tags),
            1,
            "Wrong number of tags found: version requirements should have restricted to only one",
        )
        self.assertFalse(
            "TagA" in addon.tags,
            "Found 'TagA' in tags, it should have been excluded by version requirement",
        )
        self.assertTrue(
            "TagB" in addon.tags,
            "Failed to find 'TagB' in tags, it should have been included",
        )
        self.assertFalse(
            "TagC" in addon.tags,
            "Found 'TagA' in tags, it should have been excluded by version requirement",
        )

    def test_try_find_wbname_in_files_empty_dir(self):
        with tempfile.TemporaryDirectory() as mod_dir:
            # Arrange
            test_addon = Addon("test")
            test_addon.mod_directory = mod_dir
            os.mkdir(os.path.join(mod_dir, test_addon.name))

            # Act
            wb_name = test_addon.try_find_workbench_name_in_files()

            # Assert
            self.assertEqual(wb_name, "")

    def test_try_find_wbname_in_files_non_python_ignored(self):
        with tempfile.TemporaryDirectory() as mod_dir:
            # Arrange
            test_addon = Addon("test")
            test_addon.mod_directory = mod_dir
            base_path = os.path.join(mod_dir, test_addon.name)
            os.mkdir(base_path)
            file_path = os.path.join(base_path, "test.txt")
            with open(file_path, "w", encoding="utf-8") as f:
                f.write("Gui.addWorkbench(TestWorkbench())")

            # Act
            wb_name = test_addon.try_find_workbench_name_in_files()

            # Assert
            self.assertEqual(wb_name, "")

    def test_try_find_wbname_in_files_simple(self):
        with tempfile.TemporaryDirectory() as mod_dir:
            # Arrange
            test_addon = Addon("test")
            test_addon.mod_directory = mod_dir
            base_path = os.path.join(mod_dir, test_addon.name)
            os.mkdir(base_path)
            file_path = os.path.join(base_path, "test.py")
            with open(file_path, "w", encoding="utf-8") as f:
                f.write("Gui.addWorkbench(TestWorkbench())")

            # Act
            wb_name = test_addon.try_find_workbench_name_in_files()

            # Assert
            self.assertEqual(wb_name, "TestWorkbench")

    def test_try_find_wbname_in_files_subdir(self):
        with tempfile.TemporaryDirectory() as mod_dir:
            # Arrange
            test_addon = Addon("test")
            test_addon.mod_directory = mod_dir
            base_path = os.path.join(mod_dir, test_addon.name)
            os.mkdir(base_path)
            subdir = os.path.join(base_path, "subdirectory")
            os.mkdir(subdir)
            file_path = os.path.join(subdir, "test.py")
            with open(file_path, "w", encoding="utf-8") as f:
                f.write("Gui.addWorkbench(TestWorkbench())")

            # Act
            wb_name = test_addon.try_find_workbench_name_in_files()

            # Assert
            self.assertEqual(wb_name, "TestWorkbench")

    def test_try_find_wbname_in_files_variable_used(self):
        with tempfile.TemporaryDirectory() as mod_dir:
            # Arrange
            test_addon = Addon("test")
            test_addon.mod_directory = mod_dir
            base_path = os.path.join(mod_dir, test_addon.name)
            os.mkdir(base_path)
            file_path = os.path.join(base_path, "test.py")
            with open(file_path, "w", encoding="utf-8") as f:
                f.write("Gui.addWorkbench(wb)")

            # Act
            wb_name = test_addon.try_find_workbench_name_in_files()

            # Assert
            self.assertEqual(wb_name, "")

    def test_try_find_wbname_in_files_variants(self):
        variants = [
            "Gui.addWorkbench(TestWorkbench())",
            "Gui.addWorkbench (TestWorkbench())",
            "Gui.addWorkbench( TestWorkbench() )",
            "Gui.addWorkbench(TestWorkbench( ))",
            "Gui.addWorkbench( TestWorkbench( ) )",
            "Gui.addWorkbench( TestWorkbench ( ) )",
            "Gui.addWorkbench ( TestWorkbench ( ) )",
        ]
        for variant in variants:
            with self.subTest(variant=variant):
                with tempfile.TemporaryDirectory() as mod_dir:
                    # Arrange
                    test_addon = Addon("test")
                    test_addon.mod_directory = mod_dir
                    base_path = os.path.join(mod_dir, test_addon.name)
                    os.mkdir(base_path)
                    file_path = os.path.join(base_path, "test.py")
                    with open(file_path, "w", encoding="utf-8") as f:
                        f.write(variant)

                    # Act
                    wb_name = test_addon.try_find_workbench_name_in_files()

                    # Assert
                    self.assertEqual(wb_name, "TestWorkbench")

    def test_addon_less_than(self):
        a = Addon("A")
        b = Addon("B")
        self.assertLess(a, b)

        aa = Addon("A", branch="A")
        ab = Addon("A", branch="B")
        self.assertLess(aa, ab)

        same = Addon("A", branch="A")
        self.assertGreaterEqual(same, aa)
        self.assertGreaterEqual(aa, same)

    def test_addon_less_than_or_equal_to(self):
        a = Addon("A")
        b = Addon("B")
        self.assertLessEqual(a, b)

        aa = Addon("A", branch="A")
        ab = Addon("A", branch="B")
        self.assertLessEqual(aa, ab)

        same = Addon("A", branch="A")
        self.assertLessEqual(same, aa)
        self.assertLessEqual(aa, same)
