from collections import OrderedDict
from itertools import chain
from operator import itemgetter

import pytest

from snakeoil import sequences
from snakeoil._internals import deprecated
from snakeoil.sequences import (
    iter_stable_unique,
    split_elements,
    split_negations,
    unique_stable,
)


class UnhashableComplex(complex):
    def __hash__(self):
        raise TypeError


@deprecated.suppress_deprecations()
class TestStableUnique:
    def common_check(self, func):
        # silly
        assert func(()) == []
        # hashable
        assert sorted(func([1, 1, 2, 3, 2])) == [1, 2, 3]
        # neither

    def test_stable_unique(self, func=sequences.stable_unique):
        assert list(set([1, 2, 3])) == [1, 2, 3], (
            "this test is reliant on the interpreter hasing 1,2,3 into a specific ordering- "
            "for whatever reason, ordering differs, thus this test can't verify it"
        )
        assert func([3, 2, 1]) == [3, 2, 1]

    def test_iter_stable_unique(self):
        self.test_stable_unique(lambda x: list(sequences.iter_stable_unique(x)))
        o = UnhashableComplex()
        l = [1, 2, 3, o, UnhashableComplex(), 4, 3, UnhashableComplex()]
        assert list(sequences.iter_stable_unique(l)) == [1, 2, 3, o, 4]

    def test_TypeError_propagation(self):
        def iterator():
            raise TypeError()
            yield

        pytest.raises(TypeError, lambda: list(iter_stable_unique(iterator())))

    def _generator(self):
        for x in range(5, -1, -1):
            yield x

    def test_unstable_unique(self):
        self.common_check(sequences.unstable_unique)
        uc = UnhashableComplex
        res = sequences.unstable_unique([uc(1, 0), uc(0, 1), uc(1, 0)])
        # sortable
        assert sorted(sequences.unstable_unique([[1, 2], [1, 3], [1, 2], [1, 3]])) == [
            [1, 2],
            [1, 3],
        ]
        assert res == [uc(1, 0), uc(0, 1)] or res == [uc(0, 1), uc(1, 0)]
        assert sorted(sequences.unstable_unique(self._generator())) == sorted(range(6))


class Test_iflatten_instance:
    func = staticmethod(sequences.iflatten_instance)

    def test_it(self):
        o = OrderedDict((k, None) for k in range(10))
        for l, correct, skip in (
            (
                ["asdf", ["asdf", "asdf"], 1, None],
                ["asdf", "asdf", "asdf", 1, None],
                str,
            ),
            ([o, 1, "fds"], [o, 1, "fds"], (str, OrderedDict)),
            ([o, 1, "fds"], list(range(10)) + [1, "fds"], str),
            ("fds", ["fds"], str),
            ("fds", ["f", "d", "s"], int),
            ("", [""], str),
            (1, [1], int),
        ):
            iterator = self.func(l, skip)
            assert list(iterator) == correct
            assert list(iterator) == []

        # There is a small difference between the cpython and native
        # version: the cpython one raises immediately, for native we
        # have to iterate.
        def fail():
            return list(self.func(None))

        with pytest.raises(TypeError):
            fail()

        # Yes, no sane code does this, but even insane code shouldn't
        # kill the cpython version.
        iters = []
        iterator = self.func(iters)
        iters.append(iterator)
        with pytest.raises(ValueError):
            next(iterator)

        # Regression test: this was triggered through demandload.
        # **{} is there to explicitly force a dict.
        assert self.func((), **{})


class Test_iflatten_func:
    func = staticmethod(sequences.iflatten_func)

    def test_it(self):
        o = OrderedDict((k, None) for k in range(10))
        for l, correct, skip in (
            (
                ["asdf", ["asdf", "asdf"], 1, None],
                ["asdf", "asdf", "asdf", 1, None],
                str,
            ),
            ([o, 1, "fds"], [o, 1, "fds"], (str, OrderedDict)),
            ([o, 1, "fds"], list(range(10)) + [1, "fds"], str),
            ("fds", ["fds"], str),
            (1, [1], int),
        ):
            iterator = self.func(l, lambda x: isinstance(x, skip))
            assert list(iterator) == correct
            assert list(iterator) == []

        # There is a small difference between the cpython and native
        # version: the cpython one raises immediately, for native we
        # have to iterate.
        def fail():
            return list(self.func(None, lambda x: False))

        with pytest.raises(TypeError):
            fail()

        # Yes, no sane code does this, but even insane code shouldn't
        # kill the cpython version.
        iters = []
        iterator = self.func(iters, lambda x: False)
        iters.append(iterator)
        with pytest.raises(ValueError):
            next(iterator)

        # Regression test: this was triggered through demandload.
        # **{} is there to explicitly force a dict to the underlay cpy
        assert self.func((), lambda x: True, **{})


class Test_predicate_split:
    kls = staticmethod(sequences.predicate_split)

    def test_simple(self):
        false_l, true_l = self.kls(lambda x: x % 2 == 0, range(100))
        assert false_l == list(range(1, 100, 2))
        assert true_l == list(range(0, 100, 2))

    def test_key(self):
        false_l, true_l = self.kls(
            lambda x: x % 2 == 0, ([0, x] for x in range(100)), key=itemgetter(1)
        )
        assert false_l == [[0, x] for x in range(1, 100, 2)]
        assert true_l == [[0, x] for x in range(0, 100, 2)]


class TestSplitNegations:
    def test_empty(self):
        # empty input
        seq = ""
        assert split_negations(seq) == ((), ())

    def test_bad_value(self):
        # no-value negation should raise a ValueError
        bad_values = (
            "-",
            "a b c - d f e",
        )

        for s in bad_values:
            with pytest.raises(ValueError):
                split_negations(s.split())

    def test_negs(self):
        # all negs
        seq = ("-" + str(x) for x in range(100))
        assert split_negations(seq) == (tuple(map(str, range(100))), ())

    def test_pos(self):
        # all pos
        seq = (str(x) for x in range(100))
        assert split_negations(seq) == ((), tuple(map(str, range(100))))

    def test_neg_pos(self):
        # both
        seq = (("-" + str(x), str(x)) for x in range(100))
        seq = chain.from_iterable(seq)
        assert split_negations(seq) == (
            tuple(map(str, range(100))),
            tuple(map(str, range(100))),
        )

    def test_converter(self):
        # converter method
        seq = (("-" + str(x), str(x)) for x in range(100))
        seq = chain.from_iterable(seq)
        assert split_negations(seq, int) == (tuple(range(100)), tuple(range(100)))


class TestSplitElements:
    def test_empty(self):
        # empty input
        seq = ""
        assert split_elements(seq) == ((), (), ())

    def test_bad_value(self):
        # no-value neg/pos should raise ValueErrors
        bad_values = (
            "-",
            "+",
            "a b c - d f e",
            "a b c + d f e",
        )

        for s in bad_values:
            with pytest.raises(ValueError):
                split_elements(s.split())

    def test_negs(self):
        # all negs
        seq = ("-" + str(x) for x in range(100))
        assert split_elements(seq) == (tuple(map(str, range(100))), (), ())

    def test_neutral(self):
        # all neutral
        seq = (str(x) for x in range(100))
        assert split_elements(seq) == ((), tuple(map(str, range(100))), ())

    def test_pos(self):
        # all pos
        seq = ("+" + str(x) for x in range(100))
        assert split_elements(seq) == ((), (), tuple(map(str, range(100))))

    def test_neg_pos(self):
        # both negative and positive values
        seq = (("-" + str(x), "+" + str(x)) for x in range(100))
        seq = chain.from_iterable(seq)
        assert split_elements(seq) == (
            tuple(map(str, range(100))),
            (),
            tuple(map(str, range(100))),
        )

    def test_neg_neu_pos(self):
        # all three value types
        seq = (("-" + str(x), str(x), "+" + str(x)) for x in range(100))
        seq = chain.from_iterable(seq)
        assert split_elements(seq) == (
            tuple(map(str, range(100))),
            tuple(map(str, range(100))),
            tuple(map(str, range(100))),
        )

    def test_converter(self):
        # converter method
        seq = (("-" + str(x), str(x), "+" + str(x)) for x in range(100))
        seq = chain.from_iterable(seq)
        assert split_elements(seq, int) == (
            tuple(range(100)),
            tuple(range(100)),
            tuple(range(100)),
        )


def test_unique_stable():
    with pytest.raises(TypeError):
        # verify our assumptions of object being unhashable
        hash([])
    with pytest.raises(TypeError):
        list(unique_stable([[]]))  # pyright: ignore[reportArgumentType]

    # force iter so it's not possible to infer from the type.
    assert ["c", "a", "b"] == list(
        unique_stable(iter(["c", "a", "a", "c", "a", "b", "b"]))
    )
