require 'rspec/matchers/dsl'

module RSpec
  module Matchers
    module BuiltIn
      class BeTruthy < BaseMatcher
        def match(_, actual)
          !!actual
        end

        def failure_message_for_should
          "expected: truthy value\n     got: #{actual.inspect}"
        end

        def failure_message_for_should_not
          "expected: falsey value\n     got: #{actual.inspect}"
        end
      end

      class BeFalsey < BaseMatcher
        def match(_, actual)
          !actual
        end

        def failure_message_for_should
          "expected: falsey value\n     got: #{actual.inspect}"
        end

        def failure_message_for_should_not
          "expected: truthy value\n     got: #{actual.inspect}"
        end
      end

      class BeNil < BaseMatcher
        def match(_, actual)
          actual.nil?
        end

        def failure_message_for_should
          "expected: nil\n     got: #{actual.inspect}"
        end

        def failure_message_for_should_not
          "expected: not nil\n     got: nil"
        end
      end

      module BeHelpers
        private

        def args_to_s
          @args.empty? ? "" : parenthesize(inspected_args.join(', '))
        end

        def parenthesize(string)
          "(#{string})"
        end

        def inspected_args
          @args.collect{|a| a.inspect}
        end

        def expected_to_sentence
          split_words(@expected)
        end

        def args_to_sentence
          to_sentence(@args)
        end
      end

      class Be < BaseMatcher
        include BeHelpers

        def initialize(*args, &block)
          @args = args
        end

        def match(_, actual)
          !!actual
        end

        def failure_message_for_should
          "expected #{@actual.inspect} to evaluate to true"
        end

        def failure_message_for_should_not
          "expected #{@actual.inspect} to evaluate to false"
        end

        [:==, :<, :<=, :>=, :>, :===, :=~].each do |operator|
          define_method operator do |operand|
            BeComparedTo.new(operand, operator)
          end
        end
      end

      class BeComparedTo < Be
        def initialize(operand, operator)
          @expected, @operator = operand, operator
            @args = []
        end

        def matches?(actual)
          @actual = actual
          @actual.__send__ @operator, @expected
        end

        def failure_message_for_should
          "expected: #{@operator} #{@expected.inspect}\n     got: #{@operator.to_s.gsub(/./, ' ')} #{@actual.inspect}"
        end

        def failure_message_for_should_not
          message = <<-MESSAGE
`#{negative_expectation_expression}` not only FAILED,
it is a bit confusing.
          MESSAGE

          raise message << ([:===,:==].include?(@operator) ?
                            "It might be more clearly expressed without the \"be\"?" :
                            "It might be more clearly expressed in the positive?")
        end

        def description
          "be #{@operator} #{expected_to_sentence}#{args_to_sentence}"
        end

        def negative_expectation_expression
          Expectations::Syntax.negative_expression("actual", "be #{@operator} #{@expected}")
        end
      end

      class BePredicate < BaseMatcher
        include BeHelpers

        def initialize(*args, &block)
          @expected = parse_expected(args.shift)
          @args = args
          @block = block
        end

        def matches?(actual)
          @actual = actual

          if is_private_on?( @actual )
            RSpec.deprecate "matching with be_#{predicate.to_s.gsub(/\?$/,'')} on private method #{predicate}",
              :replacement => "`expect(object.send(#{predicate.inspect})).to be_true` or change the method's visibility to public",
              :call_site => caller(0)[3]
          end

          begin
            @result = actual.__send__(predicate, *@args, &@block)
            check_respond_to(predicate)
            return @result
          rescue NameError => predicate_missing_error
            "this needs to be here or rcov will not count this branch even though it's executed in a code example"
          end

          begin
            @result = actual.__send__(present_tense_predicate, *@args, &@block)
            check_respond_to(present_tense_predicate)
            return @result
          rescue NameError
            raise predicate_missing_error
          end
        end

        alias === matches?

        def failure_message_for_should
          "expected #{predicate}#{args_to_s} to return true, got #{@result.inspect}"
        end

        def failure_message_for_should_not
          "expected #{predicate}#{args_to_s} to return false, got #{@result.inspect}"
        end

        def description
          "#{prefix_to_sentence}#{expected_to_sentence}#{args_to_sentence}"
        end

      private

        # support 1.8.7
        if String === methods.first
          def is_private_on? actual
            actual.private_methods.include? predicate.to_s
          end
        else
          def is_private_on? actual
            actual.private_methods.include? predicate
          end
        end

        def predicate
          "#{@expected}?".to_sym
        end

        def present_tense_predicate
          "#{@expected}s?".to_sym
        end

        def parse_expected(expected)
          @prefix, expected = prefix_and_expected(expected)
          expected
        end

        def prefix_and_expected(symbol)
          symbol.to_s =~ /^(be_(an?_)?)(.*)/
          return $1, $3
        end

        def prefix_to_sentence
          split_words(@prefix)
        end

        def check_respond_to(method)
          RSpec.deprecate(
            "Matching with #{@prefix}#{@expected} on an object that doesn't respond to `#{method}`",
            :replacement => "`respond_to_missing?` or `respond_to?` on your object"
          ) unless actual.respond_to?(method)
        end
      end
    end
  end
end
