import pytest
from boltons.funcutils import wraps, FunctionBuilder


def pita_wrap(flag=False):

    def cedar_dec(func):
        @wraps(func)
        def cedar_wrapper(*a, **kw):
            return (flag, func.__name__, func(*a, **kw))
        return cedar_wrapper

    return cedar_dec


def wrappable_func(a, b):
    return a, b


def wrappable_varkw_func(a, b, **kw):
    return a, b


def test_wraps_basic():

    @pita_wrap(flag=True)
    def simple_func():
        '''"""a tricky docstring"""'''
        return 'hello'

    assert simple_func() == (True, 'simple_func', 'hello')
    assert simple_func.__doc__ == '''"""a tricky docstring"""'''

    assert callable(simple_func.__wrapped__)
    assert simple_func.__wrapped__() == 'hello'
    assert simple_func.__wrapped__.__doc__ == '''"""a tricky docstring"""'''

    @pita_wrap(flag=False)
    def less_simple_func(arg='hello'):
        return arg

    assert less_simple_func() == (False, 'less_simple_func', 'hello')
    assert less_simple_func(arg='bye') == (False, 'less_simple_func', 'bye')

    with pytest.raises(TypeError):
        simple_func(no_such_arg='nope')

    @pita_wrap(flag=False)
    def default_non_roundtrippable_repr(x=lambda y: y + 1):
        return x(1)

    assert default_non_roundtrippable_repr() == (
        False, 'default_non_roundtrippable_repr', 2)


def test_wraps_injected():
    def inject_string(func):
        @wraps(func, injected="a")
        def wrapped(*args, **kwargs):
            return func(1, *args, **kwargs)
        return wrapped

    assert inject_string(wrappable_func)(2) == (1, 2)

    def inject_list(func):
        @wraps(func, injected=["b"])
        def wrapped(a, *args, **kwargs):
            return func(a, 2, *args, **kwargs)
        return wrapped

    assert inject_list(wrappable_func)(1) == (1, 2)

    def inject_nonexistent_arg(func):
        @wraps(func, injected=["X"])
        def wrapped(*args, **kwargs):
            return func(*args, **kwargs)
        return wrapped

    with pytest.raises(ValueError):
        inject_nonexistent_arg(wrappable_func)

    def inject_missing_argument(func):
        @wraps(func, injected="c")
        def wrapped(*args, **kwargs):
            return func(1, *args, **kwargs)
        return wrapped

    def inject_misc_argument(func):
        # inject_to_varkw is default True, just being explicit
        @wraps(func, injected="c", inject_to_varkw=True)
        def wrapped(*args, **kwargs):
            return func(c=1, *args, **kwargs)
        return wrapped

    assert inject_misc_argument(wrappable_varkw_func)(1, 2) == (1, 2)

    def inject_misc_argument_no_varkw(func):
        @wraps(func, injected="c", inject_to_varkw=False)
        def wrapped(*args, **kwargs):
            return func(c=1, *args, **kwargs)
        return wrapped

    with pytest.raises(ValueError):
        inject_misc_argument_no_varkw(wrappable_varkw_func)


def test_wraps_update_dict():

    def updated_dict(func):
        @wraps(func, update_dict=True)
        def wrapped(*args, **kwargs):
            return func(*args, **kwargs)
        return wrapped

    def f(a, b):
        return a, b

    f.something = True

    assert getattr(updated_dict(f), 'something')


def test_wraps_unknown_args():

    def fails(func):
        @wraps(func, foo="bar")
        def wrapped(*args, **kwargs):
            return func(*args, **kwargs)
        return wrapped

    with pytest.raises(TypeError):
        fails(wrappable_func)


def test_FunctionBuilder_invalid_args():
    with pytest.raises(TypeError):
        FunctionBuilder(name="fails", foo="bar")


def test_FunctionBuilder_invalid_body():
    with pytest.raises(SyntaxError):
        FunctionBuilder(name="fails", body="*").get_func()


def test_FunctionBuilder_modify():
    fb = FunctionBuilder('return_five', doc='returns the integer 5',
                         body='return 5')
    f = fb.get_func()
    assert f() == 5

    fb.varkw = 'kw'
    f_kw = fb.get_func()
    assert f_kw(ignored_arg='ignored_val') == 5


def test_wraps_wrappers():
    call_list = []

    def call_list_appender(func):
        @wraps(func)
        def appender(*a, **kw):
            call_list.append((a, kw))
            return func(*a, **kw)
        return appender

    with pytest.raises(TypeError):
        class Num:
            def __init__(self, num):
                self.num = num

            @call_list_appender
            @classmethod
            def added(cls, x, y=1):
                return cls(x + y)

    return


def test_FunctionBuilder_add_arg():
    fb = FunctionBuilder('return_five', doc='returns the integer 5',
                         body='return 5')
    f = fb.get_func()
    assert f() == 5

    fb.add_arg('val')
    f = fb.get_func()
    assert f(val='ignored') == 5

    with pytest.raises(ValueError) as excinfo:
        fb.add_arg('val')
    excinfo.typename == 'ExistingArgument'

    fb = FunctionBuilder('return_val', doc='returns the value',
                         body='return val')

    broken_func = fb.get_func()
    with pytest.raises(NameError):
        broken_func()

    fb.add_arg('val', default='default_val')

    better_func = fb.get_func()
    assert better_func() == 'default_val'

    assert better_func('positional') == 'positional'
    assert better_func(val='keyword') == 'keyword'


def test_wraps_expected():
    def expect_string(func):
        @wraps(func, expected="c")
        def wrapped(*args, **kwargs):
            args, c = args[:2], args[-1]
            return func(*args, **kwargs) + (c,)
        return wrapped

    expected_string = expect_string(wrappable_func)
    assert expected_string(1, 2, 3) == (1, 2, 3)

    with pytest.raises(TypeError) as excinfo:
        expected_string(1, 2)

    # a rough way of making sure we got the kind of error we expected
    assert 'argument' in repr(excinfo.value)

    def expect_list(func):
        @wraps(func, expected=["c"])
        def wrapped(*args, **kwargs):
            args, c = args[:2], args[-1]
            return func(*args, **kwargs) + (c,)
        return wrapped

    assert expect_list(wrappable_func)(1, 2, c=4) == (1, 2, 4)

    def expect_pair(func):
        @wraps(func, expected=[('c', 5)])
        def wrapped(*args, **kwargs):
            args, c = args[:2], args[-1]
            return func(*args, **kwargs) + (c,)
        return wrapped

    assert expect_pair(wrappable_func)(1, 2) == (1, 2, 5)

    def expect_dict(func):
        @wraps(func, expected={'c': 6})
        def wrapped(*args, **kwargs):
            args, c = args[:2], args[-1]
            return func(*args, **kwargs) + (c,)
        return wrapped

    assert expect_dict(wrappable_func)(1, 2) == (1, 2, 6)


def test_defaults_dict():
    def example(req, test='default'):
        return req

    fb_example = FunctionBuilder.from_func(example)
    assert 'test' in fb_example.args
    dd = fb_example.get_defaults_dict()
    assert dd['test'] == 'default'
    assert 'req' not in dd


def test_get_arg_names():
    def example(req, test='default'):
        return req

    fb_example = FunctionBuilder.from_func(example)
    assert 'test' in fb_example.args
    assert fb_example.get_arg_names() == ('req', 'test')
    assert fb_example.get_arg_names(only_required=True) == ('req',)


@pytest.mark.parametrize(
    "args, varargs, varkw, defaults, invocation_str, sig_str",
    [
        (["a", "b"], None, None, None, "a, b", "(a, b)"),
        (None, "args", "kwargs", None, "*args, **kwargs", "(*args, **kwargs)"),
        ("a", None, None, dict(a="a"), "a", "(a)"),
    ],
)
def test_get_invocation_sig_str(
    args, varargs, varkw, defaults, invocation_str, sig_str
):
    fb = FunctionBuilder(
        name='return_five',
        body='return 5',
        args=args,
        varargs=varargs,
        varkw=varkw,
        defaults=defaults
    )

    assert fb.get_invocation_str() == invocation_str
    assert fb.get_sig_str() == sig_str
