# Copyright (C) 2018, 2019, 2020, 2021 The Meme Factory, Inc.
# http://www.karlpinc.com/

# This file is part of PGWUI_Common.
#
# This program is free software: you can redistribute it and/or
# modify it under the terms of the GNU Affero 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
# Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License along with this program.  If not, see
# <http://www.gnu.org/licenses/>.
#

# Karl O. Pinc <kop@karlpinc.com>

import pytest

import pgwui_common.exceptions as ex
import pgwui_common
from pgwui_common import check_settings
from pgwui_common import constants
from pgwui_develop import testing

# Mark all tests as unit tests
pytestmark = pytest.mark.unittest

mock_unknown_settings = testing.make_mock_fixture(
    pgwui_common.checkset, 'unknown_settings')


# key_to_ini()

def test_key_to_ini():
    '''The return value is as expected
    '''
    key = 'pgwui_example'
    result = check_settings.key_to_ini(key)

    assert result == 'pgwui:' + key


mock_key_to_ini = testing.make_mock_fixture(
    check_settings, 'key_to_ini')


# require_setting()

def test_require_setting_missing():
    '''Deliver exception when a required setting is missing'''
    errors = []
    check_settings.require_setting(errors, 'key', {}, lambda x: x)

    assert errors
    assert isinstance(errors[0], ex.MissingSettingError)


def test_require_setting_present():
    '''Does nothing when a required setting is present'''
    errors = []
    check_settings.require_setting(
        errors, 'key', {'key': 'value'}, lambda x: x)

    assert errors == []


mock_require_setting = testing.make_mock_fixture(
    check_settings, 'require_setting')


# boolean_setting()

def test_boolean_setting_missing():
    '''Does nothing when the setting is not in the settings'''
    errors = []
    check_settings.boolean_setting(errors, 'key', {})

    assert errors == []


def test_boolean_setting_true():
    '''Does nothing when the setting is "True"'''
    errors = []
    check_settings.boolean_setting(errors, 'key', {'key': 'True'})

    assert errors == []


def test_boolean_setting_false():
    '''Does nothing when the setting is "False"'''
    errors = []
    check_settings.boolean_setting(errors, 'key', {'key': 'False'})

    assert errors == []


def test_boolean_setting_notboolean():
    '''Deliver an exception when the setting does not evaluate to a boolean'''
    errors = []
    check_settings.boolean_setting(errors, 'key', {'key': '0'})

    assert errors
    assert isinstance(errors[0], ex.NotBooleanSettingError)


def test_boolean_setting_notparsable():
    '''Deliver an exception when the setting does not evaluate to a
    boolean because it is not parseable
    '''
    errors = []
    check_settings.boolean_setting(errors, 'key', {'key': 'a'})

    assert errors
    assert isinstance(errors[0], ex.NotBooleanSettingError)


mock_boolean_setting = testing.make_mock_fixture(
    check_settings, 'boolean_setting')


# validate_setting_values()

def test_validate_setting_values(mock_require_setting, mock_boolean_setting):
    '''Calls require_setting() and boolean_setting()'''

    check_settings.validate_setting_values([], {'pgwui': {}})

    assert mock_require_setting.called
    assert mock_boolean_setting.called


mock_validate_setting_values = testing.make_mock_fixture(
    check_settings, 'validate_setting_values')


# do_validate_hmac()

def test_do_validate_hmac_none():
    '''pgwui.validate_hmac defaults to True'''
    assert check_settings.do_validate_hmac({'pgwui': {}}) is True


def test_do_validate_hmac_True():
    '''Require hmac validation when pgwui.validate_hmac is True'''
    result = check_settings.do_validate_hmac(
        {'pgwui': {'validate_hmac': 'True'}})
    assert result is True


def test_do_validate_hmac_False():
    '''No hmac validation when pgwui.validate_hmac is False'''
    result = check_settings.do_validate_hmac(
        {'pgwui': {'validate_hmac': 'False'}})
    assert result is False


mock_do_validate_hmac = testing.make_mock_fixture(
    check_settings, 'do_validate_hmac')


# validate_hmac()

def test_validate_hmac_unvalidated(mock_do_validate_hmac):
    '''No error is returned when hmac validation is off'''
    mock_do_validate_hmac.return_value = False
    errors = []
    check_settings.validate_hmac(errors, {})

    assert errors == []


def test_validate_hmac_success(mock_do_validate_hmac):
    '''No error is returned when hmac is validated an the right length'''
    mock_do_validate_hmac.return_value = True
    errors = []
    check_settings.validate_hmac(
        errors, {'session.secret': 'x' * constants.HMAC_LEN})

    assert errors == []


def test_validate_hmac_missing(mock_do_validate_hmac):
    '''Deliver error when hmac is validated and missing'''
    mock_do_validate_hmac.return_value = True
    errors = []
    check_settings.validate_hmac(errors, {})

    assert errors
    assert isinstance(errors[0], ex.NoHMACError)


def test_validate_hmac_length(mock_do_validate_hmac):
    '''Deliver error when hmac is validated and the wrong length'''
    mock_do_validate_hmac.return_value = True
    errors = []
    check_settings.validate_hmac(errors, {'session.secret': ''})

    assert errors
    assert isinstance(errors[0], ex.HMACLengthError)


mock_validate_hmac = testing.make_mock_fixture(
    check_settings, 'validate_hmac')


# page_key_to_ini()

def test_page_key_to_ini(mock_key_to_ini):
    '''key_to_ini() is called, expected result returned
    '''
    mock_key_to_ini.return_value = 'foo'
    result = check_settings.page_key_to_ini(None, None)
    assert result == 'foo'


mock_page_key_to_ini = testing.make_mock_fixture(
    check_settings, 'page_key_to_ini')


# require_page_settings()

@pytest.mark.parametrize(
    ('required_settings', 'rs_results', 'expected'), [
        # Settings exist, return True
        (['s1', 's2'], [True, True], True),
        # One setting does not exist, return False
        (['s1', 's2'], [True, False], False)])
def test_require_page_settings_result(
        mock_page_key_to_ini, mock_require_setting,
        required_settings, rs_results, expected):
    '''Returns the expected result
    '''
    mock_require_setting.side_effect = rs_results
    result = check_settings.require_page_settings(
        None, required_settings, None, None)
    assert result == expected


def test_require_page_settings_subfunc(
        mock_page_key_to_ini, mock_require_setting):
    '''Calls page_key_to_ini() when function is passed to require_setting()
    '''
    def mock_rs(x, subkey, z, subkey_to_ini):
        subkey_to_ini(subkey)
        return True

    required_settings = ['s1', 's2']
    mock_require_setting.side_effect = mock_rs
    check_settings.require_page_settings(None, required_settings, None, None)

    assert mock_page_key_to_ini.call_count == len(required_settings)


mock_require_page_settings = testing.make_mock_fixture(
    check_settings, 'require_page_settings')


# validate_url_source()

@pytest.mark.parametrize(
    ('source', 'expected_error'), [
        ('/', None),
        ('/foo', None),
        ('//www.example.com', None),
        ('//www.example.com/', None),
        ('//www.example.com/foo', None),
        ('http://www.example.com', None),
        ('https://www.example.com', None),
        ('anything://www.example.com', None),
        ('http://www.example.com/', None),
        ('http://www.example.com/foo', None),
        # No domain
        ('//', ex.BadURLSourceError),
        # Nothing
        ('', ex.BadURLSourceError),
        # Missing / after scheme
        ('http:/www.example.com', ex.BadURLSourceError),
        # Extra / after scheme
        ('http:///www.example.com', ex.BadURLSourceError)])
def test_validate_url_source(mock_page_key_to_ini, source, expected_error):
    '''The test url produces the expected error, or no error as may be
    '''
    errors = []
    check_settings.validate_url_source(errors, None, source)

    if expected_error:
        assert len(errors) == 1
        assert isinstance(errors[0], expected_error)
    else:
        assert len(errors) == 0


mock_validate_url_source = testing.make_mock_fixture(
    check_settings, 'validate_url_source')


# validate_url_path()

@pytest.mark.parametrize(
    ('path',), [
        ('',),
        ('foo',)])
def test_validate_url_path_no_slash(mock_page_key_to_ini, path):
    '''When the path does not begin with a /,
    the right error is added to errors
    '''
    errors = []
    check_settings.validate_url_path(errors, 'ignored', {'url_path': path})

    assert len(errors) == 1
    assert isinstance(errors[0], ex.BadFileURLPathError)


@pytest.mark.parametrize(
    ('path',), [
        ('/',),
        ('/foo',)])
def test_validate_url_path_slash(mock_page_key_to_ini, path):
    '''When the path begins with a '/',  no error is added to errors
    '''
    errors = []
    check_settings.validate_url_path(errors, 'ignored', {'url_path': path})

    assert len(errors) == 0


mock_validate_url_path = testing.make_mock_fixture(
    check_settings, 'validate_url_path')


# validate_file_source()

@pytest.mark.parametrize(
    ('source',), [
        ('',),
        ('foo',)])
def test_validate_file_source_no_slash(mock_page_key_to_ini, source):
    '''When the source does not begin with a /,
    the right error is added to errors
    '''
    errors = []
    check_settings.validate_file_source(errors, 'ignored', source)

    assert len(errors) == 1
    assert isinstance(errors[0], ex.BadFileSourceError)


@pytest.mark.parametrize(
    ('source',), [
        ('/',),
        ('/foo',)])
def test_validate_file_source_slash(mock_page_key_to_ini, source):
    '''When the source begins with a '/',  no error is added to errors
    '''
    errors = []
    check_settings.validate_file_source(errors, 'ignored', source)

    assert len(errors) == 0


mock_validate_file_source = testing.make_mock_fixture(
    check_settings, 'validate_file_source')


# validate_route_source()

def test_validate_route_source_empty(mock_page_key_to_ini):
    '''When there is no source the right error is added to errors
    '''
    errors = []
    check_settings.validate_route_source(errors, 'ignored', '')

    assert len(errors) == 1
    assert isinstance(errors[0], ex.BadRouteSourceError)


def test_validate_route_source_not_empty(mock_page_key_to_ini):
    '''When there is a source no error is added to errors
    '''
    errors = []
    check_settings.validate_route_source(errors, 'ignored', 'something')

    assert len(errors) == 0


mock_validate_route_source = testing.make_mock_fixture(
    check_settings, 'validate_route_source')


# validate_asset_source()

def test_validate_asset_source_empty(mock_page_key_to_ini):
    '''When there is no source the right error is added to errors
    '''
    errors = []
    check_settings.validate_asset_source(errors, 'ignored', '')

    assert len(errors) == 1
    assert isinstance(errors[0], ex.BadAssetSourceError)


def test_validate_asset_source_not_empty(mock_page_key_to_ini):
    '''When there is a source no error is added to errors
    '''
    errors = []
    check_settings.validate_asset_source(errors, 'ignored', 'something')

    assert len(errors) == 0


mock_validate_asset_source = testing.make_mock_fixture(
    check_settings, 'validate_asset_source')


# validate_file_content()

@pytest.mark.parametrize(
    ('have_settings', 'vup_called'), [
        (True, 1),
        (False, 0)])
def test_validate_file_content(
        mock_validate_file_source, mock_require_page_settings,
        mock_validate_url_path,
        mock_unknown_settings, have_settings, vup_called):
    '''validate_file_source() is called, validate_url_path()
    is called when settings validate, the unknown_settings()
    return value is appended to the errors
    '''
    expected_errors = ['some error']
    mock_require_page_settings.return_value = have_settings
    mock_unknown_settings.return_value = expected_errors

    errors = []
    check_settings.validate_file_content(errors, None, None, None)

    mock_validate_file_source.assert_called_once()
    mock_require_page_settings.assert_called_once()
    assert mock_validate_url_path.call_count == vup_called
    assert errors == expected_errors


mock_validate_file_content = testing.make_mock_fixture(
    check_settings, 'validate_file_content')


# validate_type_content()

@pytest.mark.parametrize(
    ('page_settings',
     'vus_called',
     'vfc_called',
     'vrs_called',
     'vas_called',
     'pkti_called',
     'error_class'), [
         # URL type
         ({'type': 'URL',
           'source': 'ignored'},
          1, 0, 0, 0, 0,
          ex.UnknownSettingKeyError),
         # file type
         ({'type': 'file',
           'source': 'ignored'},
          0, 1, 0, 0, 0,
          ex.MissingSettingError),
         # route type
         ({'type': 'route',
           'source': 'ignored'},
          0, 0, 1, 0, 0,
          ex.UnknownSettingKeyError),
         # asset type
         ({'type': 'asset',
           'source': 'ignored'},
          0, 0, 0, 1, 0,
          ex.UnknownSettingKeyError),
         # a unknown type
         ({'type': 'unknown',
           'source': 'ignored'},
          0, 0, 0, 0, 1,
          ex.BadPageTypeError)])
def test_validate_type_content(
        mock_validate_url_source, mock_unknown_settings,
        mock_validate_file_content, mock_validate_route_source,
        mock_validate_asset_source, mock_page_key_to_ini,
        page_settings, vus_called, vfc_called,
        vrs_called, vas_called, pkti_called, error_class):
    '''The expected calls are make, the expected errors returned
    '''
    mock_validate_file_content.side_effect = (
        lambda errors, *args:
        errors.append(ex.MissingSettingError('ignored')))
    mock_unknown_settings.return_value = [ex.UnknownSettingKeyError(
        'ignored')]

    errors = []
    check_settings.validate_type_content(errors, 'some_page', page_settings)

    assert mock_validate_url_source.call_count == vus_called
    assert mock_validate_file_content.call_count == vfc_called
    assert mock_validate_asset_source.call_count == vas_called
    assert mock_validate_route_source.call_count == vrs_called
    assert len(errors) == 1
    assert isinstance(errors[0], error_class)


mock_validate_type_content = testing.make_mock_fixture(
    check_settings, 'validate_type_content')


# validate_page_setting()

def test_validate_page_setting_nopage(
        mock_require_page_settings, mock_validate_type_content):
    '''When the page does not have a setting, nothing is done
    '''
    errors = []
    settings = {'pgwui': {}}
    result = check_settings.validate_page_setting(
        errors, settings, 'test_page')

    assert errors == []
    assert result is None
    mock_require_page_settings.assert_not_called()
    mock_validate_type_content.assert_not_called()


def test_validate_page_setting_not_required(
        mock_require_page_settings, mock_validate_type_content):
    '''When require_page_settings() says something is missing, nothing is done
    '''
    errors = []
    settings = {'pgwui': {'test_page': 'ignored'}}
    mock_require_page_settings.return_value = False
    result = check_settings.validate_page_setting(
        errors, settings, 'test_page')

    assert errors == []
    assert result is None
    mock_require_page_settings.assert_called_once()
    mock_validate_type_content.assert_not_called()


def test_validate_page_setting_required(
        mock_require_page_settings, mock_validate_type_content):
    '''When require_page_settings() says nothing is missing,
    validate_type_content() is called
    '''
    errors = []
    settings = {'pgwui': {'test_page': 'ignored'}}
    mock_require_page_settings.return_value = True
    result = check_settings.validate_page_setting(
        errors, settings, 'test_page')

    assert errors == []
    assert result is None
    mock_require_page_settings.assert_called_once()
    mock_validate_type_content.assert_called_once()


mock_validate_page_setting = testing.make_mock_fixture(
    check_settings, 'validate_page_setting')


# validate_settings()

def test_validate_settings(
        mock_validate_setting_values, mock_validate_hmac,
        mock_validate_page_setting):
    '''The expected calls are made
    '''
    check_settings.validate_settings(None, None)

    mock_validate_setting_values.assert_called_once()
    mock_validate_hmac.assert_called_once()
    assert mock_validate_page_setting.call_count == 2
