from __future__ import generator_stop

from collections import defaultdict
from functools import wraps
from itertools import product, zip_longest
from os import remove
from tempfile import NamedTemporaryFile
from time import sleep
from typing import TYPE_CHECKING
from unittest import TestCase
from unittest.result import TestResult
from unittest.runner import TextTestRunner

from .file import _check_arguments, equal_files
from .signal import HandleKeyboardInterrupt  # problem here

if TYPE_CHECKING:
    from typing import Any, Callable, Collection, DefaultDict, Hashable, Iterable, Mapping, Optional, Tuple, TypeVar

    T = TypeVar("T")
    U = TypeVar("U")


class nullcontext:  # see: .compat.contextlib
    def __init__(self, *args, **kwargs):
        pass

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        pass


anullcontext = nullcontext()


class MyTestResult(TestResult):
    """Extension of TestResult to support numbering test cases"""

    def __init__(self, stream, descriptions, verbosity):
        """Initializes the test number generator, then calls super impl"""
        self.stream = stream
        self.current_test = 0
        self.current_subtest = 0
        return TestResult.__init__(self, stream, descriptions, verbosity)

    def progress(self):
        self.stream.write(f"Running test #{self.current_test}, subtest #{self.current_subtest}\r")

    def startTest(self, test):
        self.current_test += 1
        TestResult.startTest(self, test)
        self.progress()

    def stopTest(self, test):
        TestResult.stopTest(self, test)

    def stopTestRun(self):
        self.stream.write("\n")
        TestResult.stopTestRun(self)

    def addSubTest(self, test, subtest, outcome):
        """Called when a subtest finishes."""

        TestResult.addSubTest(self, test, subtest, outcome)
        self.testsRun += 1
        self.current_subtest += 1
        self.progress()


class MyTestRunner(TextTestRunner):
    """Extension of TestRunner to support numbering test cases"""

    resultclass = MyTestResult

    def run(self, test):
        """Stores the total count of test cases, then calls super impl"""

        self.total_test_cases = test.countTestCases()
        return TextTestRunner.run(self, test)

    def _makeResult(self):
        return TextTestRunner._makeResult(self)


class NoRaise:
    def __init__(self, testcase, message=None):
        # type: (TestCase, Optional[str]) -> None

        self.testcase = testcase
        self.message = message

    def __enter__(self):
        pass

    def __exit__(self, exc_type, exc_value, traceback):
        if exc_type:
            if self.message:
                self.testcase.fail(self.message)
            else:
                self.testcase.fail(exc_value)


def make_comparable(d):
    # type: (Any, ) -> Any

    if isinstance(d, (str, int, type(None))):
        return d
    elif isinstance(d, list):
        return list(make_comparable(i) for i in d)
    elif isinstance(d, tuple):
        return tuple(make_comparable(i) for i in d)
    elif isinstance(d, dict):
        return make_comparable(list(d.items()))
    else:
        raise ValueError(f"must be list, tuple or dict, not {type(d)}")


def is_equal_unordered(seq_a, seq_b):
    # type: (Collection, Collection) -> bool

    if isinstance(seq_a, set) and isinstance(seq_b, set):
        return seq_a == seq_b

    seq_a = sorted(make_comparable(seq_a))
    seq_b = sorted(make_comparable(seq_b))
    return seq_a == seq_b


class MyTestCase(TestCase):
    def subTest(self, msg=None, **params):
        try:
            func = TestCase.subTest
        except AttributeError:
            return anullcontext
        else:
            return func(self, msg, **params)

    def assertAnd(self, first, second, msg=None):
        # type: (Any, Any, Optional[str]) -> None

        self.assertTrue(first and second, msg)

    def assertTypeEqual(self, first, second, msg=None):
        # type: (Any, Any, Optional[str]) -> None

        self.assertEqual(type(first), type(second), msg)

    def assertFilesEqual(self, first_path, second_path, msg=None):
        # type: (Any, Any, Optional[str]) -> None

        self.assertTrue(equal_files(first_path, second_path), msg)

    def assertIterEqual(self, first, second, msg=None):
        # type: (Iterable, Iterable, Optional[str]) -> None

        for i, (a, b) in enumerate(zip_longest(first, second)):
            if msg:
                msg = " : " + str(msg)
            self.assertEqual(a, b, msg=f"in iteration index {i}: {msg}")

    def assertIterAlmostEqual(self, first, second, msg=None):
        # type: (Iterable, Iterable, Optional[str]) -> None

        for i, (a, b) in enumerate(zip_longest(first, second)):
            if msg:
                msg = " : " + str(msg)
            try:
                self.assertAlmostEqual(a, b, msg=f"in iteration index {i}: {msg}")
            except TypeError:
                raise AssertionError("Invalid types (probably different length iters)")  # from None

    def assertAllEqual(self, args, msg=None):  # *args doesn't work in python2!?
        # type: (Iterable, Optional[str]) -> None

        it = iter(args)
        first = next(it)
        for second in it:
            self.assertEqual(first, second, msg)

    def assertIterIterEqual(self, first, second, msg=None):  # UNTESTED
        # type: (Iterable[Iterable], Iterable[Iterable], Optional[str]) -> None

        for i, (a, b) in enumerate(zip_longest(first, second)):
            if msg:
                msg = " : " + str(msg)
            self.assertIterEqual(a, b, msg=f"in iteration index {i}: {msg}")

    def assertPriorityEqual(self, first, second, msg=None):
        # type: (Iterable[Optional[Tuple[Hashable, Hashable]]], Iterable[Optional[Tuple[Hashable, Hashable]]], Optional[str]) -> None

        d1 = defaultdict(set)  # type: DefaultDict[Hashable, set]
        d2 = defaultdict(set)  # type: DefaultDict[Hashable, set]

        for a, b in zip_longest(first, second):
            if a is not None:
                val_1, p_1 = a
                if b is None and p_1 == 0:
                    continue

            if b is not None:
                val_2, p_2 = b
                if a is None and p_2 == 0:
                    continue

            self.assertAnd(a, b)

            if val_1 == 0 and val_2 == 0:
                continue

            d1[p_1].add(val_1)
            d2[p_2].add(val_2)
        self.assertEqual(d1, d2, msg)

    def assertUnorderedSeqEqual(self, first, second, msg=None):
        # type: (Iterable, Iterable, Optional[str]) -> None

        # fixme: use is_equal_unordered

        first = sorted(first)
        second = sorted(second)
        self.assertEqual(first, second, msg)

    def assertNoRaise(self, msg=None):
        # type: (Optional[str], ) -> NoRaise

        return NoRaise(self, msg)

    def assertEqualsOneOf(self, result, truths):
        # type: (T, Iterable[T]) -> None

        for truth in truths:
            if result == truth:
                return

        raise AssertionError("Result does not match one of the provided truths")  # from None

    def assertUnorderedMappingEqual(self, first, second, msg=None):
        # type: (Mapping[T, U], Mapping[T, U], Optional[str]) -> None

        self.assertEqual(len(first), len(second), msg)

        for a, b in zip(sorted(first.keys()), sorted(second.keys())):
            self.assertEqual(a, b)
            self.assertEqual(first[a], second[b])


def random_arguments(n, *funcs):
    # type: (int, *Callable[[], Any]) -> Callable[[Callable], Callable]

    def decorator(func):
        @wraps(func)
        def inner(self):
            for i in range(n):
                with self.subTest(str(i)):
                    if func(self, *(f() for f in funcs)) is not None:
                        raise AssertionError

        return inner

    return decorator


# also called: parameterize
def parametrize(*args_list):
    # # type: (*tuple, ) -> Callable[[Callable], Callable] # mypy error: syntax error in type comment

    def decorator(func):
        @wraps(func)
        def inner(self):
            for args in args_list:
                with self.subTest(str(args)[:1000]):
                    if func(self, *args) is not None:
                        raise AssertionError

        return inner

    return decorator


def parametrize_product(*args_list):
    # # type: (*tuple, ) -> Callable[[Callable], Callable] # mypy error: syntax error in type comment

    def decorator(func):
        @wraps(func)
        def inner(self):
            for args in product(*args_list):
                with self.subTest(str(args)):
                    if func(self, *args) is not None:
                        raise AssertionError

        return inner

    return decorator


def repeat(number):
    # type: (int, ) -> Callable[[Callable], Callable]

    def decorator(func):
        @wraps(func)
        def inner(self):
            for i in range(number):
                if func(self) is not None:  # no self.subTest(str(i))
                    raise AssertionError

        return inner

    return decorator


class closeable_tempfile:

    Uninterrupted = HandleKeyboardInterrupt()

    def __init__(self, mode="w+b", encoding=None):

        encoding = _check_arguments(mode, encoding)
        self.f = NamedTemporaryFile(mode=mode, encoding=encoding, delete=False)

    def __enter__(self):
        return self.f, self.f.name

    def __exit__2(self, type, value, traceback):
        with self.Uninterrupted:
            self.f.close()  # close in case user has not closed the file
            remove(self.f.name)

    def __exit__(self, type, value, traceback):
        try:
            if type == EOFError:
                sleep(1)  # wait for KeyboardInterrupt which might follow EOFError...
        finally:
            self.f.close()  # close in case user has not closed the file
            remove(self.f.name)
