require 'rubygems'
require 'uuidtools'
require File.dirname(__FILE__) + '/messenger'
require File.dirname(__FILE__) + '/descriptor'

module Caricature

  # Groups the methods for interception together
  # this is a mix-in for the created isolations for classes
  module Interception

    # the class methods of this intercepting object
    module ClassMethods

      # the context of this isolation instance.
      # this context takes care of responding to method calls etc.
      def isolation_context
        @___context___
      end

      # Replaces the call to the proxy with the one you create with this method.
      # You can specify more specific criteria in the block to configure the expectation.
      #
      # Example:
      #
      #     an_isolation.class.when_receiving(:a_method) do |method_call|
      #       method_call.with(3, "a").return(5)
      #     end
      #
      # is equivalent to:
      #
      #     an_isolation.class.when_receiving(:a_method).with(3, "a").return(5)
      #
      # You will most likely use this method when you want your stubs to return something else than +nil+
      # when they get called during the run of the test they are defined in.
      def when_receiving(method_name, &block)
        isolation_context.create_class_override method_name, &block
      end

      # Verifies whether the specified method has been called
      # You can specify constraints in the block
      #
      # The most complex configuration you can make currently is one that is constrained by arguments.
      # This is most likely to be extended in the future to allow for more complex verifications.
      #
      # Example:
      #
      #     an_isolation.class.did_receive?(:a_method) do |method_call|
      #       method_call.with(3, "a")
      #     end.should.be.successful
      #
      # is equivalent to:
      #
      #     an_isolation.class.did_receive?(:a_method).with(3, "a").should.be.successful
      #
      # You will probably be using this method only when you're interested in whether a method has been called
      # during the course of the test you're running.
      def did_receive?(method_name, &block)
        isolation_context.class_verify method_name, &block
      end

    end

    # mixes in the class methods of this module when it gets included in a class.
    def self.included(base)
      base.extend ClassMethods
    end

    # the context of this isolation instance.
    # this context takes care of responding to method calls etc.
    def isolation_context
      self.class.isolation_context
    end

    # Replaces the call to the proxy with the one you create with this method.
    # You can specify more specific criteria in the block to configure the expectation.
    #
    # Example:
    #
    #     an_isolation.when_receiving(:a_method) do |method_call|
    #       method_call.with(3, "a").return(5)
    #     end
    #
    # is equivalent to:
    #
    #     an_isolation.when_receiving(:a_method).with(3, "a").return(5)
    #
    # You will most likely use this method when you want your stubs to return something else than +nil+
    # when they get called during the run of the test they are defined in.
    def when_receiving(method_name, &block)
      isolation_context.create_override method_name, &block
    end

    # Replaces the call to the class of the proxy with the one you create with this method.
    # You can specify more specific criteria in the block to configure the expectation.
    #
    # Example:
    #
    #     an_isolation.when_class_receives(:a_method) do |method_call|
    #       method_call.with(3, "a").return(5)
    #     end
    #
    # is equivalent to:
    #
    #     an_isolation.when_class_receives(:a_method).with(3, "a").return(5)
    #
    # You will most likely use this method when you want your stubs to return something else than +nil+
    # when they get called during the run of the test they are defined in.
    def when_class_receives(method_name, &block)
      self.class.when_receiving method_name, &block
    end

    # Verifies whether the specified method has been called
    # You can specify constraints in the block
    #
    # The most complex configuration you can make currently is one that is constrained by arguments.
    # This is most likely to be extended in the future to allow for more complex verifications.
    #
    # Example:
    #
    #     an_isolation.did_receive?(:a_method) do |method_call|
    #       method_call.with(3, "a")
    #     end.should.be.successful
    #
    # is equivalent to:
    #
    #     an_isolation.did_receive?(:a_method).with(3, "a").should.be.successful
    #
    # You will probably be using this method only when you're interested in whether a method has been called
    # during the course of the test you're running.
    def did_receive?(method_name, &block)
      isolation_context.verify method_name, &block
    end

    # Verifies whether the specified class method has been called
    # You can specify constraints in the block
    #
    # The most complex configuration you can make currently is one that is constrained by arguments.
    # This is likely to be extended in the future to allow for more complex verifications.
    #
    # Example:
    #
    #     an_isolation.did_class_receive?(:a_method) do |method_call|
    #       method_call.with(3, "a")
    #     end.should.be.successful
    #
    # is equivalent to:
    #
    #     an_isolation.did_class_receive?(:a_method).with(3, "a").should.be.successful
    #
    # You will probably be using this method only when you're interested in whether a method has been called
    # during the course of the test you're running.
    def did_class_receive?(method_name, &block)
      self.class.did_receive?(method_name, &block)
    end    
    
    # Initializes the underlying subject
    # It expects the constructor parameters if they are needed.
    def with_subject(*args, &b)
      isolation_context.instance = self.class.superclass.new *args 
      b.call self if b
      self
    end

  end

  # A base class for +Isolator+ objects
  # to stick with the +Isolation+ nomenclature the strategies for creating isolations
  # are called isolators.
  # An isolator functions as a barrier between the code in your test and the
  # underlying type/instance. It allows you to take control over the value
  # that is returned from a specific method, if you want to pass the method call along to
  # the underlying instance etc.  It also contains the ability to verify if a method
  # was called, with which arguments etc.
  class Isolator

    # holds the isolation created by this isolator
    attr_reader :isolation

    # holds the subject of this isolator
    attr_reader :subject

    # holds the descriptor for this type of object
    attr_reader :descriptor

    # creates a new instance of an isolator
    def initialize(context)
      @context = context
    end

    # builds up the isolation class instance
    def build_isolation(klass, inst=nil)
      pxy = create_isolation_for klass
      @isolation = pxy.new
      @subject = inst
      initialize_messenger
    end

    # initializes the messaging strategy for the isolator
    def initialize_messenger
      raise NotImplementedError
    end

    # Creates the new class name for the isolation
    def class_name(subj)
      nm = subj.respond_to?(:class_eval) ? subj.demodulize : subj.class.demodulize
      @class_name = "#{nm}#{UUIDTools::UUID.random_create.to_s.gsub /-/, ''}"
      @class_name
    end

    # Sets up the necessary instance variables for the isolation
    def initialize_isolation(klass, context)
      pxy = klass.new
      pxy.instance_variable_set("@___context___", context)
      pxy
    end

    class << self

      # Creates the actual proxy object for the +subject+ and initializes it with a
      # +recorder+ and +expectations+
      # This is the actual isolation that will be used to in your tests.
      # It implements all the methods of the +subject+ so as long as you're in Ruby
      # and just need to isolate out some classes defined in a statically compiled language
      # it should get you all the way there for public instance methods at this point.
      # when you're going to isolation for usage within a statically compiled language type
      # then  you're bound to most of their rules. So you need to either isolate interfaces
      # or mark the methods you want to isolate as virtual in your implementing classes.
      def for(context)
        context.recorder ||= MethodCallRecorder.new
        context.expectations ||= Expectations.new
        new(context)
      end
    end
  end

  # A proxy to Ruby objects that records method calls
  # this implements all the instance methods that are defined on the class.
  class RubyIsolator < Isolator

    # implemented template method for creating Ruby isolations
    def initialize(context)
      super
      klass = @context.subject.respond_to?(:class_eval) ? @context.subject : @context.subject.class
      inst = @context.subject.respond_to?(:class_eval) ? nil : @context.subject
      # inst = @context.subject.respond_to?(:class_eval) ? @context.subject.new : @context.subject            
      @descriptor = RubyObjectDescriptor.new klass
      build_isolation klass, inst
    end

    # initializes the messaging strategy for the isolator
    def initialize_messenger
      @context.messenger = RubyMessenger.new @context.expectations, @subject
    end

    # creates the ruby isolator for the specified subject
    def create_isolation_for(subj)
      imembers = @descriptor.instance_members
      cmembers = @descriptor.class_members

      klass = Object.const_set(class_name(subj), Class.new(subj))
      klass.class_eval do

        include Interception

        # access to the proxied subject
        def ___super___
          isolation_context.instance
        end

        imembers.each do |mn|
          mn = mn.name.to_s.to_sym
          define_method mn do |*args|
            b = nil
            b = Proc.new { yield } if block_given?  
            isolation_context.send_message(mn, nil, *args, &b)
          end
        end  
        
        def initialize(*args)  
          self                      
        end 
                   
        cmembers.each do |mn|
          mn = mn.name.to_s.to_sym
          define_cmethod mn do |*args|        
            b = nil
            b = Proc.new { yield } if block_given?  
            isolation_context.send_class_message(mn, nil, *args, &b)
          end
        end   
              


      end

      klass
    end


  end

end