# -*- 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/>. #
##########################################################################

"""
This module contains tests for the CCLI SongSelect importer.
It needs re-writing at some point to load real HTML pages from disk and
then test the behaviour based on those. That way if and when CCLI change
their page layout, changing the tests would just be a case of
re-downloading the HTML pages and changing the code to use the new layout.
"""
from unittest import TestCase
from unittest.mock import MagicMock, call, patch
from urllib.error import URLError

from PyQt5 import QtWidgets

from openlp.core.common.registry import Registry
from openlp.plugins.songs.forms.songselectform import SearchWorker, SongSelectForm
from openlp.plugins.songs.lib import Song
from openlp.plugins.songs.lib.songselect import BASE_URL, LOGOUT_URL, SongSelectImport
from tests.helpers.songfileimport import SongImportTestHelper
from tests.helpers.testmixin import TestMixin
from tests.utils.constants import RESOURCE_PATH


TEST_PATH = RESOURCE_PATH / 'songs' / 'songselect'


class TestSongSelectImport(TestCase, TestMixin):
    """
    Test the :class:`~openlp.plugins.songs.lib.songselect.SongSelectImport` class
    """
    @patch('openlp.plugins.songs.lib.songselect.build_opener')
    def test_constructor(self, mocked_build_opener):
        """
        Test that constructing a basic SongSelectImport object works correctly
        """
        # GIVEN: The SongSelectImporter class and a mocked out build_opener
        # WHEN: An object is instantiated
        importer = SongSelectImport(None)

        # THEN: The object should have the correct properties
        assert importer.db_manager is None, 'The db_manager should be None'
        assert importer.html_parser is not None, 'There should be a valid html_parser object'
        assert importer.opener is not None, 'There should be a valid opener object'
        assert 1 == mocked_build_opener.call_count, 'The build_opener method should have been called once'

    @patch('openlp.plugins.songs.lib.songselect.build_opener')
    @patch('openlp.plugins.songs.lib.songselect.BeautifulSoup')
    def test_login_fails(self, MockedBeautifulSoup, mocked_build_opener):
        """
        Test that when logging in to SongSelect fails, the login method returns None
        """
        # GIVEN: A bunch of mocked out stuff and an importer object
        mocked_opener = MagicMock()
        mocked_build_opener.return_value = mocked_opener
        mocked_login_page = MagicMock()
        mocked_login_page.find.side_effect = [{'value': 'blah'}, None]
        mocked_posted_page = MagicMock()
        mocked_posted_page.find.return_value = None
        MockedBeautifulSoup.side_effect = [mocked_login_page, mocked_posted_page]
        mock_callback = MagicMock()
        importer = SongSelectImport(None)

        # WHEN: The login method is called after being rigged to fail
        result = importer.login('username', 'password', mock_callback)

        # THEN: callback was called 3 times, open was called twice, find was called twice, and None was returned
        assert 3 == mock_callback.call_count, 'callback should have been called 3 times'
        assert 2 == mocked_login_page.find.call_count, 'find should have been called twice'
        assert 2 == mocked_posted_page.find.call_count, 'find should have been called twice'
        assert 2 == mocked_opener.open.call_count, 'opener should have been called twice'
        assert result is None, 'The login method should have returned None'

    @patch('openlp.plugins.songs.lib.songselect.build_opener')
    def test_login_except(self, mocked_build_opener):
        """
        Test that when logging in to SongSelect fails, the login method raises URLError
        """
        # GIVEN: A bunch of mocked out stuff and an importer object
        mocked_build_opener.open.side_effect = URLError('Fake URLError')
        mock_callback = MagicMock()
        importer = SongSelectImport(None)

        # WHEN: The login method is called after being rigged to fail
        result = importer.login('username', 'password', mock_callback)

        # THEN: callback was called 1 time and False was returned
        assert 1 == mock_callback.call_count, 'callback should have been called 1 times'
        assert result is False, 'The login method should have returned False'

    @patch('openlp.plugins.songs.lib.songselect.build_opener')
    @patch('openlp.plugins.songs.lib.songselect.BeautifulSoup')
    def test_login_succeeds(self, MockedBeautifulSoup, mocked_build_opener):
        """
        Test that when logging in to SongSelect succeeds, the login method returns True
        """
        # GIVEN: A bunch of mocked out stuff and an importer object
        mocked_opener = MagicMock()
        mocked_build_opener.return_value = mocked_opener
        mocked_login_page = MagicMock()
        mocked_login_page.find.side_effect = [{'value': 'blah'}, None]
        mocked_posted_page = MagicMock()
        mocked_posted_page.find.return_value = MagicMock()
        MockedBeautifulSoup.side_effect = [mocked_login_page, mocked_posted_page]
        mock_callback = MagicMock()
        importer = SongSelectImport(None)

        # WHEN: The login method is called after being rigged to fail
        result = importer.login('username', 'password', mock_callback)

        # THEN: callback was called 3 times, open was called twice, find was called twice, and True was returned
        assert 3 == mock_callback.call_count, 'callback should have been called 3 times'
        assert 2 == mocked_login_page.find.call_count, 'find should have been called twice on the login page'
        assert 1 == mocked_posted_page.find.call_count, 'find should have been called once on the posted page'
        assert 2 == mocked_opener.open.call_count, 'opener should have been called twice'
        assert result is None, 'The login method should have returned the subscription level'

    @patch('openlp.plugins.songs.lib.songselect.build_opener')
    @patch('openlp.plugins.songs.lib.songselect.BeautifulSoup')
    def test_login_url_from_form(self, MockedBeautifulSoup, mocked_build_opener):
        """
        Test that the login URL is from the form
        """
        # GIVEN: A bunch of mocked out stuff and an importer object
        mocked_opener = MagicMock()
        mocked_build_opener.return_value = mocked_opener
        mocked_form = MagicMock()
        mocked_form.attrs = {'action': 'do/login'}
        mocked_login_page = MagicMock()
        mocked_login_page.find.side_effect = [{'value': 'blah'}, mocked_form]
        mocked_posted_page = MagicMock()
        mocked_posted_page.find.return_value = MagicMock()
        mocked_home_page = MagicMock()
        MockedBeautifulSoup.side_effect = [mocked_login_page, mocked_posted_page, mocked_home_page]
        mock_callback = MagicMock()
        importer = SongSelectImport(None)

        # WHEN: The login method is called after being rigged to fail
        result = importer.login('username', 'password', mock_callback)

        # THEN: callback was called 3 times, open was called twice, find was called twice, and True was returned
        assert 3 == mock_callback.call_count, 'callback should have been called 3 times'
        assert 2 == mocked_login_page.find.call_count, 'find should have been called twice on the login page'
        assert 1 == mocked_posted_page.find.call_count, 'find should have been called once on the posted page'
        assert 'https://profile.ccli.com/do/login', mocked_opener.open.call_args_list[1][0][0]
        assert result is None, 'The login method should have returned the subscription level'

    @patch('openlp.plugins.songs.lib.songselect.build_opener')
    def test_logout(self, mocked_build_opener):
        """
        Test that when the logout method is called, it logs the user out of SongSelect
        """
        # GIVEN: A bunch of mocked out stuff and an importer object
        mocked_opener = MagicMock()
        mocked_build_opener.return_value = mocked_opener
        importer = SongSelectImport(None)

        # WHEN: The login method is called after being rigged to fail
        importer.logout()

        # THEN: The opener is called once with the logout url
        assert 1 == mocked_opener.open.call_count, 'opener should have been called once'
        mocked_opener.open.assert_called_with(LOGOUT_URL)

    @patch('openlp.plugins.songs.lib.songselect.build_opener')
    @patch('openlp.plugins.songs.lib.songselect.BeautifulSoup')
    def test_search_returns_no_results(self, MockedBeautifulSoup, mocked_build_opener):
        """
        Test that when the search finds no results, it simply returns an empty list
        """
        # GIVEN: A bunch of mocked out stuff and an importer object
        mocked_opener = MagicMock()
        mocked_build_opener.return_value = mocked_opener
        mocked_results_page = MagicMock()
        mocked_results_page.find_all.return_value = []
        MockedBeautifulSoup.return_value = mocked_results_page
        mock_callback = MagicMock()
        importer = SongSelectImport(None)
        importer.subscription_level = 'premium'

        # WHEN: The login method is called after being rigged to fail
        results = importer.search('text', 1000, mock_callback)

        # THEN: callback was never called, open was called once, find_all was called once, an empty list returned
        assert 0 == mock_callback.call_count, 'callback should not have been called'
        assert 1 == mocked_opener.open.call_count, 'open should have been called once'
        assert 1 == mocked_results_page.find_all.call_count, 'find_all should have been called once'
        mocked_results_page.find_all.assert_called_with('div', 'song-result')
        assert [] == results, 'The search method should have returned an empty list'

    @patch('openlp.plugins.songs.lib.songselect.build_opener')
    @patch('openlp.plugins.songs.lib.songselect.BeautifulSoup')
    def test_search_returns_ccli_song_number_result(self, MockedBeautifulSoup, mocked_build_opener):
        """
        Test that search can find a single song by CCLI number
        """
        # GIVEN: A bunch of mocked out stuff and an importer object
        mocked_opener = MagicMock()
        mocked_build_opener.return_value = mocked_opener
        mocked_results_page = MagicMock()
        mocked_results_page.find_all.return_value = []
        MockedBeautifulSoup.return_value = mocked_results_page
        mock_callback = MagicMock()
        importer = SongSelectImport(None)
        importer.subscription_level = 'premium'

        # WHEN: The search is performed
        results = importer.search('1234567', 1000, mock_callback)

        # THEN: callback was called once and the results are as expected
        assert 1 == mock_callback.call_count, 'callback should not have been called'
        assert 1 == mocked_opener.open.call_count, 'open should have been called once'
        assert 1 == mocked_results_page.find_all.call_count, 'find_all should have been called once'
        mocked_results_page.find_all.assert_called_with('div', 'song-result')

        assert 1 == len(results), 'The search method should have returned an single song in a list'
        assert 'https://songselect.ccli.com/Songs/1234567' == results[0]['link'],\
            'The correct link should have been returned'

    @patch('openlp.plugins.songs.lib.songselect.build_opener')
    @patch('openlp.plugins.songs.lib.songselect.BeautifulSoup')
    def test_search_returns_two_results(self, MockedBeautifulSoup, mocked_build_opener):
        """
        Test that when the search finds 2 results, it simply returns a list with 2 results
        """
        # GIVEN: A bunch of mocked out stuff and an importer object
        # first search result
        mocked_result1 = MagicMock()
        mocked_result1.find.side_effect = [
            MagicMock(find=MagicMock(return_value=MagicMock(string='Title 1'))),
            MagicMock(string='James, John'),
            MagicMock(find=MagicMock(return_value={'href': '/url1'}))
        ]
        # second search result
        mocked_result2 = MagicMock()
        mocked_result2.find.side_effect = [
            MagicMock(find=MagicMock(return_value=MagicMock(string='Title 2'))),
            MagicMock(string='Philip'),
            MagicMock(find=MagicMock(return_value={'href': '/url2'}))
        ]
        # rest of the stuff
        mocked_opener = MagicMock()
        mocked_build_opener.return_value = mocked_opener
        mocked_results_page = MagicMock()
        mocked_results_page.find_all.side_effect = [[mocked_result1, mocked_result2], []]
        MockedBeautifulSoup.return_value = mocked_results_page
        mock_callback = MagicMock()
        importer = SongSelectImport(None)
        importer.subscription_level = 'premium'

        # WHEN: The search method is called
        results = importer.search('text', 1000, mock_callback)

        # THEN: callback was never called, open was called once, find_all was called once, an empty list returned
        assert 2 == mock_callback.call_count, 'callback should have been called twice'
        assert 2 == mocked_opener.open.call_count, 'open should have been called twice'
        assert 2 == mocked_results_page.find_all.call_count, 'find_all should have been called twice'
        mocked_results_page.find_all.assert_called_with('div', 'song-result')
        expected_list = [
            {'title': 'Title 1', 'authors': ['James', 'John'], 'link': BASE_URL + '/url1'},
            {'title': 'Title 2', 'authors': ['Philip'], 'link': BASE_URL + '/url2'}
        ]
        assert expected_list == results, 'The search method should have returned two songs'

    @patch('openlp.plugins.songs.lib.songselect.build_opener')
    @patch('openlp.plugins.songs.lib.songselect.BeautifulSoup')
    def test_search_reaches_max_results(self, MockedBeautifulSoup, mocked_build_opener):
        """
        Test that when the search finds MAX (2) results, it simply returns a list with those (2)
        """
        # GIVEN: A bunch of mocked out stuff and an importer object
        # first search result
        mocked_result1 = MagicMock()
        mocked_result1.find.side_effect = [
            MagicMock(find=MagicMock(return_value=MagicMock(string='Title 1'))),
            MagicMock(string='James, John'),
            MagicMock(find=MagicMock(return_value={'href': '/url1'}))
        ]
        # second search result
        mocked_result2 = MagicMock()
        mocked_result2.find.side_effect = [
            MagicMock(find=MagicMock(return_value=MagicMock(string='Title 2'))),
            MagicMock(string='Philip'),
            MagicMock(find=MagicMock(return_value={'href': '/url2'}))
        ]
        # third search result
        mocked_result3 = MagicMock()
        mocked_result3.find.side_effect = [
            MagicMock(find=MagicMock(return_value=MagicMock(string='Title 3'))),
            MagicMock(string='Luke, Matthew'),
            MagicMock(find=MagicMock(return_value={'href': '/url3'}))
        ]
        # rest of the stuff
        mocked_opener = MagicMock()
        mocked_build_opener.return_value = mocked_opener
        mocked_results_page = MagicMock()
        mocked_results_page.find_all.side_effect = [[mocked_result1, mocked_result2, mocked_result3], []]
        MockedBeautifulSoup.return_value = mocked_results_page
        mock_callback = MagicMock()
        importer = SongSelectImport(None)
        importer.subscription_level = 'premium'

        # WHEN: The search method is called
        results = importer.search('text', 2, mock_callback)

        # THEN: callback was called twice, open was called once, find_all was called once, max results returned
        assert 2 == mock_callback.call_count, 'callback should have been called twice'
        assert 1 == mocked_opener.open.call_count, 'open should have been called once'
        assert 1 == mocked_results_page.find_all.call_count, 'find_all should have been called once'
        mocked_results_page.find_all.assert_called_with('div', 'song-result')
        expected_list = [{'title': 'Title 1', 'authors': ['James', 'John'], 'link': BASE_URL + '/url1'},
                         {'title': 'Title 2', 'authors': ['Philip'], 'link': BASE_URL + '/url2'}]
        assert expected_list == results, 'The search method should have returned two songs'

    @patch('openlp.plugins.songs.lib.songselect.build_opener')
    @patch('openlp.plugins.songs.lib.songselect.BeautifulSoup')
    def test_stop_called(self, MockedBeautifulSoup, mocked_build_opener):
        """
        Test that the search is stopped with stop() is called
        """
        # GIVEN: An importer object that is currently "searching"
        importer = SongSelectImport(None)
        importer.run_search = True

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

        # THEN: Searching should have stopped
        assert importer.run_search is False, 'Searching should have been stopped'

    @patch('openlp.plugins.songs.lib.songselect.build_opener')
    def test_get_song_page_raises_exception(self, mocked_build_opener):
        """
        Test that when BeautifulSoup gets a bad song page the get_song() method returns None
        """
        # GIVEN: A bunch of mocked out stuff and an importer object
        mocked_opener = MagicMock()
        mocked_build_opener.return_value = mocked_opener
        mocked_opener.open.read.side_effect = URLError('[Errno -2] Name or service not known')
        mocked_callback = MagicMock()
        importer = SongSelectImport(None)

        # WHEN: get_song is called
        result = importer.get_song({'link': 'link'}, callback=mocked_callback)

        # THEN: The callback should have been called once and None should be returned
        mocked_callback.assert_called_with()
        assert result is None, 'The get_song() method should have returned None'

    @patch('openlp.plugins.songs.lib.songselect.build_opener')
    @patch('openlp.plugins.songs.lib.songselect.BeautifulSoup')
    def test_get_song_lyrics_raise_exception(self, MockedBeautifulSoup, mocked_build_opener):
        """
        Test that when BeautifulSoup gets a bad lyrics page the get_song() method returns None
        """
        # GIVEN: A bunch of mocked out stuff and an importer object
        song_page = MagicMock(return_value={'href': '/lyricpage'})
        MockedBeautifulSoup.side_effect = [song_page, TypeError('Test Error')]
        mocked_callback = MagicMock()
        importer = SongSelectImport(None)

        # WHEN: get_song is called
        result = importer.get_song({'link': 'link'}, callback=mocked_callback)

        # THEN: The callback should have been called twice and None should be returned
        assert 2 == mocked_callback.call_count, 'The callback should have been called twice'
        assert result is None, 'The get_song() method should have returned None'

    @patch('openlp.plugins.songs.lib.songselect.build_opener')
    @patch('openlp.plugins.songs.lib.songselect.BeautifulSoup')
    def test_get_song(self, MockedBeautifulSoup, mocked_build_opener):
        """
        Test that the get_song() method returns the correct song details
        """
        # GIVEN: A bunch of mocked out stuff and an importer object
        mocked_song_page = MagicMock()
        mocked_copyright = MagicMock()
        mocked_copyright.find_all.return_value = [MagicMock(string='Copyright 1'), MagicMock(string='Copyright 2')]
        mocked_song_page.find.side_effect = [
            mocked_copyright,
            MagicMock(find=MagicMock(string='CCLI: 123456'))
        ]
        mocked_lyrics_page = MagicMock()
        mocked_find_all = MagicMock()
        mocked_find_all.side_effect = [
            [
                MagicMock(contents='The Lord told Noah: there\'s gonna be a floody, floody'),
                MagicMock(contents='So, rise and shine, and give God the glory, glory'),
                MagicMock(contents='The Lord told Noah to build him an arky, arky')
            ],
            [MagicMock(string='Verse 1'), MagicMock(string='Chorus'), MagicMock(string='Verse 2')]
        ]
        mocked_lyrics_page.find.return_value = MagicMock(find_all=mocked_find_all)
        MockedBeautifulSoup.side_effect = [mocked_song_page, mocked_lyrics_page]
        mocked_callback = MagicMock()
        importer = SongSelectImport(None)
        fake_song = {'title': 'Title', 'authors': ['Author 1', 'Author 2'], 'link': 'url'}

        # WHEN: get_song is called
        result = importer.get_song(fake_song, callback=mocked_callback)

        # THEN: The callback should have been called three times and the song should be returned
        assert 3 == mocked_callback.call_count, 'The callback should have been called twice'
        assert result is not None, 'The get_song() method should have returned a song dictionary'
        assert 2 == mocked_lyrics_page.find.call_count, 'The find() method should have been called twice'
        assert 2 == mocked_find_all.call_count, 'The find_all() method should have been called twice'
        assert [call('div', 'song-viewer lyrics'), call('div', 'song-viewer lyrics')] == \
            mocked_lyrics_page.find.call_args_list, 'The find() method should have been called with the right arguments'
        assert [call('p'), call('h3')] == mocked_find_all.call_args_list, \
            'The find_all() method should have been called with the right arguments'
        assert 'copyright' in result, 'The returned song should have a copyright'
        assert 'ccli_number' in result, 'The returned song should have a CCLI number'
        assert 'verses' in result, 'The returned song should have verses'
        assert 3 == len(result['verses']), 'Three verses should have been returned'

    @patch('openlp.plugins.songs.lib.songselect.clean_song')
    @patch('openlp.plugins.songs.lib.songselect.Topic')
    @patch('openlp.plugins.songs.lib.songselect.Author')
    def test_save_song_new_author(self, MockedAuthor, MockedTopic, mocked_clean_song):
        """
        Test that saving a song with a new author performs the correct actions
        """
        # GIVEN: A song to save, and some mocked out objects
        song_dict = {
            'title': 'Arky Arky',
            'authors': ['Public Domain'],
            'verses': [
                {'label': 'Verse 1', 'lyrics': 'The Lord told Noah: there\'s gonna be a floody, floody'},
                {'label': 'Chorus 1', 'lyrics': 'So, rise and shine, and give God the glory, glory'},
                {'label': 'Verse 2', 'lyrics': 'The Lord told Noah to build him an arky, arky'}
            ],
            'copyright': 'Public Domain',
            'ccli_number': '123456'
        }
        MockedAuthor.display_name.__eq__.return_value = False
        MockedTopic.name.__eq__.return_value = False
        mocked_db_manager = MagicMock()
        mocked_db_manager.get_object_filtered.return_value = None
        importer = SongSelectImport(mocked_db_manager)

        # WHEN: The song is saved to the database
        result = importer.save_song(song_dict)

        # THEN: The return value should be a Song class and the mocked_db_manager should have been called
        assert isinstance(result, Song), 'The returned value should be a Song object'
        mocked_clean_song.assert_called_with(mocked_db_manager, result)
        assert 2 == mocked_db_manager.save_object.call_count, \
            'The save_object() method should have been called twice'
        mocked_db_manager.get_object_filtered.assert_called_with(MockedAuthor, False)
        MockedAuthor.populate.assert_called_with(first_name='Public', last_name='Domain', display_name='Public Domain')
        assert 1 == len(result.authors_songs), 'There should only be one author'

    @patch('openlp.plugins.songs.lib.songselect.clean_song')
    @patch('openlp.plugins.songs.lib.songselect.Author')
    def test_save_song_existing_author(self, MockedAuthor, mocked_clean_song):
        """
        Test that saving a song with an existing author performs the correct actions
        """
        # GIVEN: A song to save, and some mocked out objects
        song_dict = {
            'title': 'Arky Arky',
            'authors': ['Public Domain'],
            'verses': [
                {'label': 'Verse 1', 'lyrics': 'The Lord told Noah: there\'s gonna be a floody, floody'},
                {'label': 'Chorus 1', 'lyrics': 'So, rise and shine, and give God the glory, glory'},
                {'label': 'Verse 2', 'lyrics': 'The Lord told Noah to build him an arky, arky'}
            ],
            'copyright': 'Public Domain',
            'ccli_number': '123456'
        }
        MockedAuthor.display_name.__eq__.return_value = False
        mocked_db_manager = MagicMock()
        mocked_db_manager.get_object_filtered.return_value = MagicMock()
        importer = SongSelectImport(mocked_db_manager)

        # WHEN: The song is saved to the database
        result = importer.save_song(song_dict)

        # THEN: The return value should be a Song class and the mocked_db_manager should have been called
        assert isinstance(result, Song), 'The returned value should be a Song object'
        mocked_clean_song.assert_called_with(mocked_db_manager, result)
        assert 2 == mocked_db_manager.save_object.call_count, \
            'The save_object() method should have been called twice'
        mocked_db_manager.get_object_filtered.assert_called_with(MockedAuthor, False)
        assert 0 == MockedAuthor.populate.call_count, 'A new author should not have been instantiated'
        assert 1 == len(result.authors_songs), 'There should only be one author'

    @patch('openlp.plugins.songs.lib.songselect.clean_song')
    @patch('openlp.plugins.songs.lib.songselect.Author')
    def test_save_song_unknown_author(self, MockedAuthor, mocked_clean_song):
        """
        Test that saving a song with an author name of only one word performs the correct actions
        """
        # GIVEN: A song to save, and some mocked out objects
        song_dict = {
            'title': 'Arky Arky',
            'authors': ['Unknown'],
            'verses': [
                {'label': 'Verse 1', 'lyrics': 'The Lord told Noah: there\'s gonna be a floody, floody'},
                {'label': 'Chorus 1', 'lyrics': 'So, rise and shine, and give God the glory, glory'},
                {'label': 'Verse 2', 'lyrics': 'The Lord told Noah to build him an arky, arky'}
            ],
            'copyright': 'Public Domain',
            'ccli_number': '123456'
        }
        MockedAuthor.display_name.__eq__.return_value = False
        mocked_db_manager = MagicMock()
        mocked_db_manager.get_object_filtered.return_value = None
        importer = SongSelectImport(mocked_db_manager)

        # WHEN: The song is saved to the database
        result = importer.save_song(song_dict)

        # THEN: The return value should be a Song class and the mocked_db_manager should have been called
        assert isinstance(result, Song), 'The returned value should be a Song object'
        mocked_clean_song.assert_called_with(mocked_db_manager, result)
        assert 2 == mocked_db_manager.save_object.call_count, \
            'The save_object() method should have been called twice'
        mocked_db_manager.get_object_filtered.assert_called_with(MockedAuthor, False)
        MockedAuthor.populate.assert_called_with(first_name='Unknown', last_name='', display_name='Unknown')
        assert 1 == len(result.authors_songs), 'There should only be one author'


class TestSongSelectForm(TestCase, TestMixin):
    """
    Test the :class:`~openlp.plugins.songs.forms.songselectform.SongSelectForm` class
    """
    def setUp(self):
        """
        Some set up for this test suite
        """
        self.setup_application()
        self.app.setApplicationVersion('0.0')
        self.app.process_events = lambda: None
        Registry.create()
        Registry().register('application', self.app)

    def test_create_form(self):
        """
        Test that we can create the SongSelect form
        """
        # GIVEN: The SongSelectForm class and a mocked db manager
        mocked_plugin = MagicMock()
        mocked_db_manager = MagicMock()

        # WHEN: We create an instance
        ssform = SongSelectForm(None, mocked_plugin, mocked_db_manager)

        # THEN: The correct properties should have been assigned
        assert mocked_plugin == ssform.plugin, 'The correct plugin should have been assigned'
        assert mocked_db_manager == ssform.db_manager, 'The correct db_manager should have been assigned'

    @patch('openlp.plugins.songs.forms.songselectform.SongSelectImport')
    @patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QMessageBox.critical')
    @patch('openlp.plugins.songs.forms.songselectform.translate')
    def test_login_fails(self, mocked_translate, mocked_critical, MockedSongSelectImport):
        """
        Test that when the login fails, the form returns to the correct state
        """
        # GIVEN: A valid SongSelectForm with a mocked out SongSelectImport, and a bunch of mocked out controls
        mocked_song_select_import = MagicMock()
        mocked_song_select_import.login.return_value = False
        MockedSongSelectImport.return_value = mocked_song_select_import
        mocked_translate.side_effect = lambda *args: args[1]
        ssform = SongSelectForm(None, MagicMock(), MagicMock())
        ssform.initialise()
        with patch.object(ssform, 'username_edit') as mocked_username_edit, \
                patch.object(ssform, 'password_edit') as mocked_password_edit, \
                patch.object(ssform, 'save_password_checkbox') as mocked_save_password_checkbox, \
                patch.object(ssform, 'login_button') as mocked_login_button, \
                patch.object(ssform, 'login_spacer') as mocked_login_spacer, \
                patch.object(ssform, 'login_progress_bar') as mocked_login_progress_bar, \
                patch.object(ssform.application, 'process_events') as mocked_process_events:

            # WHEN: The login button is clicked, and the login is rigged to fail
            ssform.on_login_button_clicked()

            # THEN: The right things should have happened in the right order
            expected_username_calls = [call(False), call(True)]
            expected_password_calls = [call(False), call(True)]
            expected_save_password_calls = [call(False), call(True)]
            expected_login_btn_calls = [call(False), call(True)]
            expected_login_spacer_calls = [call(False), call(True)]
            expected_login_progress_visible_calls = [call(True), call(False)]
            expected_login_progress_value_calls = [call(0), call(0)]
            assert expected_username_calls == mocked_username_edit.setEnabled.call_args_list, \
                'The username edit should be disabled then enabled'
            assert expected_password_calls == mocked_password_edit.setEnabled.call_args_list, \
                'The password edit should be disabled then enabled'
            assert expected_save_password_calls == mocked_save_password_checkbox.setEnabled.call_args_list, \
                'The save password checkbox should be disabled then enabled'
            assert expected_login_btn_calls == mocked_login_button.setEnabled.call_args_list, \
                'The login button should be disabled then enabled'
            assert expected_login_spacer_calls == mocked_login_spacer.setVisible.call_args_list, \
                'Thee login spacer should be make invisible, then visible'
            assert expected_login_progress_visible_calls == mocked_login_progress_bar.setVisible.call_args_list, \
                'Thee login progress bar should be make visible, then invisible'
            assert expected_login_progress_value_calls == mocked_login_progress_bar.setValue.call_args_list, \
                'Thee login progress bar should have the right values set'
            assert 2 == mocked_process_events.call_count, 'The process_events() method should be called twice'
            mocked_critical.assert_called_with(ssform, 'Error Logging In', 'There was a problem logging in, '
                                                                           'perhaps your username or password is '
                                                                           'incorrect?')

    @patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QMessageBox.question')
    @patch('openlp.plugins.songs.forms.songselectform.translate')
    def test_on_import_yes_clicked(self, mocked_translate, mocked_question):
        """
        Test that when a song is imported and the user clicks the "yes" button, the UI goes back to the previous page
        """
        # GIVEN: A valid SongSelectForm with a mocked out QMessageBox.question() method
        mocked_translate.side_effect = lambda *args: args[1]
        mocked_question.return_value = QtWidgets.QMessageBox.Yes
        ssform = SongSelectForm(None, MagicMock(), MagicMock())
        mocked_song_select_importer = MagicMock()
        ssform.song_select_importer = mocked_song_select_importer
        ssform.song = None

        # WHEN: The import button is clicked, and the user clicks Yes
        with patch.object(ssform, 'on_back_button_clicked') as mocked_on_back_button_clicked:
            ssform.on_import_button_clicked()

        # THEN: The on_back_button_clicked() method should have been called
        mocked_song_select_importer.save_song.assert_called_with(None)
        mocked_question.assert_called_with(ssform, 'Song Imported',
                                           'Your song has been imported, would you like to import more songs?',
                                           defaultButton=QtWidgets.QMessageBox.Yes)
        mocked_on_back_button_clicked.assert_called_with()
        assert ssform.song is None

    @patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QMessageBox.question')
    @patch('openlp.plugins.songs.forms.songselectform.translate')
    def test_on_import_no_clicked(self, mocked_translate, mocked_question):
        """
        Test that when a song is imported and the user clicks the "no" button, the UI exits
        """
        # GIVEN: A valid SongSelectForm with a mocked out QMessageBox.question() method
        mocked_translate.side_effect = lambda *args: args[1]
        mocked_question.return_value = QtWidgets.QMessageBox.No
        ssform = SongSelectForm(None, MagicMock(), MagicMock())
        mocked_song_select_importer = MagicMock()
        ssform.song_select_importer = mocked_song_select_importer
        ssform.song = None

        # WHEN: The import button is clicked, and the user clicks Yes
        with patch.object(ssform, 'done') as mocked_done:
            ssform.on_import_button_clicked()

        # THEN: The on_back_button_clicked() method should have been called
        mocked_song_select_importer.save_song.assert_called_with(None)
        mocked_question.assert_called_with(ssform, 'Song Imported',
                                           'Your song has been imported, would you like to import more songs?',
                                           defaultButton=QtWidgets.QMessageBox.Yes)
        mocked_done.assert_called_with(QtWidgets.QDialog.Accepted)
        assert ssform.song is None

    def test_on_back_button_clicked(self):
        """
        Test that when the back button is clicked, the stacked widget is set back one page
        """
        # GIVEN: A SongSelect form
        ssform = SongSelectForm(None, MagicMock(), MagicMock())

        # WHEN: The back button is clicked
        with patch.object(ssform, 'stacked_widget') as mocked_stacked_widget, \
                patch.object(ssform, 'search_combobox') as mocked_search_combobox:
            ssform.on_back_button_clicked()

        # THEN: The stacked widget should be set back one page
        mocked_stacked_widget.setCurrentIndex.assert_called_with(1)
        mocked_search_combobox.setFocus.assert_called_with()

    @patch('openlp.plugins.songs.forms.songselectform.QtWidgets.QMessageBox.information')
    def test_on_search_show_info(self, mocked_information):
        """
        Test that when the search_show_info signal is emitted, the on_search_show_info() method shows a dialog
        """
        # GIVEN: A SongSelect form
        ssform = SongSelectForm(None, MagicMock(), MagicMock())
        expected_title = 'Test Title'
        expected_text = 'This is a test'

        # WHEN: on_search_show_info is called
        ssform.on_search_show_info(expected_title, expected_text)

        # THEN: An information dialog should be shown
        mocked_information.assert_called_with(ssform, expected_title, expected_text)

    def test_update_login_progress(self):
        """
        Test the _update_login_progress() method
        """
        # GIVEN: A SongSelect form
        ssform = SongSelectForm(None, MagicMock(), MagicMock())

        # WHEN: _update_login_progress() is called
        with patch.object(ssform, 'login_progress_bar') as mocked_login_progress_bar:
            mocked_login_progress_bar.value.return_value = 3
            ssform._update_login_progress()

        # THEN: The login progress bar should be updated
        mocked_login_progress_bar.setValue.assert_called_with(4)

    def test_update_song_progress(self):
        """
        Test the _update_song_progress() method
        """
        # GIVEN: A SongSelect form
        ssform = SongSelectForm(None, MagicMock(), MagicMock())

        # WHEN: _update_song_progress() is called
        with patch.object(ssform, 'song_progress_bar') as mocked_song_progress_bar:
            mocked_song_progress_bar.value.return_value = 2
            ssform._update_song_progress()

        # THEN: The song progress bar should be updated
        mocked_song_progress_bar.setValue.assert_called_with(3)

    def test_on_search_results_widget_double_clicked(self):
        """
        Test that a song is retrieved when a song in the results list is double-clicked
        """
        # GIVEN: A SongSelect form
        ssform = SongSelectForm(None, MagicMock(), MagicMock())
        expected_song = {'title': 'Amazing Grace'}

        # WHEN: A song result is double-clicked
        with patch.object(ssform, '_view_song') as mocked_view_song:
            ssform.on_search_results_widget_double_clicked(expected_song)

        # THEN: The song is fetched and shown to the user
        mocked_view_song.assert_called_with(expected_song)

    def test_on_view_button_clicked(self):
        """
        Test that a song is retrieved when the view button is clicked
        """
        # GIVEN: A SongSelect form
        ssform = SongSelectForm(None, MagicMock(), MagicMock())
        expected_song = {'title': 'Amazing Grace'}

        # WHEN: A song result is double-clicked
        with patch.object(ssform, '_view_song') as mocked_view_song, \
                patch.object(ssform, 'search_results_widget') as mocked_search_results_widget:
            mocked_search_results_widget.currentItem.return_value = expected_song
            ssform.on_view_button_clicked()

        # THEN: The song is fetched and shown to the user
        mocked_view_song.assert_called_with(expected_song)

    def test_on_search_results_widget_selection_changed(self):
        """
        Test that the view button is updated when the search results list is changed
        """
        # GIVEN: A SongSelect form
        ssform = SongSelectForm(None, MagicMock(), MagicMock())

        # WHEN: There is at least 1 item selected
        with patch.object(ssform, 'search_results_widget') as mocked_search_results_widget, \
                patch.object(ssform, 'view_button') as mocked_view_button:
            mocked_search_results_widget.selectedItems.return_value = [1]
            ssform.on_search_results_widget_selection_changed()

        # THEN: The view button should be enabled
        mocked_view_button.setEnabled.assert_called_with(True)

    @patch('openlp.plugins.songs.forms.songselectform.SongSelectImport')
    def test_on_stop_button_clicked(self, MockedSongSelectImport):
        """
        Test that the search is stopped when the stop button is clicked
        """
        # GIVEN: A mocked SongSelectImporter and a SongSelect form
        mocked_song_select_importer = MagicMock()
        MockedSongSelectImport.return_value = mocked_song_select_importer
        ssform = SongSelectForm(None, MagicMock(), MagicMock())
        ssform.initialise()

        # WHEN: The stop button is clicked
        ssform.on_stop_button_clicked()

        # THEN: The view button, search box and search button should be enabled
        mocked_song_select_importer.stop.assert_called_with()
        assert ssform.search_button.isEnabled() is True
        assert ssform.search_combobox.isEnabled() is True

    @patch('openlp.plugins.songs.forms.songselectform.Settings')
    @patch('openlp.plugins.songs.forms.songselectform.run_thread')
    @patch('openlp.plugins.songs.forms.songselectform.SearchWorker')
    def test_on_search_button_clicked(self, MockedSearchWorker, mocked_run_thread, MockedSettings):
        """
        Test that search fields are disabled when search button is clicked.
        """
        # GIVEN: A mocked SongSelect form
        ssform = SongSelectForm(None, MagicMock(), MagicMock())
        ssform.initialise()

        # WHEN: The search button is clicked
        ssform.on_search_button_clicked()

        # THEN: The search box and search button should be disabled
        assert ssform.search_button.isEnabled() is False
        assert ssform.search_combobox.isEnabled() is False

    def test_on_search_finished(self):
        """
        Test that search fields are enabled when search is finished.
        """
        # GIVEN: A mocked SongSelect form
        ssform = SongSelectForm(None, MagicMock(), MagicMock())
        ssform.initialise()

        # WHEN: The search is finished
        ssform.on_search_finished()

        # THEN: The search box and search button should be enabled
        assert ssform.search_button.isEnabled() is True
        assert ssform.search_combobox.isEnabled() is True


class TestSongSelectFileImport(SongImportTestHelper):

    def __init__(self, *args, **kwargs):
        self.importer_class_name = 'CCLIFileImport'
        self.importer_module_name = 'cclifile'
        super().__init__(*args, **kwargs)

    def test_song_import(self):
        """
        Test that loading an OpenSong file works correctly on various files
        """
        self.file_import([TEST_PATH / 'TestSong.bin'], self.load_external_result_data(TEST_PATH / 'TestSong-bin.json'))
        self.file_import([TEST_PATH / 'TestSong.txt'], self.load_external_result_data(TEST_PATH / 'TestSong-txt.json'))


class TestSearchWorker(TestCase, TestMixin):
    """
    Test the SearchWorker class
    """
    def test_constructor(self):
        """
        Test the SearchWorker constructor
        """
        # GIVEN: An importer mock object and some search text
        importer = MagicMock()
        search_text = 'Jesus'

        # WHEN: The search worker is created
        worker = SearchWorker(importer, search_text)

        # THEN: The correct values should be set
        assert importer is worker.importer, 'The importer should be the right object'
        assert search_text == worker.search_text, 'The search text should be correct'

    def test_start(self):
        """
        Test the start() method of the SearchWorker class
        """
        # GIVEN: An importer mock object, some search text and an initialised SearchWorker
        importer = MagicMock()
        importer.search.return_value = ['song1', 'song2']
        search_text = 'Jesus'
        worker = SearchWorker(importer, search_text)

        # WHEN: The start() method is called
        with patch.object(worker, 'finished') as mocked_finished, patch.object(worker, 'quit') as mocked_quit:
            worker.start()

        # THEN: The "finished" and "quit" signals should be emitted
        importer.search.assert_called_with(search_text, 1000, worker._found_song_callback)
        mocked_finished.emit.assert_called_with()
        mocked_quit.emit.assert_called_with()

    @patch('openlp.plugins.songs.forms.songselectform.translate')
    def test_start_over_1000_songs(self, mocked_translate):
        """
        Test the start() method of the SearchWorker class when it finds over 1000 songs
        """
        # GIVEN: An importer mock object, some search text and an initialised SearchWorker
        mocked_translate.side_effect = lambda x, y: y
        importer = MagicMock()
        importer.search.return_value = ['song%s' % num for num in range(1050)]
        search_text = 'Jesus'
        worker = SearchWorker(importer, search_text)

        # WHEN: The start() method is called
        with patch.object(worker, 'finished') as mocked_finished, patch.object(worker, 'quit') as mocked_quit, \
                patch.object(worker, 'show_info') as mocked_show_info:
            worker.start()

        # THEN: The "finished" and "quit" signals should be emitted
        importer.search.assert_called_with(search_text, 1000, worker._found_song_callback)
        mocked_show_info.emit.assert_called_with('More than 1000 results', 'Your search has returned more than 1000 '
                                                                           'results, it has been stopped. Please '
                                                                           'refine your search to fetch better '
                                                                           'results.')
        mocked_finished.emit.assert_called_with()
        mocked_quit.emit.assert_called_with()

    def test_found_song_callback(self):
        """
        Test that when the _found_song_callback() function is called, the "found_song" signal is emitted
        """
        # GIVEN: An importer mock object, some search text and an initialised SearchWorker
        importer = MagicMock()
        search_text = 'Jesus'
        song = {'title': 'Amazing Grace'}
        worker = SearchWorker(importer, search_text)

        # WHEN: The start() method is called
        with patch.object(worker, 'found_song') as mocked_found_song:
            worker._found_song_callback(song)

        # THEN: The "found_song" signal should have been emitted
        mocked_found_song.emit.assert_called_with(song)
