#!/usr/bin/env ruby

#---
# Copyright 2003-2012 by Jim Weirich (jim.weirich@gmail.com).
# All rights reserved.
#
# Permission is granted for use, copying, modification, distribution,
# and distribution of modified versions of this work as long as the
# above copyright notice is included.
#+++

require 'flexmock/errors'
require 'flexmock/composite'
require 'flexmock/ordering'
require 'flexmock/argument_matching'
require 'flexmock/explicit_needed'
require 'flexmock/class_extensions'

######################################################################
# FlexMock is a flexible mock object framework for supporting testing.
#
# FlexMock has a simple interface that's easy to remember, and leaves
# the hard stuff to all those other mock object implementations.
#
# Basic Usage:
#
#   m = flexmock("name")
#   m.should_receive(:upcase).with("stuff").
#     and_return("STUFF")
#   m.should_receive(:downcase).with(String).
#     and_return { |s| s.downcase }.once
#
# With Test::Unit Integration:
#
#   class TestSomething < Test::Unit::TestCase
#     include FlexMock::TestCase
#
#     def test_something
#       m = flexmock("name")
#       m.should_receive(:hi).and_return("Hello")
#       m.hi
#     end
#   end
#
# Note: When using Test::Unit integeration, don't forget to include
# FlexMock::TestCase.  Also, if you override +teardown+, make sure you
# call +super+.
#
class FlexMock
  include Ordering

  attr_reader :flexmock_name
  attr_accessor :flexmock_container

  class << self
    attr_accessor :partials_are_based
  end
  self.partials_are_based = false

  # Create a FlexMock object with the given name.  The name is used in
  # error messages.  If no container is given, create a new, one-off
  # container for this mock.
  def initialize(name="unknown", container=nil)
    @flexmock_name = name
    @expectations = Hash.new
    @ignore_missing = false
    @verified = false
    @calls = []
    @base_class = nil
    container = UseContainer.new if container.nil?
    container.flexmock_remember(self)
  end

  # Return the inspection string for a mock.
  def inspect
    "<FlexMock:#{flexmock_name}>"
  end

  # Verify that each method that had an explicit expected count was
  # actually called that many times.
  def flexmock_verify
    return if @verified
    @verified = true
    flexmock_wrap do
      @expectations.each do |sym, handler|
        handler.flexmock_verify
      end
    end
  end

  # Teardown and infrastructure setup for this mock.
  def flexmock_teardown
  end

  # Ignore all undefined (missing) method calls.
  def should_ignore_missing
    @ignore_missing = true
    self
  end
  alias mock_ignore_missing should_ignore_missing

  def by_default
    @last_expectation.by_default
    self
  end

  CallRecord = Struct.new(:method_name, :args, :block_given, :expectation) do
    def matches?(sym, actual_args, options)
      method_name == sym &&
        ArgumentMatching.all_match?(actual_args, args) &&
        matches_block?(options[:with_block])
    end

    private

    def matches_block?(block_option)
      block_option.nil? ||
        (block_option && block_given) ||
        (!block_option && !block_given)
    end
  end

  # Handle missing methods by attempting to look up a handler.
  def method_missing(sym, *args, &block)
    enhanced_args = block_given? ? args + [block] : args
    call_record = CallRecord.new(sym, enhanced_args, block_given?)
    @calls << call_record
    flexmock_wrap do
      if handler = @expectations[sym]
        handler.call(enhanced_args, call_record)
      elsif @base_class && @base_class.flexmock_defined?(sym)
        FlexMock.undefined
      elsif @ignore_missing
        FlexMock.undefined
      else
        super(sym, *args, &block)
      end
    end
  end

  # Save the original definition of respond_to? for use a bit later.
  alias flexmock_respond_to? respond_to?

  # Override the built-in respond_to? to include the mocked methods.
  def respond_to?(sym, *args)
    super || (@expectations[sym] ? true : @ignore_missing)
  end

  # Find the mock expectation for method sym and arguments.
  def flexmock_find_expectation(method_name, *args) # :nodoc:
    exp = @expectations[method_name]
    exp ? exp.find_expectation(*args) : nil
  end

  # Return the expectation director for a method name.
  def flexmock_expectations_for(method_name) # :nodoc:
    @expectations[method_name]
  end

  def flexmock_based_on(base_class)
    @base_class = base_class
    should_receive(:class => base_class)
  end

  # True if the mock received the given method and arguments.
  def flexmock_received?(sym, args, options={})
    count = 0
    additional = options[:and] || []
    additional = [additional] if additional.is_a?(Proc)
    @calls.each { |call_record|
      if call_record.matches?(sym, args, options)
        count += 1
        if options[:on_count].nil? || count == options[:on_count]
          additional.each do |add| add.call(*call_record.args) end
        end
      end
    }
    if options[:times]
      result = count == options[:times]
    else
      result = count > 0
    end
    result
  end

  # Return the list of calls made on this mock. Used in formatting
  # error messages.
  def flexmock_calls
    @calls
  end

  # Invocke the original non-mocked functionality for the given
  # symbol.
  def flexmock_invoke_original(sym, args)
    return FlexMock.undefined
  end

  # Override the built-in +method+ to include the mocked methods.
  def method(sym)
    @expectations[sym] || super
  rescue NameError => ex
    if @ignore_missing
      proc { FlexMock.undefined }
    else
      raise ex
    end
  end

  # :call-seq:
  #    mock.should_receive(:method_name)
  #    mock.should_receive(:method1, method2, ...)
  #    mock.should_receive(:meth1 => result1, :meth2 => result2, ...)
  #
  # Declare that the mock object should receive a message with the given name.
  #
  # If more than one method name is given, then the mock object should expect
  # to receive all the listed melthods.  If a hash of method name/value pairs
  # is given, then the each method will return the associated result.  Any
  # expectations applied to the result of +should_receive+ will be applied to
  # all the methods defined in the argument list.
  #
  # An expectation object for the method name is returned as the result of
  # this method.  Further expectation constraints can be added by chaining to
  # the result.
  #
  # See Expectation for a list of declarators that can be used.
  #
  def should_receive(*args)
    location = caller.first
    flexmock_define_expectation(location, *args)
  end

  def flexmock_define_expectation(location, *args)
    @last_expectation = ContainerHelper.parse_should_args(self, args) do |sym|
      @expectations[sym] ||= ExpectationDirector.new(sym)
      result = Expectation.new(self, sym, location)
      @expectations[sym] << result
      override_existing_method(sym) if flexmock_respond_to?(sym)
      result = ExplicitNeeded.new(result, sym, @base_class) if
        @base_class && ! @base_class.flexmock_defined?(sym)
      result
    end
  end

  # Declare that the mock object should expect methods by providing a
  # recorder for the methods and having the user invoke the expected
  # methods in a block.  Further expectations may be applied the
  # result of the recording call.
  #
  # Example Usage:
  #
  #   mock.should_expect do |record|
  #     record.add(Integer, 4) { |a, b|
  #       a + b
  #     }.at_least.once
  #
  def should_expect
    yield Recorder.new(self)
  end

  private

  # Wrap a block of code so the any assertion errors are wrapped so
  # that the mock name is added to the error message .
  def flexmock_wrap(&block)
    yield
  rescue FlexMock.framework_adapter.assertion_failed_error => ex
    raise FlexMock.framework_adapter.assertion_failed_error,
    "in mock '#{@flexmock_name}': #{ex.message}",
    ex.backtrace
  end


  # Override the existing definition of method +sym+ in the mock.
  # Most methods depend on the method_missing trick to be invoked.
  # However, if the method already exists, it will not call
  # method_missing.  This method defines a singleton method on the
  # mock to explicitly invoke the method_missing logic.
  def override_existing_method(sym)
    sclass.class_eval <<-EOS
      def #{sym}(*args, &block)
        method_missing(:#{sym}, *args, &block)
      end
    EOS
  end

  # Return the singleton class of the mock object.
  def sclass
    class << self; self; end
  end
end

require 'flexmock/core_class_methods'
