module RSpec
  module Core
    module Hooks
      include MetadataHashBuilder::WithConfigWarning

      class Hook
        attr_reader :options

        def initialize(options, &block)
          @options = options
          raise "no block given for #{display_name}" unless block
          @block = block
        end

        def options_apply?(example_or_group)
          example_or_group.all_apply?(options)
        end

        def to_proc
          @block
        end

        def call
          @block.call
        end

        def display_name
          self.class.name.split('::').last.gsub('Hook','').downcase << " hook"
        end
      end

      class BeforeHook < Hook
        def run_in(example_group_instance)
          if example_group_instance
            example_group_instance.instance_eval(&self)
          else
            call
          end
        end
      end

      class AfterHook < Hook
        def run_in(example_group_instance)
          if example_group_instance
            example_group_instance.instance_eval_with_rescue(&self)
          else
            call
          end
        end
      end

      class AroundHook < Hook
        def call(wrapped_example)
          @block.call(wrapped_example)
        end
      end

      class HookCollection < Array
        def find_hooks_for(example_or_group)
          self.class.new(select {|hook| hook.options_apply?(example_or_group)})
        end

        def without_hooks_for(example_or_group)
          self.class.new(reject {|hook| hook.options_apply?(example_or_group)})
        end
      end

      class BeforeHooks < HookCollection
        def run_all(example_group_instance)
          each {|h| h.run_in(example_group_instance) } unless empty?
        end

        def run_all!(example_group_instance)
          shift.run_in(example_group_instance) until empty?
        end
      end

      class AfterHooks < HookCollection
        def run_all(example_group_instance)
          reverse.each {|h| h.run_in(example_group_instance) }
        end

        def run_all!(example_group_instance)
          pop.run_in(example_group_instance) until empty?
        end
      end

      class AroundHooks < HookCollection; end

      # @private
      def hooks
        @hooks ||= {
          :around => { :each => AroundHooks.new },
          :before => { :each => BeforeHooks.new, :all => BeforeHooks.new, :suite => BeforeHooks.new },
          :after => { :each => AfterHooks.new, :all => AfterHooks.new, :suite => AfterHooks.new }
        }
      end

      # @api public
      # @overload before(&block)
      # @overload before(scope, &block)
      # @overload before(scope, conditions, &block)
      # @overload before(conditions, &block)
      #
      # @param [Symbol] scope `:each`, `:all`, or `:suite` (defaults to `:each`)
      # @param [Hash] conditions
      #   constrains this hook to examples matching these conditions e.g.
      #   `before(:each, :ui => true) { ... }` will only run with examples or
      #   groups declared with `:ui => true`.
      #
      # @see #after
      # @see #around
      # @see ExampleGroup
      # @see SharedContext
      # @see SharedExampleGroup
      # @see Configuration
      #
      # Declare a block of code to be run before each example (using `:each`)
      # or once before any example (using `:all`). These are usually declared
      # directly in the [ExampleGroup](ExampleGroup) to which they apply, but
      # they can also be shared across multiple groups.
      #
      # You can also use `before(:suite)` to run a block of code before any
      # example groups are run. This should be declared in
      # [RSpec.configure](../../RSpec#configure-class_method)
      #
      # Instance variables declared in `before(:each)` or `before(:all)` are
      # accessible within each example.
      #
      # ### Order
      #
      # `before` hooks are stored in three scopes, which are run in order:
      # `:suite`, `:all`, and `:each`. They can also be declared in several
      # different places: `RSpec.configure`, a parent group, the current group.
      # They are run in the following order:
      #
      #     before(:suite) # declared in RSpec.configure
      #     before(:all)   # declared in RSpec.configure
      #     before(:all)   # declared in a parent group
      #     before(:all)   # declared in the current group
      #     before(:each)  # declared in RSpec.configure
      #     before(:each)  # declared in a parent group
      #     before(:each)  # declared in the current group
      #
      # If more than one `before` is declared within any one scope, they are run
      # in the order in which they are declared.
      #
      # ### Conditions
      #
      # When you add a conditions hash to `before(:each)` or `before(:all)`,
      # RSpec will only apply that hook to groups or examples that match the
      # conditions. e.g.
      #
      #     RSpec.configure do |config|
      #       config.before(:each, :authorized => true) do
      #         log_in_as :authorized_user
      #       end
      #     end
      #
      #     describe Something, :authorized => true do
      #       # the before hook will run in before each example in this group
      #     end
      #
      #     describe SomethingElse do
      #       it "does something", :authorized => true do
      #         # the before hook will run before this example
      #       end
      #
      #       it "does something else" do
      #         # the hook will not run before this example
      #       end
      #     end
      #
      # ### Warning: `before(:suite, :with => :conditions)`
      #
      # The conditions hash is used to match against specific examples. Since
      # `before(:suite)` is not run in relation to any specific example or
      # group, conditions passed along with `:suite` are effectively ignored.
      #
      # ### Exceptions
      #
      # When an exception is raised in a `before` block, RSpec skips any
      # subsequent `before` blocks and the example, but runs all of the
      # `after(:each)` and `after(:all)` hooks.
      #
      # ### Warning: implicit before blocks
      #
      # `before` hooks can also be declared in shared contexts which get
      # included implicitly either by you or by extension libraries. Since
      # RSpec runs these in the order in which they are declared within each
      # scope, load order matters, and can lead to confusing results when one
      # before block depends on state that is prepared in another before block
      # that gets run later.
      #
      # ### Warning: `before(:all)`
      #
      # It is very tempting to use `before(:all)` to speed things up, but we
      # recommend that you avoid this as there are a number of gotchas, as well
      # as things that simply don't work.
      #
      # #### context
      #
      # `before(:all)` is run in an example that is generated to provide group
      # context for the block.
      #
      # #### instance variables
      #
      # Instance variables declared in `before(:all)` are shared across all the
      # examples in the group.  This means that each example can change the
      # state of a shared object, resulting in an ordering dependency that can
      # make it difficult to reason about failures.
      #
      # ### other frameworks
      #
      # Mock object frameworks and database transaction managers (like
      # ActiveRecord) are typically designed around the idea of setting up
      # before an example, running that one example, and then tearing down.
      # This means that mocks and stubs can (sometimes) be declared in
      # `before(:all)`, but get torn down before the first real example is ever
      # run.
      #
      # You _can_ create database-backed model objects in a `before(:all)` in
      # rspec-rails, but it will not be wrapped in a transaction for you, so
      # you are on your own to clean up in an `after(:all)` block.
      #
      # @example before(:each) declared in an [ExampleGroup](ExampleGroup)
      #
      #     describe Thing do
      #       before(:each) do
      #         @thing = Thing.new
      #       end
      #
      #       it "does something" do
      #         # here you can access @thing
      #       end
      #     end
      #
      # @example before(:all) declared in an [ExampleGroup](ExampleGroup)
      #
      #     describe Parser do
      #       before(:all) do
      #         File.open(file_to_parse, 'w') do |f|
      #           f.write <<-CONTENT
      #             stuff in the file
      #           CONTENT
      #         end
      #       end
      #
      #       it "parses the file" do
      #         Parser.parse(file_to_parse)
      #       end
      #
      #       after(:all) do
      #         File.delete(file_to_parse)
      #       end
      #     end
      def before(*args, &block)
        scope, options = scope_and_options_from(*args)
        hooks[:before][scope] << BeforeHook.new(options, &block)
      end

      # @api public
      # @overload after(&block)
      # @overload after(scope, &block)
      # @overload after(scope, conditions, &block)
      # @overload after(conditions, &block)
      #
      # @param [Symbol] scope `:each`, `:all`, or `:suite` (defaults to `:each`)
      # @param [Hash] conditions
      #   constrains this hook to examples matching these conditions e.g.
      #   `after(:each, :ui => true) { ... }` will only run with examples or
      #   groups declared with `:ui => true`.
      #
      # @see #before
      # @see #around
      # @see ExampleGroup
      # @see SharedContext
      # @see SharedExampleGroup
      # @see Configuration
      #
      # Declare a block of code to be run after each example (using `:each`) or
      # once after all examples (using `:all`). See
      # [#before](Hooks#before-instance_method) for more information about
      # ordering.
      #
      # ### Exceptions
      #
      # `after` hooks are guaranteed to run even when there are exceptions in
      # `before` hooks or examples.  When an exception is raised in an after
      # block, the exception is captured for later reporting, and subsequent
      # `after` blocks are run.
      #
      # ### Order
      #
      # `after` hooks are stored in three scopes, which are run in order:
      # `:each`, `:all`, and `:suite`. They can also be declared in several
      # different places: `RSpec.configure`, a parent group, the current group.
      # They are run in the following order:
      #
      #     after(:each) # declared in the current group
      #     after(:each) # declared in a parent group
      #     after(:each) # declared in RSpec.configure
      #     after(:all)  # declared in the current group
      #     after(:all)  # declared in a parent group
      #     after(:all)  # declared in RSpec.configure
      #
      # This is the reverse of the order in which `before` hooks are run.
      # Similarly, if more than one `after` is declared within any one scope,
      # they are run in reverse order of that in which they are declared.
      def after(*args, &block)
        scope, options = scope_and_options_from(*args)
        hooks[:after][scope] << AfterHook.new(options, &block)
      end

      # @api public
      # @overload around(&block)
      # @overload around(scope, &block)
      # @overload around(scope, conditions, &block)
      # @overload around(conditions, &block)
      #
      # @param [Symbol] scope `:each` (defaults to `:each`)
      #   present for syntax parity with `before` and `after`, but `:each` is
      #   the only supported value.
      #
      # @param [Hash] conditions
      #   constrains this hook to examples matching these conditions e.g.
      #   `around(:each, :ui => true) { ... }` will only run with examples or
      #   groups declared with `:ui => true`.
      #
      # @yield [Example] the example to run
      #
      # @note the syntax of `around` is similar to that of `before` and `after`
      #   but the semantics are quite different. `before` and `after` hooks are
      #   run in the context of of the examples with which they are associated,
      #   whereas `around` hooks are actually responsible for running the
      #   examples. Consequently, `around` hooks do not have direct access to
      #   resources that are made available within the examples and their
      #   associated `before` and `after` hooks.
      #
      # @note `:each` is the only supported scope.
      #
      # Declare a block of code, parts of which will be run before and parts
      # after the example. It is your responsibility to run the example:
      #
      #     around(:each) do |ex|
      #       # do some stuff before
      #       ex.run
      #       # do some stuff after
      #     end
      #
      # The yielded example aliases `run` with `call`, which lets you treat it
      # like a `Proc`.  This is especially handy when working with libaries
      # that manage their own setup and teardown using a block or proc syntax,
      # e.g.
      #
      #     around(:each) {|ex| Database.transaction(&ex)}
      #     around(:each) {|ex| FakeFS(&ex)}
      #
      def around(*args, &block)
        scope, options = scope_and_options_from(*args)
        hooks[:around][scope] << AroundHook.new(options, &block)
      end

      # @private
      # Runs all of the blocks stored with the hook in the context of the
      # example. If no example is provided, just calls the hook directly.
      def run_hook(hook, scope, example_group_instance=nil)
        hooks[hook][scope].run_all(example_group_instance)
      end

      # @private
      # Just like run_hook, except it removes the blocks as it evalutes them,
      # ensuring that they will only be run once.
      def run_hook!(hook, scope, example_group_instance)
        hooks[hook][scope].run_all!(example_group_instance)
      end

      # @private
      def run_hook_filtered(hook, scope, group, example_group_instance, example = nil)
        find_hook(hook, scope, group, example).run_all(example_group_instance)
      end

      # @private
      def find_hook(hook, scope, example_group_class, example = nil)
        found_hooks = hooks[hook][scope].find_hooks_for(example || example_group_class)

        # ensure we don't re-run :all hooks that were applied to any of the parent groups
        if scope == :all
          super_klass = example_group_class.superclass
          while super_klass != RSpec::Core::ExampleGroup
            found_hooks = found_hooks.without_hooks_for(super_klass)
            super_klass = super_klass.superclass
          end
        end

        found_hooks
      end

    private

      SCOPES = [:each, :all, :suite]

      def scope_and_options_from(*args)
        scope = if SCOPES.include?(args.first)
          args.shift
        elsif args.any? { |a| a.is_a?(Symbol) }
          raise ArgumentError.new("You must explicitly give a scope (:each, :all, or :suite) when using symbols as metadata for a hook.")
        else
          :each
        end

        options = build_metadata_hash_from(args)
        return scope, options
      end
    end
  end
end
