# Copyright (C) 2017 The Meme Factory, Inc.  http://www.meme.com/
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

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

import os
import pytest

from enforcer import exceptions as ex
from enforcer import fstree
from enforcer import get_rules
from enforcer import startup

import libtest


# Initialization
BAD_RULES_CONFIG_PATH = 'tests/data/no_such_config.ini'
INVALID_RULES_CONFIG_PATH = 'tests/data/bad_config.ini'


#
# Class tests
#

# ErrorManager
def test_errormanager_express_stderr(capsys):
    '''Does the error manager send to stdout?'''
    em = startup.ErrorManager()
    em.express(ex.Error(
        'samp error', 'samp descr', 'samp detail', 'samp hint'))
    (out, err) = capsys.readouterr()
    assert err != ''


def test_errormanager_exit_if_errored_exit():
    '''Does the error manager exit early on error?'''
    em = startup.ErrorManager()
    em.express(ex.Error('sample error'))
    with pytest.raises(SystemExit):
        em.exit_if_errored()


def test_errormanager_exit_if_errored_noexit():
    '''Does the error manager continue on warning?'''
    em = startup.ErrorManager()
    em.express(ex.Warning('sample warning'))
    em.exit_if_errored()
    assert True


#
# Function tests
#

# Helper functions

def mock_require_config(cfg, unused_keys, cfile, key, hint=None):
    startup.check_missing_config_declaration(cfg, cfile, key, hint)


def monkeypatch_errormanager(monkeypatch):
    # Keep from breaking global state
    em = startup.ErrorManager()
    monkeypatch.setattr(startup, 'em', em)


#
# Test functions
#

# check_missing_config_declaration()

def test_check_missing_config_declaration():
    '''Missing required config directives raise an exception'''
    with pytest.raises(ex.MissingConfigDeclarationError):
        startup.check_missing_config_declaration(
            {'foo': 'value'}, 'sample.ini', 'missing', None)


# require_config()

def test_require_config_fail():
    '''Missing config directives result in errors'''
    startup.require_config(
        {'foo': 'value'}, ['missing'], 'sample.ini', 'missing')
    with pytest.raises(SystemExit):
        startup.em.exit_if_errored()


def test_require_config_success():
    '''Existing config directives raise no error'''
    startup.require_config({'foo': 'value'}, ['foo'], 'sample.ini', 'foo')
    startup.em.exit_if_errored()
    assert True


# check_bad_max_depth()

def test_check_bad_max_depth_no_int():
    '''Non-integral max_depth config values raise an exception'''
    with pytest.raises(ex.BadMaxDepthError):
        startup.check_bad_max_depth('foo', 'sample.ini')


def test_check_bad_max_depth_toosmall():
    '''max_depth config values below 1 raise an exception'''
    with pytest.raises(ex.BadMaxDepthError):
        startup.check_bad_max_depth('0', 'sample.ini')


def test_check_bad_max_depth():
    '''String max_depth values that are integers convert to int'''
    assert startup.check_bad_max_depth('3', 'sample.ini') == 3


# convert_max_depth()

def test_convert_max_depth_fail():
    '''Bad max_depth config values result in an error'''
    startup.convert_max_depth('foo', 'sample.ini')
    with pytest.raises(SystemExit):
        startup.em.exit_if_errored()


def test_convert_max_depth_succeed():
    '''String max_depth values that are integers convert to int'''
    startup.em.exit_if_errored()
    assert startup.convert_max_depth('3', 'sample.ini') == 3


# warn_extra_declaration()

def test_warn_extra_declaration_no_warn():
    '''When the declaration is used, no warning'''
    startup.warn_extra_declaration([], 'somecfg')
    assert True


def test_warn_extra_declaration_warn():
    '''When the declaration is unused a warning is raised'''
    with pytest.raises(ex.UnusedDeclarationWarning):
        startup.warn_extra_declaration(['somecfg'], 'somecfg')


# report_extra_declaration()

def test_report_extra_declaration_no_warn(capsys):
    '''When there's no warning there's no output'''
    startup.report_extra_declaration([], 'somecfg')
    (out, err) = capsys.readouterr()
    assert out == ''
    assert err == ''


def test_report_extra_declaration_warn(capsys):
    '''When there's a warning there's output on stderr'''
    startup.report_extra_declaration(['somecfg'], 'somecfg')
    (out, err) = capsys.readouterr()
    assert out == ''
    assert err


# raise_unknown_declaration()

def test_raise_unknown_declaration():
    '''Raises an error'''
    with pytest.raises(ex.UnknownDeclarationError):
        startup.raise_unknown_declaration('unk_decl')


# report_unknown_declaration()

def test_report_unknown_declaration(capsys, monkeypatch):
    '''When there's an error there's output'''
    monkeypatch_errormanager(monkeypatch)

    startup.report_unknown_declaration('somecfg')
    (out, err) = capsys.readouterr()
    assert out == ''
    assert err


# report_extra_declarations()

def test_report_extra_declarations_no_extras(capsys):
    '''When there are no extra declarations nothing gets reported'''
    startup.report_extra_declarations([], get_rules.FILE_SOURCE)
    (out, err) = capsys.readouterr()
    assert out == ''
    assert err == ''


def test_report_extra_declarations_file_creds(capsys, monkeypatch):
    '''google_credentials declaration reports error when rules are in a file'''
    monkeypatch_errormanager(monkeypatch)

    startup.report_extra_declarations(
        ['google_credentials'], get_rules.FILE_SOURCE)
    (out, err) = capsys.readouterr()
    assert out == ''
    assert err


def test_report_extra_declarations_file_name(capsys, monkeypatch):
    '''spreadsheet_name declaration reports error when rules are in a file'''
    monkeypatch_errormanager(monkeypatch)

    startup.report_extra_declarations(
        ['spreadsheet_name'], get_rules.FILE_SOURCE)
    (out, err) = capsys.readouterr()
    assert out == ''
    assert err


def test_report_extra_declarations_google(capsys, monkeypatch):
    '''File related declarations report errors when rules are at google'''
    monkeypatch_errormanager(monkeypatch)

    startup.report_extra_declarations(['rules_path'], get_rules.GOOGLE_SOURCE)
    (out, err) = capsys.readouterr()
    assert out == ''
    assert err


def test_report_extra_declarations_unknown(capsys, monkeypatch):
    '''There are errors when there are extra declarations'''
    monkeypatch_errormanager(monkeypatch)

    startup.report_extra_declarations(
        ['unknown1', 'unknown2'], get_rules.FILE_SOURCE)
    (out, err) = capsys.readouterr()
    assert out == ''
    assert err


def test_report_extra_declarations_multi(monkeypatch):
    '''Multiple errors are reported when there are multiple extra
    declarations'''
    # In python 3 we could use "nonlocal" instead of making a namespace
    class Namespace():
        pass

    ns = Namespace()
    ns.count = 0

    def mock_raise_unknown_declaration(key):
        ns.count += 1
    monkeypatch.setattr(
        startup, 'raise_unknown_declaration', mock_raise_unknown_declaration)

    startup.report_extra_declarations(
        ['unknown1', 'unknown2'], get_rules.FILE_SOURCE)
    assert ns.count == 2


# optional_key()

def test_optional_key():
    '''Unused keys are removed from key list'''
    keys = ['key1']
    startup.optional_key(keys, 'key1')
    assert len(keys) == 0


# read_config()

def test_read_config_existance():
    '''Non-existant config files raise an exception'''
    with pytest.raises(ex.NoConfigFileError):
        startup.read_config('no_such.ini')


def test_read_config_succeed():
    '''Config declartions can be read'''
    config = startup.read_config('sample_config.ini')
    assert config['rules_path'] == '../enforcer/my_conventions.csv'


def test_read_config_bad_file():
    '''We get an exception when the configparser library has a problem
    with the config file'''
    with pytest.raises(ex.ConfigParserError):
        startup.read_config(INVALID_RULES_CONFIG_PATH)


def test_read_config_no_enforcer_section():
    '''Config file must contain an [enforcer] section'''
    with pytest.raises(ex.NoEnforcerSectionError):
        startup.read_config(os.devnull)


# optional_config_set()

def test_optional_config_set_key():
    '''The expected value is the config value when the key exists'''
    config = {}
    startup.optional_config_set(config, {'key': 'abc\ndef\nghi'}, 'key')
    assert config['key'] == set(['abc', 'def', 'ghi'])


def test_optional_config_set_no_key():
    '''An empty set is the config value when the key does not exist'''
    config = {}
    startup.optional_config_set(config, {}, 'key')
    assert len(config['key']) == 0


# parse_config()

def test_parse_config_errors(capsys):
    '''A bad config file returns multiple errors'''
    try:
        startup.parse_config('bad_config.ini', {})
    except SystemExit:
        (out, err) = capsys.readouterr()
        assert libtest.multiple_errors(err)
    else:
        assert False


def test_parse_config_no_google_credentials(monkeypatch):
    '''A config that gets the sheet from google but has no credentials fails'''
    monkeypatch.setattr(startup, 'require_config', mock_require_config)
    with pytest.raises(ex.MissingConfigDeclarationError):
        startup.parse_config('bad_config.ini',
                             {'rules_source': get_rules.GOOGLE_SOURCE,
                              'spreadsheet_name': 'anything',
                              'begin_marker': 'anything',
                              'max_depth': '3'})


def test_parse_config_no_spreadsheet_name(monkeypatch):
    '''A config that gets the sheet from google but has no name fails'''
    monkeypatch.setattr(startup, 'require_config', mock_require_config)
    with pytest.raises(ex.MissingConfigDeclarationError):
        startup.parse_config('bad_config.ini',
                             {'rules_source': get_rules.GOOGLE_SOURCE,
                              'google_credentials': 'somefile',
                              'begin_marker': 'anything',
                              'max_depth': '3'})


def test_parse_config_no_rules_path(monkeypatch):
    '''A config that gets the sheet from the fs but has no path fails'''
    monkeypatch.setattr(startup, 'require_config', mock_require_config)
    with pytest.raises(ex.MissingConfigDeclarationError):
        startup.parse_config('bad_config.ini',
                             {'rules_source': get_rules.FILE_SOURCE,
                              'begin_marker': 'anything',
                              'max_depth': '3'})


def test_parse_config_bad_rules_source():
    '''A config with a bad rules_source value fails'''
    with pytest.raises(ex.BadRulesSourceError):
        startup.parse_config('bad_config.ini',
                             {'rules_source': 'random value',
                              'begin_marker': 'anything',
                              'max_depth': '3'})


def test_parse_config_google():
    '''The google related config values are correct'''
    secrets = 'some secret'
    name = 'some name'
    cfg = startup.parse_config('simple_config.ini',
                               {'rules_source': get_rules.GOOGLE_SOURCE,
                                'google_credentials': secrets,
                                'spreadsheet_name': name,
                                'begin_marker': 'anything',
                                'max_depth': '3'})
    assert cfg['google_credentials'] == secrets
    assert cfg['spreadsheet_name'] == name


def test_parse_config_file():
    '''The rules_path config value is correct'''
    filename = 'some file'
    cfg = startup.parse_config('simple_config.ini',
                               {'rules_source': get_rules.FILE_SOURCE,
                                'rules_path': filename,
                                'begin_marker': 'anything',
                                'max_depth': '3'})
    assert cfg['rules_path'] == filename


def test_parse_config_ignored_names():
    '''The ignored_names config value is correct'''
    filename = 'some file'
    cfg = startup.parse_config('simple_config.ini',
                               {'rules_source': get_rules.FILE_SOURCE,
                                'rules_path': filename,
                                'begin_marker': 'anything',
                                'max_depth': '3',
                                'ignored_names': 'abc\ndef\nghi'})
    assert cfg['ignored_names'] == set(['abc', 'def', 'ghi'])


def test_parse_config_stock_parens():
    '''The stock_parens config value is correct'''
    filename = 'some file'
    cfg = startup.parse_config('simple_config.ini',
                               {'rules_source': get_rules.FILE_SOURCE,
                                'rules_path': filename,
                                'begin_marker': 'anything',
                                'max_depth': '3',
                                'stock_parens': 'abc\ndef\nghi'})
    assert cfg['stock_parens'] == set(['abc', 'def', 'ghi'])


# make_analyzer()

def test_make_analyzer_retval():
    '''Does make_analyzer() return something reasonable?'''
    (config, analyzer) = startup.make_analyzer(libtest.TEST_CONFIG_PATH)
    assert isinstance(analyzer, fstree.FSDir)
    assert isinstance(config, dict)


def test_make_analyzer_on_error():
    '''Does an error (non-existant config) result in SystemExit?'''
    with pytest.raises(SystemExit):
        startup.make_analyzer(BAD_RULES_CONFIG_PATH)
