module RSpec
  module Core
    # Provides methods to mark examples as pending. These methods are available
    # to be called from within any example or hook.
    module Pending
      # Raised in the middle of an example to indicate that it should be marked
      # as skipped.
      class SkipDeclaredInExample < StandardError
        attr_reader :argument

        def initialize(argument)
          @argument = argument
        end
      end

      # If Test::Unit is loaded, we'll use its error as baseclass, so that
      # Test::Unit will report unmet RSpec expectations as failures rather than
      # errors.
      begin
        class PendingExampleFixedError < Test::Unit::AssertionFailedError; end
      rescue
        class PendingExampleFixedError < StandardError; end
      end

      # @private
      NO_REASON_GIVEN = 'No reason given'

      # @private
      NOT_YET_IMPLEMENTED = 'Not yet implemented'

      # @overload pending()
      # @overload pending(message)
      #
      # Marks an example as pending. The rest of the example will still be
      # executed, and if it passes the example will fail to indicate that the
      # pending can be removed.
      #
      # @param message [String] optional message to add to the summary report.
      #
      # @example
      #     describe "some behaviour" do
      #       # reported as "Pending: no reason given"
      #       it "is pending with no message" do
      #         pending
      #         raise "broken"
      #       end
      #
      #       # reported as "Pending: something else getting finished"
      #       it "is pending with a custom message" do
      #         pending("something else getting finished")
      #         raise "broken"
      #       end
      #     end
      #
      # @note When using `pending` inside an example body using this method
      #   hooks, such as `before(:example)`, have already be run. This means that
      #   a failure from the code in the `before` hook will prevent the example
      #   from being considered pending, as the example body would not be
      #   executed. If you need to consider hooks as pending as well you can use
      #   the pending metadata as an alternative, e.g.
      #   `it "does something", pending: "message"`.
      def pending(message=nil, &_block)
        current_example = RSpec.current_example

        if block_given?
          raise ArgumentError, <<-EOS.gsub(/^\s+\|/, '')
            |The semantics of `RSpec::Core::Pending#pending` have changed in
            |RSpec 3. In RSpec 2.x, it caused the example to be skipped. In
            |RSpec 3, the rest of the example is still run but is expected to
            |fail, and will be marked as a failure (rather than as pending) if
            |the example passes.
            |
            |Passing a block within an example is now deprecated. Marking the
            |example as pending provides the same behavior in RSpec 3 which was
            |provided only by the block in RSpec 2.x.
            |
            |Move the code in the block provided to `pending` into the rest of
            |the example body.
            |
            |Called from #{CallerFilter.first_non_rspec_line}.
            |
          EOS
        elsif current_example
          Pending.mark_pending! current_example, message
        else
          raise "`pending` may not be used outside of examples, such as in " \
                "before(:context). Maybe you want `skip`?"
        end
      end

      # @overload skip()
      # @overload skip(message)
      #
      # Marks an example as pending and skips execution.
      #
      # @param message [String] optional message to add to the summary report.
      #
      # @example
      #     describe "an example" do
      #       # reported as "Pending: no reason given"
      #       it "is skipped with no message" do
      #         skip
      #       end
      #
      #       # reported as "Pending: something else getting finished"
      #       it "is skipped with a custom message" do
      #         skip "something else getting finished"
      #       end
      #     end
      def skip(message=nil)
        current_example = RSpec.current_example

        Pending.mark_skipped!(current_example, message) if current_example

        raise SkipDeclaredInExample.new(message)
      end

      # @private
      #
      # Mark example as skipped.
      #
      # @param example [RSpec::Core::Example] the example to mark as skipped
      # @param message_or_bool [Boolean, String] the message to use, or true
      def self.mark_skipped!(example, message_or_bool)
        Pending.mark_pending! example, message_or_bool
        example.metadata[:skip] = true
      end

      # @private
      #
      # Mark example as pending.
      #
      # @param example [RSpec::Core::Example] the example to mark as pending
      # @param message_or_bool [Boolean, String] the message to use, or true
      def self.mark_pending!(example, message_or_bool)
        message = if !message_or_bool || !(String === message_or_bool)
                    NO_REASON_GIVEN
                  else
                    message_or_bool
                  end

        example.metadata[:pending] = true
        example.execution_result.pending_message = message
        example.execution_result.pending_fixed = false
      end

      # @private
      #
      # Mark example as fixed.
      #
      # @param example [RSpec::Core::Example] the example to mark as fixed
      def self.mark_fixed!(example)
        example.execution_result.pending_fixed = true
      end
    end
  end
end
