# frozen_string_literal: true

module RuboCop
  module Cop
    module Style
      # Checks for uses of `fail` and `raise`.
      #
      # @example EnforcedStyle: only_raise (default)
      #   # The `only_raise` style enforces the sole use of `raise`.
      #   # bad
      #   begin
      #     fail
      #   rescue Exception
      #     # handle it
      #   end
      #
      #   def watch_out
      #     fail
      #   rescue Exception
      #     # handle it
      #   end
      #
      #   Kernel.fail
      #
      #   # good
      #   begin
      #     raise
      #   rescue Exception
      #     # handle it
      #   end
      #
      #   def watch_out
      #     raise
      #   rescue Exception
      #     # handle it
      #   end
      #
      #   Kernel.raise
      #
      # @example EnforcedStyle: only_fail
      #   # The `only_fail` style enforces the sole use of `fail`.
      #   # bad
      #   begin
      #     raise
      #   rescue Exception
      #     # handle it
      #   end
      #
      #   def watch_out
      #     raise
      #   rescue Exception
      #     # handle it
      #   end
      #
      #   Kernel.raise
      #
      #   # good
      #   begin
      #     fail
      #   rescue Exception
      #     # handle it
      #   end
      #
      #   def watch_out
      #     fail
      #   rescue Exception
      #     # handle it
      #   end
      #
      #   Kernel.fail
      #
      # @example EnforcedStyle: semantic
      #   # The `semantic` style enforces the use of `fail` to signal an
      #   # exception, then will use `raise` to trigger an offense after
      #   # it has been rescued.
      #   # bad
      #   begin
      #     raise
      #   rescue Exception
      #     # handle it
      #   end
      #
      #   def watch_out
      #     # Error thrown
      #   rescue Exception
      #     fail
      #   end
      #
      #   Kernel.fail
      #   Kernel.raise
      #
      #   # good
      #   begin
      #     fail
      #   rescue Exception
      #     # handle it
      #   end
      #
      #   def watch_out
      #     fail
      #   rescue Exception
      #     raise 'Preferably with descriptive message'
      #   end
      #
      #   explicit_receiver.fail
      #   explicit_receiver.raise
      class SignalException < Base
        include ConfigurableEnforcedStyle
        extend AutoCorrector

        FAIL_MSG = 'Use `fail` instead of `raise` to signal exceptions.'
        RAISE_MSG = 'Use `raise` instead of `fail` to rethrow exceptions.'

        RESTRICT_ON_SEND = %i[raise fail].freeze

        # @!method kernel_call?(node, name)
        def_node_matcher :kernel_call?, '(send (const {nil? cbase} :Kernel) %1 ...)'

        # @!method custom_fail_methods(node)
        def_node_search :custom_fail_methods, '{(def :fail ...) (defs _ :fail ...)}'

        def on_new_investigation
          ast = processed_source.ast
          @custom_fail_defined = ast && custom_fail_methods(ast).any?
        end

        def on_rescue(node)
          return unless style == :semantic

          begin_node, *rescue_nodes, _else_node = *node
          check_scope(:raise, begin_node)

          rescue_nodes.each do |rescue_node|
            check_scope(:fail, rescue_node)
            allow(:raise, rescue_node)
          end
        end

        def on_send(node)
          case style
          when :semantic
            check_send(:raise, node) unless ignored_node?(node)
          when :only_raise
            return if @custom_fail_defined

            check_send(:fail, node)
          when :only_fail
            check_send(:raise, node)
          end
        end

        private

        def message(method_name)
          case style
          when :semantic
            method_name == :fail ? RAISE_MSG : FAIL_MSG
          when :only_raise
            'Always use `raise` to signal exceptions.'
          when :only_fail
            'Always use `fail` to signal exceptions.'
          end
        end

        def check_scope(method_name, node)
          return unless node

          each_command_or_kernel_call(method_name, node) do |send_node|
            next if ignored_node?(send_node)

            add_offense(send_node.loc.selector, message: message(method_name)) do |corrector|
              autocorrect(corrector, send_node)
            end
            ignore_node(send_node)
          end
        end

        def check_send(method_name, node)
          return unless node && command_or_kernel_call?(method_name, node)

          add_offense(node.loc.selector, message: message(method_name)) do |corrector|
            autocorrect(corrector, node)
          end
        end

        def autocorrect(corrector, node)
          name =
            case style
            when :semantic
              command_or_kernel_call?(:raise, node) ? 'fail' : 'raise'
            when :only_raise then 'raise'
            when :only_fail then 'fail'
            end

          corrector.replace(node.loc.selector, name)
        end

        def command_or_kernel_call?(name, node)
          return unless node.method?(name)

          node.command?(name) || kernel_call?(node, name)
        end

        def allow(method_name, node)
          each_command_or_kernel_call(method_name, node) { |send_node| ignore_node(send_node) }
        end

        def each_command_or_kernel_call(method_name, node)
          on_node(:send, node, :rescue) do |send_node|
            yield send_node if command_or_kernel_call?(method_name, send_node)
          end
        end
      end
    end
  end
end
