# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# *                                                                         *
# *   Copyright (c) 2022 FreeCAD Project Association                        *
# *                                                                         *
# *   This file is part of FreeCAD.                                         *
# *                                                                         *
# *   FreeCAD 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.                       *
# *                                                                         *
# *   FreeCAD 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 FreeCAD. If not, see                               *
# *   <https://www.gnu.org/licenses/>.                                      *
# *                                                                         *
# ***************************************************************************

from time import sleep
import unittest
import FreeCAD

from Addon import Addon

from PySide import QtCore, QtWidgets

from addonmanager_update_all_gui import UpdateAllGUI, AddonStatus


class MockUpdater(QtCore.QObject):
    success = QtCore.Signal(object)
    failure = QtCore.Signal(object)
    finished = QtCore.Signal()

    def __init__(self, addon, addons=[]):
        super().__init__()
        self.addon_to_install = addon
        self.addons = addons
        self.has_run = False
        self.emit_success = True
        self.work_function = (
            None  # Set to some kind of callable to make this function take time
        )

    def run(self):
        self.has_run = True
        if self.work_function is not None and callable(self.work_function):
            self.work_function()
        if self.emit_success:
            self.success.emit(self.addon_to_install)
        else:
            self.failure.emit(self.addon_to_install)
        self.finished.emit()


class MockUpdaterFactory:
    def __init__(self, addons):
        self.addons = addons
        self.work_function = None
        self.updater = None

    def get_updater(self, addon):
        self.updater = MockUpdater(addon, self.addons)
        self.updater.work_function = self.work_function
        return self.updater


class MockAddon:
    def __init__(self, name):
        self.display_name = name
        self.name = name
        self.macro = None

    def status(self):
        return Addon.Status.UPDATE_AVAILABLE


class CallInterceptor:
    def __init__(self):
        self.called = False
        self.args = None

    def intercept(self, *args):
        self.called = True
        self.args = args


class TestUpdateAllGui(unittest.TestCase):
    def setUp(self):
        self.addons = []
        for i in range(3):
            self.addons.append(MockAddon(f"Mock Addon {i}"))
        self.factory = MockUpdaterFactory(self.addons)
        self.test_object = UpdateAllGUI(self.addons)
        self.test_object.updater_factory = self.factory

    def tearDown(self):
        pass

    def test_run(self):
        self.factory.work_function = lambda: sleep(0.1)
        self.test_object.run()
        while self.test_object.is_running():
            QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 100)
        self.test_object.dialog.accept()

    def test_setup_dialog(self):
        self.test_object._setup_dialog()
        self.assertIsNotNone(
            self.test_object.dialog.buttonBox.button(QtWidgets.QDialogButtonBox.Cancel)
        )
        self.assertEqual(self.test_object.dialog.tableWidget.rowCount(), 3)

    def test_cancelling_installation(self):
        class Worker:
            def __init__(self):
                self.counter = 0
                self.LIMIT = 100
                self.limit_reached = False

            def run(self):
                while self.counter < self.LIMIT:
                    if QtCore.QThread.currentThread().isInterruptionRequested():
                        return
                    self.counter += 1
                    sleep(0.01)
                self.limit_reached = True

        worker = Worker()
        self.factory.work_function = worker.run
        self.test_object.run()
        cancel_timer = QtCore.QTimer()
        cancel_timer.timeout.connect(
            self.test_object.dialog.buttonBox.button(
                QtWidgets.QDialogButtonBox.Cancel
            ).click
        )
        cancel_timer.start(90)
        while self.test_object.is_running():
            QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 10)
        self.assertGreater(len(self.test_object.addons_with_update), 0)

    def test_add_addon_to_table(self):
        mock_addon = MockAddon("MockAddon")
        self.test_object.dialog.tableWidget.clear()
        self.test_object._add_addon_to_table(mock_addon)
        self.assertEqual(self.test_object.dialog.tableWidget.rowCount(), 1)

    def test_update_addon_status(self):
        self.test_object._setup_dialog()
        self.test_object._update_addon_status(0, AddonStatus.WAITING)
        self.assertEqual(
            self.test_object.dialog.tableWidget.item(0, 1).text(),
            AddonStatus.WAITING.ui_string(),
        )
        self.test_object._update_addon_status(0, AddonStatus.INSTALLING)
        self.assertEqual(
            self.test_object.dialog.tableWidget.item(0, 1).text(),
            AddonStatus.INSTALLING.ui_string(),
        )
        self.test_object._update_addon_status(0, AddonStatus.SUCCEEDED)
        self.assertEqual(
            self.test_object.dialog.tableWidget.item(0, 1).text(),
            AddonStatus.SUCCEEDED.ui_string(),
        )
        self.test_object._update_addon_status(0, AddonStatus.FAILED)
        self.assertEqual(
            self.test_object.dialog.tableWidget.item(0, 1).text(),
            AddonStatus.FAILED.ui_string(),
        )

    def test_process_next_update(self):
        self.test_object._setup_dialog()
        self.test_object._launch_active_installer = lambda: None
        self.test_object._process_next_update()
        self.assertEqual(
            self.test_object.dialog.tableWidget.item(0, 1).text(),
            AddonStatus.INSTALLING.ui_string(),
        )

        self.test_object._process_next_update()
        self.assertEqual(
            self.test_object.dialog.tableWidget.item(1, 1).text(),
            AddonStatus.INSTALLING.ui_string(),
        )

        self.test_object._process_next_update()
        self.assertEqual(
            self.test_object.dialog.tableWidget.item(2, 1).text(),
            AddonStatus.INSTALLING.ui_string(),
        )

        self.test_object._process_next_update()

    def test_launch_active_installer(self):
        self.test_object.active_installer = self.factory.get_updater(self.addons[0])
        self.test_object._update_succeeded = lambda _: None
        self.test_object._update_failed = lambda _: None
        self.test_object.process_next_update = lambda: None
        self.test_object._launch_active_installer()
        # The above call does not block, so spin until it has completed (basically instantly in testing)
        while self.test_object.worker_thread.isRunning():
            QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 100)
        self.test_object.dialog.accept()

    def test_update_succeeded(self):
        self.test_object._setup_dialog()
        self.test_object._update_succeeded(self.addons[0])
        self.assertEqual(
            self.test_object.dialog.tableWidget.item(0, 1).text(),
            AddonStatus.SUCCEEDED.ui_string(),
        )

    def test_update_failed(self):
        self.test_object._setup_dialog()
        self.test_object._update_failed(self.addons[0])
        self.assertEqual(
            self.test_object.dialog.tableWidget.item(0, 1).text(),
            AddonStatus.FAILED.ui_string(),
        )

    def test_update_finished(self):
        self.test_object._setup_dialog()
        call_interceptor = CallInterceptor()
        self.test_object.worker_thread = QtCore.QThread()
        self.test_object.worker_thread.start()
        self.test_object._process_next_update = call_interceptor.intercept
        self.test_object.active_installer = self.factory.get_updater(self.addons[0])
        self.test_object._update_finished()
        self.assertFalse(self.test_object.worker_thread.isRunning())
        self.test_object.worker_thread.quit()
        self.assertTrue(call_interceptor.called)
        self.test_object.worker_thread.wait()

    def test_finalize(self):
        self.test_object._setup_dialog()
        self.test_object.worker_thread = QtCore.QThread()
        self.test_object.worker_thread.start()
        self.test_object._finalize()
        self.assertFalse(self.test_object.worker_thread.isRunning())
        self.test_object.worker_thread.quit()
        self.test_object.worker_thread.wait()
        self.assertFalse(self.test_object.running)
        self.assertIsNotNone(
            self.test_object.dialog.buttonBox.button(QtWidgets.QDialogButtonBox.Close)
        )
        self.assertIsNone(
            self.test_object.dialog.buttonBox.button(QtWidgets.QDialogButtonBox.Cancel)
        )

    def test_is_running(self):
        self.assertFalse(self.test_object.is_running())
        self.test_object.run()
        self.assertTrue(self.test_object.is_running())
        while self.test_object.is_running():
            QtCore.QCoreApplication.processEvents(QtCore.QEventLoop.AllEvents, 100)
        self.test_object.dialog.accept()
