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

##########################################################################
# OpenLP - Open Source Lyrics Projection                                 #
# ---------------------------------------------------------------------- #
# Copyright (c) 2008-2022 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.lib package.
"""
import io
import os
import pytest
from pathlib import Path
from unittest.mock import MagicMock, patch

from PyQt5 import QtCore, QtGui

from openlp.core.lib import DataType, build_icon, check_item_selected, create_separated_list, create_thumb, \
    get_text_file_string, image_to_byte, read_or_fail, read_int, resize_image, seek_or_fail, str_to_bool, validate_thumb
from tests.utils.constants import RESOURCE_PATH


def test_str_to_bool_with_bool_true():
    """
    Test the str_to_bool function with boolean input of True
    """
    # GIVEN: A boolean value set to true
    true_boolean = True

    # WHEN: We "convert" it to a bool
    true_result = str_to_bool(true_boolean)

    # THEN: We should get back a True bool
    assert isinstance(true_result, bool), 'The result should be a boolean'
    assert true_result is True, 'The result should be True'


def test_str_to_bool_with_bool_false():
    """
    Test the str_to_bool function with boolean input of False
    """
    # GIVEN: A boolean value set to false
    false_boolean = False

    # WHEN: We "convert" it to a bool
    false_result = str_to_bool(false_boolean)

    # THEN: We should get back a True bool
    assert isinstance(false_result, bool), 'The result should be a boolean'
    assert false_result is False, 'The result should be True'


def test_str_to_bool_with_integer():
    """
    Test the str_to_bool function with an integer input
    """
    # GIVEN: An integer value
    int_string = 1

    # WHEN: we convert it to a bool
    int_result = str_to_bool(int_string)

    # THEN: we should get back a false
    assert int_result is False, 'The result should be False'


def test_str_to_bool_with_invalid_string():
    """
    Test the str_to_bool function with an invalid string
    """
    # GIVEN: An string value with completely invalid input
    invalid_string = 'my feet are wet'

    # WHEN: we convert it to a bool
    str_result = str_to_bool(invalid_string)

    # THEN: we should get back a false
    assert str_result is False, 'The result should be False'


def test_str_to_bool_with_string_false():
    """
    Test the str_to_bool function with a string saying "false"
    """
    # GIVEN: A string set to "false"
    false_string = 'false'

    # WHEN: we convert it to a bool
    false_result = str_to_bool(false_string)

    # THEN: we should get back a false
    assert false_result is False, 'The result should be False'


def test_str_to_bool_with_string_no():
    """
    Test the str_to_bool function with a string saying "NO"
    """
    # GIVEN: An string set to "NO"
    no_string = 'NO'

    # WHEN: we convert it to a bool
    str_result = str_to_bool(no_string)

    # THEN: we should get back a false
    assert str_result is False, 'The result should be False'


def test_str_to_bool_with_true_string_value():
    """
    Test the str_to_bool function with a string set to "True"
    """
    # GIVEN: A string set to "True"
    true_string = 'True'

    # WHEN: we convert it to a bool
    true_result = str_to_bool(true_string)

    # THEN: we should get back a true
    assert true_result is True, 'The result should be True'


def test_str_to_bool_with_yes_string_value():
    """
    Test the str_to_bool function with a string set to "yes"
    """
    # GIVEN: An string set to "yes"
    yes_string = 'yes'

    # WHEN: we convert it to a bool
    str_result = str_to_bool(yes_string)

    # THEN: we should get back a true
    assert str_result is True, 'The result should be True'


def test_get_text_file_string_no_file():
    """
    Test the get_text_file_string() function when a file does not exist
    """
    # GIVEN: A patched is_file which returns False, and a file path
    with patch.object(Path, 'is_file', return_value=False):
        file_path = Path('testfile.txt')

        # WHEN: get_text_file_string is called
        result = get_text_file_string(file_path)

        # THEN: The result should be False
        file_path.is_file.assert_called_with()
        assert result is False, 'False should be returned if no file exists'


def test_get_text_file_string_read_error():
    """
    Test the get_text_file_string() method when a read error happens
    """
    # GIVEN: A patched open which raises an exception and is_file which returns True
    with patch.object(Path, 'is_file'), \
            patch.object(Path, 'open'):
        file_path = Path('testfile.txt')
        file_path.is_file.return_value = True
        file_path.open.side_effect = OSError()

        # WHEN: get_text_file_string is called
        result = get_text_file_string(file_path)

        # THEN: None should be returned
        file_path.is_file.assert_called_once_with()
        file_path.open.assert_called_once_with('r', encoding='utf-8')
        assert result is None, 'None should be returned if the file cannot be opened'


def test_build_icon_with_qicon():
    """
    Test the build_icon() function with a QIcon instance
    """
    # GIVEN: An icon QIcon
    icon = QtGui.QIcon()

    # WHEN: We pass a QIcon instance in
    result = build_icon(icon)

    # THEN: The result should be the same icon as we passed in
    assert icon is result, 'The result should be the same icon as we passed in'


def test_build_icon_with_resource():
    """
    Test the build_icon() function with a resource URI
    """
    with patch('openlp.core.lib.QtGui') as MockedQtGui, \
            patch('openlp.core.lib.QtGui.QPixmap') as MockedQPixmap:
        # GIVEN: A mocked QIcon and a mocked QPixmap
        MockedQtGui.QIcon = MagicMock
        MockedQtGui.QIcon.Normal = 1
        MockedQtGui.QIcon.Off = 2
        MockedQPixmap.return_value = 'mocked_pixmap'
        resource_uri = ':/resource/uri'

        # WHEN: We pass a QIcon instance in
        result = build_icon(resource_uri)

        # THEN: The result should be our mocked QIcon
        MockedQPixmap.assert_called_with(resource_uri)
        # There really should be more assert statements here but due to type checking and things they all break. The
        # best we can do is to assert that we get back a MagicMock object.
        assert isinstance(result, MagicMock), 'The result should be a MagicMock, because we mocked it out'


def test_image_to_byte():
    """
    Test the image_to_byte() function
    """
    with patch('openlp.core.lib.QtCore') as MockedQtCore:
        # GIVEN: A set of mocked-out Qt classes
        mocked_byte_array = MagicMock()
        MockedQtCore.QByteArray.return_value = mocked_byte_array
        mocked_buffer = MagicMock()
        MockedQtCore.QBuffer.return_value = mocked_buffer
        MockedQtCore.QIODevice.WriteOnly = 'writeonly'
        mocked_image = MagicMock()

        # WHEN: We convert an image to a byte array
        result = image_to_byte(mocked_image, base_64=False)

        # THEN: We should receive the mocked_buffer
        MockedQtCore.QByteArray.assert_called_with()
        MockedQtCore.QBuffer.assert_called_with(mocked_byte_array)
        mocked_buffer.open.assert_called_with('writeonly')
        mocked_image.save.assert_called_with(mocked_buffer, "JPEG")
        assert mocked_byte_array.toBase64.called is False
        assert mocked_byte_array == result, 'The mocked out byte array should be returned'


def test_image_to_byte_base_64():
    """
    Test the image_to_byte() function
    """
    with patch('openlp.core.lib.QtCore') as MockedQtCore, \
            patch('openlp.core.lib.base64.b64encode') as mocked_b64encode:
        # GIVEN: A set of mocked-out Qt classes
        mocked_b64encode.side_effect = lambda x: MagicMock(decode=MagicMock(return_value='{} base64ified'.format(x)))
        mocked_byte_array = "byte_array"
        MockedQtCore.QByteArray.return_value = mocked_byte_array
        mocked_buffer = MagicMock()
        MockedQtCore.QBuffer.return_value = mocked_buffer
        MockedQtCore.QIODevice.WriteOnly = 'writeonly'
        mocked_image = MagicMock()

        # WHEN: We convert an image to a byte array
        result = image_to_byte(mocked_image)

        # THEN: We should receive a value of 'base64mock'
        MockedQtCore.QByteArray.assert_called_with()
        MockedQtCore.QBuffer.assert_called_with(mocked_byte_array)
        mocked_buffer.open.assert_called_with('writeonly')
        mocked_image.save.assert_called_with(mocked_buffer, "JPEG")
        mocked_b64encode.assert_called_with(mocked_byte_array)
        assert 'byte_array base64ified' == result, 'The result should be the return value of the mocked base64 method'


def test_create_thumb_with_size():
    """
    Test the create_thumb() function with a given size.
    """
    # GIVEN: An image to create a thumb of.
    image_path = RESOURCE_PATH / 'church.jpg'
    thumb_path = RESOURCE_PATH / 'church_thumb.jpg'
    thumb_size = QtCore.QSize(10, 20)

    # Remove the thumb so that the test actually tests if the thumb will be created. Maybe it was not deleted in the
    # last test.
    try:
        thumb_path.unlink()
    except Exception:
        pass

    # Only continue when the thumb does not exist.
    assert thumb_path.exists() is False, 'Test was not run, because the thumb already exists.'

    # WHEN: Create the thumb.
    icon = create_thumb(image_path, thumb_path, size=thumb_size)

    # THEN: Check if the thumb was created and scaled to the given size.
    assert thumb_path.exists() is True, 'Test was not ran, because the thumb already exists'
    assert isinstance(icon, QtGui.QIcon), 'The icon should be a QIcon'
    assert icon.isNull() is False, 'The icon should not be null'
    assert thumb_size == QtGui.QImageReader(str(thumb_path)).size(), 'The thumb should have the given size'

    # Remove the thumb so that the test actually tests if the thumb will be created.
    try:
        thumb_path.unlink()
    except Exception:
        pass


def test_create_thumb_no_size():
    """
    Test the create_thumb() function with no size specified.
    """
    # GIVEN: An image to create a thumb of.
    image_path = RESOURCE_PATH / 'church.jpg'
    thumb_path = RESOURCE_PATH / 'church_thumb.jpg'
    expected_size = QtCore.QSize(63, 88)

    # Remove the thumb so that the test actually tests if the thumb will be created. Maybe it was not deleted in the
    # last test.
    try:
        thumb_path.unlink()
    except Exception:
        pass

    # Only continue when the thumb does not exist.
    assert thumb_path.exists() is False, 'Test was not run, because the thumb already exists.'

    # WHEN: Create the thumb.
    icon = create_thumb(image_path, thumb_path)

    # THEN: Check if the thumb was created, retaining its aspect ratio.
    assert thumb_path.exists() is True, 'Test was not ran, because the thumb already exists'
    assert isinstance(icon, QtGui.QIcon), 'The icon should be a QIcon'
    assert icon.isNull() is False, 'The icon should not be null'
    assert expected_size == QtGui.QImageReader(str(thumb_path)).size(), 'The thumb should have the given size'

    # Remove the thumb so that the test actually tests if the thumb will be created.
    try:
        thumb_path.unlink()
    except Exception:
        pass


def test_create_thumb_invalid_size():
    """
    Test the create_thumb() function with invalid size specified.
    """
    # GIVEN: An image to create a thumb of.
    image_path = RESOURCE_PATH / 'church.jpg'
    thumb_path = RESOURCE_PATH / 'church_thumb.jpg'
    thumb_size = QtCore.QSize(-1, -1)
    expected_size = QtCore.QSize(63, 88)

    # Remove the thumb so that the test actually tests if the thumb will be created. Maybe it was not deleted in the
    # last test.
    try:
        thumb_path.unlink()
    except Exception:
        pass

    # Only continue when the thumb does not exist.
    assert thumb_path.exists() is False, 'Test was not run, because the thumb already exists.'

    # WHEN: Create the thumb.
    icon = create_thumb(image_path, thumb_path, size=thumb_size)

    # THEN: Check if the thumb was created, retaining its aspect ratio.
    assert thumb_path.exists() is True, 'Test was not ran, because the thumb already exists'
    assert isinstance(icon, QtGui.QIcon), 'The icon should be a QIcon'
    assert icon.isNull() is False, 'The icon should not be null'
    assert expected_size == QtGui.QImageReader(str(thumb_path)).size(), 'The thumb should have the given size'

    # Remove the thumb so that the test actually tests if the thumb will be created.
    try:
        thumb_path.unlink()
    except Exception:
        pass


def test_create_thumb_width_only():
    """
    Test the create_thumb() function with a size of only width specified.
    """
    # GIVEN: An image to create a thumb of.
    image_path = RESOURCE_PATH / 'church.jpg'
    thumb_path = RESOURCE_PATH / 'church_thumb.jpg'
    thumb_size = QtCore.QSize(100, -1)
    expected_size = QtCore.QSize(100, 137)

    # Remove the thumb so that the test actually tests if the thumb will be created. Maybe it was not deleted in the
    # last test.
    try:
        thumb_path.unlink()
    except Exception:
        pass

    # Only continue when the thumb does not exist.
    assert thumb_path.exists() is False, 'Test was not run, because the thumb already exists.'

    # WHEN: Create the thumb.
    icon = create_thumb(image_path, thumb_path, size=thumb_size)

    # THEN: Check if the thumb was created, retaining its aspect ratio.
    assert thumb_path.exists() is True, 'Test was not ran, because the thumb already exists'
    assert isinstance(icon, QtGui.QIcon), 'The icon should be a QIcon'
    assert icon.isNull() is False, 'The icon should not be null'
    assert expected_size == QtGui.QImageReader(str(thumb_path)).size(), 'The thumb should have the given size'

    # Remove the thumb so that the test actually tests if the thumb will be created.
    try:
        thumb_path.unlink()
    except Exception:
        pass


def test_create_thumb_height_only():
    """
    Test the create_thumb() function with a size of only height specified.
    """
    # GIVEN: An image to create a thumb of.
    image_path = RESOURCE_PATH / 'church.jpg'
    thumb_path = RESOURCE_PATH / 'church_thumb.jpg'
    thumb_size = QtCore.QSize(-1, 100)
    expected_size = QtCore.QSize(72, 100)

    # Remove the thumb so that the test actually tests if the thumb will be created. Maybe it was not deleted in the
    # last test.
    try:
        thumb_path.unlink()
    except Exception:
        pass

    # Only continue when the thumb does not exist.
    assert thumb_path.exists() is False, 'Test was not run, because the thumb already exists.'

    # WHEN: Create the thumb.
    icon = create_thumb(image_path, thumb_path, size=thumb_size)

    # THEN: Check if the thumb was created, retaining its aspect ratio.
    assert thumb_path.exists() is True, 'Test was not ran, because the thumb already exists'
    assert isinstance(icon, QtGui.QIcon), 'The icon should be a QIcon'
    assert icon.isNull() is False, 'The icon should not be null'
    assert expected_size == QtGui.QImageReader(str(thumb_path)).size(), 'The thumb should have the given size'

    # Remove the thumb so that the test actually tests if the thumb will be created.
    try:
        thumb_path.unlink()
    except Exception:
        pass


def test_create_thumb_empty_img():
    """
    Test the create_thumb() function with a size of only height specified.
    """
    # GIVEN: An image to create a thumb of.
    image_path = RESOURCE_PATH / 'church.jpg'
    thumb_path = RESOURCE_PATH / 'church_thumb.jpg'
    thumb_size = QtCore.QSize(-1, 100)
    expected_size_1 = QtCore.QSize(88, 88)
    expected_size_2 = QtCore.QSize(100, 100)

    # Remove the thumb so that the test actually tests if the thumb will be created. Maybe it was not deleted in the
    # last test.
    try:
        thumb_path.unlink()
    except Exception:
        pass

    # Only continue when the thumb does not exist.
    assert thumb_path.exists() is False, 'Test was not run, because the thumb already exists.'

    # WHEN: Create the thumb.
    with patch('openlp.core.lib.QtGui.QImageReader.size') as mocked_size:
        mocked_size.return_value = QtCore.QSize(0, 0)
        icon = create_thumb(image_path, thumb_path, size=None)

    # THEN: Check if the thumb was created with aspect ratio of 1.
    assert thumb_path.exists() is True, 'Test was not ran, because the thumb already exists'
    assert isinstance(icon, QtGui.QIcon), 'The icon should be a QIcon'
    assert icon.isNull() is False, 'The icon should not be null'
    assert expected_size_1 == QtGui.QImageReader(str(thumb_path)).size(), 'The thumb should have the given size'

    # WHEN: Create the thumb.
    with patch('openlp.core.lib.QtGui.QImageReader.size') as mocked_size:
        mocked_size.return_value = QtCore.QSize(0, 0)
        icon = create_thumb(image_path, thumb_path, size=thumb_size)

    # THEN: Check if the thumb was created with aspect ratio of 1.
    assert isinstance(icon, QtGui.QIcon), 'The icon should be a QIcon'
    assert icon.isNull() is False, 'The icon should not be null'
    assert expected_size_2 == QtGui.QImageReader(str(thumb_path)).size(), 'The thumb should have the given size'

    # Remove the thumb so that the test actually tests if the thumb will be created.
    try:
        thumb_path.unlink()
    except Exception:
        pass


@patch('openlp.core.lib.QtGui.QImageReader')
@patch('openlp.core.lib.build_icon')
def test_create_thumb_path_fails(mocked_build_icon, MockQImageReader):
    """
    Test that build_icon() is run against the image_path when the thumbnail fails to be created
    """
    # GIVEN: A bunch of mocks
    image_path = RESOURCE_PATH / 'church.jpg'
    thumb_path = Path('doesnotexist')
    mocked_image_reader = MagicMock()
    mocked_image_reader.size.return_value.isEmpty.return_value = True

    # WHEN: Create the thumb
    create_thumb(image_path, thumb_path)

    # THEN: The image path should have been used
    mocked_build_icon.assert_called_once_with(image_path)


@patch('openlp.core.lib.QtWidgets', MagicMock())
def test_check_item_selected_true():
    """
    Test that the check_item_selected() function returns True when there are selected indexes
    """
    # GIVEN: A mocked out QtWidgets module and a list widget with selected indexes
    mocked_list_widget = MagicMock()
    mocked_list_widget.selectedIndexes.return_value = True
    message = 'message'

    # WHEN: We check if there are selected items
    result = check_item_selected(mocked_list_widget, message)

    # THEN: The selectedIndexes function should have been called and the result should be true
    mocked_list_widget.selectedIndexes.assert_called_with()
    assert result is True, 'The result should be True'


def test_check_item_selected_false():
    """
    Test that the check_item_selected() function returns False when there are no selected indexes.
    """
    # GIVEN: A mocked out QtWidgets module and a list widget with selected indexes
    with patch('openlp.core.lib.QtWidgets') as MockedQtWidgets, \
            patch('openlp.core.lib.translate') as mocked_translate:
        mocked_translate.return_value = 'mocked translate'
        mocked_list_widget = MagicMock()
        mocked_list_widget.selectedIndexes.return_value = False
        mocked_list_widget.parent.return_value = 'parent'
        message = 'message'

        # WHEN: We check if there are selected items
        result = check_item_selected(mocked_list_widget, message)

        # THEN: The selectedIndexes function should have been called and the result should be true
        mocked_list_widget.selectedIndexes.assert_called_with()
        MockedQtWidgets.QMessageBox.information.assert_called_with('parent', 'mocked translate', 'message')
        assert result is False, 'The result should be False'


def test_validate_thumb_file_does_not_exist():
    """
    Test the validate_thumb() function when the thumbnail does not exist
    """
    # GIVEN: A mocked out os module, with path.exists returning False, and fake paths to a file and a thumb
    with patch.object(Path, 'exists', return_value=False):
        file_path = Path('path', 'to', 'file')
        thumb_path = Path('path', 'to', 'thumb')

        # WHEN: we run the validate_thumb() function
        result = validate_thumb(file_path, thumb_path)

        # THEN: we should have called a few functions, and the result should be False
        thumb_path.exists.assert_called_once_with()
        assert result is False, 'The result should be False'


def test_validate_thumb_file_exists_and_newer():
    """
    Test the validate_thumb() function when the thumbnail exists and has a newer timestamp than the file
    """
    with patch.object(Path, 'exists'), patch.object(Path, 'stat'):
        # GIVEN: Mocked file_path and thumb_path which return different values fo the modified times
        file_path = MagicMock(**{'stat.return_value': MagicMock(st_mtime=10)})
        thumb_path = MagicMock(**{'exists.return_value': True, 'stat.return_value': MagicMock(st_mtime=11)})

        # WHEN: we run the validate_thumb() function
        result = validate_thumb(file_path, thumb_path)

        # THEN: `validate_thumb` should return True
        assert result is True


def test_validate_thumb_file_exists_and_older():
    """
    Test the validate_thumb() function when the thumbnail exists but is older than the file
    """
    # GIVEN: Mocked file_path and thumb_path which return different values fo the modified times
    file_path = MagicMock(**{'stat.return_value': MagicMock(st_mtime=10)})
    thumb_path = MagicMock(**{'exists.return_value': True, 'stat.return_value': MagicMock(st_mtime=9)})

    # WHEN: we run the validate_thumb() function
    result = validate_thumb(file_path, thumb_path)

    # THEN: `validate_thumb` should return False
    thumb_path.stat.assert_called_once_with()
    assert result is False, 'The result should be False'


def test_resize_thumb():
    """
    Test the resize_thumb() function
    """
    # GIVEN: A path to an image.
    image_path = str(RESOURCE_PATH / 'church.jpg')
    wanted_width = 777
    wanted_height = 72
    # We want the background to be white.
    wanted_background_hex = '#FFFFFF'
    wanted_background_rgb = QtGui.QColor(wanted_background_hex).rgb()

    # WHEN: Resize the image and add a background.
    image = resize_image(image_path, wanted_width, wanted_height, wanted_background_hex)

    # THEN: Check if the size is correct and the background was set.
    result_size = image.size()
    assert wanted_height == result_size.height(), 'The image should have the requested height.'
    assert wanted_width == result_size.width(), 'The image should have the requested width.'
    assert image.pixel(0, 0) == wanted_background_rgb, 'The background should be white.'


def test_resize_thumb_ignoring_aspect_ratio():
    """
    Test the resize_thumb() function ignoring aspect ratio
    """
    # GIVEN: A path to an image.
    image_path = str(RESOURCE_PATH / 'church.jpg')
    wanted_width = 1000
    wanted_height = 1000
    # We want the background to be white.
    wanted_background_hex = '#FFFFFF'
    wanted_background_rgb = QtGui.QColor(wanted_background_hex).rgb()

    # WHEN: Resize the image and add a background.
    image = resize_image(image_path, wanted_width, wanted_height, wanted_background_hex, True)

    # THEN: Check if the size is correct and the background was set.
    result_size = image.size()
    assert wanted_height == result_size.height(), 'The image should have the requested height.'
    assert wanted_width == result_size.width(), 'The image should have the requested width.'
    assert image.pixel(0, 0) == wanted_background_rgb, 'The background should be white.'


def test_resize_thumb_width_aspect_ratio():
    """
    Test the resize_thumb() function using the image's width as the reference
    """
    # GIVEN: A path to an image.
    image_path = str(RESOURCE_PATH / 'church.jpg')
    wanted_width = 10
    wanted_height = 1000

    # WHEN: Resize the image and add a background.
    image = resize_image(image_path, wanted_width, wanted_height)

    # THEN: Check if the size is correct and the background was set.
    result_size = image.size()
    assert wanted_height == result_size.height(), 'The image should have the requested height.'
    assert wanted_width == result_size.width(), 'The image should have the requested width.'


def test_resize_thumb_same_aspect_ratio():
    """
    Test the resize_thumb() function when the image and the wanted aspect ratio are the same
    """
    # GIVEN: A path to an image.
    image_path = str(RESOURCE_PATH / 'church.jpg')
    wanted_width = 1122
    wanted_height = 1544

    # WHEN: Resize the image and add a background.
    image = resize_image(image_path, wanted_width, wanted_height)

    # THEN: Check if the size is correct and the background was set.
    result_size = image.size()
    assert wanted_height == result_size.height(), 'The image should have the requested height.'
    assert wanted_width == result_size.width(), 'The image should have the requested width.'


@patch('openlp.core.lib.QtCore.QLocale.createSeparatedList')
def test_create_separated_list_qlocate(mocked_createSeparatedList):
    """
    Test the create_separated_list function using the Qt provided method
    """
    # GIVEN: A list of strings and the mocked Qt module.
    mocked_createSeparatedList.return_value = 'Author 1, Author 2, and Author 3'
    string_list = ['Author 1', 'Author 2', 'Author 3']

    # WHEN: We get a string build from the entries it the list and a separator.
    string_result = create_separated_list(string_list)

    # THEN: We should have "Author 1, Author 2, and Author 3"
    assert string_result == 'Author 1, Author 2 and Author 3', \
        'The string should be "Author 1, Author 2, and Author 3".'


def test_create_separated_list_empty_list():
    """
    Test the create_separated_list function with an empty list
    """
    # GIVEN: An empty list
    string_list = []

    # WHEN: We get a string build from the entries it the list and a separator.
    string_result = create_separated_list(string_list)

    # THEN: We shoud have an emptry string.
    assert string_result == '', 'The string sould be empty.'


def test_create_separated_list_with_one_item():
    """
    Test the create_separated_list function with a list consisting of only one entry
    """
    # GIVEN: A list with a string.
    string_list = ['Author 1']

    # WHEN: We get a string build from the entries it the list and a separator.
    string_result = create_separated_list(string_list)

    # THEN: We should have "Author 1"
    assert string_result == 'Author 1', 'The string should be "Author 1".'


def test_create_separated_list_with_two_items():
    """
    Test the create_separated_list function with a list of two entries
    """
    # GIVEN: A list with two strings.
    string_list = ['Author 1', 'Author 2']

    # WHEN: We get a string build from the entries it the list and a seperator.
    string_result = create_separated_list(string_list)

    # THEN: We should have "Author 1 and Author 2"
    assert string_result == 'Author 1 and Author 2', 'The string should be "Author 1 and Author 2".'


def test_create_separated_list_with_three_items():
    """
    Test the create_separated_list function with a list of three items
    """
    # GIVEN: A list with three strings.
    string_list = ['Author 1', 'Author 2', 'Author 3']

    # WHEN: We get a string build from the entries it the list and a seperator.
    string_result = create_separated_list(string_list)

    # THEN: We should have "Author 1, Author 2 and Author 3"
    assert string_result == 'Author 1, Author 2 and Author 3', \
        'The string should be "Author 1, Author 2, and Author 3".'


def test_read_or_fail_fail():
    """
    Test the :func:`read_or_fail` function when attempting to read more data than the buffer contains.
    """
    # GIVEN: Some test data
    test_data = io.BytesIO(b'test data')

    # WHEN: Attempting to read past the end of the buffer
    # THEN: An OSError should be raised.
    with pytest.raises(OSError):
        read_or_fail(test_data, 15)


def test_read_or_fail_success():
    """
    Test the :func:`read_or_fail` function when reading data that is in the buffer.
    """
    # GIVEN: Some test data
    test_data = io.BytesIO(b'test data')

    # WHEN: Attempting to read data that should exist.
    result = read_or_fail(test_data, 4)

    # THEN: The data of the requested length should be returned
    assert result == b'test'


def test_read_int_u8_big():
    """
    Test the :func:`read_int` function when reading an unsigned 8-bit int using 'big' endianness.
    """
    # GIVEN: Some test data
    test_data = io.BytesIO(b'\x0f\xf0\x0f\xf0')

    # WHEN: Reading a an unsigned 8-bit int
    result = read_int(test_data, DataType.U8, 'big')

    # THEN: The an int should have been returned of the expected value
    assert result == 15


def test_read_int_u8_little():
    """
    Test the :func:`read_int` function when reading an unsigned 8-bit int using 'little' endianness.
    """
    # GIVEN: Some test data
    test_data = io.BytesIO(b'\x0f\xf0\x0f\xf0')

    # WHEN: Reading a an unsigned 8-bit int
    result = read_int(test_data, DataType.U8, 'little')

    # THEN: The an int should have been returned of the expected value
    assert result == 15


def test_read_int_u16_big():
    """
    Test the :func:`read_int` function when reading an unsigned 16-bit int using 'big' endianness.
    """
    # GIVEN: Some test data
    test_data = io.BytesIO(b'\x0f\xf0\x0f\xf0')

    # WHEN: Reading a an unsigned 16-bit int
    result = read_int(test_data, DataType.U16, 'big')

    # THEN: The an int should have been returned of the expected value
    assert result == 4080


def test_read_int_u16_little():
    """
    Test the :func:`read_int` function when reading an unsigned 16-bit int using 'little' endianness.
    """
    # GIVEN: Some test data
    test_data = io.BytesIO(b'\x0f\xf0\x0f\xf0')

    # WHEN: Reading a an unsigned 16-bit int
    result = read_int(test_data, DataType.U16, 'little')

    # THEN: The an int should have been returned of the expected value
    assert result == 61455


def test_read_int_u32_big():
    """
    Test the :func:`read_int` function when reading an unsigned 32-bit int using 'big' endianness.
    """
    # GIVEN: Some test data
    test_data = io.BytesIO(b'\x0f\xf0\x0f\xf0')

    # WHEN: Reading a an unsigned 32-bit int
    result = read_int(test_data, DataType.U32, 'big')

    # THEN: The an int should have been returned of the expected value
    assert result == 267390960


def test_read_int_u32_little():
    """
    Test the :func:`read_int` function when reading an unsigned 32-bit int using 'little' endianness.
    """
    # GIVEN: Some test data
    test_data = io.BytesIO(b'\x0f\xf0\x0f\xf0')

    # WHEN: Reading a an unsigned 32-bit int
    result = read_int(test_data, DataType.U32, 'little')

    # THEN: The an int should have been returned of the expected value
    assert result == 4027576335


def test_seek_or_fail_default_method():
    """
    Test the :func:`seek_or_fail` function when using the default value for the :arg:`how`
    """
    # GIVEN: A mocked_file_like_object
    mocked_file_like_object = MagicMock(**{'seek.return_value': 5, 'tell.return_value': 0})

    # WHEN: Calling seek_or_fail with out the how arg set
    seek_or_fail(mocked_file_like_object, 5)

    # THEN: seek should be called using the os.SEEK_SET constant
    mocked_file_like_object.seek.assert_called_once_with(5, os.SEEK_SET)


def test_seek_or_fail_os_end():
    """
    Test the :func:`seek_or_fail` function when called with an unsupported seek operation.
    """
    # GIVEN: A Mocked object
    # WHEN: Attempting to seek relative to the end
    # THEN: An NotImplementedError should have been raised
    with pytest.raises(NotImplementedError):
        seek_or_fail(MagicMock(), 1, os.SEEK_END)


def test_seek_or_fail_valid_seek_set():
    """
    Test that :func:`seek_or_fail` successfully seeks to the correct position.
    """
    # GIVEN: A mocked file-like object
    mocked_file_like_object = MagicMock(**{'tell.return_value': 3, 'seek.return_value': 5})

    # WHEN: Attempting to seek from the beginning
    result = seek_or_fail(mocked_file_like_object, 5, os.SEEK_SET)

    # THEN: The new position should be 5 from the beginning
    assert result == 5


def test_seek_or_fail_invalid_seek_set():
    """
    Test that :func:`seek_or_fail` raises an exception when seeking past the end.
    """
    # GIVEN: A Mocked file-like object
    mocked_file_like_object = MagicMock(**{'tell.return_value': 3, 'seek.return_value': 10})

    # WHEN: Attempting to seek from the beginning past the end
    # THEN: An OSError should have been raised
    with pytest.raises(OSError):
        seek_or_fail(mocked_file_like_object, 15, os.SEEK_SET)


def test_seek_or_fail_valid_seek_cur():
    """
    Test that :func:`seek_or_fail` successfully seeks to the correct position.
    """
    # GIVEN: A mocked file_like object
    mocked_file_like_object = MagicMock(**{'tell.return_value': 3, 'seek.return_value': 8})

    # WHEN: Attempting to seek from the current position
    result = seek_or_fail(mocked_file_like_object, 5, os.SEEK_CUR)

    # THEN: The new position should be 8 (5 from its starting position)
    assert result == 8


def test_seek_or_fail_invalid_seek_cur():
    """
    Test that :func:`seek_or_fail` raises an exception when seeking past the end.
    """
    # GIVEN: A mocked file_like object
    mocked_file_like_object = MagicMock(**{'tell.return_value': 3, 'seek.return_value': 10})

    # WHEN: Attempting to seek from the current position pas the end.
    # THEN: An OSError should have been raised
    with pytest.raises(OSError):
        seek_or_fail(mocked_file_like_object, 15, os.SEEK_CUR)
