import functools
import inspect
import itertools
import operator
import sys
import cytoolz
from cytoolz.functoolz import (curry, is_valid_args, is_partial_args, is_arity,
                             num_required_args, has_varargs, has_keywords)
from cytoolz._signatures import builtins
import cytoolz._signatures as _sigs
from cytoolz.utils import raises


def make_func(param_string, raise_if_called=True):
    if not param_string.startswith('('):
        param_string = '(%s)' % param_string
    if raise_if_called:
        body = 'raise ValueError("function should not be called")'
    else:
        body = 'return True'
    d = {}
    exec(f'def func{param_string}:\n    {body}', globals(), d)
    return d['func']


def test_make_func():
    f = make_func('')
    assert raises(ValueError, lambda: f())
    assert raises(TypeError, lambda: f(1))

    f = make_func('', raise_if_called=False)
    assert f()
    assert raises(TypeError, lambda: f(1))

    f = make_func('x, y=1', raise_if_called=False)
    assert f(1)
    assert f(x=1)
    assert f(1, 2)
    assert f(x=1, y=2)
    assert raises(TypeError, lambda: f(1, 2, 3))

    f = make_func('(x, y=1)', raise_if_called=False)
    assert f(1)
    assert f(x=1)
    assert f(1, 2)
    assert f(x=1, y=2)
    assert raises(TypeError, lambda: f(1, 2, 3))


def test_is_valid(check_valid=is_valid_args, incomplete=False):
    orig_check_valid = check_valid
    check_valid = lambda func, *args, **kwargs: orig_check_valid(func, args, kwargs)

    f = make_func('')
    assert check_valid(f)
    assert check_valid(f, 1) is False
    assert check_valid(f, x=1) is False

    f = make_func('x')
    assert check_valid(f) is incomplete
    assert check_valid(f, 1)
    assert check_valid(f, x=1)
    assert check_valid(f, 1, x=2) is False
    assert check_valid(f, 1, y=2) is False
    assert check_valid(f, 1, 2) is False
    assert check_valid(f, x=1, y=2) is False

    f = make_func('x=1')
    assert check_valid(f)
    assert check_valid(f, 1)
    assert check_valid(f, x=1)
    assert check_valid(f, 1, x=2) is False
    assert check_valid(f, 1, y=2) is False
    assert check_valid(f, 1, 2) is False
    assert check_valid(f, x=1, y=2) is False

    f = make_func('*args')
    assert check_valid(f)
    assert check_valid(f, 1)
    assert check_valid(f, 1, 2)
    assert check_valid(f, x=1) is False

    f = make_func('**kwargs')
    assert check_valid(f)
    assert check_valid(f, x=1)
    assert check_valid(f, x=1, y=2)
    assert check_valid(f, 1) is False

    f = make_func('x, *args')
    assert check_valid(f) is incomplete
    assert check_valid(f, 1)
    assert check_valid(f, 1, 2)
    assert check_valid(f, x=1)
    assert check_valid(f, 1, x=1) is False
    assert check_valid(f, 1, y=1) is False

    f = make_func('x, y=1, **kwargs')
    assert check_valid(f) is incomplete
    assert check_valid(f, 1)
    assert check_valid(f, x=1)
    assert check_valid(f, 1, 2)
    assert check_valid(f, x=1, y=2, z=3)
    assert check_valid(f, 1, 2, y=3) is False

    f = make_func('a, b, c=3, d=4')
    assert check_valid(f) is incomplete
    assert check_valid(f, 1) is incomplete
    assert check_valid(f, 1, 2)
    assert check_valid(f, 1, c=3) is incomplete
    assert check_valid(f, 1, e=3) is False
    assert check_valid(f, 1, 2, e=3) is False
    assert check_valid(f, 1, 2, b=3) is False

    assert check_valid(1) is False


def test_is_valid_py3(check_valid=is_valid_args, incomplete=False):
    orig_check_valid = check_valid
    check_valid = lambda func, *args, **kwargs: orig_check_valid(func, args, kwargs)

    f = make_func('x, *, y=1')
    assert check_valid(f) is incomplete
    assert check_valid(f, 1)
    assert check_valid(f, x=1)
    assert check_valid(f, 1, y=2)
    assert check_valid(f, 1, 2) is False
    assert check_valid(f, 1, z=2) is False

    f = make_func('x, *args, y=1')
    assert check_valid(f) is incomplete
    assert check_valid(f, 1)
    assert check_valid(f, x=1)
    assert check_valid(f, 1, y=2)
    assert check_valid(f, 1, 2, y=2)
    assert check_valid(f, 1, 2)
    assert check_valid(f, 1, z=2) is False

    f = make_func('*, y=1')
    assert check_valid(f)
    assert check_valid(f, 1) is False
    assert check_valid(f, y=1)
    assert check_valid(f, z=1) is False

    f = make_func('x, *, y')
    assert check_valid(f) is incomplete
    assert check_valid(f, 1) is incomplete
    assert check_valid(f, x=1) is incomplete
    assert check_valid(f, 1, y=2)
    assert check_valid(f, x=1, y=2)
    assert check_valid(f, 1, 2) is False
    assert check_valid(f, 1, z=2) is False
    assert check_valid(f, 1, y=1, z=2) is False

    f = make_func('x=1, *, y, z=3')
    assert check_valid(f) is incomplete
    assert check_valid(f, 1, z=3) is incomplete
    assert check_valid(f, y=2)
    assert check_valid(f, 1, y=2)
    assert check_valid(f, x=1, y=2)
    assert check_valid(f, x=1, y=2, z=3)
    assert check_valid(f, 1, x=1, y=2) is False
    assert check_valid(f, 1, 3, y=2) is False

    f = make_func('w, x=2, *args, y, z=4')
    assert check_valid(f) is incomplete
    assert check_valid(f, 1) is incomplete
    assert check_valid(f, 1, y=3)

    f = make_func('a, b, c=3, d=4, *args, e=5, f=6, g, h')
    assert check_valid(f) is incomplete
    assert check_valid(f, 1) is incomplete
    assert check_valid(f, 1, 2) is incomplete
    assert check_valid(f, 1, 2, g=7) is incomplete
    assert check_valid(f, 1, 2, g=7, h=8)
    assert check_valid(f, 1, 2, 3, 4, 5, 6, 7, 8, 9) is incomplete

    f = make_func('a: int, b: float')
    assert check_valid(f) is incomplete
    assert check_valid(f, 1) is incomplete
    assert check_valid(f, b=1) is incomplete
    assert check_valid(f, 1, 2)

    f = make_func('(a: int, b: float) -> float')
    assert check_valid(f) is incomplete
    assert check_valid(f, 1) is incomplete
    assert check_valid(f, b=1) is incomplete
    assert check_valid(f, 1, 2)

    f.__signature__ = 34
    assert check_valid(f) is False

    class RaisesValueError:
        def __call__(self):
            pass
        @property
        def __signature__(self):
            raise ValueError('Testing Python 3.4')

    f = RaisesValueError()
    assert check_valid(f) is None


def test_is_partial():
    test_is_valid(check_valid=is_partial_args, incomplete=True)
    test_is_valid_py3(check_valid=is_partial_args, incomplete=True)


def test_is_valid_curry():
    def check_curry(func, args, kwargs, incomplete=True):
        try:
            curry(func)(*args, **kwargs)
            curry(func, *args)(**kwargs)
            curry(func, **kwargs)(*args)
            curry(func, *args, **kwargs)()
            if not isinstance(func, type(lambda: None)):
                return None
            return incomplete
        except ValueError:
            return True
        except TypeError:
            return False

    check_valid = functools.partial(check_curry, incomplete=True)
    test_is_valid(check_valid=check_valid, incomplete=True)
    test_is_valid_py3(check_valid=check_valid, incomplete=True)

    check_valid = functools.partial(check_curry, incomplete=False)
    test_is_valid(check_valid=check_valid, incomplete=False)
    test_is_valid_py3(check_valid=check_valid, incomplete=False)


def test_func_keyword():
    def f(func=None):
        pass
    assert is_valid_args(f, (), {})
    assert is_valid_args(f, (None,), {})
    assert is_valid_args(f, (), {'func': None})
    assert is_valid_args(f, (None,), {'func': None}) is False
    assert is_partial_args(f, (), {})
    assert is_partial_args(f, (None,), {})
    assert is_partial_args(f, (), {'func': None})
    assert is_partial_args(f, (None,), {'func': None}) is False


def test_has_unknown_args():
    assert has_varargs(1) is False
    assert has_varargs(map)
    assert has_varargs(make_func('')) is False
    assert has_varargs(make_func('x, y, z')) is False
    assert has_varargs(make_func('*args'))
    assert has_varargs(make_func('**kwargs')) is False
    assert has_varargs(make_func('x, y, *args, **kwargs'))
    assert has_varargs(make_func('x, y, z=1')) is False
    assert has_varargs(make_func('x, y, z=1, **kwargs')) is False

    f = make_func('*args')
    f.__signature__ = 34
    assert has_varargs(f) is False

    class RaisesValueError:
        def __call__(self):
            pass
        @property
        def __signature__(self):
            raise ValueError('Testing Python 3.4')

    f = RaisesValueError()
    assert has_varargs(f) is None


def test_num_required_args():
    assert num_required_args(lambda: None) == 0
    assert num_required_args(lambda x: None) == 1
    assert num_required_args(lambda x, *args: None) == 1
    assert num_required_args(lambda x, **kwargs: None) == 1
    assert num_required_args(lambda x, y, *args, **kwargs: None) == 2
    assert num_required_args(map) == 2
    assert num_required_args(dict) is None


def test_has_keywords():
    assert has_keywords(lambda: None) is False
    assert has_keywords(lambda x: None) is False
    assert has_keywords(lambda x=1: None)
    assert has_keywords(lambda **kwargs: None)
    assert has_keywords(int)
    assert has_keywords(sorted)
    assert has_keywords(max)
    # map gained `strict=False` keyword in Python 3.14
    assert has_keywords(map) == (sys.version_info[1] >= 14)
    assert has_keywords(bytearray) is None


def test_has_varargs():
    assert has_varargs(lambda: None) is False
    assert has_varargs(lambda *args: None)
    assert has_varargs(lambda **kwargs: None) is False
    assert has_varargs(map)
    assert has_varargs(max) is None


def test_is_arity():
    assert is_arity(0, lambda: None)
    assert is_arity(1, lambda: None) is False
    assert is_arity(1, lambda x: None)
    assert is_arity(3, lambda x, y, z: None)
    assert is_arity(1, lambda x, *args: None) is False
    assert is_arity(1, lambda x, **kwargs: None) is False
    assert is_arity(1, all)
    assert is_arity(2, map) is False
    assert is_arity(2, range) is None


def test_introspect_curry_valid_py3(check_valid=is_valid_args, incomplete=False):
    orig_check_valid = check_valid
    check_valid = lambda _func, *args, **kwargs: orig_check_valid(_func, args, kwargs)

    f = cytoolz.curry(make_func('x, y, z=0'))
    assert check_valid(f)
    assert check_valid(f, 1)
    assert check_valid(f, 1, 2)
    assert check_valid(f, 1, 2, 3)
    assert check_valid(f, 1, 2, 3, 4) is False
    assert check_valid(f, invalid_keyword=True) is False
    assert check_valid(f(1))
    assert check_valid(f(1), 2)
    assert check_valid(f(1), 2, 3)
    assert check_valid(f(1), 2, 3, 4) is False
    assert check_valid(f(1), x=2) is False
    assert check_valid(f(1), y=2)
    assert check_valid(f(x=1), 2) is False
    assert check_valid(f(x=1), y=2)
    assert check_valid(f(y=2), 1)
    assert check_valid(f(y=2), 1, z=3)
    assert check_valid(f(y=2), 1, 3) is False

    f = cytoolz.curry(make_func('x, y, z=0'), 1, x=1)
    assert check_valid(f) is False
    assert check_valid(f, z=3) is False

    f = cytoolz.curry(make_func('x, y, *args, z'))
    assert check_valid(f)
    assert check_valid(f, 0)
    assert check_valid(f(1), 0)
    assert check_valid(f(1, 2), 0)
    assert check_valid(f(1, 2, 3), 0)
    assert check_valid(f(1, 2, 3, 4), 0)
    assert check_valid(f(1, 2, 3, 4), z=4)
    assert check_valid(f(x=1))
    assert check_valid(f(x=1), 1) is False
    assert check_valid(f(x=1), y=2)


def test_introspect_curry_partial_py3():
    test_introspect_curry_valid_py3(check_valid=is_partial_args, incomplete=True)


def test_introspect_curry_py3():
    f = cytoolz.curry(make_func(''))
    assert num_required_args(f) == 0
    assert is_arity(0, f)
    assert has_varargs(f) is False
    assert has_keywords(f) is False

    f = cytoolz.curry(make_func('x'))
    assert num_required_args(f) == 0
    assert is_arity(0, f) is False
    assert is_arity(1, f) is False
    assert has_varargs(f) is False
    assert has_keywords(f)  # A side-effect of being curried

    f = cytoolz.curry(make_func('x, y, z=0'))
    assert num_required_args(f) == 0
    assert is_arity(0, f) is False
    assert is_arity(1, f) is False
    assert is_arity(2, f) is False
    assert is_arity(3, f) is False
    assert has_varargs(f) is False
    assert has_keywords(f)

    f = cytoolz.curry(make_func('*args, **kwargs'))
    assert num_required_args(f) == 0
    assert has_varargs(f)
    assert has_keywords(f)


def test_introspect_builtin_modules():
    mods = [builtins, functools, itertools, operator, cytoolz,
            cytoolz.functoolz, cytoolz.itertoolz, cytoolz.dicttoolz, cytoolz.recipes]

    denylist = set()

    def add_denylist(mod, attr):
        if hasattr(mod, attr):
            denylist.add(getattr(mod, attr))

    add_denylist(builtins, 'basestring')
    add_denylist(builtins, 'NoneType')
    add_denylist(builtins, '__metaclass__')
    add_denylist(builtins, 'sequenceiterator')

    def is_missing(modname, name, func):
        if name.startswith('_') and not name.startswith('__'):
            return False
        if name.startswith('__pyx_unpickle_') or name.endswith('_cython__'):
            return False
        try:
            if issubclass(func, BaseException):
                return False
        except TypeError:
            pass
        try:
            return (callable(func)
                    and func.__module__ is not None
                    and modname in func.__module__
                    and is_partial_args(func, (), {}) is not True
                    and func not in denylist)
        except AttributeError:
            return False

    missing = {}
    for mod in mods:
        modname = mod.__name__
        for name, func in vars(mod).items():
            if is_missing(modname, name, func):
                if modname not in missing:
                    missing[modname] = []
                missing[modname].append(name)
    if missing:
        messages = []
        for modname, names in sorted(missing.items()):
            msg = '{}:\n    {}'.format(modname, '\n    '.join(sorted(names)))
            messages.append(msg)
        message = 'Missing introspection for the following callables:\n\n'
        raise AssertionError(message + '\n\n'.join(messages))


def test_inspect_signature_property():

    # By adding AddX to our signature registry, we can inspect the class
    # itself and objects of the class.  `inspect.signature` doesn't like
    # it when `obj.__signature__` is a property.
    class AddX:
        def __init__(self, func):
            self.func = func

        def __call__(self, addx, *args, **kwargs):
            return addx + self.func(*args, **kwargs)

        @property
        def __signature__(self):
            sig = inspect.signature(self.func)
            params = list(sig.parameters.values())
            kind = inspect.Parameter.POSITIONAL_OR_KEYWORD
            newparam = inspect.Parameter('addx', kind)
            params = [newparam] + params
            return sig.replace(parameters=params)

    addx = AddX(lambda x: x)
    sig = inspect.signature(addx)
    assert sig == inspect.Signature(parameters=[
        inspect.Parameter('addx', inspect.Parameter.POSITIONAL_OR_KEYWORD),
        inspect.Parameter('x', inspect.Parameter.POSITIONAL_OR_KEYWORD)])

    assert num_required_args(AddX) is False
    _sigs.signatures[AddX] = (_sigs.expand_sig((0, lambda func: None)),)
    assert num_required_args(AddX) == 1
    del _sigs.signatures[AddX]


def test_inspect_wrapped_property():
    class Wrapped:
        def __init__(self, func):
            self.func = func

        def __call__(self, *args, **kwargs):
            return self.func(*args, **kwargs)

        @property
        def __wrapped__(self):
            return self.func

    func = lambda x: x
    wrapped = Wrapped(func)
    assert inspect.signature(func) == inspect.signature(wrapped)

    # inspect.signature did not used to work properly on wrappers,
    # but it was fixed in Python 3.11.9, Python 3.12.3 and Python
    # 3.13+
    inspectbroken = True
    if sys.version_info.major > 3:
        inspectbroken = False
    if sys.version_info.minor == 11 and sys.version_info.micro > 8:
        inspectbroken = False
    if sys.version_info.minor == 12 and sys.version_info.micro > 2:
        inspectbroken = False
    if sys.version_info.minor > 12:
        inspectbroken = False

    if inspectbroken:
        assert num_required_args(Wrapped) is None
        _sigs.signatures[Wrapped] = (_sigs.expand_sig((0, lambda func: None)),)

    assert num_required_args(Wrapped) == 1
