import pytest

from ansible.playbook.play_context import PlayContext

from openshift_health_check import ActionModule, resolve_checks
from openshift_health_check import copy_remote_file_to_dir, write_result_to_output_dir, write_to_output_file
from openshift_checks import OpenShiftCheckException, FileToSave


def fake_check(name='fake_check', tags=None, is_active=True, run_return=None, run_exception=None,
               run_logs=None, run_files=None, changed=False, get_var_return=None):
    """Returns a new class that is compatible with OpenShiftCheck for testing."""

    _name, _tags = name, tags

    class FakeCheck(object):
        name = _name
        tags = _tags or []

        def __init__(self, **_):
            self.changed = False
            self.failures = []
            self.logs = run_logs or []
            self.files_to_save = run_files or []

        def is_active(self):
            if isinstance(is_active, Exception):
                raise is_active
            return is_active

        def run(self):
            self.changed = changed
            if run_exception is not None:
                raise run_exception
            return run_return

        def get_var(*args, **_):
            return get_var_return

        def register_failure(self, exc):
            self.failures.append(OpenShiftCheckException(str(exc)))
            return

    return FakeCheck


# Fixtures


@pytest.fixture
def plugin():
    task = FakeTask('openshift_health_check', {'checks': ['fake_check']})
    plugin = ActionModule(task, None, PlayContext(), None, None, None)
    return plugin


class FakeTask(object):
    def __init__(self, action, args):
        self.action = action
        self.args = args
        self.async = 0


@pytest.fixture
def task_vars():
    return dict(openshift=dict(), ansible_host='unit-test-host')


# Assertion helpers


def failed(result, msg_has=None):
    if msg_has is not None:
        assert 'msg' in result
        for term in msg_has:
            assert term.lower() in result['msg'].lower()
    return result.get('failed', False)


def changed(result):
    return result.get('changed', False)


# tests whether task is skipped, not individual checks
def skipped(result):
    return result.get('skipped', False)


# Tests


@pytest.mark.parametrize('task_vars', [
    None,
    {},
])
def test_action_plugin_missing_openshift_facts(plugin, task_vars, monkeypatch):
    monkeypatch.setattr('openshift_health_check.resolve_checks', lambda *args: ['fake_check'])
    result = plugin.run(tmp=None, task_vars=task_vars)

    assert failed(result, msg_has=['openshift_facts'])


def test_action_plugin_cannot_load_checks_with_the_same_name(plugin, task_vars, monkeypatch):
    FakeCheck1 = fake_check('duplicate_name')
    FakeCheck2 = fake_check('duplicate_name')
    checks = [FakeCheck1, FakeCheck2]
    monkeypatch.setattr('openshift_checks.OpenShiftCheck.subclasses', classmethod(lambda cls: checks))

    result = plugin.run(tmp=None, task_vars=task_vars)

    assert failed(result, msg_has=['duplicate', 'duplicate_name', 'FakeCheck'])


@pytest.mark.parametrize('is_active, skipped_reason', [
    (False, "Not active for this host"),
    (Exception("borked"), "exception"),
])
def test_action_plugin_skip_non_active_checks(is_active, skipped_reason, plugin, task_vars, monkeypatch):
    checks = [fake_check(is_active=is_active)]
    monkeypatch.setattr('openshift_checks.OpenShiftCheck.subclasses', classmethod(lambda cls: checks))

    result = plugin.run(tmp=None, task_vars=task_vars)

    assert result['checks']['fake_check'].get('skipped')
    assert skipped_reason in result['checks']['fake_check'].get('skipped_reason')
    assert not failed(result)
    assert not changed(result)
    assert not skipped(result)


@pytest.mark.parametrize('to_disable', [
    'fake_check',
    ['fake_check', 'spam'],
    '*,spam,eggs',
])
def test_action_plugin_skip_disabled_checks(to_disable, plugin, task_vars, monkeypatch):
    checks = [fake_check('fake_check', is_active=True)]
    monkeypatch.setattr('openshift_checks.OpenShiftCheck.subclasses', classmethod(lambda cls: checks))

    task_vars['openshift_disable_check'] = to_disable
    result = plugin.run(tmp=None, task_vars=task_vars)

    assert result['checks']['fake_check'] == dict(skipped=True, skipped_reason="Disabled by user request")
    assert not failed(result)
    assert not changed(result)
    assert not skipped(result)


def test_action_plugin_run_list_checks(monkeypatch):
    task = FakeTask('openshift_health_check', {'checks': []})
    plugin = ActionModule(task, None, PlayContext(), None, None, None)
    monkeypatch.setattr(plugin, 'load_known_checks', lambda *_: {})
    result = plugin.run()

    assert failed(result, msg_has="Available checks")
    assert not changed(result)
    assert not skipped(result)


def test_action_plugin_run_check_ok(plugin, task_vars, monkeypatch):
    check_return_value = {'ok': 'test'}
    check_class = fake_check(run_return=check_return_value, run_files=[None])
    monkeypatch.setattr(plugin, 'load_known_checks', lambda *_: {'fake_check': check_class()})
    monkeypatch.setattr('openshift_health_check.resolve_checks', lambda *args: ['fake_check'])

    result = plugin.run(tmp=None, task_vars=task_vars)

    assert result['checks']['fake_check'] == check_return_value
    assert not failed(result)
    assert not changed(result)
    assert not skipped(result)


def test_action_plugin_run_check_changed(plugin, task_vars, monkeypatch):
    check_return_value = {'ok': 'test'}
    check_class = fake_check(run_return=check_return_value, changed=True)
    monkeypatch.setattr(plugin, 'load_known_checks', lambda *_: {'fake_check': check_class()})
    monkeypatch.setattr('openshift_health_check.resolve_checks', lambda *args: ['fake_check'])

    result = plugin.run(tmp=None, task_vars=task_vars)

    assert result['checks']['fake_check'] == check_return_value
    assert changed(result['checks']['fake_check'])
    assert not failed(result)
    assert changed(result)
    assert not skipped(result)


def test_action_plugin_run_check_fail(plugin, task_vars, monkeypatch):
    check_return_value = {'failed': True, 'msg': 'this is a failure'}
    check_class = fake_check(run_return=check_return_value)
    monkeypatch.setattr(plugin, 'load_known_checks', lambda *_: {'fake_check': check_class()})
    monkeypatch.setattr('openshift_health_check.resolve_checks', lambda *args: ['fake_check'])

    result = plugin.run(tmp=None, task_vars=task_vars)

    assert result['checks']['fake_check'] == check_return_value
    assert failed(result, msg_has=['failed'])
    assert not changed(result)
    assert not skipped(result)


@pytest.mark.parametrize('exc_class, expect_traceback', [
    (OpenShiftCheckException, False),
    (Exception, True),
])
def test_action_plugin_run_check_exception(plugin, task_vars, exc_class, expect_traceback, monkeypatch):
    exception_msg = 'fake check has an exception'
    run_exception = exc_class(exception_msg)
    check_class = fake_check(run_exception=run_exception, changed=True)
    monkeypatch.setattr(plugin, 'load_known_checks', lambda *_: {'fake_check': check_class()})
    monkeypatch.setattr('openshift_health_check.resolve_checks', lambda *args: ['fake_check'])

    result = plugin.run(tmp=None, task_vars=task_vars)

    assert failed(result['checks']['fake_check'], msg_has=exception_msg)
    assert expect_traceback == ("Traceback" in result['checks']['fake_check']['msg'])
    assert failed(result, msg_has=['failed'])
    assert changed(result['checks']['fake_check'])
    assert changed(result)
    assert not skipped(result)


def test_action_plugin_run_check_output_dir(plugin, task_vars, tmpdir, monkeypatch):
    check_class = fake_check(
        run_return={},
        run_logs=[('thing', 'note')],
        run_files=[
            FileToSave('save.file', 'contents', None),
            FileToSave('save.file', 'duplicate', None),
            FileToSave('copy.file', None, 'foo'),  # note: copy runs execute_module => exception
        ],
    )
    task_vars['openshift_checks_output_dir'] = str(tmpdir)
    check_class.get_var = lambda self, name, **_: task_vars.get(name)
    monkeypatch.setattr(plugin, 'load_known_checks', lambda *_: {'fake_check': check_class()})
    monkeypatch.setattr('openshift_health_check.resolve_checks', lambda *args: ['fake_check'])

    plugin.run(tmp=None, task_vars=task_vars)
    assert any(path.basename == task_vars['ansible_host'] for path in tmpdir.listdir())
    assert any(path.basename == 'fake_check.log.json' for path in tmpdir.visit())
    assert any(path.basename == 'save.file' for path in tmpdir.visit())
    assert any(path.basename == 'save.file.2' for path in tmpdir.visit())


def test_action_plugin_resolve_checks_exception(plugin, task_vars, monkeypatch):
    monkeypatch.setattr(plugin, 'load_known_checks', lambda *_: {})

    result = plugin.run(tmp=None, task_vars=task_vars)

    assert failed(result, msg_has=['unknown', 'name'])
    assert not changed(result)
    assert not skipped(result)


@pytest.mark.parametrize('names,all_checks,expected', [
    ([], [], set()),
    (
        ['a', 'b'],
        [
            fake_check('a'),
            fake_check('b'),
        ],
        set(['a', 'b']),
    ),
    (
        ['a', 'b', '@group'],
        [
            fake_check('from_group_1', ['group', 'another_group']),
            fake_check('not_in_group', ['another_group']),
            fake_check('from_group_2', ['preflight', 'group']),
            fake_check('a'),
            fake_check('b'),
        ],
        set(['a', 'b', 'from_group_1', 'from_group_2']),
    ),
])
def test_resolve_checks_ok(names, all_checks, expected):
    assert resolve_checks(names, all_checks) == expected


@pytest.mark.parametrize('names,all_checks,words_in_exception', [
    (
        ['testA', 'testB'],
        [],
        ['check', 'name', 'testA', 'testB'],
    ),
    (
        ['@group'],
        [],
        ['tag', 'name', 'group'],
    ),
    (
        ['testA', 'testB', '@group'],
        [],
        ['check', 'name', 'testA', 'testB', 'tag', 'group'],
    ),
    (
        ['testA', 'testB', '@group'],
        [
            fake_check('from_group_1', ['group', 'another_group']),
            fake_check('not_in_group', ['another_group']),
            fake_check('from_group_2', ['preflight', 'group']),
        ],
        ['check', 'name', 'testA', 'testB'],
    ),
])
def test_resolve_checks_failure(names, all_checks, words_in_exception):
    with pytest.raises(Exception) as excinfo:
        resolve_checks(names, all_checks)
    for word in words_in_exception:
        assert word in str(excinfo.value)


@pytest.mark.parametrize('give_output_dir, result, expect_file', [
    (False, None, False),
    (True, dict(content="c3BhbQo=", encoding="base64"), True),
    (True, dict(content="encoding error", encoding="base64"), False),
    (True, dict(content="spam", no_encoding=None), True),
    (True, dict(failed=True, msg="could not slurp"), False),
])
def test_copy_remote_file_to_dir(give_output_dir, result, expect_file, tmpdir):
    check = fake_check()()
    check.execute_module = lambda *args, **_: result
    copy_remote_file_to_dir(check, "remote_file", str(tmpdir) if give_output_dir else "", "local_file")
    assert expect_file == any(path.basename == "local_file" for path in tmpdir.listdir())


def test_write_to_output_exceptions(tmpdir, monkeypatch, capsys):

    class Spam(object):
        def __str__(self):
            raise Exception("break str")

    test = {1: object(), 2: Spam()}
    test[3] = test
    write_result_to_output_dir(str(tmpdir), test)
    assert "Error writing" in test["output_files"]

    output_dir = tmpdir.join("eggs")
    output_dir.write("spam")  # so now it's not a dir
    write_to_output_file(str(output_dir), "somefile", "somedata")
    assert "Could not write" in capsys.readouterr()[1]

    monkeypatch.setattr("openshift_health_check.prepare_output_dir", lambda *_: False)
    write_result_to_output_dir(str(tmpdir), test)
    assert "Error creating" in test["output_files"]
