# frozen_string_literal: true

module RuboCop
  module Cop
    module RSpec
      # A helper for `inflected` style
      module InflectedHelper
        include RuboCop::RSpec::Language
        extend NodePattern::Macros

        MSG_INFLECTED = 'Prefer using `%<matcher_name>s` matcher over ' \
                        '`%<predicate_name>s`.'

        private

        def check_inflected(node)
          predicate_in_actual?(node) do |predicate, to, matcher|
            msg = message_inflected(predicate)
            add_offense(node, message: msg) do |corrector|
              remove_predicate(corrector, predicate)
              corrector.replace(node.loc.selector,
                                true?(to, matcher) ? 'to' : 'not_to')
              rewrite_matcher(corrector, predicate, matcher)
            end
          end
        end

        # @!method predicate_in_actual?(node)
        def_node_matcher :predicate_in_actual?, <<-PATTERN
          (send
            (send nil? :expect {
              (block $(send !nil? #predicate? ...) ...)
              $(send !nil? #predicate? ...)})
            $#Runners.all
            $#boolean_matcher?)
        PATTERN

        # @!method be_bool?(node)
        def_node_matcher :be_bool?, <<-PATTERN
          (send nil? {:be :eq :eql :equal} {true false})
        PATTERN

        # @!method be_boolthy?(node)
        def_node_matcher :be_boolthy?, <<-PATTERN
          (send nil? {:be_truthy :be_falsey :be_falsy :a_truthy_value :a_falsey_value :a_falsy_value})
        PATTERN

        def boolean_matcher?(node)
          if cop_config['Strict']
            be_boolthy?(node)
          else
            be_bool?(node) || be_boolthy?(node)
          end
        end

        def predicate?(sym)
          sym.to_s.end_with?('?')
        end

        def message_inflected(predicate)
          format(MSG_INFLECTED,
                 predicate_name: predicate.method_name,
                 matcher_name: to_predicate_matcher(predicate.method_name))
        end

        # rubocop:disable Metrics/MethodLength
        def to_predicate_matcher(name)
          case name = name.to_s
          when 'is_a?'
            'be_a'
          when 'instance_of?'
            'be_an_instance_of'
          when 'include?', 'respond_to?'
            name[0..-2]
          when 'exist?', 'exists?'
            'exist'
          when /^has_/
            name.sub('has_', 'have_')[0..-2]
          else
            "be_#{name[0..-2]}"
          end
        end
        # rubocop:enable Metrics/MethodLength

        def remove_predicate(corrector, predicate)
          range = predicate.loc.dot.with(
            end_pos: predicate.loc.expression.end_pos
          )

          corrector.remove(range)

          block_range = block_loc(predicate)
          corrector.remove(block_range) if block_range
        end

        def rewrite_matcher(corrector, predicate, matcher)
          args = args_loc(predicate).source
          block_loc = block_loc(predicate)
          block = block_loc ? block_loc.source : ''

          corrector.replace(
            matcher.loc.expression,
            to_predicate_matcher(predicate.method_name) + args + block
          )
        end

        def true?(to_symbol, matcher)
          result = case matcher.method_name
                   when :be, :eq
                     matcher.first_argument.true_type?
                   when :be_truthy, :a_truthy_value
                     true
                   when :be_falsey, :be_falsy, :a_falsey_value, :a_falsy_value
                     false
                   end
          to_symbol == :to ? result : !result
        end
      end

      # A helper for `explicit` style
      module ExplicitHelper
        include RuboCop::RSpec::Language
        extend NodePattern::Macros

        MSG_EXPLICIT = 'Prefer using `%<predicate_name>s` over ' \
                       '`%<matcher_name>s` matcher.'
        BUILT_IN_MATCHERS = %w[
          be_truthy be_falsey be_falsy
          have_attributes have_received
          be_between be_within
        ].freeze

        private

        def allowed_explicit_matchers
          cop_config.fetch('AllowedExplicitMatchers', []) + BUILT_IN_MATCHERS
        end

        def check_explicit(node) # rubocop:disable Metrics/MethodLength
          predicate_matcher_block?(node) do |actual, matcher|
            add_offense(node, message: message_explicit(matcher)) do |corrector|
              to_node = node.send_node
              corrector_explicit(corrector, to_node, actual, matcher, to_node)
            end
            ignore_node(node.children.first)
            return
          end

          return if part_of_ignored_node?(node)

          predicate_matcher?(node) do |actual, matcher|
            add_offense(node, message: message_explicit(matcher)) do |corrector|
              corrector_explicit(corrector, node, actual, matcher, matcher)
            end
          end
        end

        # @!method predicate_matcher?(node)
        def_node_matcher :predicate_matcher?, <<-PATTERN
          (send
            (send nil? :expect $!nil?)
            #Runners.all
            {$(send nil? #predicate_matcher_name? ...)
              (block $(send nil? #predicate_matcher_name? ...) ...)})
        PATTERN

        # @!method predicate_matcher_block?(node)
        def_node_matcher :predicate_matcher_block?, <<-PATTERN
          (block
            (send
              (send nil? :expect $!nil?)
              #Runners.all
              $(send nil? #predicate_matcher_name?))
            ...)
        PATTERN

        def predicate_matcher_name?(name)
          name = name.to_s

          return false if allowed_explicit_matchers.include?(name)

          name.start_with?('be_', 'have_') && !name.end_with?('?')
        end

        def message_explicit(matcher)
          format(MSG_EXPLICIT,
                 predicate_name: to_predicate_method(matcher.method_name),
                 matcher_name: matcher.method_name)
        end

        def corrector_explicit(corrector, to_node, actual, matcher, block_child)
          replacement_matcher = replacement_matcher(to_node)
          corrector.replace(matcher.loc.expression, replacement_matcher)
          move_predicate(corrector, actual, matcher, block_child)
          corrector.replace(to_node.loc.selector, 'to')
        end

        def move_predicate(corrector, actual, matcher, block_child)
          predicate = to_predicate_method(matcher.method_name)
          args = args_loc(matcher).source
          block_loc = block_loc(block_child)
          block = block_loc ? block_loc.source : ''

          corrector.remove(block_loc) if block_loc
          corrector.insert_after(actual.loc.expression,
                                 ".#{predicate}" + args + block)
        end

        # rubocop:disable Metrics/MethodLength
        def to_predicate_method(matcher)
          case matcher = matcher.to_s
          when 'be_a', 'be_an', 'be_a_kind_of', 'a_kind_of', 'be_kind_of'
            'is_a?'
          when 'be_an_instance_of', 'be_instance_of', 'an_instance_of'
            'instance_of?'
          when 'include'
            'include?'
          when 'respond_to'
            'respond_to?'
          when /^have_(.+)/
            "has_#{Regexp.last_match(1)}?"
          else
            "#{matcher[/^be_(.+)/, 1]}?"
          end
        end
        # rubocop:enable Metrics/MethodLength

        def replacement_matcher(node)
          case [cop_config['Strict'], node.method?(:to)]
          when [true, true]
            'be(true)'
          when [true, false]
            'be(false)'
          when [false, true]
            'be_truthy'
          when [false, false]
            'be_falsey'
          end
        end
      end

      # Prefer using predicate matcher over using predicate method directly.
      #
      # RSpec defines magic matchers for predicate methods.
      # This cop recommends to use the predicate matcher instead of using
      # predicate method directly.
      #
      # @example Strict: true, EnforcedStyle: inflected (default)
      #   # bad
      #   expect(foo.something?).to be_truthy
      #
      #   # good
      #   expect(foo).to be_something
      #
      #   # also good - It checks "true" strictly.
      #   expect(foo.something?).to be(true)
      #
      # @example Strict: false, EnforcedStyle: inflected
      #   # bad
      #   expect(foo.something?).to be_truthy
      #   expect(foo.something?).to be(true)
      #
      #   # good
      #   expect(foo).to be_something
      #
      # @example Strict: true, EnforcedStyle: explicit
      #   # bad
      #   expect(foo).to be_something
      #
      #   # good - the above code is rewritten to it by this cop
      #   expect(foo.something?).to be(true)
      #
      # @example Strict: false, EnforcedStyle: explicit
      #   # bad
      #   expect(foo).to be_something
      #
      #   # good - the above code is rewritten to it by this cop
      #   expect(foo.something?).to be_truthy
      #
      class PredicateMatcher < Base
        extend AutoCorrector
        include ConfigurableEnforcedStyle
        include InflectedHelper
        include ExplicitHelper

        def on_send(node)
          case style
          when :inflected
            check_inflected(node)
          when :explicit
            check_explicit(node)
          end
        end

        def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
          check_explicit(node) if style == :explicit
        end

        private

        # returns args location with whitespace
        # @example
        #   foo 1, 2
        #      ^^^^^
        def args_loc(send_node)
          send_node.loc.selector.end.with(
            end_pos: send_node.loc.expression.end_pos
          )
        end

        # returns block location with whitespace
        # @example
        #   foo { bar }
        #      ^^^^^^^^
        def block_loc(send_node)
          parent = send_node.parent
          return unless parent.block_type?

          send_node.loc.expression.end.with(
            end_pos: parent.loc.expression.end_pos
          )
        end
      end
    end
  end
end
