""" Test that functions are reasonably behaved with None as input.

Typed Cython objects (like dict) may also be None.  Using functions from
Python's C API that expect a specific type but receive None instead can cause
problems such as throwing an uncatchable SystemError (and some systems may
segfault instead).  We obviously don't what that to happen!  As the tests
below discovered, this turned out to be a rare occurence.  The only changes
required were to use `d.copy()` instead of `PyDict_Copy(d)`, and to always
return Python objects from functions instead of int or bint (so exceptions
can propagate).

The vast majority of functions throw TypeError.  The vast majority of
functions also behave the same in `toolz` and `cytoolz`.  However, there
are a few minor exceptions.  Since passing None to functions are edge cases
that don't have well-established behavior yet (other than raising TypeError),
the tests in this file serve to verify that the behavior is at least
reasonably well-behaved and don't cause SystemErrors.

"""
# XXX: This file could be back-ported to `toolz` once unified testing exists.
import cytoolz
from cytoolz import *
from cytoolz.utils import raises
from operator import add


class GenException:
    def __init__(self, exc):
        self.exc = exc

    def __iter__(self):
        return self

    def __next__(self):
        raise self.exc

    def next(self):
        raise self.exc


def test_dicttoolz():
    tested = []
    assert raises((TypeError, AttributeError), lambda: assoc(None, 1, 2))
    tested.append('assoc')

    assert raises((TypeError, AttributeError), lambda: dissoc(None, 1))
    tested.append('dissoc')

    # XXX
    assert (raises(TypeError, lambda: get_in(None, {})) or
            get_in(None, {}) is None)

    assert raises(TypeError, lambda: get_in(None, {}, no_default=True))
    assert get_in([0, 1], None) is None
    assert raises(TypeError, lambda: get_in([0, 1], None, no_default=True))
    tested.append('get_in')

    assert raises(TypeError, lambda: keyfilter(None, {1: 2}))
    assert raises((AttributeError, TypeError), lambda: keyfilter(identity, None))
    tested.append('keyfilter')

    # XXX
    assert (raises(TypeError, lambda: keymap(None, {1: 2})) or
            keymap(None, {1: 2}) == {(1,): 2})
    assert raises((AttributeError, TypeError), lambda: keymap(identity, None))
    tested.append('keymap')

    assert raises(TypeError, lambda: merge(None))
    assert raises((TypeError, AttributeError), lambda: merge(None, None))
    tested.append('merge')

    assert raises(TypeError, lambda: merge_with(None, {1: 2}, {3: 4}))
    assert raises(TypeError, lambda: merge_with(identity, None))
    assert raises((TypeError, AttributeError),
                  lambda: merge_with(identity, None, None))
    tested.append('merge_with')

    assert raises(TypeError, lambda: update_in({1: {2: 3}}, [1, 2], None))
    assert raises(TypeError, lambda: update_in({1: {2: 3}}, None, identity))
    assert raises((TypeError, AttributeError),
                  lambda:  update_in(None, [1, 2], identity))
    tested.append('update_in')

    assert raises(TypeError, lambda: valfilter(None, {1: 2}))
    assert raises((AttributeError, TypeError), lambda: valfilter(identity, None))
    tested.append('valfilter')

    # XXX
    assert (raises(TypeError, lambda: valmap(None, {1: 2})) or
            valmap(None, {1: 2}) == {1: (2,)})
    assert raises((AttributeError, TypeError), lambda: valmap(identity, None))
    tested.append('valmap')

    assert (raises(TypeError, lambda: itemmap(None, {1: 2})) or
            itemmap(None, {1: 2}) == {1: (2,)})
    assert raises((AttributeError, TypeError), lambda: itemmap(identity, None))
    tested.append('itemmap')

    assert raises(TypeError, lambda: itemfilter(None, {1: 2}))
    assert raises((AttributeError, TypeError), lambda: itemfilter(identity, None))
    tested.append('itemfilter')

    assert raises((AttributeError, TypeError), lambda: assoc_in(None, [2, 2], 3))
    assert raises(TypeError, lambda: assoc_in({}, None, 3))
    tested.append('assoc_in')

    s1 = set(tested)
    s2 = set(cytoolz.dicttoolz.__all__)
    assert s1 == s2, '%s not tested for being None-safe' % ', '.join(s2 - s1)


def test_functoolz():
    tested = []
    assert raises(TypeError, lambda: complement(None)())
    tested.append('complement')

    assert compose(None) is None
    assert raises(TypeError, lambda: compose(None, None)())
    tested.append('compose')

    assert compose_left(None) is None
    assert raises(TypeError, lambda: compose_left(None, None)())
    tested.append('compose_left')

    assert raises(TypeError, lambda: curry(None))
    tested.append('curry')

    assert raises(TypeError, lambda: do(None, 1))
    tested.append('do')

    assert identity(None) is None
    tested.append('identity')

    assert raises(TypeError, lambda: juxt(None))
    assert raises(TypeError, lambda: list(juxt(None, None)()))
    tested.append('juxt')

    assert memoize(identity, key=None)(1) == 1
    assert memoize(identity, cache=None)(1) == 1
    tested.append('memoize')

    assert raises(TypeError, lambda: pipe(1, None))
    tested.append('pipe')

    assert thread_first(1, None) is None
    tested.append('thread_first')

    assert thread_last(1, None) is None
    tested.append('thread_last')

    assert flip(lambda a, b: (a, b))(None)(None) == (None, None)
    tested.append('flip')

    assert apply(identity, None) is None
    assert raises(TypeError, lambda: apply(None))
    tested.append('apply')

    excepts(None, lambda x: x)
    excepts(TypeError, None)
    tested.append('excepts')

    s1 = set(tested)
    s2 = set(cytoolz.functoolz.__all__)
    assert s1 == s2, '%s not tested for being None-safe' % ', '.join(s2 - s1)


def test_itertoolz():
    tested = []
    assert raises(TypeError, lambda: list(accumulate(None, [1, 2])))
    assert raises(TypeError, lambda: list(accumulate(identity, None)))
    tested.append('accumulate')

    assert raises(TypeError, lambda: concat(None))
    assert raises(TypeError, lambda: list(concat([None])))
    tested.append('concat')

    assert raises(TypeError, lambda: list(concatv(None)))
    tested.append('concatv')

    assert raises(TypeError, lambda: list(cons(1, None)))
    tested.append('cons')

    assert raises(TypeError, lambda: count(None))
    tested.append('count')

    # XXX
    assert (raises(TypeError, lambda: list(drop(None, [1, 2]))) or
            list(drop(None, [1, 2])) == [1, 2])

    assert raises(TypeError, lambda: list(drop(1, None)))
    tested.append('drop')

    assert raises(TypeError, lambda: first(None))
    tested.append('first')

    assert raises(TypeError, lambda: frequencies(None))
    tested.append('frequencies')

    assert raises(TypeError, lambda: get(1, None))
    assert raises(TypeError, lambda: get([1, 2], None))
    tested.append('get')

    assert raises(TypeError, lambda: groupby(None, [1, 2]))
    assert raises(TypeError, lambda: groupby(identity, None))
    tested.append('groupby')

    assert raises(TypeError, lambda: list(interleave(None)))
    assert raises(TypeError, lambda: list(interleave([None, None])))
    assert raises(TypeError,
                  lambda: list(interleave([[1, 2], GenException(ValueError)],
                                          pass_exceptions=None)))
    tested.append('interleave')

    assert raises(TypeError, lambda: list(interpose(1, None)))
    tested.append('interpose')

    assert raises(TypeError, lambda: isdistinct(None))
    tested.append('isdistinct')

    assert isiterable(None) is False
    tested.append('isiterable')

    assert raises(TypeError, lambda: list(iterate(None, 1)))
    tested.append('iterate')

    assert raises(TypeError, lambda: last(None))
    tested.append('last')

    # XXX
    assert (raises(TypeError, lambda: list(mapcat(None, [[1], [2]]))) or
            list(mapcat(None, [[1], [2]])) == [[1], [2]])
    assert raises(TypeError, lambda: list(mapcat(identity, [None, [2]])))
    assert raises(TypeError, lambda: list(mapcat(identity, None)))
    tested.append('mapcat')

    assert raises(TypeError, lambda: list(merge_sorted(None, [1, 2])))
    tested.append('merge_sorted')

    assert raises(TypeError, lambda: nth(None, [1, 2]))
    assert raises(TypeError, lambda: nth(0, None))
    tested.append('nth')

    assert raises(TypeError, lambda: partition(None, [1, 2, 3]))
    assert raises(TypeError, lambda: partition(1, None))
    tested.append('partition')

    assert raises(TypeError, lambda: list(partition_all(None, [1, 2, 3])))
    assert raises(TypeError, lambda: list(partition_all(1, None)))
    tested.append('partition_all')

    assert raises(TypeError, lambda: list(pluck(None, [[1], [2]])))
    assert raises(TypeError, lambda: list(pluck(0, [None, [2]])))
    assert raises(TypeError, lambda: list(pluck(0, None)))
    tested.append('pluck')

    assert raises(TypeError, lambda: reduceby(None, add, [1, 2, 3], 0))
    assert raises(TypeError, lambda: reduceby(identity, None, [1, 2, 3], 0))
    assert raises(TypeError, lambda: reduceby(identity, add, None, 0))
    tested.append('reduceby')

    assert raises(TypeError, lambda: list(remove(None, [1, 2])))
    assert raises(TypeError, lambda: list(remove(identity, None)))
    tested.append('remove')

    assert raises(TypeError, lambda: second(None))
    tested.append('second')

    # XXX
    assert (raises(TypeError, lambda: list(sliding_window(None, [1, 2, 3]))) or
            list(sliding_window(None, [1, 2, 3])) == [])
    assert raises(TypeError, lambda: list(sliding_window(1, None)))
    tested.append('sliding_window')

    # XXX
    assert (raises(TypeError, lambda: list(take(None, [1, 2])) == [1, 2]) or
            list(take(None, [1, 2])) == [1, 2])
    assert raises(TypeError, lambda: list(take(1, None)))
    tested.append('take')

    # XXX
    assert (raises(TypeError, lambda: list(tail(None, [1, 2])) == [1, 2]) or
            list(tail(None, [1, 2])) == [1, 2])
    assert raises(TypeError, lambda: list(tail(1, None)))
    tested.append('tail')

    # XXX
    assert (raises(TypeError, lambda: list(take_nth(None, [1, 2]))) or
            list(take_nth(None, [1, 2])) == [1, 2])
    assert raises(TypeError, lambda: list(take_nth(1, None)))
    tested.append('take_nth')

    assert raises(TypeError, lambda: list(unique(None)))
    assert list(unique([1, 1, 2], key=None)) == [1, 2]
    tested.append('unique')

    assert raises(TypeError, lambda: join(first, None, second, (1, 2, 3)))
    assert raises(TypeError, lambda: join(first, (1, 2, 3), second, None))
    tested.append('join')

    assert raises(TypeError, lambda: topk(None, [1, 2, 3]))
    assert raises(TypeError, lambda: topk(3, None))
    tested.append('topk')

    assert raises(TypeError, lambda: list(diff(None, [1, 2, 3])))
    assert raises(TypeError, lambda: list(diff(None)))
    assert raises(TypeError, lambda: list(diff([None])))
    assert raises(TypeError, lambda: list(diff([None, None])))
    tested.append('diff')

    assert raises(TypeError, lambda: peek(None))
    tested.append('peek')

    assert raises(TypeError, lambda: peekn(None, [1, 2, 3]))
    assert raises(TypeError, lambda: peekn(3, None))
    tested.append('peekn')

    assert raises(TypeError, lambda: list(random_sample(None, [1])))
    assert raises(TypeError, lambda: list(random_sample(0.1, None)))
    tested.append('random_sample')

    s1 = set(tested)
    s2 = set(cytoolz.itertoolz.__all__)
    assert s1 == s2, '%s not tested for being None-safe' % ', '.join(s2 - s1)


def test_recipes():
    tested = []
    # XXX
    assert (raises(TypeError, lambda: countby(None, [1, 2])) or
            countby(None, [1, 2]) == {(1,): 1, (2,): 1})
    assert raises(TypeError, lambda: countby(identity, None))
    tested.append('countby')

    # XXX
    assert (raises(TypeError, lambda: list(partitionby(None, [1, 2]))) or
            list(partitionby(None, [1, 2])) == [(1,), (2,)])
    assert raises(TypeError, lambda: list(partitionby(identity, None)))
    tested.append('partitionby')

    s1 = set(tested)
    s2 = set(cytoolz.recipes.__all__)
    assert s1 == s2, '%s not tested for being None-safe' % ', '.join(s2 - s1)
