module Spec
  module Mocks

    class BaseExpectation
      attr_reader :sym
      attr_writer :expected_received_count, :method_block, :expected_from
      protected :expected_received_count=, :method_block=, :expected_from=
      attr_accessor :error_generator
      protected :error_generator, :error_generator=
      
      def initialize(error_generator, expectation_ordering, expected_from, sym, method_block, expected_received_count=1, opts={})
        @error_generator = error_generator
        @error_generator.opts = opts
        @expected_from = expected_from
        @sym = sym
        @method_block = method_block
        @return_block = nil
        @actual_received_count = 0
        @expected_received_count = expected_received_count
        @args_expectation = ArgumentExpectation.new([ArgumentConstraints::AnyArgsConstraint.new])
        @consecutive = false
        @exception_to_raise = nil
        @symbol_to_throw = nil
        @order_group = expectation_ordering
        @at_least = nil
        @at_most = nil
        @args_to_yield = []
      end
      
      def build_child(expected_from, method_block, expected_received_count, opts={})
        child = clone
        child.expected_from = expected_from
        child.method_block = method_block
        child.expected_received_count = expected_received_count
        new_gen = error_generator.clone
        new_gen.opts = opts
        child.error_generator = new_gen
        child.clone_args_to_yield @args_to_yield
        child
      end
      
      def error_generator_opts=(opts={})
        @error_generator.opts = opts
      end
      protected :error_generator_opts=
      
      def expected_args
        @args_expectation.args
      end

      def and_return(*values, &return_block)
        Kernel::raise AmbiguousReturnError unless @method_block.nil?
        case values.size
          when 0 then value = nil
          when 1 then value = values[0]
        else
          value = values
          @consecutive = true
          @expected_received_count = values.size if !ignoring_args? &&
                                                    @expected_received_count < values.size
        end
        @return_block = block_given? ? return_block : lambda { value }
        # Ruby 1.9 - see where this is used below
        @ignore_args = !block_given?
      end
      
      # :call-seq:
      #   and_raise()
      #   and_raise(Exception) #any exception class
      #   and_raise(exception) #any exception object
      #
      # == Warning
      #
      # When you pass an exception class, the MessageExpectation will
      # raise an instance of it, creating it with +new+. If the exception
      # class initializer requires any parameters, you must pass in an
      # instance and not the class.
      def and_raise(exception=Exception)
        @exception_to_raise = exception
      end
      
      def and_throw(symbol)
        @symbol_to_throw = symbol
      end
      
      def and_yield(*args)
        if @args_to_yield_were_cloned
          @args_to_yield.clear
          @args_to_yield_were_cloned = false
        end
        
        @args_to_yield << args
        self
      end
  
      def matches(sym, args)
        @sym == sym and @args_expectation.args_match?(args)
      end
      
      def invoke(args, block)
        if @expected_received_count == 0
          @failed_fast = true
          @actual_received_count += 1
          @error_generator.raise_expectation_error @sym, @expected_received_count, @actual_received_count, *args
        end
        
        @order_group.handle_order_constraint self

        begin
          Kernel::raise @exception_to_raise unless @exception_to_raise.nil?
          Kernel::throw @symbol_to_throw unless @symbol_to_throw.nil?
          
          
          if !@method_block.nil?
            default_return_val = invoke_method_block(args)
          elsif @args_to_yield.size > 0
            default_return_val = invoke_with_yield(block)
          else
            default_return_val = nil
          end
          
          if @consecutive
            return invoke_consecutive_return_block(args, block)
          elsif @return_block
            return invoke_return_block(args, block)
          else
            return default_return_val
          end
        ensure
          @actual_received_count += 1
        end
      end

      def called_max_times?
        @expected_received_count != :any && @expected_received_count > 0 &&
          @actual_received_count >= @expected_received_count
      end
      
      protected

      def invoke_method_block(args)
        begin
          @method_block.call(*args)
        rescue => detail
          @error_generator.raise_block_failed_error @sym, detail.message
        end
      end
      
      def invoke_with_yield(block)
        if block.nil?
          @error_generator.raise_missing_block_error @args_to_yield
        end
        value = nil
        @args_to_yield.each do |args_to_yield_this_time|
          if block.arity > -1 && args_to_yield_this_time.length != block.arity
            @error_generator.raise_wrong_arity_error args_to_yield_this_time, block.arity
          end
          value = block.call(*args_to_yield_this_time)
        end
        value
      end
      
      def invoke_consecutive_return_block(args, block)
        args << block unless block.nil?
        value = @return_block.call(*args)
        
        index = [@actual_received_count, value.size-1].min
        value[index]
      end
      
      def invoke_return_block(args, block)
        args << block unless block.nil?
        # Ruby 1.9 - when we set @return_block to return values
        # regardless of arguments, any arguments will result in
        # a "wrong number of arguments" error
        if @ignore_args
          @return_block.call()
        else
          @return_block.call(*args)
        end
      end

      def clone_args_to_yield(args)
        @args_to_yield = args.clone
        @args_to_yield_were_cloned = true
      end
      
      def failed_fast?
        @failed_fast
      end
    end
    
    class MessageExpectation < BaseExpectation
      
      def matches_name_but_not_args(sym, args)
        @sym == sym and not @args_expectation.args_match?(args)
      end
       
      def verify_messages_received   
        return if expected_messages_received? || failed_fast?
    
        generate_error
      rescue Spec::Mocks::MockExpectationError => error
        error.backtrace.insert(0, @expected_from)
        Kernel::raise error
      end
      
      def expected_messages_received?
        ignoring_args? || matches_exact_count? ||
           matches_at_least_count? || matches_at_most_count?
      end
      
      def ignoring_args?
        @expected_received_count == :any
      end
      
      def matches_at_least_count?
        @at_least && @actual_received_count >= @expected_received_count
      end
      
      def matches_at_most_count?
        @at_most && @actual_received_count <= @expected_received_count
      end
      
      def matches_exact_count?
        @expected_received_count == @actual_received_count
      end
      
      def similar_messages
        @similar_messages ||= []
      end

      def advise(args, block)
        similar_messages << args
      end
      
      def generate_error
        if similar_messages.empty?
          @error_generator.raise_expectation_error(@sym, @expected_received_count, @actual_received_count, *@args_expectation.args)
        else
          @error_generator.raise_unexpected_message_args_error(self, *@similar_messages.first)
        end
      end

      def with(*args, &block)
        @args_expectation = ArgumentExpectation.new(args, &block)
        self
      end
      
      def exactly(n)
        set_expected_received_count :exactly, n
        self
      end
      
      def at_least(n)
        set_expected_received_count :at_least, n
        self
      end
      
      def at_most(n)
        set_expected_received_count :at_most, n
        self
      end

      def times(&block)
        @method_block = block if block
        self
      end
  
      def any_number_of_times(&block)
        @method_block = block if block
        @expected_received_count = :any
        self
      end
  
      def never
        @expected_received_count = 0
        self
      end
  
      def once(&block)
        @method_block = block if block
        @expected_received_count = 1
        self
      end
  
      def twice(&block)
        @method_block = block if block
        @expected_received_count = 2
        self
      end
  
      def ordered(&block)
        @method_block = block if block
        @order_group.register(self)
        @ordered = true
        self
      end
      
      def negative_expectation_for?(sym)
        return false
      end
      
      protected
        def set_expected_received_count(relativity, n)
          @at_least = (relativity == :at_least)
          @at_most = (relativity == :at_most)
          @expected_received_count = case n
            when Numeric
              n
            when :once
              1
            when :twice
              2
          end
        end
      
    end
    
    class NegativeMessageExpectation < MessageExpectation
      def initialize(message, expectation_ordering, expected_from, sym, method_block)
        super(message, expectation_ordering, expected_from, sym, method_block, 0)
      end
      
      def negative_expectation_for?(sym)
        return @sym == sym
      end
    end
    
  end
end
