# = mock.rb
#
# == Copyright (c) 2005 George Moschovitis, Michael Granger
#
#   Ruby License
#
#   This module is free software. You may use, modify, and/or redistribute this
#   software under the same terms as Ruby.
#
#   This program is distributed in the hope that it will be useful, but WITHOUT
#   ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
#   FOR A PARTICULAR PURPOSE.
#
# == Special Thanks
#
# Thanks goes to Michael Granger for Test::Unit::Mock on which
# this library is partially based.
#
# == Author(s)
#
# * Thomas Sawyer
#
# == Developer Notes
#
#   TODO Rename to MockObject ?
#        Should this be under Test::Unit too ?

# Author::    George Moschovitis, Michael Granger
# Copyright:: Copyright (c) 2005 George Moschovitis, Michael Granger
# License::   Ruby License

require 'ostruct'

# = Mock
#
# A straightfoward mocking facility. Typically used in test cases.
# The Mock class offers a few constructors for quickly building
# mockups.
#
#   mock - Returns a static reponse.
#   echo - Returns the arguments passed-in.
#   spin - Returns a rotation of responses.
#   keys - Returns an index of responses.
#
# Mock classes can be built from sratch or partially framed
# against other classes.
#
# === Usage
#
#   class ContextMock < Mock
#     mock :response_headers, {}
#     spin :host_url, ['http://www.nitrohq.com','http://www.rubyforge.com']
#   end
#
#   ctx = ContextMock.new
#   ctx.response_headers['location'] = url
#   ctx.host_url   #=> "http://www.nitrohq.com"
#   ctx.host_url   #=> "http://www.rubyforge.com"
#
# Or
#
#   class ContextMock < Mock(Context)
#     ...
#   end
#

class Mock < OpenStruct

  # include these?
  #include Test::Unit::Assertions

  # Certain methods are not mocked:
  #   inspect       (tricky)
  #   class         (delegated)
  #   kind_of?      (delegated)
  #   is_a?         (delegated)
  #   instance_of?  (delegated)
  #   method        (works as-is)
  #   send          (works as-is)
  #   respond_to?   (works as-is)
  #   hash          (no way to mock)
  #
  #   __id__, __call__, etc. (not meant to be mocked, ever!)
  #

  UnmockedMethods = %r{^(
    |inspect
    |kind_of\?|is_a\?|instance_of\?|class
    |method|send|respond_to\?
    |hash
    |__
  )}x

  class << self

    attr :mocked_class

    def mocks
      self.methods(false)
    end

    # Mock a static repsonse.
    def mock( sym, val )
      define_method( sym ) { |*args| val }
    end

    # Responds with input.
    def echo( sym )
      define_method( sym ) { |*args| args }
    end

    # Reponds with a rotation of reponses.
    def spin( sym, arr )
      define_method( sym ) { |*args| arr.push(arr.shift) ; arr[-1] }
    end

    # Responds according to a mapping of input parameters.
    def keys( sym, hsh )
      define_method( sym ) { |*args| hsh[args] }
    end

  end

  # Delegate methods: #class, instance_of?, kind_of?, and is_a?

  alias :__class :class

  def class # :nodoc:
    return __class.mocked_class
  end

  def instance_of?( klass ) # :nodoc:
    self.class == klass
  end

  def kind_of?( klass ) # :nodoc:
    self.class <= klass
  end

  alias_method :is_a?, :kind_of?

end


# Factory method for creating semi-functional mock objects given the
# class which is to be mocked. It looks like a constant for purposes
# of syntactic sugar.
def Mock( realclass )

  mockclass = Class.new( Mock )

  mockclass.instance_eval do @mocked_class = realclass end

  # Provide an accessor to class instance var that holds the class
  # object we're faking
  class << mockclass
    # The actual class being mocked
    attr_reader :mocked_class

    # Propagate the mocked class ivar to derivatives so it can be
    # called like:
    #   class MockFoo < Mock( RealClass )
    def inherited( subclass )
      mc = self.mockedClass
      subclass.instance_eval do @mocked_class = mc end
    end
  end

  # Build method definitions for all the mocked class's instance
  # methods, as well as those given to it by its superclasses, since
  # we're not really inheriting from it.
  imethods = realclass.instance_methods(true).collect do |name|
    next if name =~ ::Mock::UnmockedMethods

    # Figure out the argument list
    arity = realclass.instance_method( name ).arity
    optargs = false

    if arity < 0
      optargs = true
      arity = (arity+1).abs
    end

    args = []
    arity.times do |n| args << "arg#{n+1}" end
    args << "*optargs" if optargs

    # Build a method definition. Some methods need special
    # declarations.
    argsj = args.join(',')
    case name.intern
    when :initialize
      "def initialize(#{argsj}) ; super ; end"
    else
      "def #{name}(#{argsj}) ; self.send(#{name},#{argsj}) ; end"
      #"def %s( %s ) ; self.__mockRegisterCall(%s) ; end" %
      #  [ name, argstr, [":#{name}", *args].join(',') ]
    end
  end

  # Now add the instance methods to the mockclass class
  mockclass.class_eval imethods.join( "\n" )

  return mockclass
end



#  _____         _
# |_   _|__  ___| |_
#   | |/ _ \/ __| __|
#   | |  __/\__ \ |_
#   |_|\___||___/\__|
#

# TODO

=begin #test

  class MyMock < Mock
    mock :m, 10
    echo :e
    spin :s, [1,2,3]
    keys :i, { [:foo] => 'Hello', [:boo] => 'Frog' }

    def a(k) k+1 end
  end

  my = MyMock.new

  p my.m
  p my.e('hi')
  p my.s
  p my.s
  p my.s
  p my.i(:foo)
  p my.i(:boo)
  p my.a(1)

=end
