# Copyright (c) 2008-2016 Szczepan Faber, Serhiy Oplakanets, Herr Kaste
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

"""Argument matchers for stubbing and verifications.

In general the call signature you specify when stubbing or verifying in mockito
is as concrete as possible: it consists of values only::

    when(os.path).exists('/foo/bar.txt').thenReturn(True)

This is for a reason. In controlled test environments, for the scope of a
single test, you should usually know exactly how you use a function, and what
you expect its outcome to be. In mockito usually (in `strict` mode) all
invocations you did not specify upfront will throw at call time.

If you reason about your code, the above `when` tirade turns - for the time
of the test - the specific stubbed function into a constant.

You can use so called argument matchers below if you can't or don't
want to specify a single concrete value for an argument, but a type or class of
possible values. E.g.::

    when(os.path).exists(...).thenReturn(True)
    when(os.path).exists(ANY).thenReturn(True)
    when(os.path).exists(ANY(str)).thenReturn(True)

    when(requests).get(ANY(str), **kwargs)
    when(requests).get('https://example.com', ...)

    when(math).sqrt(not_(_or(ANY(float), ANY(int)))).thenRaise(TypeError)

Now what you get each time is a function that up to a degree takes various
arguments and responds with the same outcome each time. Now that's a weird
thing. So use the matchers for a reason, they're powerful.

The one usage you should not care about is a loose signature when using
:func:`verify`. Since mockito will throw for unexpected calls, a very loose
`verify` should be ok::

    verify(requests, times=1).get(...)


"""

from abc import ABC, abstractmethod
import functools
import re
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    try:
        from typing import TypeGuard
    except ImportError:
        from typing_extensions import TypeGuard

builtin_any = any

__all__ = [
    'and_', 'or_', 'not_',
    'eq', 'neq',
    'lt', 'lte',
    'gt', 'gte',
    'any', 'any_', 'ANY',
    'arg_that',
    'contains',
    'matches',
    'captor',
    'call_captor',
    'times',
    'args', 'ARGS',
    'kwargs', 'KWARGS'
]


class _ArgsSentinel(object):
    def __repr__(self):
        return '*args'


ARGS_SENTINEL = _ArgsSentinel()
ARGS = args = [ARGS_SENTINEL]
# ARGS.__doc__ = """Matches multiple positional arguments.

# Note: `args` must match at least one argument.

# Example::

#     when(manager).add_tasks(1, 2, *args)

# """

KWARGS_SENTINEL = '**'
KWARGS = kwargs = {KWARGS_SENTINEL: '_'}
# KWARGS.__doc__ = """Matches multiple keyword arguments.

# Note that `kwargs` must match at least one remaining keyword argument.

# Example::

#     when(requests).get('http://myapi/', **KWARGS)

# """


class MatcherError(RuntimeError):
    '''Indicates generic runtime error raised by mockito-python matchers
    '''
    pass


class Matcher:
    def matches(self, arg):
        pass

class Capturing(ABC):
    @abstractmethod
    def capture_value(self, value):
        pass


class Any(Matcher):
    def __init__(self, wanted_type=None):
        self.wanted_type = wanted_type

    def matches(self, arg):
        if self.wanted_type:
            return isinstance(arg, self.wanted_type)
        else:
            return True

    def __repr__(self):
        return "<Any: %s>" % _any_wanted_type_label(self.wanted_type)


def _any_wanted_type_label(wanted_type):
    if isinstance(wanted_type, type):
        return _type_label(wanted_type)

    if (
        isinstance(wanted_type, tuple)
        and all(isinstance(t, type) for t in wanted_type)
    ):
        items = [_type_label(t) for t in wanted_type]
        if len(items) == 1:
            return '(%s,)' % items[0]
        return '(%s)' % ', '.join(items)

    return _safe_repr(wanted_type)


def _type_label(type_):
    module = _safe_getattr(type_, '__module__')
    qualname = _safe_getattr(type_, '__qualname__') or _safe_getattr(type_, '__name__')
    if qualname is None:
        return _safe_repr(type_)

    if module is None or module == 'builtins':
        return qualname

    return '%s.%s' % (module, qualname)


class ValueMatcher(Matcher):
    def __init__(self, value):
        self.value = value

    def __repr__(self):
        return "<%s: %s>" % (
            self.__class__.__name__,
            _safe_repr(self.value),
        )


class Eq(ValueMatcher):
    def matches(self, arg):
        return arg == self.value


class Neq(ValueMatcher):
    def matches(self, arg):
        return arg != self.value


class Lt(ValueMatcher):
    def matches(self, arg):
        return arg < self.value


class Lte(ValueMatcher):
    def matches(self, arg):
        return arg <= self.value


class Gt(ValueMatcher):
    def matches(self, arg):
        return arg > self.value


class Gte(ValueMatcher):
    def matches(self, arg):
        return arg >= self.value


class And(Matcher):
    def __init__(self, matchers):
        self.matchers = [
            matcher if isinstance(matcher, Matcher) else Eq(matcher)
            for matcher in matchers]

    def matches(self, arg):
        return all(matcher.matches(arg) for matcher in self.matchers)

    def __repr__(self):
        return "<And: %s>" % self.matchers


class Or(Matcher):
    def __init__(self, matchers):
        self.matchers = [
            matcher if isinstance(matcher, Matcher) else Eq(matcher)
            for matcher in matchers]

    def matches(self, arg):
        return builtin_any([matcher.matches(arg) for matcher in self.matchers])

    def __repr__(self):
        return "<Or: %s>" % self.matchers


class Not(Matcher):
    def __init__(self, matcher):
        self.matcher = matcher if isinstance(matcher, Matcher) else Eq(matcher)

    def matches(self, arg):
        return not self.matcher.matches(arg)

    def __repr__(self):
        return "<Not: %s>" % self.matcher


class ArgThat(Matcher):
    def __init__(self, predicate):
        self.predicate = predicate

    def matches(self, arg):
        return self.predicate(arg)

    def __repr__(self):
        return "<ArgThat: %s>" % _arg_that_predicate_label(self.predicate)


def _arg_that_predicate_label(predicate):
    try:
        return _arg_that_predicate_label_unchecked(predicate)
    except Exception:
        predicate_class = _safe_getattr(
            _safe_getattr(predicate, '__class__'),
            '__name__',
        )
        if predicate_class is None:
            return 'callable'

        return 'callable %s' % predicate_class


def _arg_that_predicate_label_unchecked(predicate):
    if isinstance(predicate, functools.partial):
        return _arg_that_partial_label(predicate)

    function_line = _line_of_callable(predicate)
    function_name = _safe_getattr(predicate, '__name__')
    if function_name is not None:
        if function_name == '<lambda>':
            return _label_with_line('lambda', function_line)
        return _label_with_line('def %s' % function_name, function_line)

    predicate_class = _safe_getattr(
        _safe_getattr(predicate, '__class__'),
        '__name__',
    )
    if predicate_class is None:
        predicate_class = 'object'

    call = _safe_getattr(predicate, '__call__')
    call_line = _line_of_callable(call)
    return _label_with_line(
        'callable %s.__call__' % predicate_class,
        call_line,
    )


def _arg_that_partial_label(predicate):
    partial_func = _safe_getattr(predicate, 'func')
    partial_name = _safe_getattr(partial_func, '__name__')

    if partial_name is not None:
        return 'partial %s' % partial_name

    return 'partial'


def _line_of_callable(value):
    if value is None:
        return None

    func = _safe_getattr(value, '__func__', value)
    code = _safe_getattr(func, '__code__')
    if code is None:
        return None

    return _safe_getattr(code, 'co_firstlineno')


def _safe_getattr(value, name, default=None):
    try:
        return getattr(value, name)
    except Exception:
        return default


def _safe_repr(value):
    try:
        return repr(value)
    except Exception:
        try:
            return object.__repr__(value)
        except Exception:
            return '<unrepresentable>'


def _label_with_line(label, line_number):
    if line_number is None:
        return label

    return '%s at line %s' % (label, line_number)


class Contains(Matcher):
    def __init__(self, sub):
        self.sub = sub

    def matches(self, arg):
        if not hasattr(arg, 'find'):
            return
        return self.sub and len(self.sub) > 0 and arg.find(self.sub) > -1

    def __repr__(self):
        return "<Contains: %s>" % _safe_repr(self.sub)


class Matches(Matcher):
    def __init__(self, regex, flags=0):
        self.regex = re.compile(regex, flags)
        self.flags = _explicit_regex_flags(regex, flags)

    def matches(self, arg):
        if not isinstance(arg, str):
            return
        return self.regex.match(arg) is not None

    def __repr__(self):
        if self.flags:
            return "<Matches: %r flags=%d>" % (self.regex.pattern, self.flags)
        else:
            return "<Matches: %r>" % self.regex.pattern


def _explicit_regex_flags(regex, flags):
    if flags:
        return flags

    compiled_flags = _safe_getattr(regex, 'flags')
    pattern = _safe_getattr(regex, 'pattern')
    if compiled_flags is None or pattern is None:
        return 0

    try:
        baseline_flags = re.compile(pattern).flags
    except Exception:
        return compiled_flags

    return compiled_flags & ~baseline_flags


class ArgumentCaptor(Matcher, Capturing):
    def __init__(self, matcher=None):
        self.matcher = matcher or Any()
        self.all_values = []

    def matches(self, arg):
        result = self.matcher.matches(arg)
        if not result:
            return
        return True

    def __iter__(self):
        yield CaptorArgsSentinel(self)

    def keys(self):
        return [KWARGS_SENTINEL]

    def __getitem__(self, key):
        if key is not KWARGS_SENTINEL and key != KWARGS_SENTINEL:
            raise KeyError(key)
        return CaptorKwargsSentinel(self)

    @property
    def value(self):
        if not self.all_values:
            raise MatcherError("No argument value was captured!")
        return self.all_values[-1]

    def capture_value(self, value):
        self.all_values.append(value)

    def __repr__(self):
        return "<ArgumentCaptor: matcher=%s values=%s>" % (
            repr(self.matcher), self.all_values,
        )


class CallCaptor:
    def __init__(self):
        self.all_values = []

    @property
    def value(self):
        if not self.all_values:
            raise MatcherError("No call value was captured!")
        return self.all_values[-1]

    def capture_call(self, args, kwargs):
        self.all_values.append((tuple(args), dict(kwargs)))

    def __repr__(self):
        return "<CallCaptor: values=%s>" % self.all_values


class CaptorArgsSentinel:
    def __init__(self, captor):
        self.captor = captor

    def matches(self, args):
        return all(self.captor.matches(arg) for arg in args)

    def capture_value(self, value):
        self.captor.capture_value(value)

    def __repr__(self):
        return "<CaptorArgsSentinel: %r>" % self.captor


class CaptorKwargsSentinel:
    def __init__(self, captor):
        self.captor = captor

    def matches(self, kwargs):
        return all(self.captor.matches(value) for value in kwargs.values())

    def capture_value(self, value):
        self.captor.capture_value(value)

    def __repr__(self):
        return "<CaptorKwargsSentinel: %r>" % self.captor


def is_call_captor(value: object) -> 'TypeGuard[CallCaptor]':
    return isinstance(value, CallCaptor)


def is_captor_args_sentinel(value: object) -> 'TypeGuard[CaptorArgsSentinel]':
    return isinstance(value, CaptorArgsSentinel)


def is_captor_kwargs_sentinel(value: object) -> 'TypeGuard[CaptorKwargsSentinel]':
    return isinstance(value, CaptorKwargsSentinel)


def is_args_sentinel(value):
    return value is ARGS_SENTINEL or is_captor_args_sentinel(value)


def any(wanted_type=None):
    """Matches against type of argument (`isinstance`).

    If you want to match *any* type, use either `ANY` or `ANY()`.

    Examples::

        when(mock).foo(any).thenReturn(1)
        verify(mock).foo(any(int))

    """
    return Any(wanted_type)


ANY = any_ = any


def eq(value):
    """Matches particular value (`==`)"""
    return Eq(value)


def neq(value):
    """Matches any but given value (`!=`)"""
    return Neq(value)


def lt(value):
    """Matches any value that is less than given value (`<`)"""
    return Lt(value)


def lte(value):
    """Matches any value that is less than or equal to given value (`<=`)"""
    return Lte(value)


def gt(value):
    """Matches any value that is greater than given value (`>`)"""
    return Gt(value)


def gte(value):
    """Matches any value that is greater than or equal to given value (`>=`)"""
    return Gte(value)


def and_(*matchers):
    """Matches if all given matchers match

    Example::

        when(mock).foo(and_(ANY(str), contains('foo')))

    """
    return And(matchers)


def or_(*matchers):
    """Matches if any given matcher match

    Example::

        when(mock).foo(or_(ANY(int), ANY(float)))

    """
    return Or(matchers)


def not_(matcher):
    """Matches if given matcher does not match

    Example::

        when(mock).foo(not_(ANY(str))).thenRaise(TypeError)

    """
    return Not(matcher)


def arg_that(predicate):
    """Matches any argument for which predicate returns True

    Example::

        verify(mock).foo(arg_that(lambda arg: arg > 3 and arg < 7))

    """
    return ArgThat(predicate)


def contains(sub):
    """Matches any string containing given substring

    Example::

        mock.foo([120, 121, 122, 123])
        verify(mock).foo(contains(123))

    """
    return Contains(sub)


def matches(regex, flags=0):
    """Matches any string that matches given regex"""
    return Matches(regex, flags)


def captor(matcher=None):
    """Returns argument captor that captures values for further assertions

    Example::

        arg = captor()
        mock.do_something(123)
        mock.do_something(456)
        verify(mock).do_something(arg)
        assert arg.value == 456
        assert arg.all_values == [123, 456]

    You can restrict what the captor captures using the other matchers
    shown herein::

        arg = captor(any(str))
        arg = captor(contains("foo"))

    captor can also be used to capture rest arguments::

        args = captor()
        kwargs = captor()
        when(mock).do(*args, **kwargs)
    """
    return ArgumentCaptor(matcher)


def call_captor():
    """Returns a call captor that captures ``(args, kwargs)`` tuples.

    Example::

        call = call_captor()
        when(mock).do(call).thenReturn("ok")
        mock.do(1, 2, x=3)
        assert call.value == ((1, 2), {"x": 3})

    """
    return CallCaptor()


def times(count):
    return count
