from __future__ import with_statement

import copy
from functools import partial
from operator import isMappingType
import os
import sys
from contextlib import contextmanager

from fudge import Fake, patched_context, with_fakes
from nose.tools import ok_, eq_

from fabric.decorators import hosts, roles, task
from fabric.context_managers import settings
from fabric.main import (parse_arguments, _escape_split,
        load_fabfile as _load_fabfile, list_commands, _task_names,
        COMMANDS_HEADER, NESTED_REMINDER)
import fabric.state
from fabric.state import _AttributeDict
from fabric.tasks import Task, WrappedCallableTask
from fabric.task_utils import _crawl, crawl, merge

from utils import mock_streams, eq_, FabricTest, fabfile, path_prefix, aborts


# Stupid load_fabfile wrapper to hide newly added return value.
# WTB more free time to rewrite all this with objects :)
def load_fabfile(*args, **kwargs):
    return _load_fabfile(*args, **kwargs)[:2]


#
# Basic CLI stuff
#

def test_argument_parsing():
    for args, output in [
        # Basic 
        ('abc', ('abc', [], {}, [], [], [])),
        # Arg
        ('ab:c', ('ab', ['c'], {}, [], [], [])),
        # Kwarg
        ('a:b=c', ('a', [], {'b':'c'}, [], [], [])),
        # Arg and kwarg
        ('a:b=c,d', ('a', ['d'], {'b':'c'}, [], [], [])),
        # Multiple kwargs
        ('a:b=c,d=e', ('a', [], {'b':'c','d':'e'}, [], [], [])),
        # Host
        ('abc:host=foo', ('abc', [], {}, ['foo'], [], [])),
        # Hosts with single host
        ('abc:hosts=foo', ('abc', [], {}, ['foo'], [], [])),
        # Hosts with multiple hosts
        # Note: in a real shell, one would need to quote or escape "foo;bar".
        # But in pure-Python that would get interpreted literally, so we don't.
        ('abc:hosts=foo;bar', ('abc', [], {}, ['foo', 'bar'], [], [])),

        # Exclude hosts
        ('abc:hosts=foo;bar,exclude_hosts=foo', ('abc', [], {}, ['foo', 'bar'], [], ['foo'])),
        ('abc:hosts=foo;bar,exclude_hosts=foo;bar', ('abc', [], {}, ['foo', 'bar'], [], ['foo','bar'])),
       # Empty string args
        ("task:x=y,z=", ('task', [], {'x': 'y', 'z': ''}, [], [], [])),
        ("task:foo,,x=y", ('task', ['foo', ''], {'x': 'y'}, [], [], [])),
    ]:
        yield eq_, parse_arguments([args]), [output]


def test_escaped_task_arg_split():
    """
    Allow backslashes to escape the task argument separator character
    """
    argstr = r"foo,bar\,biz\,baz,what comes after baz?"
    eq_(
        _escape_split(',', argstr),
        ['foo', 'bar,biz,baz', 'what comes after baz?']
    )


def test_escaped_task_kwarg_split():
    """
    Allow backslashes to escape the = in x=y task kwargs
    """
    argstr = r"cmd:arg,escaped\,arg,nota\=kwarg,regular=kwarg,escaped=regular\=kwarg"
    args = ['arg', 'escaped,arg', 'nota=kwarg']
    kwargs = {'regular': 'kwarg', 'escaped': 'regular=kwarg'}
    eq_(
        parse_arguments([argstr])[0],
        ('cmd', args, kwargs, [], [], []),
    )



#
# Host/role decorators
#

# Allow calling Task.get_hosts as function instead (meh.)
def get_hosts_and_effective_roles(command, *args):
    return WrappedCallableTask(command).get_hosts_and_effective_roles(*args)

def eq_hosts(command, expected_hosts, cli_hosts=None, excluded_hosts=None, env=None, func=set):
    eq_(func(get_hosts_and_effective_roles(command, cli_hosts or [], [], excluded_hosts or [], env)[0]),
        func(expected_hosts))

def eq_effective_roles(command, expected_effective_roles, cli_roles=None, env=None, func=set):
    eq_(func(get_hosts_and_effective_roles(command, [], cli_roles or [], [], env)[1]),
        func(expected_effective_roles))

true_eq_hosts = partial(eq_hosts, func=lambda x: x)

def test_hosts_decorator_by_itself():
    """
    Use of @hosts only
    """
    host_list = ['a', 'b']

    @hosts(*host_list)
    def command():
        pass

    eq_hosts(command, host_list)


fake_roles = {
    'r1': ['a', 'b'],
    'r2': ['b', 'c']
}

def test_roles_decorator_by_itself():
    """
    Use of @roles only
    """
    @roles('r1')
    def command():
        pass
    eq_hosts(command, ['a', 'b'], env={'roledefs': fake_roles})
    eq_effective_roles(command, ['r1'], env={'roledefs': fake_roles})

def test_roles_decorator_overrides_env_roles():
    """
    If @roles is used it replaces any env.roles value
    """
    @roles('r1')
    def command():
        pass
    eq_effective_roles(command, ['r1'], env={'roledefs': fake_roles,
                                             'roles': ['r2']})

def test_cli_roles_override_decorator_roles():
    """
    If CLI roles are provided they replace roles defined in @roles.
    """
    @roles('r1')
    def command():
        pass
    eq_effective_roles(command, ['r2'], cli_roles=['r2'], env={'roledefs': fake_roles})


def test_hosts_and_roles_together():
    """
    Use of @roles and @hosts together results in union of both
    """
    @roles('r1', 'r2')
    @hosts('d')
    def command():
        pass
    eq_hosts(command, ['a', 'b', 'c', 'd'], env={'roledefs': fake_roles})
    eq_effective_roles(command, ['r1', 'r2'], env={'roledefs': fake_roles})

def test_host_role_merge_deduping():
    """
    Use of @roles and @hosts dedupes when merging
    """
    @roles('r1', 'r2')
    @hosts('a')
    def command():
        pass
    # Not ['a', 'a', 'b', 'c'] or etc
    true_eq_hosts(command, ['a', 'b', 'c'], env={'roledefs': fake_roles})

def test_host_role_merge_deduping_off():
    """
    Allow turning deduping off
    """
    @roles('r1', 'r2')
    @hosts('a')
    def command():
        pass
    with settings(dedupe_hosts=False):
        true_eq_hosts(
            command,
            # 'a' 1x host 1x role
            # 'b' 1x r1 1x r2
            ['a', 'a', 'b', 'b', 'c'],
            env={'roledefs': fake_roles}
        )


tuple_roles = {
    'r1': ('a', 'b'),
    'r2': ('b', 'c'),
}

def test_roles_as_tuples():
    """
    Test that a list of roles as a tuple succeeds
    """
    @roles('r1')
    def command():
        pass
    eq_hosts(command, ['a', 'b'], env={'roledefs': tuple_roles})
    eq_effective_roles(command, ['r1'], env={'roledefs': fake_roles})


def test_hosts_as_tuples():
    """
    Test that a list of hosts as a tuple succeeds
    """
    def command():
        pass
    eq_hosts(command, ['foo', 'bar'], env={'hosts': ('foo', 'bar')})


def test_hosts_decorator_overrides_env_hosts():
    """
    If @hosts is used it replaces any env.hosts value
    """
    @hosts('bar')
    def command():
        pass
    eq_hosts(command, ['bar'], env={'hosts': ['foo']})

def test_hosts_decorator_overrides_env_hosts_with_task_decorator_first():
    """
    If @hosts is used it replaces any env.hosts value even with @task
    """
    @task
    @hosts('bar')
    def command():
        pass
    eq_hosts(command, ['bar'], env={'hosts': ['foo']})

def test_hosts_decorator_overrides_env_hosts_with_task_decorator_last():
    @hosts('bar')
    @task
    def command():
        pass
    eq_hosts(command, ['bar'], env={'hosts': ['foo']})

def test_hosts_stripped_env_hosts():
    """
    Make sure hosts defined in env.hosts are cleaned of extra spaces
    """
    def command():
        pass
    myenv = {'hosts': [' foo ', 'bar '], 'roles': [], 'exclude_hosts': []}
    eq_hosts(command, ['foo', 'bar'], env=myenv)


spaced_roles = {
    'r1': [' a ', ' b '],
    'r2': ['b', 'c'],
}

def test_roles_stripped_env_hosts():
    """
    Make sure hosts defined in env.roles are cleaned of extra spaces
    """
    @roles('r1')
    def command():
        pass