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


import pytest
import argparse
import datetime
import logging
import psutil
import sys
import os
from unittest import mock
from psoft2ldap2 import conflib
from psoft2ldap2 import getcfg


#
# Unit tests
#

# identifier_of()

@pytest.mark.unittest
def test_identifier_of_none():
    '''Returns the empty string when the identifier argument is None
    '''
    assert getcfg.identifier_of({'identifier': None}) == ''


@pytest.mark.unittest
def test_identifier_of_not_none():
    '''The identifier is somewhere in the returned string when the
    identifier argument is not None
    '''
    id = 'someid'
    result = getcfg.identifier_of({'identifier': id})

    assert id in result


# get_parent_command()

@pytest.mark.unittest
def test_get_parent_command_success(monkeypatch):
    '''Returns basename of psutil.Process().name()
    '''
    test_command_name = 'somecommand.sh'
    test_process_name = f'/some/path/{test_command_name}'

    mock_process = mock.create_autospec(psutil.Process)()
    mock_process.name.return_value = test_process_name
    MockProcess = mock.create_autospec(psutil.Process)
    MockProcess.return_value = mock_process
    monkeypatch.setattr(psutil, 'Process', MockProcess)

    result = getcfg.get_parent_command()

    assert result == test_command_name


# find_conf_file()

@pytest.mark.unittest
def test_find_conf_file_path():
    '''When 'config' is in the args it's value is returned
    '''
    test_value = 'somename'
    parent_command = 'somecommand'

    result = getcfg.find_conf_file({'config': test_value}, parent_command)

    assert result == test_value


@pytest.mark.unittest
def test_find_conf_file_no_path():
    '''When 'path' is not in the args the result of get_parent_command()
    is integrated into the return value
    '''
    parent_command = 'somecommand'

    result = getcfg.find_conf_file({'config': None}, parent_command)

    assert result == (
        f'{os.path.join(getcfg.ETC, parent_command)}{getcfg.CONF_SUFFIX}')


# read_conf_file()

@pytest.mark.unittest
def test_read_conf_file_success(monkeypatch):
    '''conflib.dict_from_path() is called and the result returned
    '''
    test_confpath = '/foo/bar'
    test_dict = 'foo'

    mock_find_conf_file = mock.Mock(spec=getcfg.find_conf_file,
                                    return_value=test_confpath)
    monkeypatch.setattr(getcfg, 'find_conf_file', mock_find_conf_file)

    mock_dict_from_path = mock.Mock(
        return_value=test_dict, spec=conflib.dict_from_path)
    monkeypatch.setattr(conflib, 'dict_from_path', mock_dict_from_path)

    result = getcfg.read_conf_file(None, None, None)

    assert result == (test_confpath, test_dict)


# find_section_name()

@pytest.mark.unittest
def test_find_section_name_return_section_arg():
    '''Returns the section argument when there is one
    '''
    test_section = 'somesection'

    result = getcfg.find_section_name({'section': test_section}, None)

    assert result == test_section


@pytest.mark.unittest
def test_find_section_name_return_parent():
    '''Returns the "parent" argument when there's no "section" in args
    '''
    test_parent = 'someparent'

    result = getcfg.find_section_name({'section': None}, test_parent)

    assert result == test_parent


# get_section()

@pytest.mark.unittest
def test_get_section_success():
    '''Returns the requested section of the config
    '''
    test_key = 'some key'
    test_value = 'some sample value'
    result = getcfg.get_section(None, {test_key: test_value}, test_key)

    assert result == test_value


@pytest.mark.unittest
def test_get_sname_error():
    '''Raises a MissingSection exception when the requested section
    does not exist
    '''
    sample_config = {'a': 1, 'b': 2}

    with pytest.raises(getcfg.MissingSection):
        getcfg.get_section(None, sample_config, 'not there')


# parse_identifier()

@pytest.mark.unittest
def test_parse_identifier():
    '''The identifier is split on the colon character
    '''
    result = getcfg.parse_identifier('key1:key2:key3')
    assert list(result) == ['key1', 'key2', 'key3']


# value_from_section()

@pytest.fixture
def mock_parse_identifier(monkeypatch):
    mocked = mock.Mock(spec=getcfg.parse_identifier)
    monkeypatch.setattr(getcfg, 'parse_identifier', mocked)

    return mocked


@pytest.mark.unittest
def test_value_from_section_no_identifier():
    '''When there is no identifer in the arguments return the supplied
    section'''
    test_section = 'sample section'

    result = getcfg.value_from_section(
        {'identifier': None}, None, None, test_section)

    assert result == test_section


@pytest.mark.unittest
def test_value_from_section_exists(mock_parse_identifier):
    '''When there is an identifier and the identified key exists
    the identifier parser is called and the identified section is returned
    '''
    test_identifier = 'some id'
    test_section = {'key1': 'value1',
                    'key2': {'subkey1': 'subvalue1',
                             'subkey2': 'subvalue2'}}

    mock_parse_identifier.return_value = ['key2', 'subkey1']
    result = getcfg.value_from_section(
        {'identifier': test_identifier}, None, None, test_section)

    mock_parse_identifier.assert_called_with(test_identifier)
    assert result == 'subvalue1'


@pytest.mark.unittest
def test_value_from_section_not_exists(mock_parse_identifier):
    '''When there is an identifier and the identified key does not exist
    a MissingIdentifier exception is raised
    '''
    test_section = {'key1': 'value1',
                    'key2': {'subkey1': 'subvalue1',
                             'subkey2': 'subvalue2'}}

    mock_parse_identifier.return_value = ['key2', 'subkey5']
    with pytest.raises(getcfg.MissingIdentifier):
        getcfg.value_from_section(
            {'identifier': 'unused'}, None, None, test_section)


# get_value()

@pytest.fixture
def parent_command_result(monkeypatch):
    '''Mock get_parent_command and return the test command name
    '''
    test_command = 'parentcommand'

    mock_get_parent_command = mock.Mock(
        return_value=test_command, spec=getcfg.get_parent_command)
    monkeypatch.setattr(getcfg, 'get_parent_command', mock_get_parent_command)

    return test_command


@pytest.fixture
def read_conf_file_result(monkeypatch):
    '''Mock read_conf_file and return its test result
    '''
    test_confpath = '/some/path'
    test_config = {}
    test_result = (test_confpath, test_config)

    mock_read_conf_file = mock.Mock(spec=getcfg.read_conf_file,
                                    return_value=test_result)
    monkeypatch.setattr(getcfg, 'read_conf_file', mock_read_conf_file)

    return test_result


@pytest.fixture
def find_section_name_result(monkeypatch):
    '''Mock find_section_name() and return its test result
    '''
    test_sname = 'testkey'

    mock_find_section_name = mock.Mock(spec=getcfg.find_section_name,
                                       return_value=test_sname)
    monkeypatch.setattr(getcfg, 'find_section_name', mock_find_section_name)

    return test_sname


@pytest.fixture
def value_from_section_result(monkeypatch):
    '''Mock value_from_section and return its result
    '''
    test_value = 'value_from_section_result'

    mock_value_from_section = mock.Mock(spec=getcfg.value_from_section,
                                        return_value=test_value)
    monkeypatch.setattr(
        getcfg, 'value_from_section', mock_value_from_section)

    return test_value


@pytest.mark.unittest
def test_get_value_success(parent_command_result,
                           read_conf_file_result,
                           find_section_name_result,
                           value_from_section_result,
                           monkeypatch):
    '''Result of value_from_section() is returned
    '''
    test_section = {'somekey': 'somevalue'}

    mock_get_section = mock.Mock(
        spec=getcfg.get_section,
        return_value=test_section)
    monkeypatch.setattr(getcfg, 'get_section', mock_get_section)

    result = getcfg.get_value(None, None)

    (test_confpath, test_conf) = read_conf_file_result
    test_sname = find_section_name_result
    test_value = value_from_section_result
    assert result == (test_confpath, test_sname, test_value)


@pytest.fixture
def w_value_from_section_exception(monkeypatch):
    '''Pretend the identifer can't be found; have calls to
    value_from_section raise a MissingIdentifier exception
    '''
    mock_value_from_section = mock.Mock(spec=getcfg.value_from_section,
                                        side_effect=getcfg.MissingIdentifier())
    monkeypatch.setattr(getcfg, 'value_from_section', mock_value_from_section)


@pytest.fixture
def get_section_result(monkeypatch):
    '''A get_section() that does nothing
    '''
    test_section = {'somekey': 'someval'}

    mock_get_section = mock.Mock(
        spec=getcfg.get_section,
        return_value=test_section)
    monkeypatch.setattr(getcfg, 'get_section', mock_get_section)

    return test_section


@pytest.fixture
def w_get_section_exception(monkeypatch):
    '''Pretend the section cannot be found; have calls to
    get_section raise a MissingSection exception
    '''
    mock_get_section = mock.Mock(
        spec=getcfg.get_section,
        side_effect=getcfg.MissingSection())
    monkeypatch.setattr(getcfg, 'get_section', mock_get_section)


@pytest.mark.unittest
def test_get_value_w_default_w_section(parent_command_result,
                                       read_conf_file_result,
                                       find_section_name_result,
                                       w_value_from_section_exception,
                                       get_section_result):
    '''When a default is supplied and the requested section can be found
    but the reqested identifer can't be found, the value of the
    default is used.
    '''
    test_value = 'sample value'

    result = getcfg.get_value(None, {'default': test_value})

    (test_confpath, test_conf) = read_conf_file_result
    test_sname = find_section_name_result
    assert result == (test_confpath, test_sname, test_value)


@pytest.mark.unittest
def test_get_value_wo_default_w_section(parent_command_result,
                                        read_conf_file_result,
                                        find_section_name_result,
                                        w_value_from_section_exception,
                                        get_section_result):
    '''When no default is supplied and the requested section is found but
    the requested identifier can't be found, a MissingIdentifier is raised
    '''
    logger = logging.getLogger()
    with pytest.raises(getcfg.MissingIdentifier):
        getcfg.get_value(logger, {'default': None})


@pytest.mark.unittest
def test_get_value_w_default_wo_section_wo_id(parent_command_result,
                                              read_conf_file_result,
                                              find_section_name_result,
                                              value_from_section_result,
                                              w_get_section_exception):
    '''When a default is supplied and the requested section cannot be found
    and no identifer is given, the value of the default is used.
    '''
    test_value = 'sample value'

    logger = logging.getLogger()
    result = getcfg.get_value(logger, {'identifier': None,
                                       'default': test_value})

    (test_confpath, test_conf) = read_conf_file_result
    test_sname = find_section_name_result
    assert result == (test_confpath, test_sname, test_value)


@pytest.mark.unittest
def test_get_value_wo_default_w_section_w_id(parent_command_result,
                                             read_conf_file_result,
                                             find_section_name_result,
                                             value_from_section_result,
                                             w_get_section_exception):
    '''When no default is supplied and the requested section cannot
    be found and an identifier is given, a MissingSection is raised
    '''
    logger = logging.getLogger()
    with pytest.raises(getcfg.MissingSection):
        getcfg.get_value(logger, {'identifier': 'someid', 'default': None})


@pytest.mark.unittest
def test_get_value_w_default_w_section_w_id(parent_command_result,
                                            read_conf_file_result,
                                            find_section_name_result,
                                            value_from_section_result,
                                            w_get_section_exception):
    '''When a default is supplied and the requested section cannot
    be found and an identifier is given, a MissingSection is raised
    '''
    logger = logging.getLogger()
    with pytest.raises(getcfg.MissingSection):
        getcfg.get_value(logger, {'identifier': 'someid',
                                  'default': 'defaultvalue'})


# format_float()

@pytest.mark.unittest
def test_format_float_real():
    '''A floating point number is its string representation
    '''
    assert getcfg.format_float(123.456) == '123.456'


@pytest.mark.unittest
def test_format_float_trailing0():
    '''A floating point number does not have it's trailing ".0" stripped
    '''
    assert getcfg.format_float(1.0)[-2:] == '.0'


@pytest.mark.unittest
def test_format_float_leading0():
    '''A floating point number does not have its leading "0." stripped
    '''
    assert getcfg.format_float(0.1)[0:2] == '0.'


@pytest.mark.unittest
def test_format_float_not_exp():
    '''A large floating point number that can be represented with
    precision given the size of float, is not represented
    in scientific notation
    '''
    assert 'e' not in getcfg.format_float(10.0 ** (sys.float_info.dig - 1))


@pytest.mark.unittest
def test_format_float_exp():
    '''A large floating point number that can not be represented with
    precision given the size of float, is represented
    in scientific notation
    '''
    assert 'e' in getcfg.format_float(10.0 ** sys.float_info.dig)


@pytest.mark.unittest
def test_format_float_4digits():
    '''A float with less than 4 zeros immediately to the right of the decimal
    will not be formatted in scientific notation
    '''
    assert 'e' not in getcfg.format_float(0.0004)


@pytest.mark.unittest
def test_format_float_5digits():
    '''A float with 4 or more zeros immediately to the right of the decimal
    will be formatted in scientific notation
    '''
    assert 'e' in getcfg.format_float(0.00005)


@pytest.mark.unittest
def test_format_float_nan():
    '''Formatting "nan" returns "NAN"
    '''
    assert getcfg.format_float(float('nan')) == 'NAN'


@pytest.mark.unittest
def test_format_float_plus_inf():
    '''Formatting positive infinity returns "INF"
    '''
    assert getcfg.format_float(float('Infinity')) == 'INF'


@pytest.mark.unittest
def test_format_float_minus_inf():
    '''Formatting negative infinity returns "-INF"
    '''
    assert getcfg.format_float(float('-Infinity')) == '-INF'


# format_int()

@pytest.mark.unittest
def test_format_int():
    '''Formatting a number returns its string representation
    '''
    assert getcfg.format_int(12345) == '12345'


# format_bool()

@pytest.mark.unittest
def test_format_bool_true():
    '''Formatting True returns "true"
    '''
    assert getcfg.format_bool(True) == 'true'


@pytest.mark.unittest
def test_format_bool_false():
    '''Formatting False returns "false"
    '''
    assert getcfg.format_bool(False) == 'false'


# format_datetime()

@pytest.mark.unittest
def test_format_datetime_wo_tz():
    '''Datetimes without timezones are always in UTC
    '''
    input = datetime.datetime(1997, 7, 16, 19, 20, 30, 450000)

    result = getcfg.format_datetime(input)

    assert result == '1997-07-16T19:20:30.450000+00:00'


@pytest.mark.unittest
def test_format_datetime_w_tz():
    '''Datetimes with timezones have the time zone preserved
    '''
    input = datetime.datetime(1997, 7, 16, 19, 20, 30, 450000,
                              datetime.timezone(datetime.timedelta(hours=-2)))

    result = getcfg.format_datetime(input)

    assert result == '1997-07-16T19:20:30.450000-02:00'


# format_none()

@pytest.mark.unittest
def test_format_none():
    '''Always returns "null"
    '''
    assert getcfg.format_none() == 'null'


# format_scalar()


@pytest.fixture
def mock_identifier_of(monkeypatch):
    '''Mock identifier_of()
    '''
    mock_identifier_of = mock.Mock(spec=getcfg.identifier_of)
    monkeypatch.setattr(getcfg, 'identifier_of', mock_identifier_of)

    return mock_identifier_of


@pytest.fixture
def make_formatter(monkeypatch):
    '''Return a function which makes a mock formatter as
    those used by format_scalar()
    '''
    def run(name):
        mock_formatter = mock.Mock(name=name)
        monkeypatch.setattr(getcfg, name, mock_formatter)
        return mock_formatter

    return run


@pytest.fixture
def mock_formatters():
    '''Return a function which mocks all the formatters and returns them
    in a list
    '''
    def run(make_formatter, formatter_name, format_result):
        formatters = {}
        for name in ['format_float',
                     'format_int',
                     'format_bool',
                     'format_datetime',
                     'format_none']:
            formatter = make_formatter(name)
            if name == formatter_name:
                formatter.return_value = format_result
            formatters[name] = formatter

        return formatters

    return run


@pytest.mark.unittest
@pytest.mark.parametrize(
    ('value', 'formatter_name', 'format_result'),
    [(12345.67890, 'format_float', '12345.67890'),
     (12345, 'format_int', '12345'),
     (True, 'format_bool', 'true'),
     (datetime.datetime(1997, 7, 16, 19, 20, 30, 450000,
                        datetime.timezone(datetime.timedelta(hours=1))),
      'format_datetime',
      '1997-07-16T19:20:30.450000+01:00'),
     (None, 'format_none', 'none')])
def test_format_scalars_w_formatters(value,
                                     formatter_name,
                                     format_result,
                                     make_formatter,
                                     mock_formatters,
                                     mock_identifier_of):
    '''Exactly one of the formatters is called and the format_result returned
    '''
    formatters = mock_formatters(
        make_formatter, formatter_name, format_result)

    result = getcfg.format_scalar({}, None, None, value)

    assert result == format_result
    assert sum([formatter.call_count
                for formatter
                in formatters.values()]) == 1


@pytest.mark.unittest
def test_format_scalar_str(make_formatter,
                           mock_formatters,
                           mock_identifier_of):
    '''None the formatters is called and the format_result returned
    '''
    test_string = 'string_value'

    formatters = mock_formatters(
        make_formatter, None, test_string)

    result = getcfg.format_scalar({}, None, None, test_string)

    assert result == test_string
    assert sum([formatter.call_count
                for formatter
                in formatters.values()]) == 0


@pytest.mark.unittest
@pytest.mark.parametrize(
    ('value'),
    [(dict()),
     (list()),
     (set())])
def test_format_scalar(value,
                       make_formatter,
                       mock_formatters,
                       mock_identifier_of):
    '''None of the formatters is called and a UnknownYAMLData exception raised
    '''
    formatters = mock_formatters(
        make_formatter, None, None)

    with pytest.raises(getcfg.UnknownYAMLData):
        getcfg.format_scalar({}, None, None, value)

        assert sum([formatter.call_count for formatter in formatters]) == 0


# is_yaml_mapping()

@pytest.mark.unittest
@pytest.mark.parametrize(
    ('type', 'result'),
    [(str(), False),
     (float(), False),
     (int(), False),
     (bool(), False),
     (datetime.datetime.now(), False),
     (None, False),
     (dict(), True),
     (list(), False),
     (set(), False)])
def test_is_yaml_mapping(type, result):
    '''Does the given type produce the expected result?
    '''
    assert getcfg.is_yaml_mapping(type) is result


# is_yaml_listlike()

@pytest.mark.unittest
@pytest.mark.parametrize(
    ('type', 'result'),
    [(str(), False),
     (float(), False),
     (int(), False),
     (bool(), False),
     (datetime.datetime.now(), False),
     (None, False),
     (dict(), False),
     (list(), True),
     (set(), True)])
def test_is_yaml_listlike(type, result):
    '''Does the given type produce the expected result?
    '''
    assert getcfg.is_yaml_listlike(type) is result


@pytest.fixture
def is_yaml_mapping_true(monkeypatch):
    '''Have is_yaml_mapping return True
    '''
    mock_is_yaml_mapping = mock.Mock(spec=getcfg.is_yaml_mapping,
                                     return_value=True)
    monkeypatch.setattr(getcfg, 'is_yaml_mapping', mock_is_yaml_mapping)


@pytest.fixture
def is_yaml_mapping_false(monkeypatch):
    '''Have is_yaml_mapping return False
    '''
    mock_is_yaml_mapping = mock.Mock(spec=getcfg.is_yaml_mapping,
                                     return_value=False)
    monkeypatch.setattr(getcfg, 'is_yaml_mapping', mock_is_yaml_mapping)


@pytest.fixture
def is_yaml_listlike_true(monkeypatch):
    '''Have is_yaml_listlike return True
    '''
    mock_is_yaml_listlike = mock.Mock(spec=getcfg.is_yaml_listlike,
                                      return_value=True)
    monkeypatch.setattr(getcfg, 'is_yaml_listlike', mock_is_yaml_listlike)


@pytest.fixture
def is_yaml_listlike_false(monkeypatch):
    '''Have is_yaml_listlike return False
    '''
    mock_is_yaml_listlike = mock.Mock(spec=getcfg.is_yaml_listlike,
                                      return_value=False)
    monkeypatch.setattr(getcfg, 'is_yaml_listlike', mock_is_yaml_listlike)


# output_scalar()

@pytest.fixture
def mock_format_scalar(monkeypatch):
    '''Mock format_scalar()
    '''
    mock_format_scalar = mock.Mock(spec=getcfg.format_scalar)
    monkeypatch.setattr(getcfg, 'format_scalar', mock_format_scalar)

    return mock_format_scalar


@pytest.mark.unittest
def test_output_scalar_maplike(is_yaml_mapping_true,
                               is_yaml_listlike_false,
                               mock_identifier_of,
                               mock_format_scalar):
    '''When passed a maplike value raises MappingReturnedInSequence
    '''
    with pytest.raises(getcfg.MappingReturnedInSequence):
        getcfg.output_scalar(None, None, None, None)


@pytest.mark.unittest
def test_output_scalar_listlike(is_yaml_mapping_false,
                                is_yaml_listlike_true,
                                mock_identifier_of,
                                mock_format_scalar):
    '''When passed a maplike value raises SequenceReturnedInSequence
    '''
    with pytest.raises(getcfg.SequenceReturnedInSequence):
        getcfg.output_scalar(None, None, None, None)


@pytest.mark.unittest
def test_output_scalar_success(is_yaml_mapping_false,
                               is_yaml_listlike_false,
                               mock_identifier_of,
                               mock_format_scalar,
                               capsys):
    '''When passed a scalar value the format_scalar() return value is
    printed and called with the args output_scalar() was called with.
    '''
    args = ('args', 'sname', 'confpath', 'value')
    format_scalar_returns = "value's printable representation"

    mock_format_scalar.return_value = format_scalar_returns

    getcfg.output_scalar(*args)

    assert mock_format_scalar.call_args[0] == args

    (out, err) = capsys.readouterr()
    assert out == f'{format_scalar_returns}\n'
    assert err == ''


# output_conf_value()

@pytest.fixture
def mock_output_scalar(monkeypatch):
    '''Mock output scalar
    '''
    mock_output_scalar = mock.Mock(spec=getcfg.output_scalar)
    monkeypatch.setattr(getcfg, 'output_scalar', mock_output_scalar)

    return mock_output_scalar


@pytest.fixture
def mock_get_value(monkeypatch):
    '''Mock get_value()
    '''
    default_result = (None, None, None)

    mock_get_value = mock.Mock(spec=getcfg.get_value,
                               return_value=default_result)
    monkeypatch.setattr(getcfg, 'get_value', mock_get_value)

    return mock_get_value


@pytest.mark.unittest
def test_output_conf_value_mapping(mock_get_value,
                                   mock_identifier_of,
                                   is_yaml_mapping_true,
                                   is_yaml_listlike_false,
                                   mock_output_scalar):
    '''When passed a mapping a MappingReturned exception is raised
    and output_scalar() is not called
    '''
    with pytest.raises(getcfg.MappingReturned):
        getcfg.output_conf_value(None, {})

    mock_output_scalar.assert_not_called()


@pytest.mark.unittest
def test_output_conf_value_listlike(mock_get_value,
                                    mock_identifier_of,
                                    is_yaml_mapping_false,
                                    is_yaml_listlike_true,
                                    mock_output_scalar):
    '''When passed a listlike value output_scalar() is called
    for every element of the list
    '''
    get_value_value = ['one', 2, 'three']
    get_value_result = (None, None, get_value_value)
    mock_get_value.return_value = get_value_result

    getcfg.output_conf_value(None, {})

    value_args = [argtup[0][3] for argtup in mock_output_scalar.call_args_list]
    assert value_args == get_value_value


@pytest.mark.unittest
def test_output_conf_value_not_collection(mock_get_value,
                                          mock_identifier_of,
                                          is_yaml_mapping_false,
                                          is_yaml_listlike_false,
                                          mock_output_scalar):
    '''When passed a not mapping and not listlike value output_scalar()
    is called with the value
    '''
    get_value_value = 'somevalue'
    get_value_result = (None, None, get_value_value)
    mock_get_value.return_value = get_value_result

    getcfg.output_conf_value(None, {})

    mock_output_scalar.assert_called_once()
    # assert mock_output_scalar.call_args.args[3] == get_value_result
    assert mock_output_scalar.call_args[0][3] == get_value_value


# deliver_result()

@pytest.mark.unittest
def test_deliver_result_success(monkeypatch, caplog):
    '''Calls output_conf_value(), does not log
    '''
    mock_output_conf_value = mock.Mock(spec=getcfg.output_conf_value)
    monkeypatch.setattr(getcfg, 'output_conf_value', mock_output_conf_value)

    logger = logging.getLogger()
    getcfg.deliver_result(logger, {})

    mock_output_conf_value.assert_called()

    logs = caplog.record_tuples
    assert len(logs) == 0


@pytest.mark.unittest
def test_deliver_result_exit_logs(monkeypatch, caplog):
    '''When output_conf_value() raises an exception a critical error
    is logged and the program exits with a non-zero exit code
    '''
    mock_output_conf_value = mock.Mock(spec=getcfg.output_conf_value,
                                       side_effect=getcfg.GetcfgException())
    monkeypatch.setattr(getcfg, 'output_conf_value', mock_output_conf_value)

    logger = logging.getLogger()
    with pytest.raises(SystemExit) as exinfo:
        getcfg.deliver_result(logger, {})

    assert exinfo.value.code != 0

    logs = caplog.record_tuples
    assert len(logs) > 0
    assert logging.CRITICAL in [log[1] for log in logs]


# parse_args()

@pytest.mark.unittest
def test_parse_args(monkeypatch):
    '''Calls the argparse parser, returns its result
    '''
    class TestClass:
        pass

    mock_parser = mock.create_autospec(argparse.ArgumentParser())
    mock_parser.parse_args.return_value = TestClass
    MockArgumentParser = mock.create_autospec(argparse.ArgumentParser)
    MockArgumentParser.return_value = mock_parser
    monkeypatch.setattr(argparse, 'ArgumentParser', MockArgumentParser)

    result = getcfg.parse_args([])

    mock_parser.parse_args.assert_called()
    assert result == vars(TestClass)


# main()

@pytest.mark.unittest
def test_main_exit0(monkeypatch):
    '''Exits with a 0 status code on success, parses arguments
    '''
    # Technically, we should also mock sys.argv.
    mock_parse_args = mock.Mock()
    monkeypatch.setattr(getcfg, 'parse_args', mock_parse_args)
    mock_deliver_result = mock.Mock()
    monkeypatch.setattr(getcfg, 'deliver_result', mock_deliver_result)

    with pytest.raises(SystemExit) as exinfo:
        getcfg.main()

    assert exinfo.value.code == 0
    mock_parse_args.assert_called_once()


#
# Integration tests
#

#
# WARNING:
# You do not want to be reading ETC when doing integration tests!
# Be sure to use the disable_etc fixture on all integration tests.
#

@pytest.fixture
def disable_etc(monkeypatch):
    '''Make sure that /etc (or whereever the default conf file
    is located) is not read
    '''
    mock_build_default_confpath = mock.Mock(
        spec=getcfg.build_default_confpath, return_value='')
    monkeypatch.setattr(
        getcfg, 'build_default_confpath', mock_build_default_confpath)


@pytest.mark.integrationtest
def test_main_integration(disable_etc, monkeypatch):
    '''Exits with a return code of 0
    '''
    monkeypatch.setattr(sys, 'argv',
                        ['commandname',
                         '--config', 'examples/psoft2ldap2.conf',
                         '--section', 'logger',
                         'version'])

    with pytest.raises(SystemExit) as exinfo:
        getcfg.main()

    assert exinfo.value.code == 0


@pytest.mark.integrationtest
def test_return_version(disable_etc, monkeypatch, capsys):
    '''Get the logger version from the example config file
    '''
    monkeypatch.setattr(sys, 'argv',
                        ['commandname',
                         '--config', 'examples/psoft2ldap2.conf',
                         '--section', 'logger',
                         'version'])

    with pytest.raises(SystemExit) as exinfo:
        getcfg.main()

    assert exinfo.value.code == 0

    (out, err) = capsys.readouterr()
    assert out == '1\n'
    assert err == ''


@pytest.mark.integrationtest
def test_fail_when_no_section(disable_etc, monkeypatch, caplog):
    '''When the requested section does not exist and there's no default,
    exit with a non-zero status code and a critical error message
    '''
    monkeypatch.setattr(sys, 'argv',
                        ['commandname',
                         '--config', 'examples/psoft2ldap2.conf',
                         '--section', 'omitted_section'])

    with pytest.raises(SystemExit) as exinfo:
        getcfg.main()

    assert exinfo.value.code != 0

    logs = caplog.record_tuples
    assert len(logs) > 0
    assert logging.CRITICAL in [log[1] for log in logs]


@pytest.mark.integrationtest
def test_fail_when_no_section_w_default_and_identifier(
        disable_etc, monkeypatch, caplog):
    '''When the requested section does not exist and there is a default
    and an identifier is also supplied,
    exit with a non-zero status code and a critical error message
    '''
    default_value = 'default value'

    monkeypatch.setattr(sys, 'argv',
                        ['commandname',
                         '--config', 'examples/psoft2ldap2.conf',
                         '--section', 'omitted_section',
                         '--default', default_value,
                         'identifer'])

    with pytest.raises(SystemExit) as exinfo:
        getcfg.main()

    assert exinfo.value.code != 0

    logs = caplog.record_tuples
    assert len(logs) > 0
    assert logging.CRITICAL in [log[1] for log in logs]


@pytest.mark.integrationtest
def test_default_used_when_no_section(disable_etc, monkeypatch, capsys):
    '''When the requested section does not exist but a default
    is supplied and no identifier is supplied, the default is used
    '''
    default_value = 'default value'

    monkeypatch.setattr(sys, 'argv',
                        ['commandname',
                         '--config', 'examples/psoft2ldap2.conf',
                         '--section', 'omitted_section',
                         '--default', default_value])

    with pytest.raises(SystemExit) as exinfo:
        getcfg.main()

    assert exinfo.value.code == 0

    (out, err) = capsys.readouterr()
    assert out == f'{default_value}\n'
    assert err == ''


@pytest.mark.integrationtest
def test_default_used_when_missing_identifier(
        disable_etc, monkeypatch, capsys):
    '''When the requested identifier does not exist but a default
    is supplied and no identifier is supplied, the default is used
    '''
    default_value = 'default value'

    monkeypatch.setattr(sys, 'argv',
                        ['commandname',
                         '--config', 'examples/psoft2ldap2.conf',
                         '--section', 'logger',
                         '--default', default_value,
                         'missing_key'])

    with pytest.raises(SystemExit) as exinfo:
        getcfg.main()

    assert exinfo.value.code == 0

    (out, err) = capsys.readouterr()
    assert out == f'{default_value}\n'
    assert err == ''
