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

# This file is part of PGWUI_Server.
#
# 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 copy
import logging
import pytest
import pyramid.exceptions
import sys

import pgwui_common.assets
import pgwui_common.exceptions as common_ex
import pgwui_common.check_settings
import pgwui_common.plugin
import pgwui_common.routes
import pgwui_common.urls
# Use as a regular module, not a plugin, so lint checks work
from pgwui_develop import testing

import pgwui_server.pgwui_server as pgwui_server
import pgwui_server.exceptions as server_ex

# Activiate the PGWUI pytest plugin
pytest_plugins = ("pgwui",)

# Mark all tests with "unittest"
pytestmark = pytest.mark.unittest


MockConfigurator = testing.make_magicmock_fixture(
    pgwui_server, 'Configurator')

mock_override_assets = testing.make_mock_fixture(
    pgwui_common.assets, 'override_assets')

mock_find_pgwui_components = testing.make_mock_fixture(
    pgwui_common.plugin, 'find_pgwui_components')
mock_find_pgwui_check_settings = testing.make_mock_fixture(
    pgwui_common.plugin, 'find_pgwui_check_settings')

mock_validate_settings = testing.make_mock_fixture(
    pgwui_common.check_settings, 'validate_settings')

mock_add_routes = testing.make_mock_fixture(
    pgwui_common.routes, 'add_routes')

mock_set_urls = testing.make_mock_fixture(
    pgwui_common.urls, 'set_urls')


# Unit tests

# dot_to_multiline_setting()

def test_dot_to_multiline_setting_new(mock_parse_assignments):
    '''Adds a new dict and puts the settings in it
    '''
    comp_settings = {'foo': 'foo', 'bar': 'bar'}
    component = 'pgwui_component'
    key = 'pgwui.' + component
    settings = {'pgwui': {},
                key: comp_settings}
    expected = {'pgwui': {component: comp_settings}}

    mock_parse_assignments.return_value = comp_settings
    pgwui_server.dot_to_multiline_setting(settings, key, component)

    assert settings == expected


def test_dot_to_multiline_setting_old(mock_parse_assignments):
    '''Extends an existing dict in the settings
    '''
    comp_settings = {'foo': 'foo', 'bar': 'bar'}
    component = 'pgwui_component'
    key = 'pgwui.' + component
    settings = {'pgwui': {component: {'foo': 'bar', 'baz': 'baz'}},
                key: comp_settings}
    expected = {'pgwui':
                {component: {'foo': 'foo', 'bar': 'bar', 'baz': 'baz'}}}

    mock_parse_assignments.return_value = comp_settings
    pgwui_server.dot_to_multiline_setting(settings, key, component)

    assert settings == expected


def test_dot_to_multiline_setting_bad(mock_parse_assignments):
    '''When the value is bad we get the expected error
    '''
    component = 'pgwui_component'
    key = 'pgwui.' + component
    settings = {'pgwui': {},
                key: 'ignored'}

    mock_parse_assignments.side_effect = server_ex.MissingEqualError('text')
    with pytest.raises(server_ex.MissingEqualError):
        pgwui_server.dot_to_multiline_setting(settings, key, component)

    assert True


mock_dot_to_multiline_setting = testing.make_mock_fixture(
    pgwui_server, 'dot_to_multiline_setting')


# component_setting_into_dict()

def test_component_setting_into_dict_no_checker(
        mock_dot_to_multiline_setting):
    '''When there's no checker nothing is done
    '''
    errors = []

    pgwui_server.component_setting_into_dict(
        errors, {}, 'pgwui.pgwui_component', None, 'pgwui_component')

    assert errors == []


def test_component_setting_into_dict_checker(
        mock_dot_to_multiline_setting):
    '''When there's a checker its result is appended to the errors
    '''
    errors = ['someerror']
    new_errors = ['new1', 'new2']
    expected = copy.deepcopy(errors)
    expected.extend(new_errors)

    pgwui_server.component_setting_into_dict(
        errors, {'pgwui_component': lambda settings: new_errors},
        'pgwui.pgwui_component', {'pgwui': {'pgwui_component': {}}},
        'pgwui_component')

    assert errors == expected


def test_component_setting_into_dict_nosettings(
        mock_dot_to_multiline_setting):
    '''When there's no settings due to a syntax error the right error
    is appended to the errors
    '''
    errors = []
    mock_dot_to_multiline_setting.side_effect = server_ex.MissingEqualError(0)

    pgwui_server.component_setting_into_dict(
        errors, {}, 'pgwui.pgwui_component', None, 'pgwui_component')

    assert len(errors) == 1
    assert isinstance(errors[0], server_ex.BadValueError)


mock_component_setting_into_dict = testing.make_mock_fixture(
    pgwui_server, 'component_setting_into_dict')


# dot_to_dict()

def test_dot_to_dict():
    '''Removes pgwui.* settings, replaces them with a dict entry
    '''
    settings = {'foo': 1,
                'pgwui': {},
                'pgwui.abc': 'abc',
                'pgwui.def': 'def'}
    expected = {'foo': 1,
                'pgwui': {'abc': 'abc'},
                'pgwui.def': 'def'}

    pgwui_server.dot_to_dict(settings, 'pgwui.abc', 'abc')

    assert settings == expected


mock_dot_to_dict = testing.make_mock_fixture(
    pgwui_server, 'dot_to_dict')


# parse_multiline_assigments()


def test_parse_multiline_assignments_str():
    '''Appends key/value string tuples and when there's no "=",
    and more than just whitespace, a list is the result
    '''
    lines = ('key1 = value1\n'  # whitespace around = is ignored
             '\n'
             'second\n'
             'third\n'
             'key2=value2\n'    # missing whitespace is fine
             'key3= value3=withequals\n'
             )
    result = []
    pgwui_server.parse_multiline_assignments(lines, result)
    assert result == [('key1', ['value1', 'second', 'third']),
                      ('key2', 'value2'),
                      ('key3', 'value3=withequals')]


def test_parse_multiline_assignments_no_equal():
    '''When the line contains no equal sign the right exception is raised
    '''
    with pytest.raises(server_ex.MissingEqualError):
        pgwui_server.parse_multiline_assignments('noequal\n', [])


mock_parse_multiline_assignments = testing.make_mock_fixture(
    pgwui_server, 'parse_multiline_assignments')


# parse_assignments()

def test_parse_assignments_str(mock_parse_multiline_assignments):
    '''Calls parse_multiline_assignments'''
    lines = ('key1 = value1\n'  # whitespace around = is ignored
             '\n'
             'ignored\n'
             'key2=value2\n'    # missing whitespace is fine
             'key3= value3=withequals\n'
             )
    pgwui_server.parse_assignments(lines)
    mock_parse_multiline_assignments.assert_called_once()


def test_parse_assignments_dict(mock_parse_multiline_assignments):
    '''Returns key value tuples.
    '''
    lines = {'key1': 'value1',
             'key2': 'value2',
             }
    result = pgwui_server.parse_assignments(lines)
    assert set(result) == set([('key1', 'value1'),
                               ('key2', 'value2'),
                               ])


mock_parse_assignments = testing.make_mock_fixture(
    pgwui_server, 'parse_assignments')


# setting_into_dict()

def test_setting_into_dict_unknown(
        mock_component_setting_into_dict,
        mock_dot_to_dict,
        mock_dot_to_multiline_setting):
    '''No new errors when there's a non-pgwui setting'''
    errors = []
    pgwui_server.setting_into_dict(errors, [], {}, 'foo', {})

    assert errors == []


def test_setting_into_dict_bad(
        mock_parse_assignments,
        mock_component_setting_into_dict,
        mock_dot_to_dict,
        mock_dot_to_multiline_setting):
    '''Delivers an error on a bad pgwui setting'''
    errors = []

    pgwui_server.setting_into_dict(
        errors, [], {}, 'pgwui.foo', {})

    assert errors
    assert isinstance(errors[0], common_ex.UnknownSettingKeyError)


def test_setting_into_dict_good(
        mock_component_setting_into_dict,
        mock_dot_to_dict,
        mock_dot_to_multiline_setting):
    '''Calls dot_to_dict when a known pgwui setting is supplied
    '''
    errors = []

    pgwui_server.setting_into_dict(
        errors, [], {}, 'pgwui.pg_host', {})

    mock_dot_to_dict.assert_called_once()
    mock_dot_to_multiline_setting.assert_not_called()
    assert errors == []


def test_setting_into_dict_multiline(
        mock_component_setting_into_dict,
        mock_dot_to_dict,
        mock_dot_to_multiline_setting):
    '''Calls dot_to_multiline_setting when a known pgwui multi-line
    setting is supplied
    '''
    errors = []

    pgwui_server.setting_into_dict(
        errors, [], {}, 'pgwui.home_page', {})

    mock_dot_to_dict.assert_not_called()
    mock_dot_to_multiline_setting.assert_called_once()
    assert errors == []


def test_setting_into_dict_plugin_component(
        mock_component_setting_into_dict,
        mock_dot_to_dict,
        mock_dot_to_multiline_setting):
    '''When a setting is for a component the setting is parsed and
    moved into a dict
    '''
    key = 'pgwui.pgwui_component'
    settings = {key: None}
    errors = []
    mock_parse_assignments.return_value = {}

    pgwui_server.setting_into_dict(
        errors, ['pgwui_component'], {}, key, settings)

    mock_component_setting_into_dict.assert_called_once()
    assert errors == []


def test_setting_into_dict_bad_assignment(
        mock_parse_assignments,
        mock_component_setting_into_dict,
        mock_dot_to_dict,
        mock_dot_to_multiline_setting):
    '''Delivers an error on a setting that has no "="
    '''
    errors = []
    mock_dot_to_multiline_setting.side_effect = server_ex.MissingEqualError(0)

    pgwui_server.setting_into_dict(
        errors, [], {}, 'pgwui.home_page', {})

    mock_component_setting_into_dict.assert_not_called()
    mock_dot_to_dict.assert_not_called()
    mock_dot_to_multiline_setting.assert_called_once()

    assert len(errors) == 1
    assert isinstance(errors[0], server_ex.BadValueError)


mock_setting_into_dict = testing.make_mock_fixture(
    pgwui_server, 'setting_into_dict')


# dictify_settings()

def test_dictify_settings(mock_find_pgwui_check_settings,
                          mock_setting_into_dict):
    '''Calls setting_into_dict() for each key in setting,
    with the proper list of plugin components
    '''
    settings = {'key1': 'value1',
                'key2': 'value2'}
    components = ['pgwui_server']

    errors = []
    pgwui_server.dictify_settings(errors, settings, components)

    assert mock_setting_into_dict.call_count == len(settings)
    assert mock_setting_into_dict.call_args[0][1] == components


mock_dictify_settings = testing.make_mock_fixture(
    pgwui_server, 'dictify_settings')


# exit_reporting_errors()

@pytest.fixture
def assert_exit1():
    def run():

        exit1_called = False

        def mock_exit(status):
            nonlocal exit1_called
            exit1_called = status == 1

        return mock_exit

        assert exit1_called

    return run


def test_exit_reporting_errors_logged(
        assert_exit1, monkeypatch, caplog, capsys):
    '''All errors are logged at ERROR, and a extra one at CRITICAL
    '''
    monkeypatch.setattr(sys, 'exit', assert_exit1())
    caplog.set_level(logging.INFO)
    errors = ['one', 'two', 'three']
    pgwui_server.exit_reporting_errors(errors)

    logs = caplog.record_tuples

    assert len(logs) == 4

    levels = [log[1] for log in logs]
    for level in levels[:-1]:
        assert level == logging.ERROR
    assert levels[-1] == logging.CRITICAL


def test_exit_reporting_errors_printed(
        assert_exit1, monkeypatch, capsys):
    '''First and last (the extra) errors are printed on stderr
    '''
    monkeypatch.setattr(sys, 'exit', assert_exit1())
    errors = ['one', 'two', 'three']
    pgwui_server.exit_reporting_errors(errors)

    (out, err) = capsys.readouterr()
    errlines = err.split('\n')[:-1]

    assert out == ''
    assert len(errlines) == 2
    assert errlines[0] == 'one'
    assert errlines[1] != 'two'
    assert errlines[1] != 'three'


mock_exit_reporting_errors = testing.make_mock_fixture(
    pgwui_server, 'exit_reporting_errors')


# add_default_settings()

def test_add_default_settings():
    '''The default settings are added
    '''
    settings = dict()
    pgwui_server.add_default_settings(settings)

    assert settings == pgwui_server.DEFAULT_SETTINGS


mock_add_default_settings = testing.make_mock_fixture(
    pgwui_server, 'add_default_settings')


# exit_on_invalid_settings()

def test_exit_on_invalid_settings_invalid(
        monkeypatch,
        mock_add_default_settings, mock_dictify_settings,
        mock_validate_settings, mock_exit_reporting_errors):
    '''Calls dictify_settings(), validate_settings(), and then
    exit_reporting_errors() when setting is invalid
    '''
    def mymock(errors, settings, components):
        errors.append('error1')

    mock_dictify_settings.side_effect = mymock
    mock_exit_reporting_errors.side_effect = lambda *args: sys.exit(1)

    with pytest.raises(SystemExit) as excinfo:
        pgwui_server.exit_on_invalid_settings({}, [])

        assert excinfo[1].code == 1

    mock_dictify_settings.assert_called_once()
    mock_validate_settings.assert_called_once()
    mock_add_default_settings.assert_called_once()
    assert mock_exit_reporting_errors.called


def test_exit_on_invalid_settings_valid(
        mock_add_default_settings, mock_dictify_settings,
        mock_validate_settings, mock_exit_reporting_errors):
    '''Returns, without exiting, when all settings are valid
    '''
    pgwui_server.exit_on_invalid_settings({}, [])

    assert mock_exit_reporting_errors.call_count == 0


mock_exit_on_invalid_settings = testing.make_mock_fixture(
    pgwui_server, 'exit_on_invalid_settings')


# autoconfigurable_components()

def test_autoconfiguable_components_no_autoconfig():
    '''When the settings have no pgwui.autoconfigure return an empty list
    '''
    test_components = ['some', 'components']

    result = pgwui_server.autoconfigurable_components(
        {'pgwui': {'autoconfigure': False}}, test_components)

    assert result == []


def test_autoconfigurable_components_log_info(caplog):
    '''When pyramid.include is in the settings an INFO message is logged
    '''
    caplog.set_level(logging.INFO)

    pgwui_server.autoconfigurable_components(
        {'pgwui': {'autoconfigure': True},
         'pyramid.include': None},
        [])

    logs = caplog.record_tuples

    assert len(logs) == 1

    level = logs[0][1]
    assert level == logging.INFO


def test_autoconfigurable_components_components_returned():
    '''The suppiled components are returned when autoconfigure is True
    '''
    test_components = ['some', 'components']

    result = pgwui_server.autoconfigurable_components(
        {'pgwui': {'pgwui.autoconfigure': True}}, test_components)

    assert result == test_components


mock_autoconfigurable_components = testing.make_mock_fixture(
    pgwui_server, 'autoconfigurable_components')


# in_development()

@pytest.mark.parametrize(
    ('settings', 'expected'), [
        ({},
         False),
        ({'pyramid.includes': 'unrecognized'},
         False),
        ({'pyramid.includes': '\nunrecognized'},
         False),
        ({'pyramid.includes': ['unrecognized']},
         False),
        ({'pyramid.includes': '\nunrecognized\npyramid_debugtoolbar'},
         True),
        ({'pyramid.includes': 'unrecognized pyramid_debugtoolbar'},
         True),
        ({'pyramid.includes': 'pyramid_debugtoolbar'},
         True),
        ({'pyramid.includes': '\npyramid_debugtoolbar'},
         True),
        ({'pyramid.includes': ['unrecognized', 'pyramid_debugtoolbar']},
         True)])
def test_in_development(caplog, settings, expected):
    '''Do the settings yield the expected result?
    '''
    caplog.set_level(logging.DEBUG)
    result = pgwui_server.in_development(settings)

    assert result == expected
    logs = caplog.record_tuples
    assert len(logs) == 1
    assert logs[0][1] == logging.DEBUG


mock_in_development = testing.make_mock_fixture(
    pgwui_server, 'in_development')


# apply_component_defaults()

def test_apply_component_defaults(caplog,
                                  mock_in_development,
                                  MockConfigurator,
                                  mock_autoconfigurable_components,
                                  mock_add_routes,
                                  mock_override_assets):
    '''A configurator is returned, a debug log entry is made for
    each autoconfigurable component
    '''
    expected_errors = 'some errors'
    caplog.set_level(logging.DEBUG)

    mock_autoconfigurable_components.return_value = \
        ['pgwui_mock_component_name']
    mock_override_assets.return_value = expected_errors

    (config, errors) = pgwui_server.apply_component_defaults(
        {'pgwui': {}}, [])

    assert str(type(config)) == str(type(MockConfigurator))
    assert errors == expected_errors

    logs = caplog.record_tuples

    assert len(logs) == 2  # One for the single autoconfig, one for finishing

    for log in logs:
        level = log[1]
        assert level == logging.DEBUG


mock_apply_component_defaults = testing.make_mock_fixture(
    pgwui_server, 'apply_component_defaults')


mock_add_urls_setting = testing.make_mock_fixture(
    pgwui_common.urls, 'add_urls_setting')


# pgwui_server_config()

@pytest.mark.parametrize(
    ('errors', 'ere_calls'), [
        ([], 0),
        (['some error'], 1)])
def test_pgwui_server_config_success(
        pyramid_config,
        mock_find_pgwui_components,
        mock_exit_on_invalid_settings,
        mock_apply_component_defaults,
        mock_add_urls_setting,
        mock_exit_reporting_errors,
        errors, ere_calls):
    '''When apply_component_defaults() returns -- makes expected calls,
    returns a configuration
    '''
    mock_apply_component_defaults.return_value = (pyramid_config, [])
    mock_add_urls_setting.return_value = errors
    mock_exit_reporting_errors.side_effect = lambda *args: sys.exit(1)

    if ere_calls:
        with pytest.raises(SystemExit):
            result = pgwui_server.pgwui_server_config({})
    else:
        result = pgwui_server.pgwui_server_config({})

    mock_find_pgwui_components.assert_called_once()
    mock_exit_on_invalid_settings.assert_called_once()
    mock_apply_component_defaults.assert_called_once()
    mock_add_urls_setting.assert_called_once()
    assert mock_exit_reporting_errors.call_count == ere_calls
    if not errors:
        assert result == pyramid_config


def test_pgwui_server_config_bad_config(
        pyramid_config,
        mock_find_pgwui_components,
        mock_exit_on_invalid_settings,
        mock_apply_component_defaults,
        mock_add_urls_setting,
        mock_exit_reporting_errors):
    '''When apply_component_defaults() raises a configuration exception
    exit_reporting_errors() is called
    '''
    mock_apply_component_defaults.side_effect = \
        pyramid.exceptions.ConfigurationError
    mock_exit_reporting_errors.side_effect = lambda *args: sys.exit(1)

    with pytest.raises(SystemExit):
        pgwui_server.pgwui_server_config({})

    assert mock_exit_reporting_errors.call_count == 1


mock_pgwui_server_config = testing.make_mock_fixture(
    pgwui_server, 'pgwui_server_config')


# main()
def test_main(MockConfigurator, mock_pgwui_server_config):
    '''Returns a wsgi app'''
    config = MockConfigurator()
    mock_pgwui_server_config.return_value = config
    config.make_wsgi_app.return_value = 'wsgi_app'
    result = pgwui_server.main({})

    assert result == 'wsgi_app'
