module Spec
  module Mocks
  
    class MatcherConstraint
      def initialize(matcher)
        @matcher = matcher
      end
      
      def matches?(value)
        @matcher.matches?(value)
      end
    end
      
    class LiteralArgConstraint
      def initialize(literal)
        @literal_value = literal
      end
      
      def matches?(value)
        @literal_value == value
      end
    end
    
    class RegexpArgConstraint
      def initialize(regexp)
        @regexp = regexp
      end
      
      def matches?(value)
        return value =~ @regexp unless value.is_a?(Regexp)
        value == @regexp
      end
    end
    
    class AnyArgConstraint
      def initialize(ignore)
      end
      
      def ==(other)
        true
      end
      
      # TODO - need this?
      def matches?(value)
        true
      end
    end
    
    class AnyArgsConstraint
      def description
        "any args"
      end
    end
    
    class NoArgsConstraint
      def description
        "no args"
      end
      
      def ==(args)
        args == []
      end
    end
    
    class NumericArgConstraint
      def initialize(ignore)
      end
      
      def matches?(value)
        value.is_a?(Numeric)
      end
    end
    
    class BooleanArgConstraint
      def initialize(ignore)
      end
      
      def ==(value)
        matches?(value)
      end
      
      def matches?(value)
        return true if value.is_a?(TrueClass)
        return true if value.is_a?(FalseClass)
        false
      end
    end
    
    class StringArgConstraint
      def initialize(ignore)
      end
      
      def matches?(value)
        value.is_a?(String)
      end
    end
    
    class DuckTypeArgConstraint
      def initialize(*methods_to_respond_to)
        @methods_to_respond_to = methods_to_respond_to
      end
  
      def matches?(value)
        @methods_to_respond_to.all? { |sym| value.respond_to?(sym) }
      end
      
      def description
        "duck_type"
      end
    end
    
    class HashIncludingConstraint
      def initialize(expected)
        @expected = expected
      end
      
      def ==(actual)
        @expected.each do | key, value |
          # check key for case that value evaluates to nil
          return false unless actual.has_key?(key) && actual[key] == value
        end
        true
      rescue NoMethodError => ex
        return false
      end
      
      def matches?(value)
        self == value
      end
      
      def description
        "hash_including(#{@expected.inspect.sub(/^\{/,"").sub(/\}$/,"")})"
      end
      
    end
    

    class ArgumentExpectation
      attr_reader :args
      @@constraint_classes = Hash.new { |hash, key| LiteralArgConstraint}
      @@constraint_classes[:anything] = AnyArgConstraint
      @@constraint_classes[:numeric] = NumericArgConstraint
      @@constraint_classes[:boolean] = BooleanArgConstraint
      @@constraint_classes[:string] = StringArgConstraint
      
      def initialize(args)
        @args = args
        if [:any_args] == args
          @expected_params = nil
          warn_deprecated(:any_args.inspect, "any_args()")
        elsif args.length == 1 && args[0].is_a?(AnyArgsConstraint) then @expected_params = nil
        elsif [:no_args] == args
          @expected_params = []
          warn_deprecated(:no_args.inspect, "no_args()")
        elsif args.length == 1 && args[0].is_a?(NoArgsConstraint) then @expected_params = []
        else @expected_params = process_arg_constraints(args)
        end
      end
      
      def process_arg_constraints(constraints)
        constraints.collect do |constraint| 
          convert_constraint(constraint)
        end
      end
      
      def warn_deprecated(deprecated_method, instead)
        Kernel.warn "The #{deprecated_method} constraint is deprecated. Use #{instead} instead."
      end
      
      def convert_constraint(constraint)
        if [:anything, :numeric, :boolean, :string].include?(constraint)
          case constraint
          when :anything
            instead = "anything()"
          when :boolean
            instead = "boolean()"
          when :numeric
            instead = "an_instance_of(Numeric)"
          when :string
            instead = "an_instance_of(String)"
          end
          warn_deprecated(constraint.inspect, instead)
          return @@constraint_classes[constraint].new(constraint)
        end
        return MatcherConstraint.new(constraint) if is_matcher?(constraint)
        return RegexpArgConstraint.new(constraint) if constraint.is_a?(Regexp)
        return LiteralArgConstraint.new(constraint)
      end
      
      def is_matcher?(obj)
        return obj.respond_to?(:matches?) && obj.respond_to?(:description)
      end
      
      def check_args(args)
        return true if @expected_params.nil?
        return true if @expected_params == args
        return constraints_match?(args)
      end
      
      def constraints_match?(args)
        return false if args.length != @expected_params.length
        @expected_params.each_index { |i| return false unless @expected_params[i].matches?(args[i]) }
        return true
      end
  
    end
    
  end
end
