import re
import inspect
from matchers import *
from matchers import _Matchers_

"""
Authors: www.iExpertos.com
   - Carlos Ble (www.carlosble.com) 

License: Apache 2 (http://www.apache.org/licenses/LICENSE-2.0.html)

Project home: http://www.pydoubles.org
"""

ANY_ARG = "<___ANY___ARG___>"

class UnexpectedBehavior(Exception):
    pass

class WrongApiUsage(Exception):
    pass

class ArgsDontMatch(Exception):
    pass

class ApiMismatch(Exception):
    pass


class _MethodHandler_():
    """
    Internal framework class. When user access a method or
    any other property in the test double, the framework
    return an instance of this class, which is callable.
    So calling this instance with parenthesis triggers
    the on_method_call event in the test double.
    """    
    
    def __init__(self, double, attr):
        self.double = double
        self.attr_name = attr
        
    def __call__(self, *args, **kwargs):
        self.double.invoked_method_name = self.attr_name
        return self.double._on_method_call(self, *args, 
                                                  **kwargs)
    
    
class _Introspector_():
    """
    Internal framework class. Specialized in introspection (reflection)
    """    
    
    def method_name(self, interceptor):
        return interceptor.attr_name
    
    def double_instance_from_method(self, interceptor):
        try:
            return interceptor.double
        except AttributeError,e:
            raise WrongApiUsage("Make sure you call this framework method passing in a method from a spy or a mock object")
    
    def original_method_signature(self, original_method):
        arg_spec = inspect.getargspec(original_method)
        kargs_spec_len = 0
        args_plus_kwargs_spec_len = len(arg_spec.args) -1
        if arg_spec.defaults is not None:
           kargs_spec_len = len(arg_spec.defaults)
        return (args_plus_kwargs_spec_len, kargs_spec_len, arg_spec)
    
    def are_synonymous(self, instance, method_name1, method_name2):
        return getattr(instance, 
                       method_name1) == getattr(instance, 
                                                method_name2)

class _IOParams_():
    """
    Data structure class. Keeps method input and output
    """
    args = kwargs = output = None
    
    def set_output(self, output):
        self.output = output
        
    def no_matter_input(self):
        return self.args is None and self.kwargs is None and \
               self.output is not None
    
    def __str__(self):
        return "args=%s,kwargs=%s,output=%s" % (str(self.args),
                                      str(self.kwargs), str(self.output))
    
        
class _MethodPool_(object):
    """
    Internal framework class. Intended to store methods info:
    The method name, the input arguments and the output for that
    input.
    """
    def __init__(self):
        self.pool = {}
        self.matchers = _Matchers_()
    
    def _are_different_arguments(self, arg1, arg2):
        return arg1 != arg2 and \
               arg1 != ANY_ARG and \
               arg2 != ANY_ARG
            
    def _every_arg_matches_or_is_any_arg(self, args, last_call_args):
        for i in range(0, len(args)):
            defined_arg = args[i]
            received_arg = last_call_args[i]
            if self._are_different_arguments(defined_arg, received_arg):
               if self.matchers.no_one_applies(defined_arg, received_arg):
                  return False
        return True
    
    def _do_args_tuple_match(self, args, last_call_args):
        if len(args) != len(last_call_args):
            return False
        return self._every_arg_matches_or_is_any_arg(args, last_call_args)
                   
    def _do_args_match(self, last_call_args, ioparams):
        if ioparams.args is not None:
           return self._do_args_tuple_match(ioparams.args, last_call_args)
        else:
           return True
            
    def _do_kwargs_match(self, kwargs, ioparams):
        if ioparams.kwargs is not None:
           return kwargs == ioparams.kwargs
        else:
           return True
            
    def _ioparams_of_matching_method(self, last_call_args, kwargs, method_name):
        for ioparams in self.pool[method_name]:
            args_match = self._do_args_match(last_call_args, ioparams)
            kwargs_match = self._do_kwargs_match(kwargs, ioparams)
            if args_match and kwargs_match:
                return ioparams                
        return None
             
    def __not_all_ioparams_match(self, called_collection, 
                                   local_collection):
        if len(called_collection) != len(local_collection):
            return True
        for i in range(0, len(local_collection)):
            if not self._do_args_match(called_collection[i].args, 
                                       local_collection[i]):
               return True
        return False

    def do_call_args_match(self, last_call_args, kwargs, method_name):
        return self._ioparams_of_matching_method(
                    last_call_args, kwargs, method_name) is not None
        
    def do_pools_match(self, called_method_pool):
        for name, ioparams in self.pool.iteritems():
            if not called_method_pool.has_method(name) or \
               self.__not_all_ioparams_match(
                          called_method_pool.pool[name], ioparams):
               return False
        return True                

    def add_method(self, method_name, input_args=None, 
                   input_kwargs=None, output=None):
        if not self.pool.has_key(method_name):
            self.pool[method_name] = []
        ioparams = _IOParams_()
        ioparams.args = input_args
        ioparams.kwargs = input_kwargs
        ioparams.set_output(output)
        self.pool[method_name].append(ioparams)
        
    def has_method(self, method_name):
        return self.pool.has_key(method_name)
    
    def stubbed_method_names(self):
        return self.pool.keys()
    
    

class _PoolReport_():
    """
    Displays the information stored in a MethodPool
    """
    def __init__(self, method_pool):
        self.methods_pool = method_pool
        
    def method_info(self, method_name):
        return ["(" + str(ioparams) + ")" for ioparams in self.methods_pool.pool[method_name]]
    
    def all_stored_methods(self):
        info = ""
        for key in self.methods_pool.pool:
            info = "%s%s, %s" % (key, str(self.method_info(key)), info)
        return info
    


class _StubPool_(_MethodPool_):
    def _ioparams_for_unspecified_method_input(self, method_name):
        for ioparams in self.pool[method_name]:
            if ioparams.no_matter_input():
               return ioparams
        return None

    def input_wasnt_specified(self, method_name):
        return self._ioparams_for_unspecified_method_input(
                            method_name) is not None    
                    
    def get_output_for_unspecified_method_input(self, method_name):
        ioparams = self._ioparams_for_unspecified_method_input(
                                            method_name)
        if ioparams is not None:
            return ioparams.output

    def get_output_for_specified_method_input(self, args, kwargs, method_name):
        ioparams = self._ioparams_of_matching_method(args, kwargs, method_name)
        if ioparams is not None:
            return ioparams.output
             
    def set_output_for_last_added_method(self, method_name, output):       
        ioparams = self.pool[method_name][-1]
        ioparams.set_output(output)
        self.pool[method_name][-1] = ioparams        
        
    def set_input_for_last_stubbed_method(self, method_name, 
                                          input_args=None,
                                          input_kwargs=None):
        ioparams = self.pool[method_name][-1]
        ioparams.args = input_args
        ioparams.kwargs = input_kwargs
        self.pool[method_name][-1] = ioparams


        
class _CallsRepository_():
    """
    Internal framework class. Basically a wrapper around
    the MethodPool, to store calls to methods
    """    
    
    def __init__(self):
        self.method_pool = _MethodPool_()
        self.report = _PoolReport_(self.method_pool)
        
    def register_call(self, method_name, *args, **kwargs):
        self.method_pool.add_method(method_name, 
                                    input_args=args,
                                    input_kwargs=kwargs)
         
    def was_registered(self, method_name):
        return self.method_pool.has_method(method_name)
  
    def _readable_kwargs(self, kwargs_str):
        if kwargs_str == "{}":
            return "No keyword args where passed in"
        return kwargs_str
      
    def _format_err_msg(self, method_name, args_str, kwargs_str):
        return "RECORDED calls were: << %s >>, \n EXPECTED call is << args = %s, keyword args = %s >>" % (
                str(self.report.method_info(method_name)), 
                args_str, self._readable_kwargs(kwargs_str))

    def match_call(self, method_name, *args, **kwargs):
        if not self.method_pool.do_call_args_match(
                        args, kwargs, method_name):
                raise ArgsDontMatch(self._format_err_msg(
                         method_name, str(args), str(kwargs)))
                
    def show_registered_calls(self):
        return self.report.all_stored_methods()
        
        
                  
                  
class _StubsRepository_():
    """
    Internal framework class. Pretty much a wrapper
    around the StubPool class, to store stubs
    """
    
    UNDEFINED = "undefined_____"
    
    def __init__(self):
        self.method_pool = _StubPool_()
        self.report = _PoolReport_(self.method_pool)
        self._clear_stub_definition()

    def _clear_stub_definition(self):
        self.last_stubbed_method = None
            
    def will_stub_any_input(self):
        return self._is_stub_for_any_input(self.last_stubbed_method)
               
    def is_stub_for_this_input(self, method_name, args, kwargs):
        return self.method_pool.has_method(method_name) and \
               self.method_pool.do_call_args_match(args, kwargs, method_name)            
             
    def _is_stub_for_any_input(self, method_name):
        return self.method_pool.has_method(method_name) and \
               self.method_pool.input_wasnt_specified(method_name)
            
    def return_value_given_input(self, method_name, args, kwargs):
        if self._is_stub_for_any_input(method_name):
           output = self.method_pool.get_output_for_unspecified_method_input(method_name)
        else:
           output = self.method_pool.get_output_for_specified_method_input(args, kwargs, method_name)
        if callable(output):
           output()
        return output
                      
    def add_stub(self, method_name, args, kwargs):
        self.create_stub(method_name)
        self.set_input_for_last_stubbed_method(args, kwargs)

    def create_stub(self, method_name):
        self._clear_stub_definition()
        self.last_input_args = self.last_input_kwargs = None
        self.last_stubbed_method = method_name
        self.method_pool.add_method(method_name)
        
    def set_input_for_last_stubbed_method(self, args, kwargs):
        self.last_input_args = args
        self.last_input_kwargs = kwargs
        self.method_pool.set_input_for_last_stubbed_method(
                                    self.last_stubbed_method, 
                                    input_args=args,
                                    input_kwargs=kwargs)
        
    def repeat_stub_times(self, times):
        for i in range(0, times -1):
            self.add_stub(self.last_stubbed_method, 
                          self.last_input_args, 
                          self.last_input_kwargs)
                
    def set_output_for_any_input_in_last_stubbed_method(self, output):
        self.method_pool.add_method(self.last_stubbed_method, 
                                    output=output)
                    
    def set_output_for_last_stubbed_method(self, output):
        self.method_pool.set_output_for_last_added_method(self.last_stubbed_method, output)
                                            
    def show_all_methods(self):
        return self.report.all_stored_methods()
                
    def repositories_are_equivalent(self, repository):
        return self.method_pool.do_pools_match(repository.method_pool)
                
    def stubbed_method_names(self):
        return self.method_pool.stubbed_method_names()
                
                
                
class _EmptyObject_():
    """
    Internal framework class. Intended to be used as the original_object
    for empty_spy and empty_mock
    """
    asserting_on_method = None
    _empty_object__ = True
    def method(self, *args, **kwargs):
        pass
    
    def __getattr__(self, attr):
        return attr
                
