from functools import partial
from humpy_toolz.functoolz import (
	apply, complement, compose, compose_left, curry, do, excepts, flip, juxt, memoize, pipe, thread_first, thread_last)
from humpy_toolz.utils import raises
from operator import add, itemgetter, mul
from typing import NoReturn
import humpy_toolz
import inspect

def iseven(x: int) -> bool:
    return x % 2 == 0

def isodd(x: int) -> bool:
    return x % 2 == 1

def inc(x: int) -> int:
    return x + 1

def double(x: int) -> int:
    return 2 * x

class AlwaysEquals:
    """useful to test correct __eq__ implementation of other objects"""

    def __eq__(self, other: object) -> bool:
        return True

    def __ne__(self, other: object) -> bool:
        return False

class NeverEquals:
    """useful to test correct __eq__ implementation of other objects"""

    def __eq__(self, other: object) -> bool:
        return False

    def __ne__(self, other: object) -> bool:
        return True

def test_apply() -> None:
    assert apply(double, 5) == 10
    assert tuple(map(apply, [double, inc, double], [10, 500, 8000])) == (20, 501, 16000)
    assert raises(TypeError, apply)

def test_thread_first() -> None:
    assert thread_first(2) == 2
    assert thread_first(2, inc) == 3
    assert thread_first(2, inc, inc) == 4
    assert thread_first(2, double, inc) == 5
    assert thread_first(2, (add, 5), double) == 14

def test_thread_last() -> None:
    assert list(thread_last([1, 2, 3], (map, inc), (filter, iseven))) == [2, 4]
    assert list(thread_last([1, 2, 3], (map, inc), (filter, isodd))) == [3]
    assert thread_last(2, (add, 5), double) == 14

def test_memoize() -> None:
    fn_calls: list[int] = [0]

    def f(x: int, y: int) -> int:
        """ A docstring """
        fn_calls[0] += 1
        return x + y
    mf = memoize(f)
    assert mf(2, 3) is mf(2, 3)
    assert fn_calls == [1]
    assert mf.__doc__ == f.__doc__
    assert raises(TypeError, lambda: mf(1, {}))

def test_memoize_kwargs() -> None:
    fn_calls: list[int] = [0]

    def f(x: int, y: int = 0) -> int:
        return x + y
    mf = memoize(f)
    assert mf(1) == f(1)
    assert mf(1, 2) == f(1, 2)
    assert mf(1, y=2) == f(1, y=2)
    assert mf(1, y=3) == f(1, y=3)

def test_memoize_curried() -> None:

    @curry
    def f(x: int, y: int = 0) -> int:
        return x + y
    f2 = f(y=1)
    fm2 = memoize(f2)
    assert fm2(3) == f2(3)
    assert fm2(3) == f2(3)

def test_memoize_partial() -> None:

    def f(x: int, y: int = 0) -> int:
        return x + y
    f2 = partial(f, y=1)
    fm2 = memoize(f2)
    assert fm2(3) == f2(3)
    assert fm2(3) == f2(3)

def test_memoize_key_signature() -> None:
    mf = memoize(lambda x: False, cache={1: True})
    assert mf(1) is True
    assert mf(2) is False
    mf = memoize(lambda x, *args: False, cache={(1,): True, (1, 2): 2})
    assert mf(1) is True
    assert mf(2) is False
    assert mf(1, 1) is False
    assert mf(1, 2) == 2
    assert mf((1, 2)) is False
    mf = memoize(lambda x, y: False, cache={(1, 2): True})
    assert mf(1, 2) is True
    assert mf(1, 3) is False
    assert raises(TypeError, lambda: mf((1, 2)))
    mf = memoize(lambda: False, cache={(): True})
    assert mf() is True
    mf = memoize(lambda x, y=0: False, cache={((1,), frozenset((('y', 2),))): 2, ((1, 2), None): 3})
    assert mf(1, y=2) == 2
    assert mf(1, 2) == 3
    assert mf(2, y=2) is False
    assert mf(2, 2) is False
    assert mf(1) is False
    assert mf((1, 2)) is False
    mf = memoize(lambda x=0: False, cache={(None, frozenset((('x', 1),))): 1, ((1,), None): 2})
    assert mf() is False
    assert mf(x=1) == 1
    assert mf(1) == 2

def test_memoize_curry_cache() -> None:

    @memoize(cache={1: True})
    def f(x: int) -> bool:
        return False
    assert f(1) is True
    assert f(2) is False

def test_memoize_key() -> None:

    @memoize(key=lambda args, kwargs: args[0])
    def f(x: int, y: int, *args: int, **kwargs: int) -> int:
        return x + y
    assert f(1, 2) == 3
    assert f(1, 3) == 3

def test_memoize_wrapped() -> None:

    def foo() -> None:
        """
        Docstring
        """
    memoized_foo = memoize(foo)
    assert memoized_foo.__wrapped__ is foo

def test_curry_simple() -> None:
    cmul = curry(mul)
    double = cmul(2)
    assert callable(double)
    assert double(10) == 20
    assert repr(cmul) == repr(mul)
    cmap = curry(map)
    assert list(cmap(inc)([1, 2, 3])) == [2, 3, 4]
    assert raises(TypeError, lambda: curry())
    assert raises(TypeError, lambda: curry({1: 2}))

def test_curry_kwargs() -> None:

    def f(a: int, b: int, c: int = 10) -> int:
        return (a + b) * c
    f = curry(f)
    assert f(1, 2, 3) == 9
    assert f(1)(2, 3) == 9
    assert f(1, 2) == 30
    assert f(1, c=3)(2) == 9
    assert f(c=3)(1, 2) == 9

    def g(a: int = 1, b: int = 10, c: int = 0) -> int:
        return a + b + c
    cg = curry(g, b=2)
    assert cg() == 3
    assert cg(b=3) == 4
    assert cg(a=0) == 2
    assert cg(a=0, b=1) == 1
    assert cg(0) == 2
    assert raises(TypeError, lambda: cg(1, 2))

    def h(x: float, func: type[int] | type[str] = int) -> int | str:
        return func(x)
    assert curry(h)(0.0) == 0
    assert curry(h)(func=str)(0.0) == '0.0'
    assert curry(h, func=str)(0.0) == '0.0'

def test_curry_passes_errors() -> None:

    @curry
    def f(a: int | str, b: int) -> int:
        if not isinstance(a, int):
            raise TypeError
        return a + b
    assert f(1, 2) == 3
    assert raises(TypeError, lambda: f('1', 2))
    assert raises(TypeError, lambda: f('1')(2))
    assert raises(TypeError, lambda: f(1, 2, 3))

def test_curry_docstring() -> None:

    def f(x: int, y: int) -> int:
        """ A docstring """
        return x
    g = curry(f)
    assert g.__doc__ == f.__doc__
    assert str(g) == str(f)
    assert f(1, 2) == g(1, 2)

def test_curry_is_like_partial() -> None:

    def foo(a: int, b: int, c: int = 1) -> int:
        return a + b + c
    p, c = (partial(foo, 1, c=2), curry(foo)(1, c=2))
    assert p.keywords == c.keywords
    assert p.args == c.args
    assert p(3) == c(3)
    p, c = (partial(foo, 1), curry(foo)(1))
    assert p.keywords == c.keywords
    assert p.args == c.args
    assert p(3) == c(3)
    assert p(3, c=2) == c(3, c=2)
    p, c = (partial(foo, c=1), curry(foo)(c=1))
    assert p.keywords == c.keywords
    assert p.args == c.args
    assert p(1, 2) == c(1, 2)

def test_curry_is_idempotent() -> None:

    def foo(a: int, b: int, c: int = 1) -> int:
        return a + b + c
    f = curry(foo, 1, c=2)
    g = curry(f)
    assert isinstance(f, curry)
    assert isinstance(g, curry)
    assert not isinstance(g.func, curry)
    assert not hasattr(g.func, 'func')
    assert f.func == g.func
    assert f.args == g.args
    assert f.keywords == g.keywords

def test_curry_attributes_readonly() -> None:

    def foo(a: int, b: int, c: int = 1) -> int:
        return a + b + c
    f = curry(foo, 1, c=2)
    assert raises(AttributeError, lambda: setattr(f, 'args', (2,)))
    assert raises(AttributeError, lambda: setattr(f, 'keywords', {'c': 3}))
    assert raises(AttributeError, lambda: setattr(f, 'func', f))
    assert raises(AttributeError, lambda: delattr(f, 'args'))
    assert raises(AttributeError, lambda: delattr(f, 'keywords'))
    assert raises(AttributeError, lambda: delattr(f, 'func'))

def test_curry_attributes_writable() -> None:

    def foo(a: int, b: int, c: int = 1) -> int:
        return a + b + c
    foo.__qualname__ = 'this.is.foo'
    f = curry(foo, 1, c=2)
    assert f.__qualname__ == 'this.is.foo'
    f.__name__ = 'newname'
    f.__doc__ = 'newdoc'
    f.__module__ = 'newmodule'
    f.__qualname__ = 'newqualname'
    assert f.__name__ == 'newname'
    assert f.__doc__ == 'newdoc'
    assert f.__module__ == 'newmodule'
    assert f.__qualname__ == 'newqualname'
    if hasattr(f, 'func_name'):
        assert f.__name__ == f.func_name

def test_curry_module() -> None:
    from humpy_toolz.curried.exceptions import merge
    assert merge.__module__ == 'humpy_toolz.curried.exceptions'

def test_curry_comparable() -> None:

    def foo(a: int, b: int, c: int = 1) -> int:
        return a + b + c
    f1 = curry(foo, 1, c=2)
    f2 = curry(foo, 1, c=2)
    g1 = curry(foo, 1, c=3)
    h1 = curry(foo, c=2)
    h2 = h1(c=2)
    h3 = h1()
    assert f1 == f2
    assert not f1 != f2
    assert f1 != g1
    assert not f1 == g1
    assert f1 != h1
    assert h1 == h2
    assert h1 == h3

    def bar(a: int, b: int, c: int = 1) -> int:
        return a + b + c
    b1 = curry(bar, 1, c=2)
    assert b1 != f1
    assert {f1, f2, g1, h1, h2, h3, b1, b1()} == {f1, g1, h1, b1}
    unhash1 = curry(foo, [])
    assert raises(TypeError, lambda: hash(unhash1))
    unhash2 = curry(foo, c=[])
    assert raises(TypeError, lambda: hash(unhash2))

def test_curry_doesnot_transmogrify() -> None:

    def f(x: int, y: int = 0) -> int:
        return x + y
    cf = curry(f)
    assert cf(y=1)(y=2)(y=3)(1) == f(1, 3)

def test_curry_on_classmethods() -> None:

    class A:
        BASE = 10

        def __init__(self, base: int) -> None:
            self.BASE = base

        @curry
        def addmethod(self, x: int, y: int) -> int:
            return self.BASE + x + y

        @classmethod
        @curry
        def addclass(cls, x: int, y: int) -> int:
            return cls.BASE + x + y

        @staticmethod
        @curry
        def addstatic(x: int, y: int) -> int:
            return x + y
    a = A(100)
    assert a.addmethod(3, 4) == 107
    assert a.addmethod(3)(4) == 107
    assert A.addmethod(a, 3, 4) == 107
    assert A.addmethod(a)(3)(4) == 107
    assert a.addclass(3, 4) == 17
    assert a.addclass(3)(4) == 17
    assert A.addclass(3, 4) == 17
    assert A.addclass(3)(4) == 17
    assert a.addstatic(3, 4) == 7
    assert a.addstatic(3)(4) == 7
    assert A.addstatic(3, 4) == 7
    assert A.addstatic(3)(4) == 7
    assert isinstance(a.addmethod, curry)
    assert isinstance(A.addmethod, curry)

def test_memoize_on_classmethods() -> None:

    class A:
        BASE = 10
        HASH = 10

        def __init__(self, base: int) -> None:
            self.BASE = base

        @memoize
        def addmethod(self, x: int, y: int) -> int:
            return self.BASE + x + y

        @classmethod
        @memoize
        def addclass(cls, x: int, y: int) -> int:
            return cls.BASE + x + y

        @staticmethod
        @memoize
        def addstatic(x: int, y: int) -> int:
            return x + y

        def __hash__(self) -> int:
            return self.HASH
    a = A(100)
    assert a.addmethod(3, 4) == 107
    assert A.addmethod(a, 3, 4) == 107
    a.BASE = 200
    assert a.addmethod(3, 4) == 107
    a.HASH = 200
    assert a.addmethod(3, 4) == 207
    assert a.addclass(3, 4) == 17
    assert A.addclass(3, 4) == 17
    A.BASE = 20
    assert A.addclass(3, 4) == 17
    A.HASH = 20
    assert A.addclass(3, 4) == 17
    assert a.addstatic(3, 4) == 7
    assert A.addstatic(3, 4) == 7

def test_curry_call() -> None:

    @curry
    def add(x: int, y: int) -> int:
        return x + y
    assert raises(TypeError, lambda: add.call(1))
    assert add(1)(2) == add.call(1, 2)
    assert add(1)(2) == add(1).call(2)

def test_curry_bind() -> None:

    @curry
    def add(x: int = 1, y: int = 2) -> int:
        return x + y
    assert add() == add(1, 2)
    assert add.bind(10)(20) == add(10, 20)
    assert add.bind(10).bind(20)() == add(10, 20)
    assert add.bind(x=10)(y=20) == add(10, 20)
    assert add.bind(x=10).bind(y=20)() == add(10, 20)

def test_curry_unknown_args() -> None:

    def add3(x: int, y: int, z: int) -> int:
        return x + y + z

    @curry
    def f(*args: int) -> int:
        return add3(*args)
    assert f()(1)(2)(3) == 6
    assert f(1)(2)(3) == 6
    assert f(1, 2)(3) == 6
    assert f(1, 2, 3) == 6
    assert f(1, 2)(3, 4) == f(1, 2, 3, 4)

def test_curry_bad_types() -> None:
    assert raises(TypeError, lambda: curry(1))

def test_curry_subclassable() -> None:

    class mycurry(curry):
        pass
    add = mycurry(lambda x, y: x + y)
    assert isinstance(add, curry)
    assert isinstance(add, mycurry)
    assert isinstance(add(1), mycurry)
    assert isinstance(add()(1), mycurry)
    assert add(1)(2) == 3
    '\n    class curry2(curry):\n        def _should_curry(self, args, kwargs, exc=None):\n            return len(self.args) + len(args) < 2\n\n    add = curry2(lambda x, y: x+y)\n    assert isinstance(add(1), curry2)\n    assert add(1)(2) == 3\n    assert isinstance(add(1)(x=2), curry2)\n    assert raises(TypeError, lambda: add(1)(x=2)(3))\n    '

def generate_compose_test_cases():
    """
    Generate test cases for parametrized tests of the compose function.
    """

    def add_then_multiply(a: int, b: int, c: int = 10) -> int:
        return (a + b) * c
    return (((), (0,), {}, 0), ((inc,), (0,), {}, 1), ((double, inc), (0,), {}, 2), ((str, iseven, inc, double), (3,), {}, 'False'), ((str, add), (1, 2), {}, '3'), ((str, inc, add_then_multiply), (1, 2), {'c': 3}, '10'))

def test_compose() -> None:
    for compose_args, args, kw, expected in generate_compose_test_cases():
        assert compose(*compose_args)(*args, **kw) == expected

def test_compose_metadata() -> None:

    def f(a: object) -> object:
        return a

    def g(a: object) -> object:
        return a
    composed = compose(f, g)
    assert composed.__name__ == 'f_of_g'
    assert composed.__doc__ == 'lambda *args, **kwargs: f(g(*args, **kwargs))'
    h = object()
    composed = compose(f, h)
    assert composed.__name__ == 'Compose'
    assert composed.__doc__ == 'A composition of functions'
    assert repr(composed) == f'Compose({f!r}, {h!r})'
    assert composed == compose(f, h)
    assert composed == AlwaysEquals()
    assert not composed == compose(h, f)
    assert not composed == object()
    assert not composed == NeverEquals()
    assert composed != compose(h, f)
    assert composed != NeverEquals()
    assert composed != object()
    assert not composed != compose(f, h)
    assert not composed != AlwaysEquals()
    assert hash(composed) == hash(compose(f, h))
    assert hash(composed) != hash(compose(h, f))
    bindable = compose(str, lambda x: x * 2, lambda x, y=0: int(x) + y)

    class MyClass:

        def __int__(self) -> int:
            return 8
        my_method = bindable
        my_static_method = staticmethod(bindable)
    assert MyClass.my_method(3) == '6'
    assert MyClass.my_method(3, y=2) == '10'
    assert MyClass.my_static_method(2) == '4'
    assert MyClass().my_method() == '16'
    assert MyClass().my_method(y=3) == '22'
    assert MyClass().my_static_method(0) == '0'
    assert MyClass().my_static_method(0, 1) == '2'
    assert compose(f, h).__wrapped__ is h
    if hasattr(humpy_toolz, 'sandbox'):
        assert compose(f, h).__class__.__wrapped__ is None

    def myfunc(a: int, b: str, c: float, *d: int, **e: bool) -> int:
        return 4

    def otherfunc(f: int) -> str:
        return f'result: {f}'
    myfunc.__annotations__ = {'a': int, 'b': str, 'c': float, 'd': int, 'e': bool, 'return': int}
    otherfunc.__annotations__ = {'f': int, 'return': str}
    composed = compose(otherfunc, myfunc)
    sig = inspect.signature(composed)
    assert sig.parameters == inspect.signature(myfunc).parameters
    assert sig.return_annotation == str

    class MyClass:
        method = composed
    assert len(inspect.signature(MyClass().method).parameters) == 4

def generate_compose_left_test_cases():
    """
    Generate test cases for parametrized tests of the compose function.

    These are based on, and equivalent to, those produced by
    generate_compose_test_cases().
    """
    return tuple(((tuple(reversed(compose_args)), args, kwargs, expected) for compose_args, args, kwargs, expected in generate_compose_test_cases()))

def test_compose_left() -> None:
    for compose_left_args, args, kw, expected in generate_compose_left_test_cases():
        assert compose_left(*compose_left_args)(*args, **kw) == expected

def test_pipe() -> None:
    assert pipe(1, inc) == 2
    assert pipe(1, inc, inc) == 3
    assert pipe(1, double, inc, iseven) is False

def test_complement() -> None:
    assert complement(lambda: False)()
    assert not complement(lambda: True)()
    assert complement(iseven)(1)
    assert not complement(iseven)(2)
    assert complement(complement(iseven))(2)
    assert not complement(complement(isodd))(2)
    both_even = lambda a, b: iseven(a) and iseven(b)
    assert complement(both_even)(1, 2)
    assert not complement(both_even)(2, 2)
    assert complement(lambda: '')()
    assert complement(lambda: 0)()
    assert complement(lambda: None)()
    assert complement(lambda: [])()
    assert not complement(lambda: 'x')()
    assert not complement(lambda: 1)()
    assert not complement(lambda: [1])()

def test_do() -> None:
    inc = lambda x: x + 1
    assert do(inc, 1) == 1
    log: list[int] = []
    assert do(log.append, 1) == 1
    assert log == [1]

def test_juxt_generator_input() -> None:
    data: list[int] = list(range(10))
    juxtfunc = juxt((itemgetter(2 * i) for i in range(5)))
    assert juxtfunc(data) == (0, 2, 4, 6, 8)
    assert juxtfunc(data) == (0, 2, 4, 6, 8)

def test_flip() -> None:

    def f(a: str, b: str) -> tuple[str, str]:
        return (a, b)
    assert flip(f, 'a', 'b') == ('b', 'a')

def test_excepts() -> None:
    assert excepts.__name__ == 'excepts'
    testlines: str = '\n'.join((line.strip() for line in excepts.__doc__.splitlines()))
    assert 'A wrapper around a function to catch exceptions and\ndispatch to a handler.\n' in testlines

    def idx(a: int) -> int:
        """idx docstring
        """
        return [1, 2].index(a)

    def handler(e):
        """handler docstring
        """
        assert isinstance(e, ValueError)
        return -1
    excepting = excepts(ValueError, idx, handler)
    assert excepting(1) == 0
    assert excepting(2) == 1
    assert excepting(3) == -1
    assert excepting.__name__ == 'idx_excepting_ValueError'
    assert 'idx docstring' in excepting.__doc__
    assert 'ValueError' in excepting.__doc__
    assert 'handler docstring' in excepting.__doc__

    def getzero(a: list[int] | dict[int, int]) -> int:
        """getzero docstring
        """
        return a[0]
    excepting = excepts((IndexError, KeyError), getzero)
    assert excepting([]) is None
    assert excepting([1]) == 1
    assert excepting({}) is None
    assert excepting({0: 1}) == 1
    assert excepting.__name__ == 'getzero_excepting_IndexError_or_KeyError'
    assert 'getzero docstring' in excepting.__doc__
    assert 'return_none' in excepting.__doc__
    assert 'Returns None' in excepting.__doc__

    def raise_(a) -> NoReturn:
        """A function that raises an instance of the exception type given.
        """
        raise a()
    excepting = excepts((ValueError, KeyError), raise_)
    assert excepting(ValueError) is None
    assert excepting(KeyError) is None
    assert raises(TypeError, lambda: excepting(TypeError))
    assert raises(NotImplementedError, lambda: excepting(NotImplementedError))
    excepting = excepts(object(), object(), object())
    assert excepting.__name__ == 'excepting'
    assert excepting.__doc__ == excepts.__doc__
