# -*- coding: utf-8 -*-
# vim: autoindent shiftwidth=4 expandtab textwidth=120 tabstop=4 softtabstop=4

###############################################################################
# OpenLP - Open Source Lyrics Projection                                      #
# --------------------------------------------------------------------------- #
# Copyright (c) 2008-2016 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; version 2 of the License.                              #
#                                                                             #
# 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, write to the Free Software Foundation, Inc., 59  #
# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
###############################################################################
"""
Functional tests to test the AppLocation class and related methods.
"""
import os
from unittest import TestCase

from openlp.core.utils import clean_filename, delete_file, get_filesystem_encoding, get_locale_key, \
    get_natural_key, split_filename, _get_user_agent, get_web_page, get_uno_instance, add_actions
from tests.functional import MagicMock, patch


class TestUtils(TestCase):
    """
    A test suite to test out various methods around the AppLocation class.
    """
    def add_actions_empty_list_test(self):
        """
        Test that no actions are added when the list is empty
        """
        # GIVEN: a mocked action list, and an empty list
        mocked_target = MagicMock()
        empty_list = []

        # WHEN: The empty list is added to the mocked target
        add_actions(mocked_target, empty_list)

        # THEN: The add method on the mocked target is never called
        self.assertEqual(0, mocked_target.addSeparator.call_count, 'addSeparator method should not have been called')
        self.assertEqual(0, mocked_target.addAction.call_count, 'addAction method should not have been called')

    def add_actions_none_action_test(self):
        """
        Test that a separator is added when a None action is in the list
        """
        # GIVEN: a mocked action list, and a list with None in it
        mocked_target = MagicMock()
        separator_list = [None]

        # WHEN: The list is added to the mocked target
        add_actions(mocked_target, separator_list)

        # THEN: The addSeparator method is called, but the addAction method is never called
        mocked_target.addSeparator.assert_called_with()
        self.assertEqual(0, mocked_target.addAction.call_count, 'addAction method should not have been called')

    def add_actions_add_action_test(self):
        """
        Test that an action is added when a valid action is in the list
        """
        # GIVEN: a mocked action list, and a list with an action in it
        mocked_target = MagicMock()
        action_list = ['action']

        # WHEN: The list is added to the mocked target
        add_actions(mocked_target, action_list)

        # THEN: The addSeparator method is not called, and the addAction method is called
        self.assertEqual(0, mocked_target.addSeparator.call_count, 'addSeparator method should not have been called')
        mocked_target.addAction.assert_called_with('action')

    def add_actions_action_and_none_test(self):
        """
        Test that an action and a separator are added when a valid action and None are in the list
        """
        # GIVEN: a mocked action list, and a list with an action and None in it
        mocked_target = MagicMock()
        action_list = ['action', None]

        # WHEN: The list is added to the mocked target
        add_actions(mocked_target, action_list)

        # THEN: The addSeparator method is called, and the addAction method is called
        mocked_target.addSeparator.assert_called_with()
        mocked_target.addAction.assert_called_with('action')

    def get_filesystem_encoding_sys_function_not_called_test(self):
        """
        Test the get_filesystem_encoding() function does not call the sys.getdefaultencoding() function
        """
        # GIVEN: sys.getfilesystemencoding returns "cp1252"
        with patch('openlp.core.utils.sys.getfilesystemencoding') as mocked_getfilesystemencoding, \
                patch('openlp.core.utils.sys.getdefaultencoding') as mocked_getdefaultencoding:
            mocked_getfilesystemencoding.return_value = 'cp1252'

            # WHEN: get_filesystem_encoding() is called
            result = get_filesystem_encoding()

            # THEN: getdefaultencoding should have been called
            mocked_getfilesystemencoding.assert_called_with()
            self.assertEqual(0, mocked_getdefaultencoding.called, 'getdefaultencoding should not have been called')
            self.assertEqual('cp1252', result, 'The result should be "cp1252"')

    def get_filesystem_encoding_sys_function_is_called_test(self):
        """
        Test the get_filesystem_encoding() function calls the sys.getdefaultencoding() function
        """
        # GIVEN: sys.getfilesystemencoding returns None and sys.getdefaultencoding returns "utf-8"
        with patch('openlp.core.utils.sys.getfilesystemencoding') as mocked_getfilesystemencoding, \
                patch('openlp.core.utils.sys.getdefaultencoding') as mocked_getdefaultencoding:
            mocked_getfilesystemencoding.return_value = None
            mocked_getdefaultencoding.return_value = 'utf-8'

            # WHEN: get_filesystem_encoding() is called
            result = get_filesystem_encoding()

            # THEN: getdefaultencoding should have been called
            mocked_getfilesystemencoding.assert_called_with()
            mocked_getdefaultencoding.assert_called_with()
            self.assertEqual('utf-8', result, 'The result should be "utf-8"')

    def split_filename_with_file_path_test(self):
        """
        Test the split_filename() function with a path to a file
        """
        # GIVEN: A path to a file.
        if os.name == 'nt':
            file_path = 'C:\\home\\user\\myfile.txt'
            wanted_result = ('C:\\home\\user', 'myfile.txt')
        else:
            file_path = '/home/user/myfile.txt'
            wanted_result = ('/home/user', 'myfile.txt')
        with patch('openlp.core.utils.os.path.isfile') as mocked_is_file:
            mocked_is_file.return_value = True

            # WHEN: Split the file name.
            result = split_filename(file_path)

            # THEN: A tuple should be returned.
            self.assertEqual(wanted_result, result, 'A tuple with the dir and file name should have been returned')

    def split_filename_with_dir_path_test(self):
        """
        Test the split_filename() function with a path to a directory
        """
        # GIVEN: A path to a dir.
        if os.name == 'nt':
            file_path = 'C:\\home\\user\\mydir'
            wanted_result = ('C:\\home\\user\\mydir', '')
        else:
            file_path = '/home/user/mydir'
            wanted_result = ('/home/user/mydir', '')
        with patch('openlp.core.utils.os.path.isfile') as mocked_is_file:
            mocked_is_file.return_value = False

            # WHEN: Split the file name.
            result = split_filename(file_path)

            # THEN: A tuple should be returned.
            self.assertEqual(wanted_result, result,
                             'A two-entry tuple with the directory and file name (empty) should have been returned.')

    def clean_filename_test(self):
        """
        Test the clean_filename() function
        """
        # GIVEN: A invalid file name and the valid file name.
        invalid_name = 'A_file_with_invalid_characters_[\\/:\*\?"<>\|\+\[\]%].py'
        wanted_name = 'A_file_with_invalid_characters______________________.py'

        # WHEN: Clean the name.
        result = clean_filename(invalid_name)

        # THEN: The file name should be cleaned.
        self.assertEqual(wanted_name, result, 'The file name should not contain any special characters.')

    def delete_file_no_path_test(self):
        """
        Test the delete_file function when called with out a valid path
        """
        # GIVEN: A blank path
        # WEHN: Calling delete_file
        result = delete_file('')

        # THEN: delete_file should return False
        self.assertFalse(result, "delete_file should return False when called with ''")

    def delete_file_path_success_test(self):
        """
        Test the delete_file function when it successfully deletes a file
        """
        # GIVEN: A mocked os which returns True when os.path.exists is called
        with patch('openlp.core.utils.os', **{'path.exists.return_value': False}):

            # WHEN: Calling delete_file with a file path
            result = delete_file('path/file.ext')

            # THEN: delete_file should return True
            self.assertTrue(result, 'delete_file should return True when it successfully deletes a file')

    def delete_file_path_no_file_exists_test(self):
        """
        Test the delete_file function when the file to remove does not exist
        """
        # GIVEN: A mocked os which returns False when os.path.exists is called
        with patch('openlp.core.utils.os', **{'path.exists.return_value': False}):

            # WHEN: Calling delete_file with a file path
            result = delete_file('path/file.ext')

            # THEN: delete_file should return True
            self.assertTrue(result, 'delete_file should return True when the file doesnt exist')

    def delete_file_path_exception_test(self):
        """
        Test the delete_file function when os.remove raises an exception
        """
        # GIVEN: A mocked os which returns True when os.path.exists is called and raises an OSError when os.remove is
        #       called.
        with patch('openlp.core.utils.os', **{'path.exists.return_value': True, 'path.exists.side_effect': OSError}), \
                patch('openlp.core.utils.log') as mocked_log:

            # WHEN: Calling delete_file with a file path
            result = delete_file('path/file.ext')

            # THEN: delete_file should log and exception and return False
            self.assertEqual(mocked_log.exception.call_count, 1)
            self.assertFalse(result, 'delete_file should return False when os.remove raises an OSError')

    def get_locale_key_test(self):
        """
        Test the get_locale_key(string) function
        """
        with patch('openlp.core.utils.languagemanager.LanguageManager.get_language') as mocked_get_language:
            # GIVEN: The language is German
            # 0x00C3 (A with diaresis) should be sorted as "A". 0x00DF (sharp s) should be sorted as "ss".
            mocked_get_language.return_value = 'de'
            unsorted_list = ['Auszug', 'Aushang', '\u00C4u\u00DFerung']

            # WHEN: We sort the list and use get_locale_key() to generate the sorting keys
            sorted_list = sorted(unsorted_list, key=get_locale_key)

            # THEN: We get a properly sorted list
            self.assertEqual(['Aushang', '\u00C4u\u00DFerung', 'Auszug'], sorted_list,
                             'Strings should be sorted properly')

    def get_natural_key_test(self):
        """
        Test the get_natural_key(string) function
        """
        with patch('openlp.core.utils.languagemanager.LanguageManager.get_language') as mocked_get_language:
            # GIVEN: The language is English (a language, which sorts digits before letters)
            mocked_get_language.return_value = 'en'
            unsorted_list = ['item 10a', 'item 3b', '1st item']

            # WHEN: We sort the list and use get_natural_key() to generate the sorting keys
            sorted_list = sorted(unsorted_list, key=get_natural_key)

            # THEN: We get a properly sorted list
            self.assertEqual(['1st item', 'item 3b', 'item 10a'], sorted_list, 'Numbers should be sorted naturally')

    def get_uno_instance_pipe_test(self):
        """
        Test that when the UNO connection type is "pipe" the resolver is given the "pipe" URI
        """
        # GIVEN: A mock resolver object and UNO_CONNECTION_TYPE is "pipe"
        mock_resolver = MagicMock()

        # WHEN: get_uno_instance() is called
        get_uno_instance(mock_resolver)

        # THEN: the resolve method is called with the correct argument
        mock_resolver.resolve.assert_called_with('uno:pipe,name=openlp_pipe;urp;StarOffice.ComponentContext')

    def get_uno_instance_socket_test(self):
        """
        Test that when the UNO connection type is other than "pipe" the resolver is given the "socket" URI
        """
        # GIVEN: A mock resolver object and UNO_CONNECTION_TYPE is "socket"
        mock_resolver = MagicMock()

        # WHEN: get_uno_instance() is called
        get_uno_instance(mock_resolver, 'socket')

        # THEN: the resolve method is called with the correct argument
        mock_resolver.resolve.assert_called_with('uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext')

    def get_user_agent_linux_test(self):
        """
        Test that getting a user agent on Linux returns a user agent suitable for Linux
        """
        with patch('openlp.core.utils.sys') as mocked_sys:

            # GIVEN: The system is Linux
            mocked_sys.platform = 'linux2'

            # WHEN: We call _get_user_agent()
            user_agent = _get_user_agent()

            # THEN: The user agent is a Linux (or ChromeOS) user agent
            result = 'Linux' in user_agent or 'CrOS' in user_agent
            self.assertTrue(result, 'The user agent should be a valid Linux user agent')

    def get_user_agent_windows_test(self):
        """
        Test that getting a user agent on Windows returns a user agent suitable for Windows
        """
        with patch('openlp.core.utils.sys') as mocked_sys:

            # GIVEN: The system is Linux
            mocked_sys.platform = 'win32'

            # WHEN: We call _get_user_agent()
            user_agent = _get_user_agent()

            # THEN: The user agent is a Linux (or ChromeOS) user agent
            self.assertIn('Windows', user_agent, 'The user agent should be a valid Windows user agent')

    def get_user_agent_macos_test(self):
        """
        Test that getting a user agent on OS X returns a user agent suitable for OS X
        """
        with patch('openlp.core.utils.sys') as mocked_sys:

            # GIVEN: The system is Linux
            mocked_sys.platform = 'darwin'

            # WHEN: We call _get_user_agent()
            user_agent = _get_user_agent()

            # THEN: The user agent is a Linux (or ChromeOS) user agent
            self.assertIn('Mac OS X', user_agent, 'The user agent should be a valid OS X user agent')

    def get_user_agent_default_test(self):
        """
        Test that getting a user agent on a non-Linux/Windows/OS X platform returns the default user agent
        """
        with patch('openlp.core.utils.sys') as mocked_sys:

            # GIVEN: The system is Linux
            mocked_sys.platform = 'freebsd'

            # WHEN: We call _get_user_agent()
            user_agent = _get_user_agent()

            # THEN: The user agent is a Linux (or ChromeOS) user agent
            self.assertIn('NetBSD', user_agent, 'The user agent should be the default user agent')

    def get_web_page_no_url_test(self):
        """
        Test that sending a URL of None to the get_web_page method returns None
        """
        # GIVEN: A None url
        test_url = None

        # WHEN: We try to get the test URL
        result = get_web_page(test_url)

        # THEN: None should be returned
        self.assertIsNone(result, 'The return value of get_web_page should be None')

    def get_web_page_test(self):
        """
        Test that the get_web_page method works correctly
        """
        with patch('openlp.core.utils.urllib.request.Request') as MockRequest, \
                patch('openlp.core.utils.urllib.request.urlopen') as mock_urlopen, \
                patch('openlp.core.utils._get_user_agent') as mock_get_user_agent, \
                patch('openlp.core.utils.Registry') as MockRegistry:
            # GIVEN: Mocked out objects and a fake URL
            mocked_request_object = MagicMock()
            MockRequest.return_value = mocked_request_object
            mocked_page_object = MagicMock()
            mock_urlopen.return_value = mocked_page_object
            mock_get_user_agent.return_value = 'user_agent'
            fake_url = 'this://is.a.fake/url'

            # WHEN: The get_web_page() method is called
            returned_page = get_web_page(fake_url)

            # THEN: The correct methods are called with the correct arguments and a web page is returned
            MockRequest.assert_called_with(fake_url)
            mocked_request_object.add_header.assert_called_with('User-Agent', 'user_agent')
            self.assertEqual(1, mocked_request_object.add_header.call_count,
                             'There should only be 1 call to add_header')
            mock_get_user_agent.assert_called_with()
            mock_urlopen.assert_called_with(mocked_request_object, timeout=30)
            mocked_page_object.geturl.assert_called_with()
            self.assertEqual(0, MockRegistry.call_count, 'The Registry() object should have never been called')
            self.assertEqual(mocked_page_object, returned_page, 'The returned page should be the mock object')

    def get_web_page_with_header_test(self):
        """
        Test that adding a header to the call to get_web_page() adds the header to the request
        """
        with patch('openlp.core.utils.urllib.request.Request') as MockRequest, \
                patch('openlp.core.utils.urllib.request.urlopen') as mock_urlopen, \
                patch('openlp.core.utils._get_user_agent') as mock_get_user_agent:
            # GIVEN: Mocked out objects, a fake URL and a fake header
            mocked_request_object = MagicMock()
            MockRequest.return_value = mocked_request_object
            mocked_page_object = MagicMock()
            mock_urlopen.return_value = mocked_page_object
            mock_get_user_agent.return_value = 'user_agent'
            fake_url = 'this://is.a.fake/url'
            fake_header = ('Fake-Header', 'fake value')

            # WHEN: The get_web_page() method is called
            returned_page = get_web_page(fake_url, header=fake_header)

            # THEN: The correct methods are called with the correct arguments and a web page is returned
            MockRequest.assert_called_with(fake_url)
            mocked_request_object.add_header.assert_called_with(fake_header[0], fake_header[1])
            self.assertEqual(2, mocked_request_object.add_header.call_count,
                             'There should only be 2 calls to add_header')
            mock_get_user_agent.assert_called_with()
            mock_urlopen.assert_called_with(mocked_request_object, timeout=30)
            mocked_page_object.geturl.assert_called_with()
            self.assertEqual(mocked_page_object, returned_page, 'The returned page should be the mock object')

    def get_web_page_with_user_agent_in_headers_test(self):
        """
        Test that adding a user agent in the header when calling get_web_page() adds that user agent to the request
        """
        with patch('openlp.core.utils.urllib.request.Request') as MockRequest, \
                patch('openlp.core.utils.urllib.request.urlopen') as mock_urlopen, \
                patch('openlp.core.utils._get_user_agent') as mock_get_user_agent:
            # GIVEN: Mocked out objects, a fake URL and a fake header
            mocked_request_object = MagicMock()
            MockRequest.return_value = mocked_request_object
            mocked_page_object = MagicMock()
            mock_urlopen.return_value = mocked_page_object
            fake_url = 'this://is.a.fake/url'
            user_agent_header = ('User-Agent', 'OpenLP/2.2.0')

            # WHEN: The get_web_page() method is called
            returned_page = get_web_page(fake_url, header=user_agent_header)

            # THEN: The correct methods are called with the correct arguments and a web page is returned
            MockRequest.assert_called_with(fake_url)
            mocked_request_object.add_header.assert_called_with(user_agent_header[0], user_agent_header[1])
            self.assertEqual(1, mocked_request_object.add_header.call_count,
                             'There should only be 1 call to add_header')
            self.assertEqual(0, mock_get_user_agent.call_count, '_get_user_agent should not have been called')
            mock_urlopen.assert_called_with(mocked_request_object, timeout=30)
            mocked_page_object.geturl.assert_called_with()
            self.assertEqual(mocked_page_object, returned_page, 'The returned page should be the mock object')

    def get_web_page_update_openlp_test(self):
        """
        Test that passing "update_openlp" as true to get_web_page calls Registry().get('app').process_events()
        """
        with patch('openlp.core.utils.urllib.request.Request') as MockRequest, \
                patch('openlp.core.utils.urllib.request.urlopen') as mock_urlopen, \
                patch('openlp.core.utils._get_user_agent') as mock_get_user_agent, \
                patch('openlp.core.utils.Registry') as MockRegistry:
            # GIVEN: Mocked out objects, a fake URL
            mocked_request_object = MagicMock()
            MockRequest.return_value = mocked_request_object
            mocked_page_object = MagicMock()
            mock_urlopen.return_value = mocked_page_object
            mock_get_user_agent.return_value = 'user_agent'
            mocked_registry_object = MagicMock()
            mocked_application_object = MagicMock()
            mocked_registry_object.get.return_value = mocked_application_object
            MockRegistry.return_value = mocked_registry_object
            fake_url = 'this://is.a.fake/url'

            # WHEN: The get_web_page() method is called
            returned_page = get_web_page(fake_url, update_openlp=True)

            # THEN: The correct methods are called with the correct arguments and a web page is returned
            MockRequest.assert_called_with(fake_url)
            mocked_request_object.add_header.assert_called_with('User-Agent', 'user_agent')
            self.assertEqual(1, mocked_request_object.add_header.call_count,
                             'There should only be 1 call to add_header')
            mock_urlopen.assert_called_with(mocked_request_object, timeout=30)
            mocked_page_object.geturl.assert_called_with()
            mocked_registry_object.get.assert_called_with('application')
            mocked_application_object.process_events.assert_called_with()
            self.assertEqual(mocked_page_object, returned_page, 'The returned page should be the mock object')
