import gc
import os
import os.path
import pytest

from ansible.utils.display import Display
from crypt import crypt
from suitable.api import list_ansible_modules, Api
from suitable.mitogen import Api as MitogenApi
from suitable.mitogen import is_mitogen_supported
from suitable.errors import UnreachableError, ModuleError
from suitable.runner_results import RunnerResults
from suitable.compat import text_type


def test_auto_localhost():
    host = Api('localhost')
    assert host.inventory['localhost']['ansible_connection'] == 'local'

    host = Api('localhost', connection='smart')
    assert 'ansible_connection' not in host.inventory['localhost']
    assert host.options.connection == 'smart'


def test_sudo():
    host = Api('localhost', sudo=True)
    try:
        assert host.command('whoami').stdout() == 'root'
    except ModuleError as e:
        assert 'password' in e.result['module_stderr']


def test_module_args():
    upgrade = (
        'apt-get upgrade -y -o Dpkg::Options::="--force-confdef" '
        '-o Dpkg::Options::="--force-confold"'
    )

    try:
        Api('localhost').command(upgrade)
    except ModuleError as e:
        assert e.result['invocation']['module_args']['_raw_params'] == upgrade


def test_results():
    result = Api('localhost').command('whoami')
    assert result.rc('localhost') == 0
    assert result.stdout('localhost') is not None
    assert result['contacted']['localhost']['rc'] == 0

    with pytest.raises(AttributeError):
        result.asdf('localhost')

    result['contacted'] = []

    with pytest.raises(KeyError):
        result.rc('localhost')


@pytest.mark.parametrize("server", ('localhost',))
def test_results_single_server(server):
    result = Api(server).command('whoami')
    assert result.rc() == 0
    assert result.rc(server) == 0


def test_results_multiple_servers():
    result = RunnerResults({
        'contacted': {
            'web.seantis.dev': {'rc': 0},
            'db.seantis.dev': {'rc': 1}
        }
    })

    with pytest.raises(KeyError):
        result.rc()

    assert result.rc('web.seantis.dev') == 0
    assert result.rc('db.seantis.dev') == 1


@pytest.mark.parametrize("server", (('localhost', 'localhost:22'),))
def test_whoami_multiple_servers(server):
    host = Api(server)
    results = host.command('whoami')
    assert results.rc(server[0]) == 0
    assert results.rc(server[1]) == 0


def test_valid_return_codes():
    host = Api('localhost')
    assert host._valid_return_codes == (0,)

    with host.valid_return_codes(0, 1):
        assert host._valid_return_codes == (0, 1)
        host.shell('whoami | grep -q asdfasdfasdf')

    assert host._valid_return_codes == (0,)


def test_list_ansible_modules():
    modules = list_ansible_modules()

    # look for some basic modules
    assert 'command' in modules
    assert 'file' in modules
    assert 'user' in modules
    assert 'shell' in modules
    assert 'git' in modules
    assert 'setup' in modules


def test_module_error():
    with pytest.raises(ModuleError):
        # command cannot include pipes
        Api('localhost').command('whoami | less')


@pytest.mark.parametrize("server", ('255.255.255.255', '255.255.255.255:22'))
def test_unreachable(server):
    host = Api(server)

    assert server in host.inventory

    try:
        host.command('whoami')
    except UnreachableError as e:
        assert server in str(e)
    else:
        assert False, "an error should have been thrown"

    assert server not in host.inventory


@pytest.mark.parametrize("server", ('255.255.255.255', '255.255.255.255:22'))
def test_ignore_unreachable(server):
    host = Api(server, ignore_unreachable=True)
    assert server in host.inventory
    result = host.command('whoami')
    assert server in result['unreachable']
    assert server in host.inventory


def test_custom_unreachable():
    class MyApi(Api):
        unreachable = []

        def on_unreachable_host(self, module, host):
            self.unreachable.append(host)
            return 'keep-trying'

    host = MyApi('255.255.255.255')

    host.command('whoami')
    assert len(host.unreachable) == 1

    host.command('whoami')
    assert len(host.unreachable) == 2

    host.command('whoami')
    assert len(host.unreachable) == 3


def test_custom_unreachable_default():
    class MyApi(Api):
        unreachable = []

        def on_unreachable_host(self, module, host):
            self.unreachable.append(host)

    host = MyApi('255.255.255.255')

    host.command('whoami')
    assert len(host.unreachable) == 1

    host.command('whoami')
    assert len(host.unreachable) == 1

    host.command('whoami')
    assert len(host.unreachable) == 1


def test_ignore_errors():
    host = Api('localhost', ignore_errors=True)
    result = host.command('whoami | less')

    assert result.rc() == 1
    assert result.cmd() == ['whoami', '|', 'less']


def test_error_string():
    try:
        Api('localhost').command('whoami | less')
    except ModuleError as e:
        # we don't have a msg so we mock that out, for coverage!
        e.result['msg'] = '0xdeadbeef'
        error_string = text_type(e)

        # we don't make many guarantees with the string messages, so
        # a basic somke test suffices here. This is not something to
        # depend on.

        assert '0xdeadbeef' in error_string
        assert 'command: whoami | less' in error_string
        assert 'Returncode: 1' in error_string
    else:
        assert False, "this needs to trigger an exception"


def test_escaping(tempdir):
    special_dir = os.path.join(tempdir, 'special dir with "-char')
    os.mkdir(special_dir)

    api = Api('localhost')
    api.file(
        dest=os.path.join(special_dir, 'foo.txt'),
        state='touch'
    )


def test_extra_vars(tempdir):
    api = Api('localhost', extra_vars={'path': tempdir})
    api.file(dest="{{ path }}/foo.txt", state='touch')

    assert os.path.exists(tempdir + '/foo.txt')


def test_environment():
    api = Api('localhost', environment={'FOO': 'BAR'})
    assert api.shell('echo $FOO').stdout() == 'BAR'

    api.environment['FOO'] = 'BAZ'
    assert api.shell('echo $FOO').stdout() == 'BAZ'


def test_same_server_multiple_ports():
    api = Api(('localhost', 'localhost:22'))
    assert len(api.inventory) == 2

    # Ansible groups these calls, so we only get one result back
    result = api.command('whoami')
    assert len(result['contacted']) == 2


def test_single_display_module():
    assert sum(1 for obj in gc.get_objects() if isinstance(obj, Display)) == 1


@pytest.mark.skipif(not is_mitogen_supported(), reason="incompatible mitogen")
def test_mitogen_integration():
    try:
        result = MitogenApi('localhost').command('whoami')
        assert len(result['contacted']) == 1
    except SystemExit:
        pass


def test_list_args():
    api = Api('localhost')

    # api.assert is not valid Python syntax
    getattr(api, 'assert')(that=[
        "'bar' != 'foo'",
        "'bar' == 'bar'"
    ])


def test_dict_args(tempdir):
    api = Api('localhost')
    api.set_stats(data={'foo': 'bar'})


def test_disable_hostkey_checking(api):
    api.host_key_checking = False
    assert api.command('whoami').stdout() == 'root'


def test_enable_hostkey_checking_vanilla(container):
    # if we do not use 'paramiko' here, we get the following error:
    # > Using a SSH password instead of a key is not possible because Host Key
    # > checking is enabled and sshpass does not support this.
    # > Please add this host's fingerprint to your known_hosts file to
    # > manage this host.
    api = container.vanilla_api(connection='paramiko')

    with pytest.raises(UnreachableError):
        assert api.command('whoami').stdout() == 'root'


def test_interleaving(container):
    # make sure we can interleave calls of different API objects
    password = crypt("foobar", "salt")

    root = container.vanilla_api(connection='paramiko')
    root.host_key_checking = False

    root.command('useradd --non-unique --uid 0 foo -p ' + password)
    root.command('useradd --non-unique --uid 0 bar -p ' + password)

    foo = container.vanilla_api(
        connection='paramiko', remote_user='foo', remote_pass='foobar')
    bar = container.vanilla_api(
        connection='paramiko', remote_user='bar', remote_pass='foobar')

    foo.host_key_checking = False
    bar.host_key_checking = False

    assert foo.command('id -g').stdout() == '1000'
    assert bar.command('id -g').stdout() == '1001'

    assert foo.command('id -g').stdout() == '1000'
    assert bar.command('id -g').stdout() == '1001'