# -*- coding: utf-8 -*-

##########################################################################
# OpenLP - Open Source Lyrics Projection                                 #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2019 OpenLP Developers                              #
# ---------------------------------------------------------------------- #
# This program is free software: you can redistribute it and/or modify   #
# it under the terms of the GNU General Public License as published by   #
# the Free Software Foundation, either version 3 of the License, or      #
# (at your option) any later version.                                    #
#                                                                        #
# This program 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 General Public License for more details.                           #
#                                                                        #
# You should have received a copy of the GNU General Public License      #
# along with this program.  If not, see <https://www.gnu.org/licenses/>. #
##########################################################################
"""
Package to test the openlp.core.ui package.
"""
import os
import time
from threading import Lock
from unittest import TestCase, skip
from unittest.mock import MagicMock, patch

from PyQt5 import QtGui

from openlp.core.common.registry import Registry
from openlp.core.display.screens import ScreenList
from openlp.core.lib.imagemanager import ImageManager, ImageWorker, Priority, PriorityQueue
from tests.helpers.testmixin import TestMixin
from tests.utils.constants import RESOURCE_PATH


TEST_PATH = str(RESOURCE_PATH)


class TestImageWorker(TestCase, TestMixin):
    """
    Test all the methods in the ImageWorker class
    """
    def test_init(self):
        """
        Test the constructor of the ImageWorker
        """
        # GIVEN: An ImageWorker class and a mocked ImageManager
        mocked_image_manager = MagicMock()

        # WHEN: Creating the ImageWorker
        worker = ImageWorker(mocked_image_manager)

        # THEN: The image_manager attribute should be set correctly
        assert worker.image_manager is mocked_image_manager, \
            'worker.image_manager should have been the mocked_image_manager'

    @patch('openlp.core.lib.imagemanager.ThreadWorker.quit')
    def test_start(self, mocked_quit):
        """
        Test that the start() method of the image worker calls the process method and then emits quit.
        """
        # GIVEN: A mocked image_manager and a new image worker
        mocked_image_manager = MagicMock()
        worker = ImageWorker(mocked_image_manager)

        # WHEN: start() is called
        worker.start()

        # THEN: process() should have been called and quit should have been emitted
        mocked_image_manager.process.assert_called_once_with()
        mocked_quit.emit.assert_called_once_with()

    def test_stop(self):
        """
        Test that the stop method does the right thing
        """
        # GIVEN: A mocked image_manager and a worker
        mocked_image_manager = MagicMock()
        worker = ImageWorker(mocked_image_manager)

        # WHEN: The stop() method is called
        worker.stop()

        # THEN: The stop_manager attrivute should have been set to True
        assert mocked_image_manager.stop_manager is True, 'mocked_image_manager.stop_manager should have been True'


class TestPriorityQueue(TestCase, TestMixin):
    """
    Test the PriorityQueue class
    """
    @patch('openlp.core.lib.imagemanager.PriorityQueue.remove')
    @patch('openlp.core.lib.imagemanager.PriorityQueue.put')
    def test_modify_priority(self, mocked_put, mocked_remove):
        """
        Test the modify_priority() method of PriorityQueue
        """
        # GIVEN: An instance of a PriorityQueue and a mocked image
        mocked_image = MagicMock()
        mocked_image.priority = Priority.Normal
        mocked_image.secondary_priority = Priority.Low
        queue = PriorityQueue()

        # WHEN: modify_priority is called with a mocked image and a new priority
        queue.modify_priority(mocked_image, Priority.High)

        # THEN: The remove() method should have been called, image priority updated and put() called
        mocked_remove.assert_called_once_with(mocked_image)
        assert mocked_image.priority == Priority.High, 'The priority should have been Priority.High'
        mocked_put.assert_called_once_with((Priority.High, Priority.Low, mocked_image))

    def test_remove(self):
        """
        Test the remove() method of PriorityQueue
        """
        # GIVEN: A PriorityQueue instance with a mocked image and queue
        mocked_image = MagicMock()
        mocked_image.priority = Priority.High
        mocked_image.secondary_priority = Priority.Normal
        queue = PriorityQueue()

        # WHEN: An image is removed
        with patch.object(queue, 'queue') as mocked_queue:
            mocked_queue.__contains__.return_value = True
            queue.remove(mocked_image)

        # THEN: The mocked queue.remove() method should have been called
        mocked_queue.remove.assert_called_once_with((Priority.High, Priority.Normal, mocked_image))


@skip('Probably not going to use ImageManager in WebEngine/Reveal.js')
class TestImageManager(TestCase, TestMixin):

    def setUp(self):
        """
        Create the UI
        """
        Registry.create()
        self.setup_application()
        ScreenList.create(self.app.desktop())
        self.image_manager = ImageManager()
        self.lock = Lock()
        self.sleep_time = 0.1

    def tearDown(self):
        """
        Delete all the C++ objects at the end so that we don't have a segfault
        """
        self.image_manager.stop_manager = True
        del self.app

    @patch('openlp.core.lib.imagemanager.run_thread')
    def test_basic_image_manager(self, mocked_run_thread):
        """
        Test the Image Manager setup basic functionality
        """
        # GIVEN: the an image add to the image manager
        full_path = os.path.normpath(os.path.join(TEST_PATH, 'church.jpg'))
        self.image_manager.add_image(full_path, 'church.jpg', None)

        # WHEN the image is retrieved
        image = self.image_manager.get_image(full_path, 'church.jpg')

        # THEN returned record is a type of image
        assert isinstance(image, QtGui.QImage), 'The returned object should be a QImage'

        # WHEN: The image bytes are requested.
        byte_array = self.image_manager.get_image_bytes(full_path, 'church.jpg')

        # THEN: Type should be a str.
        assert isinstance(byte_array, str), 'The returned object should be a str'

        # WHEN the image is retrieved has not been loaded
        # THEN a KeyError is thrown
        with self.assertRaises(KeyError) as context:
            self.image_manager.get_image(TEST_PATH, 'church1.jpg')
        assert context.exception != '', 'KeyError exception should have been thrown for missing image'

    @patch('openlp.core.lib.imagemanager.run_thread')
    def test_different_dimension_image(self, mocked_run_thread):
        """
        Test the Image Manager with dimensions
        """
        # GIVEN: add an image with specific dimensions
        full_path = os.path.normpath(os.path.join(TEST_PATH, 'church.jpg'))
        self.image_manager.add_image(full_path, 'church.jpg', None, 80, 80)

        # WHEN: the image is retrieved
        image = self.image_manager.get_image(full_path, 'church.jpg', 80, 80)

        # THEN: The return should be of type image
        assert isinstance(image, QtGui.QImage), 'The returned object should be a QImage'

        # WHEN: adding the same image with different dimensions
        self.image_manager.add_image(full_path, 'church.jpg', None, 100, 100)

        # THEN: the cache should contain two pictures
        assert len(self.image_manager._cache) == 2, \
            'Image manager should consider two dimensions of the same picture as different'

        # WHEN: adding the same image with first dimensions
        self.image_manager.add_image(full_path, 'church.jpg', None, 80, 80)

        # THEN: the cache should still contain only two pictures
        assert len(self.image_manager._cache) == 2, 'Same dimensions should not be added again'

        # WHEN: calling with correct image, but wrong dimensions
        with self.assertRaises(KeyError) as context:
            self.image_manager.get_image(full_path, 'church.jpg', 120, 120)
        assert context.exception != '', 'KeyError exception should have been thrown for missing dimension'

    @patch('openlp.core.lib.imagemanager.resize_image')
    @patch('openlp.core.lib.imagemanager.image_to_byte')
    @patch('openlp.core.lib.imagemanager.run_thread')
    def test_process_cache(self, mocked_run_thread, mocked_image_to_byte, mocked_resize_image):
        """
        Test the process_cache method
        """
        # GIVEN: Mocked functions
        mocked_resize_image.side_effect = self.mocked_resize_image
        mocked_image_to_byte.side_effect = self.mocked_image_to_byte
        image1 = 'church.jpg'
        image2 = 'church2.jpg'
        image3 = 'church3.jpg'
        image4 = 'church4.jpg'

        # WHEN: Add the images. Then get the lock (=queue can not be processed).
        self.lock.acquire()
        self.image_manager.add_image(TEST_PATH, image1, None)
        self.image_manager.add_image(TEST_PATH, image2, None)

        # THEN: All images have been added to the queue, and only the first image is not be in the list anymore, but
        #  is being processed (see mocked methods/functions).
        # Note: Priority.Normal means, that the resize_image() was not completed yet (because afterwards the #
        # priority is adjusted to Priority.Lowest).
        assert self.get_image_priority(image1) == Priority.Normal, "image1's priority should be 'Priority.Normal'"
        assert self.get_image_priority(image2) == Priority.Normal, "image2's priority should be 'Priority.Normal'"

        # WHEN: Add more images.
        self.image_manager.add_image(TEST_PATH, image3, None)
        self.image_manager.add_image(TEST_PATH, image4, None)
        # Allow the queue to process.
        self.lock.release()
        # Request some "data".
        self.image_manager.get_image_bytes(TEST_PATH, image4)
        self.image_manager.get_image(TEST_PATH, image3)
        # Now the mocked methods/functions do not have to sleep anymore.
        self.sleep_time = 0
        # Wait for the queue to finish.
        while not self.image_manager._conversion_queue.empty():
            time.sleep(0.1)
        # Because empty() is not reliable, wait a litte; just to make sure.
        time.sleep(0.1)
        # THEN: The images' priority reflect how they were processed.
        assert self.image_manager._conversion_queue.qsize() == 0, "The queue should be empty."
        assert self.get_image_priority(image1) == Priority.Lowest, \
            "The image should have not been requested (=Lowest)"
        assert self.get_image_priority(image2) == Priority.Lowest, \
            "The image should have not been requested (=Lowest)"
        assert self.get_image_priority(image3) == Priority.Low, \
            "Only the QImage should have been requested (=Low)."
        assert self.get_image_priority(image4) == Priority.Urgent, \
            "The image bytes should have been requested (=Urgent)."

    def get_image_priority(self, image):
        """
        This is a help method to get the priority of the given image out of the image_manager's cache.

        NOTE: This requires, that the image has been added to the image manager using the *TEST_PATH*.

        :param image: The name of the image. E. g. ``image1``
        """
        return self.image_manager._cache[(TEST_PATH, image, -1, -1)].priority

    def mocked_resize_image(self, *args):
        """
        This is a mocked method, so that we can control the work flow of the image manager.
        """
        self.lock.acquire()
        self.lock.release()
        # The sleep time is adjusted in the test case.
        time.sleep(self.sleep_time)
        return QtGui.QImage()

    def mocked_image_to_byte(self, *args):
        """
        This is a mocked method, so that we can control the work flow of the image manager.
        """
        self.lock.acquire()
        self.lock.release()
        # The sleep time is adjusted in the test case.
        time.sleep(self.sleep_time)
        return ''
