module RSpec
  module Matchers
    module BuiltIn
      # @api private
      #
      # Used _internally_ as a base class for matchers that ship with
      # rspec-expectations and rspec-rails.
      #
      # ### Warning:
      #
      # This class is for internal use, and subject to change without notice.
      # We strongly recommend that you do not base your custom matchers on this
      # class. If/when this changes, we will announce it and remove this warning.
      class BaseMatcher
        include RSpec::Matchers::Composable

        # @api private
        # Used to detect when no arg is passed to `initialize`.
        # `nil` cannot be used because it's a valid value to pass.
        UNDEFINED = Object.new.freeze

        # @private
        attr_reader :actual, :expected, :rescued_exception

        # @private
        attr_writer :matcher_name

        def initialize(expected=UNDEFINED)
          @expected = expected unless UNDEFINED.equal?(expected)
        end

        # @api private
        # Indicates if the match is successful. Delegates to `match`, which
        # should be defined on a subclass. Takes care of consistently
        # initializing the `actual` attribute.
        def matches?(actual)
          @actual = actual
          match(expected, actual)
        end

        # @api private
        # Used to wrap a block of code that will indicate failure by
        # raising one of the named exceptions.
        #
        # This is used by rspec-rails for some of its matchers that
        # wrap rails' assertions.
        def match_unless_raises(*exceptions)
          exceptions.unshift Exception if exceptions.empty?
          begin
            yield
            true
          rescue *exceptions => @rescued_exception
            false
          end
        end

        # @api private
        # Generates a description using {EnglishPhrasing}.
        # @return [String]
        def description
          desc = EnglishPhrasing.split_words(self.class.matcher_name)
          desc << EnglishPhrasing.list(@expected) if defined?(@expected)
          desc
        end

        # @api private
        # Matchers are not diffable by default. Override this to make your
        # subclass diffable.
        def diffable?
          false
        end

        # @api private
        # Most matchers are value matchers (i.e. meant to work with `expect(value)`)
        # rather than block matchers (i.e. meant to work with `expect { }`), so
        # this defaults to false. Block matchers must override this to return true.
        def supports_block_expectations?
          false
        end

        # @private
        def supports_value_expectations?
          true
        end

        # @api private
        def expects_call_stack_jump?
          false
        end

        # @private
        def expected_formatted
          RSpec::Support::ObjectFormatter.format(@expected)
        end

        # @private
        def actual_formatted
          RSpec::Support::ObjectFormatter.format(@actual)
        end

        # @private
        def self.matcher_name
          @matcher_name ||= underscore(name.split('::').last)
        end

        # @private
        def matcher_name
          if defined?(@matcher_name)
            @matcher_name
          else
            self.class.matcher_name
          end
        end

        # @private
        # Borrowed from ActiveSupport.
        def self.underscore(camel_cased_word)
          word = camel_cased_word.to_s.dup
          word.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
          word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
          word.tr!('-', '_')
          word.downcase!
          word
        end
        private_class_method :underscore

        # @private
        module HashFormatting
          # `{ :a => 5, :b => 2 }.inspect` produces:
          #
          #     {:a=>5, :b=>2}
          #
          # ...but it looks much better as:
          #
          #     {:a => 5, :b => 2}
          #
          # This is idempotent and safe to run on a string multiple times.
          def improve_hash_formatting(inspect_string)
            inspect_string.gsub(/(\S)=>(\S)/, '\1 => \2')
          end
          module_function :improve_hash_formatting
        end

        include HashFormatting

        # @private
        module StringEncodingFormatting
          # @api private
          # @return [Boolean] True if the actual and expected string encoding are different.
          #   i.e. the failure may be related to encoding differences and the encoding
          #   should be shown to the user. false otherwise.
          if String.method_defined?(:encoding)
            def string_encoding_differs?
              actual.is_a?(String) && expected.is_a?(String) && actual.encoding != expected.encoding
            end
          else
            # @api private
            # @return [Boolean] False always as the curent Ruby version does not support String encoding
            # :nocov:
            def string_encoding_differs?
              false
            end
            # :nocov:
          end
          module_function :string_encoding_differs?

          if String.method_defined?(:encoding)
            # @api private
            # Formats a String's encoding as a human readable string
            # @param value [String]
            # @return [String]
            def format_encoding(value)
              "#<Encoding:#{value.encoding.name}>"
            end
          else
            # @api private
            # Formats a String's encoding as a human readable string
            # @param _value [String]
            # @return [nil] nil as the curent Ruby version does not support String encoding
            # :nocov:
            def format_encoding(_value)
              nil
            end
            # :nocov:
          end
          module_function :format_encoding
        end

        include StringEncodingFormatting

        # @api private
        # Provides default implementations of failure messages, based on the `description`.
        module DefaultFailureMessages
          # @api private
          # Provides a good generic failure message. Based on `description`.
          # When subclassing, if you are not satisfied with this failure message
          # you often only need to override `description`.
          # @return [String]
          def failure_message
            "expected #{description_of @actual} to #{description}".dup
          end

          # @api private
          # Provides a good generic negative failure message. Based on `description`.
          # When subclassing, if you are not satisfied with this failure message
          # you often only need to override `description`.
          # @return [String]
          def failure_message_when_negated
            "expected #{description_of @actual} not to #{description}".dup
          end

          # @private
          def self.has_default_failure_messages?(matcher)
            matcher.method(:failure_message).owner == self &&
              matcher.method(:failure_message_when_negated).owner == self
          rescue NameError
            false
          end
        end

        include DefaultFailureMessages
      end
    end
  end
end
