# Holds the state of the +describe+ block that is being
# evaluated. Every example (i.e. +it+ block) is evaluated
# in a context, which may include state set up in <tt>before
# :each</tt> or <tt>before :all</tt> blocks.
#
#--
# A note on naming: this is named _ContextState_ rather
# than _DescribeState_ because +describe+ is the keyword
# in the DSL for referring to the context in which an example
# is evaluated, just as +it+ refers to the example itself.
#++
class ContextState
  attr_reader :state, :parent, :parents, :children, :examples, :to_s

  MOCK_VERIFY = -> { Mock.verify_count }
  MOCK_CLEANUP = -> { Mock.cleanup }
  EXPECTATION_MISSING = -> { raise SpecExpectationNotFoundError }

  def initialize(description, options = nil)
    raise "#describe options should be a Hash or nil" unless Hash === options or options.nil?
    @to_s = description.to_s
    @shared = options && options[:shared]

    @parsed   = false
    @before   = { :all => [], :each => [] }
    @after    = { :all => [], :each => [] }
    @pre      = {}
    @post     = {}
    @examples = []
    @state    = nil
    @parent   = nil
    @parents  = [self]
    @children = []
  end

  # Remove caching when a ContextState is dup'd for shared specs.
  def initialize_copy(other)
    @pre  = {}
    @post = {}
  end

  # Returns true if this is a shared +ContextState+. Essentially, when
  # created with: describe "Something", :shared => true { ... }
  def shared?
    @shared
  end

  # Set the parent (enclosing) +ContextState+ for this state. Creates
  # the +parents+ list.
  def parent=(parent)
    @description = nil

    if shared?
      @parent = nil
    else
      @parent = parent
      parent.child self if parent

      @parents = [self]
      state = parent
      while state
        @parents.unshift state
        state = state.parent
      end
    end
  end

  # Add the ContextState instance +child+ to the list of nested
  # describe blocks.
  def child(child)
    @children << child
  end

  # Adds a nested ContextState in a shared ContextState to a containing
  # ContextState.
  #
  # Normal adoption is from the parent's perspective. But adopt is a good
  # verb and it's reasonable for the child to adopt the parent as well. In
  # this case, manipulating state from inside the child avoids needlessly
  # exposing the state to manipulate it externally in the dup. (See
  # #it_should_behave_like)
  def adopt(parent)
    self.parent = parent

    @examples = @examples.map do |example|
      example = example.dup
      example.context = self
      example
    end

    children = @children
    @children = []

    children.each { |child| child.dup.adopt self }
  end

  # Returns a list of all before(+what+) blocks from self and any parents.
  def pre(what)
    @pre[what] ||= parents.inject([]) { |l, s| l.push(*s.before(what)) }
  end

  # Returns a list of all after(+what+) blocks from self and any parents.
  # The list is in reverse order. In other words, the blocks defined in
  # inner describes are in the list before those defined in outer describes,
  # and in a particular describe block those defined later are in the list
  # before those defined earlier.
  def post(what)
    @post[what] ||= parents.inject([]) { |l, s| l.unshift(*s.after(what)) }
  end

  # Records before(:each) and before(:all) blocks.
  def before(what, &block)
    return if MSpec.guarded?
    block ? @before[what].push(block) : @before[what]
  end

  # Records after(:each) and after(:all) blocks.
  def after(what, &block)
    return if MSpec.guarded?
    block ? @after[what].unshift(block) : @after[what]
  end

  # Creates an ExampleState instance for the block and stores it
  # in a list of examples to evaluate unless the example is filtered.
  def it(desc, &block)
    raise "nested #it" if @state
    example = ExampleState.new(self, desc, block)
    MSpec.actions :add, example
    return if MSpec.guarded?
    @examples << example
  end

  # Evaluates the block and resets the toplevel +ContextState+ to #parent.
  def describe(&block)
    @parsed = protect @to_s, block, false
    MSpec.register_current parent
    MSpec.register_shared self if shared?
  end

  # Returns a description string generated from self and all parents
  def description
    @description ||= parents.map { |p| p.to_s }.compact.join(" ")
  end

  # Injects the before/after blocks and examples from the shared
  # describe block into this +ContextState+ instance.
  def it_should_behave_like(desc)
    return if MSpec.guarded?

    unless state = MSpec.retrieve_shared(desc)
      raise Exception, "Unable to find shared 'describe' for #{desc}"
    end

    state.before(:all).each { |b| before :all, &b }
    state.before(:each).each { |b| before :each, &b }
    state.after(:each).each { |b| after :each, &b }
    state.after(:all).each { |b| after :all, &b }

    state.examples.each do |example|
      example = example.dup
      example.context = self
      @examples << example
    end

    state.children.each do |child|
      child.dup.adopt self
    end
  end

  # Evaluates each block in +blocks+ using the +MSpec.protect+ method
  # so that exceptions are handled and tallied. Returns true and does
  # NOT evaluate any blocks if +check+ is true and
  # <tt>MSpec.mode?(:pretend)</tt> is true.
  def protect(what, blocks, check = true)
    return true if check and MSpec.mode? :pretend
    Array(blocks).all? { |block| MSpec.protect what, &block }
  end

  # Removes filtered examples. Returns true if there are examples
  # left to evaluate.
  def filter_examples
    filtered, @examples = @examples.partition do |ex|
      ex.filtered?
    end

    filtered.each do |ex|
      MSpec.actions :tagged, ex
    end

    !@examples.empty?
  end

  # Evaluates the examples in a +ContextState+. Invokes the MSpec events
  # for :enter, :before, :after, :leave.
  def process
    MSpec.register_current self

    if @parsed and filter_examples
      MSpec.shuffle @examples if MSpec.randomize?
      MSpec.actions :enter, description

      if protect "before :all", pre(:all)
        @examples.each do |state|
          MSpec.repeat do
            @state = state
            example = state.example
            MSpec.actions :before, state

            if protect "before :each", pre(:each)
              MSpec.clear_expectations
              if example
                passed = protect nil, example
                passed = protect nil, -> { MSpec.actions :passed, state, example } if passed
                MSpec.actions :example, state, example
                protect nil, EXPECTATION_MISSING if !MSpec.expectation? and passed
              end
            end
            protect "after :each", post(:each)
            protect "Mock.verify_count", MOCK_VERIFY

            protect "Mock.cleanup", MOCK_CLEANUP
            MSpec.actions :after, state
            @state = nil
          end
        end
        protect "after :all", post(:all)
      else
        protect "Mock.cleanup", MOCK_CLEANUP
      end

      MSpec.actions :leave
    end

    MSpec.register_current nil
    children.each { |child| child.process }
  end
end
