from collections import deque, namedtuple
import functools
import sys
import threading

from ._python_compat import (
    call_parts,
    get_thread_id,
    mock
)

# alias so consumers don't need to import base `mock` too
call = mock.call

# subclass BaseException instead of Exception to allow target code to use
# `except Exception` without catching UnexpectedCall. we don't want testers
# to think their target code succeeded when it accidentally caught an error
# from a testing library.
class UnexpectedCall(BaseException):
    pass

class BadCall(ValueError):
    pass

class CallQueueNotEmpty(AssertionError):
    pass

class QMockErrorsInThreads(AssertionError):
    def __init__(self, errors):
        self.errors = errors

    def __str__(self):
        return (
            "Unhandled QMock errors raised in other threads: {0!r}"
            .format(self.errors)
        )

ErrorRecord = namedtuple(
    "ErrorRecord",
    ("thread_id", "error")
)

class patch(object):
    """
        A not-quite-drop-in replacement for unittest.mock.patch.

        Like unittest.mock.patch, qmock.patch is used to temporarily
        redirect import paths to Mocks and can be used as:
        - a class decorator:

            @qmock.patch(...)
            class Foo(unittest.TestCase):
                def test_bar(self, qm):
                    ...

        - a function decorator:

            @qmock.patch(...)
            def bar(qm):
                ...

        - a context manager:

            with qmock.patch(...) as qm:
                ...

        But, the usage of qmock.patch is a little different, and
        provides a few extra features:

        1 - A single qmock.patch() can patch multiple import paths,
            which are provided via keyword arguments.

        2 - All Mocks generated by a single qmock.patch() will be
            attached to the same QMock instance.

            For example:

                @qmock.patch(fizz="foo.bar", buzz="foo.baz")
                def my_test(qm):
                    ...

            This decorator patches the import paths `foo.bar` and
            `foo.baz`, attaches the new Mocks to `qm.fizz` and `qm.buzz`,
            then injects `qm` into the decorated function.

        3 - Stacked @qmock.patch() decorators share the same QMock
            and inject only that single QMock into the target functions or
            instance methods.

            So the single decorator in the example above produces the same
            QMock as this stack of decorators:

                @qmock.patch(buzz="foo.baz")
                @qmock.patch(fizz="foo.bar")
                def my_test(qm):
                    ...

            Of course, in simple cases like this, the single decorator is
            preferred. But stacking can be a useful alternative if there is
            ever a need to apply patches in a specific order (remember,
            decorators are applied bottom-up).

            More practically, this also allows mixing class decorators and
            function decorators (just like unittest.mock.patch):

                @qmock.patch(buzz="foo.baz")
                class MyTests(unittest.TestCase):
                    @qmock.patch(fizz="foo.bar")
                    def test_foo(self, qm):
                        ...
                    @qmock.patch(zazz="foo.blah")
                    def test_bar(self, qm):
                        ...

            Here, test_foo() will get `qm.buzz` and `qm.fizz` patches, and
            test_bar() will get `qm.buzz` and `qm.zazz` patches.

        4 - At scope exit (after a patched function returns or a patched
            context is __exit__()-ed), the patch automatically checks the
            final state of the QMock/CallQueue and raises if:
            - the CallQueue is not empty (ie: CallQueue.assert_empty());
            - there were qmock exceptions raised in other threads (these
              errors would not have been seen in the main thread where the
              tests are running).

        5 - qmock.patch() can be applied with no patches.

            Together, features 4 and 5 remove the need for consumers to
            manually call CallQueue.assert_empty() at the end of every test
            function or write shims to catch qmock errors in other threads.
            This is especially handy when applied to a unittest.TestCase
            class:

                @qmock.patch()
                class MyTests(unittest.TestCase):
                    def test_foo(self, qm):
                        ...
                    def test_bar(self, qm):
                        ...
                    ...

            Because of the class-level @patch(), the test writer doesn't
            need to remember (or worry about forgetting) to do
            qmock-related cleanup checks.

        -- NOTE --
        When using qmock.patch as a class decorator, only methods with
        the prefix "test" will be patched (just like unittest.mock.patch).

        -- NOTE --
        If a patched scope exits with an exception, then the "empty
        CallQueue" check is skipped. If the CallQueue isn't empty, it's
        likely a *result* of the target code exiting early, not a *cause*.
        So raising CallQueueNotEmpty would just be noise. Additionally, in
        Python 2.7, we can only report one exception, so raising
        CallQueueNotEmpty would hide the real exception. In contrast, the
        "exceptions in other threads" check is always done because that
        information could be useful for debugging the patched scope, eg: a
        thread dying from a qmock exception could easily be the *cause* of
        the exception in the patched scope. Again, in Python2.7, we can
        only report one exception, so QMockErrorsInThreads will hide the
        patched scope's exception. In Python3, both exceptions will be
        reported if uncaught, but still only the most-recently raised
        (QMockErrorsInThreads) will be catchable.

        -- WARNING --
        Do not mix decorators and context managers or nest multiple context
        managers. This will create mutiple QMock instances which negates
        some benefits of qmock.
    """

    def __init__(self, **patches):
        self._patches = patches

    def __call__(self, func_or_klass):
        """ borrowed from unittest.mock._patch.__call__() """
        if isinstance(func_or_klass, type):
            return self._decorate_class(func_or_klass)
        return self._decorate_callable(func_or_klass)

    def _decorate_class(self, klass):
        """ borrowed from unittest.mock._patch.decorate_class() """
        for attr in dir(klass):
            if not attr.startswith(mock.patch.TEST_PREFIX):
                continue

            attr_value = getattr(klass, attr)
            if not hasattr(attr_value, "__call__"):
                continue

            setattr(klass, attr, self(attr_value))
        return klass

    def _decorate_callable(self, func):
        # if func already has a .qmock, then keep using that. this allows
        # consumers to stack or mix @patch() decorators.
        if hasattr(func, "qmock"):
            qpatched = func
            qm = qpatched.qmock
        else:
            qm = QMock()
            @functools.wraps(func)
            def qpatched(*args, **kwargs):
                args += (qm,)
                try:
                    res = func(*args, **kwargs)
                except:
                    self._check_final_state(qm, handling_exception=True)
                    raise # if check passes with no new exception
                else:
                    self._check_final_state(qm, handling_exception=False)
                return res
        for attr, target in self._patches.items():
            mpatching = self._new_mock_patch(qm, attr, target)
            # if qpatched has no patches, this will return a new wrapper.
            # else, it will update and return the same qpatched object.
            qpatched= mpatching(qpatched)
        # in python2.x, this assignment will fail if qpatched is an
        # instancemethod. that can only happen if func is an instancemethod
        # and it doesn't get replaced with a wrapper (because it already
        # has a .qmock and at least one patch).
        if qpatched is not func:
            qpatched.qmock = qm
        return qpatched

    def __enter__(self):
        self._active_qm = QMock()
        self._active_patches = list()
        try:
            for attr, target in self._patches.items():
                mpatching = self._new_mock_patch(self._active_qm, attr, target)
                mpatching.__enter__()
                self._active_patches.append(mpatching)
        except:
            # as long as we never use mock.patch.multiple(), the above
            # mock._patch.__enter__() will be atomic: if it raises, then
            # the last patch failed completely and we don't need to clean
            # it up with mock._patch.__exit__(). but we still need to
            # __exit__() any successfully activated patches.
            self.__exit__(*sys.exc_info())
            raise
        return self._active_qm

    def __exit__(self, exc_type, exc_value, traceback):
        for mpatching in reversed(self._active_patches):
            mpatching.__exit__()
        del self._active_patches

        self._check_final_state(
            self._active_qm,
            handling_exception=exc_type is not None
        )
        del self._active_qm

    def _check_final_state(self, qm, handling_exception):
        thread_id = get_thread_id()
        pop_errors_in_threads = [
            record
            for record in qm.call_queue.pop_errors
            if record.thread_id != thread_id
        ]
        if pop_errors_in_threads:
            raise QMockErrorsInThreads(pop_errors_in_threads)

        if not handling_exception:
            qm.call_queue.assert_empty()

    def _new_mock_patch(self, qm, attr, target):
        new_attr = getattr(qm, attr)
        return mock.patch(target, new=new_attr)

_QMOCK_INST_ATTRS = frozenset(
    ("call_queue", "_mock_name", "_mock_parent", "_mock_new_name",
     "_mock_new_parent", "_mock_call_proxy", "_last_mock_result")
)
# __class__ is included to avoid unexpected results from isinstance().
_QMOCK_CLASS_ATTRS = frozenset(
    ("__class__", "__setattr__", "__call__", "__slots__", "__eq__",
     "mock_return", "_pop_mock_call_queue")
)

class QMock(object):
    """
        A not-quite-drop-in replacement for unittest.mock.Mock.

        QMock takes a very different approach to validating mocked calls.
        When testing some "target" code, mock.Mock builds a history of
        incoming calls while the target code is running. This history is
        meant to be inspected by the tester *after* the target code has
        finished.

        QMock flips things around. Instead of building a history of actual
        calls, QMock consumes a queue of expected calls (a CallQueue). So
        instead of verifying the call history after-the-fact, incoming
        calls are validated *while* the target code is running. If any
        unexpected calls come in, a QMockError is raised.

        For each TestCase, basic usage looks like:
            0 - Build a QMock: qm = QMock()
            1 - Use qm.call_queue.push(expected_call, result) to push pairs
                of call objects and their fake results onto the CallQueue.
            2 - Run your target code.
            3 - Use qm.call_queue.assert_empty() to ensure the queue was
                consumed.
        If no QMockErrors were raised, then your TestCase passes!

        If you need to mock complicated function call chains like
            call.foo().bar.baz().bonk()
        you can use .call_queue.push_all(expected_call, result) to set up
        the parent calls too, ie: call.foo(), call.foo().bar.baz()

        -- Attributes are not Methods --
        You cannot push() attributes. For example, you can't push the
        "call":
            call.foo().bar.baz().buzz
        That is technically a valid `call` object, but it doesn't represent
        a real function call that CallQueue can validate.

        If you need to set the attribute above, then you'll need to push()
        any parent calls, find the mock return value of the parent Mock
        matching the last parent call, and assign the attribute to that
        mock value:
            # find the parent Mock's return value
            baz_mock_return = qm.foo.return_value.bar.baz.return_value

            # push the parent call
            qm.call_queue.push_all(call.foo().bar.baz(), baz_mock_return)

            # set the attribute
            baz_mock_return.buzz = <your_value>

        Remember you must use `.return_value` when finding the parent's
        return value. If you call the parent Mock directly, then it will
        start consuming the CallQueue before you've finished setting it up
        and your test will not work.

        `QMock.mock_return(kall)` makes this a little easier. Given the
        parent call, mock_return() will find the return value of the
        parent Mock:
            baz_mock_return = qm.mock_return(call.foo().bar.baz())

        So, all together:
            baz_call = call.foo().bar.baz()
            baz_mock_return = qm.mock_return(baz_call)
            qm.call_queue.push_all(baz_call, baz_mock_return)
            baz_mock_return.buzz = <your_value>

        you will need to push() that baz_call as many times as you expect
        it to be called, but you only need to set the "buzz" attribute's
        value once.

        -- Magic Methods --
        QMock supports mocking all the same magic methods as the standard
        mock.MagicMock *except* `__eq__`.

        Equality checks are used to compare `call` objects, which may have
        qmock'd objects as arguments. If `__eq__` were mocked too, the
        comparison of such calls would recursively pop off the CallQueue.
        This pop would expect a call like `__eq__(<mocked obj>)`, where the
        first argument is the same qmock'd object we're already trying to
        compare, thus making the recursion infinite.

        TODO? we could allow users to opt-in to `__eq__` mocking, in cases
        where they know there are no mocks being passed as call arguments.
    """
    """
        # how it works
        mock.Mock() already knows how to do all the heavy lifting of
        creating and attaching new sub-mocks on-the-fly as well as packing
        call info into mock.call objects. QMock just needs to own a
        CallQueue and check incoming calls against it. if we're letting
        mock.Mock build our mock.call objects, then we need a way for QMock
        to get those objects from mock.Mock. luckily, mock.Mock has a
        mechanism for informing other Mock objects of new calls, so we can
        hijack that.

        when a mock.Mock creates a new sub-Mock attr, it becomes the
        "parent" of that sub-Mock. when a Mock gets called, it builds a new
        mock.call object and adds that to its own .mock_calls list. then it
        walks up the chain of parents, expanding the mock.call's path and
        appending a new call object to each parent's .mock_calls list.

        so to get notified, QMock needs to
        1) create a "root" mock.Mock and register itself as the "parent";
        2) have a .mock_calls that captures the final mock.call from the
        "root" Mock.

        (1) is easy: set `parent=self` when creating the root mock.Mock.
        (2) is a little trickier. we need to implement a custom proxy
        object with an append(mock.call) method which will forward the
        mock.call back to QMock. this is what _MockCallsProxy does.

        now, we're still missing one thing: the pre-set result returned by
        CallQueue.pop() needs to be returned to the caller.

        the "parent" relation can't help here; there is no way for one Mock
        to affect the result of another Mock's __call__(). so QMock can't
        force the originating sub-Mock to return a value or raise an error.
        QMock can't even know which sub-Mock originated the call. this is
        going to require some more intense h4xx.

        to return the result from CallQueue.pop(), QMock needs to:
        1) store the result somewhere during mock.Mock.__call__();
        2) fetch the result afterward and return it to the caller.

        if we ignore thread-safety (like the std mock library does), then
        (1) is easy: store the result of CallQueue.pop() in a QMock member.
        but again, (2) is a little trickier. we need to wrap
        mock.Mock.__call_() with a proxy that knows the root QMock so it
        can:
            - run mock.Mock.__call(),
            - get the result from the QMock, and
            - return to the caller.
        this is what QMock._last_mock_result and _CallProxy do.
    """
    __slots__ = _QMOCK_INST_ATTRS
    _ALL_ATTRIBUTES = _QMOCK_INST_ATTRS.union(_QMOCK_CLASS_ATTRS)

    def __new__(cls):
        return _new_clone_with_magic_methods(cls)

    def __init__(self):
        self.call_queue = CallQueue(root_qmock=self)

        # mock.Mock needs these 4 attrs to exist on all parents.
        self._mock_name = None
        self._mock_parent = None
        self._mock_new_name = ""
        self._mock_new_parent = None

        # `name=""` prevents real_mock from adding an extra component to
        # the path of the final mock.call object.
        real_mock = mock.MagicMock(parent=self, name="")
        self._mock_call_proxy = _CallProxy(root_qmock=self, real_mock=real_mock)

        # set by self._pop_mock_call_queue()
        #TODO? would threading.local solve the thread-safety issue?
        self._last_mock_result = None

    def __getattribute__(self, name):
        if name == "_ALL_ATTRIBUTES" or name in self._ALL_ATTRIBUTES:
            return super(QMock, self).__getattribute__(name)

        # CallableMixin._mock_call() calls `.mock_calls.append(<call>)` on
        # each parent in the chain of nested mock.Mock objects, from the
        # leaf Mock up to the root (self), building the mock.call object as
        # it goes. we need that final top-level mock.call to compare to the
        # expected call sitting at the front of self.call_queue.
        if name == "mock_calls":
            return _MockCallsProxy(self)

        return getattr(self._mock_call_proxy, name)

    def __setattr__(self, name, value):
        if name in self.__slots__:
            super(QMock, self).__setattr__(name, value)
        else:
            setattr(self._mock_call_proxy, name, value)

    def __eq__(self, other):
        return self._mock_call_proxy.__eq__(other)

    def __call__(_mock_self, *args, **kwargs):
        return _mock_self._mock_call_proxy(*args, **kwargs)

    def _pop_mock_call_queue(self, actual_call):
        """ only called by _MockCallsProxy.append() """
        self._last_mock_result = self.call_queue._pop(actual_call)

    def mock_return(self, kall):
        """
            recursively select the value located at the given call path.
            for example, if
                kall = call.foo.bar().baz()
            then
                return self.foo.bar.return_value.baz.return_value

            kall: a `call` object.
        """
        name, parent, from_kall = call_parts(kall)

        if from_kall:
            attr = "return_value"
        else:
            attr = name.split(".")[-1]
        if parent is None:
            mock_obj = self
        else:
            mock_obj = self.mock_return(parent)
        return getattr(mock_obj, attr)

class CallQueue(object):
    def __init__(self, root_qmock):
        self._qmock = root_qmock
        self._queue = deque()
        self.pop_errors = list()
        self._pop_errors_lock = threading.Lock()

    def push(self, expected_call, result):
        """
            push expected_call onto the queue with the intended result.

            expected_call must represent a function call:
                call.foo.bar()
            it cannot represent an attribute:
                call.foo.barf

            expected_call: a `call` object.
            result: anything.
        """
        _, _, from_kall = call_parts(expected_call)
        if not from_kall:
            raise BadCall(
                "Call object represents attribute fetch, not function: {0}"
                .format(expected_call)
            )

        # wrap result in a mock.Mock to handle raising when the result is
        # an exception or exception type.
        mock_result = mock.Mock()
        # also box result in a tuple so mock.Mock doesn't try to unpack
        # result into multiple responses if it happens to be iterable.
        mock_result.side_effect = (result,)
        self._queue.append((expected_call, mock_result))

    def push_all(self, expected_call, result):
        """
            push expected_call and all its parent calls onto the queue.

            `result` is expected_call's result. parent call results are
            generated dynamically from the attached QMock.

            just like push(), expected_call must represent a function call:
                call.foo.bar().baz()
            it cannot represent an attribute:
                call.foo.bar().barf

            expected_call: a `call` object.
            result: anything.
        """
        # parent calls are call_list[:-1]; expected_call is call_list[-1].
        for call in expected_call.call_list()[:-1]:
            self.push(call, self._qmock.mock_return(call))
        self.push(expected_call, result)

    def _pop(self, actual_call):
        """ only called by QMock._pop_mock_call_queue() """
        try:
            expected_call, mock_result = self._queue.popleft()
        except IndexError as ex:
            error = UnexpectedCall(
                "Queue is empty. call: {0}"
                .format(actual_call)
            )
            self._store_pop_error(error)
            raise error
        if actual_call != expected_call:
            error =  UnexpectedCall(
                "Call does not match expectation. actual: {0}; expected: {1}"
                .format(actual_call, expected_call)
            )
            self._store_pop_error(error)
            raise error
        # let it raise if the result is an exception or exception type.
        return mock_result()

    def _store_pop_error(self, error):
        thread_id = get_thread_id()
        record = ErrorRecord(thread_id=thread_id, error=error)
        with self._pop_errors_lock:
            self.pop_errors.append(record)

    def assert_empty(self):
        """
            call this at the end of each TestCase to verify that all
            expected calls were consumed.
        """
        if self._queue:
            raise CallQueueNotEmpty(
                "Queue is not empty; {0} expected calls remaining."
                .format(len(self._queue))
            )

class _MockCallsProxy(object):
    def __init__(self, root_qmock):
        self._qmock = root_qmock

    def append(self, actual_call):
        self._qmock._pop_mock_call_queue(actual_call)

_CALLPROXY_INST_ATTRS = frozenset(
    ("_qmock", "_real_mock")
)
# __class__ is included to avoid unexpected results from isinstance().
_CALLPROXY_CLASS_ATTRS = frozenset(
    ("__class__", "__setattr__", "__call__", "__slots__", "__eq__")
)

class _CallProxy(object):
    __slots__ = _CALLPROXY_INST_ATTRS
    _ALL_ATTRIBUTES = _CALLPROXY_INST_ATTRS.union(_CALLPROXY_CLASS_ATTRS)

    def __new__(cls, root_qmock, real_mock):
        # instance args (root_qmock, real_mock) are not used here.
        return _new_clone_with_magic_methods(cls)

    def __init__(self, root_qmock, real_mock):
        self._qmock = root_qmock
        self._real_mock = real_mock
        # MagicMock binds itself as the `self` argument for magic methods,
        # shedding this _CallProxy. but consumers will push calls onto the
        # CallQueue which expect this _CallProxy as `self`. so we'll save a
        # reference back up to this _CallProxy on the MagicMock so
        # __eq__() can do the proper comparison.
        self._real_mock._qmock_call_proxy = self

    def __getattribute__(self, name):
        if name == "_ALL_ATTRIBUTES" or name in self._ALL_ATTRIBUTES:
            return super(_CallProxy, self).__getattribute__(name)

        child_mock = getattr(self._real_mock, name)
        if not isinstance(child_mock, mock.Base):
            # either already proxied or some not-mock thing
            return child_mock
        proxy = _CallProxy(root_qmock=self._qmock, real_mock=child_mock)
        # we want proxies to be persistent, just like mock.Mock instances,
        # so identity tests work as expected, ie: (qm.foo is qm.foo) == True
        setattr(self._real_mock, name, proxy)
        # but mock wraps the proxy with a lambda when setting a magic
        # method, so we need to re-fetch the thing we just set to ensure we
        # always return the same object.
        return getattr(self._real_mock, name)

    def __setattr__(self, name, value):
        if name in self.__slots__:
            super(_CallProxy, self).__setattr__(name, value)
        else:
            setattr(self._real_mock, name, value)

    def __eq__(self, other):
        if isinstance(other, QMock):
            other = other._mock_call_proxy
        elif isinstance(other, mock.Base):
            # all mocks should be wrapped by a _CallProxy and thus contain
            # a reference to that _CallProxy.
            other = other._qmock_call_proxy
        return self is other

    def __call__(_mock_self, *args, **kwargs):
        """
            wrap mock.Mock.__call__() with some magic so we can get the
            result back from CallQueue.pop().

            use "_mock_self" to allow "self" in kwargs.
        """
        """
            when _real_mock() is called, _qmock.mock_calls.append()
            (ie: _MockCallsProxy.append()) will get the fully-qualified
            top-level mock.call object and forward it to
            _qmock._pop_mock_call_queue(). it will then get forwarded again
            to _CallQueue._pop() to validate it against the current expected
            call and return/raise the corresponding result. if the result
            wasn't an exception, _qmock will then assign the result back to
            _qmock._last_mock_result so it can be returned from here.

            this isn't thread-safe, but neither is mock.Mock, so whatever.
        """
        _mock_self._real_mock(*args, **kwargs)
        return _mock_self._qmock._last_mock_result

_BANNED_MAGIC_METHODS = frozenset(
    ("__eq__",)
)

def _new_clone_with_magic_methods(cls):
    """
        for each QMock and _CallProxy, we need to clone the class and
        attach _MagicMethodDescriptors to forward class-level access of
        magic methods down to QMock/_CallProxy instances.

        this essentially makes each QMock/_CallProxy instance a singleton
        of a subclass of the prime QMock/_CallProxy classes defined above. 

        this approach is adapted from the base mock lib, which does
        something similar with NonCallableMock, MagicMock, and
        MagicProxy.
    """
    name = cls.__name__
    bases = (cls,)
    attrs = {'__doc__': cls.__doc__}

    clone_cls = type(name, bases, attrs)
    clone_inst = object.__new__(clone_cls)

    possible_magics = mock._magics - _BANNED_MAGIC_METHODS
    missing_magics = possible_magics - set(clone_cls.__dict__.keys())
    for name in missing_magics:
        setattr(clone_cls, name, _MagicMethodDescriptor(name, clone_inst))

    return clone_inst

class _MagicMethodDescriptor(object):
    """
        all magic methods are accessed as class attrs, not instance
        attrs. so this attribute descriptor is used to forward class-level
        access of magic methods down to a target _CallProxy instance.
    """
    def __init__(self, magic_name, call_proxy_inst):
        self._magic_name = magic_name
        self._call_proxy_inst = call_proxy_inst

    def __get__(self, _instance, _inst_cls):
        """
            any time a magic method is accessed, either as an attr on a
            _CallProxy class or as an attr on an _CallProxy instance,
            forward it to the associated _CallProxy instance.

            `_instance` should be either `None` (accessed via class) or the
            same object as `self._call_proxy_inst` (accessed via instance).
            `_inst_cls` should be a _CallProxy clone.
        """
        return getattr(self._call_proxy_inst, self._magic_name)
