# frozen_string_literal: true

require_relative '../../code_reuse_helpers'

module RuboCop
  module Cop
    module CodeReuse
      # Cop that blacklists the use of ActiveRecord methods outside of models.
      class ActiveRecord < RuboCop::Cop::Cop
        include CodeReuseHelpers

        MSG = 'This method can only be used inside an ActiveRecord model: ' \
        'https://gitlab.com/gitlab-org/gitlab-ce/issues/49653'

        # Various methods from ActiveRecord::Querying that are blacklisted. We
        # exclude some generic ones such as `any?` and `first`, as these may
        # lead to too many false positives, since `Array` also supports these
        # methods.
        #
        # The keys of this Hash are the blacklisted method names. The values are
        # booleans that indicate if the method should only be blacklisted if any
        # arguments are provided.
        NOT_ALLOWED = {
          average: true,
          calculate: true,
          count_by_sql: true,
          create_with: true,
          distinct: false,
          eager_load: true,
          except: true,
          exists?: true,
          find_by: true,
          find_by!: true,
          find_by_sql: true,
          find_each: true,
          find_in_batches: true,
          find_or_create_by: true,
          find_or_create_by!: true,
          find_or_initialize_by: true,
          first!: false,
          first_or_create: true,
          first_or_create!: true,
          first_or_initialize: true,
          from: true,
          group: true,
          having: true,
          ids: false,
          includes: true,
          joins: true,
          limit: true,
          lock: false,
          many?: false,
          offset: true,
          order: true,
          pluck: true,
          preload: true,
          readonly: false,
          references: true,
          reorder: true,
          rewhere: true,
          sum: false,
          take: false,
          take!: false,
          unscope: false,
          where: false,
          with: true
        }.freeze

        # Directories that allow the use of the blacklisted methods. These
        # directories are checked relative to both . and ee/
        WHITELISTED_DIRECTORIES = %w[
          app/models
          config
          danger
          db
          lib/backup
          lib/banzai
          lib/gitlab/background_migration
          lib/gitlab/cycle_analytics
          lib/gitlab/database
          lib/gitlab/import_export
          lib/gitlab/project_authorizations
          lib/gitlab/sql
          lib/system_check
          lib/tasks
          qa
          rubocop
          spec
        ].freeze

        def on_send(node)
          return if in_whitelisted_directory?(node)

          receiver = node.children[0]
          send_name = node.children[1]
          first_arg = node.children[2]

          if receiver && NOT_ALLOWED.key?(send_name)
            # If the rule requires an argument to be given, but none are
            # provided, we won't register an offense. This prevents us from
            # adding offenses for `project.group`, while still covering
            # `Project.group(:name)`.
            return if NOT_ALLOWED[send_name] && !first_arg

            add_offense(node, location: :selector)
          end
        end

        # Returns true if the node resides in one of the whitelisted
        # directories.
        def in_whitelisted_directory?(node)
          path = file_path_for_node(node)

          WHITELISTED_DIRECTORIES.any? do |directory|
            path.start_with?(
              File.join(rails_root, directory),
              File.join(rails_root, 'ee', directory)
            )
          end
        end

        # We can not auto correct code like this, as it requires manual
        # refactoring. Instead, we'll just whitelist the surrounding scope.
        #
        # Despite this method's presence, you should not use it. This method
        # exists to make it possible to whitelist large chunks of offenses we
        # can't fix in the short term. If you are writing new code, follow the
        # code reuse guidelines, instead of whitelisting any new offenses.
        def autocorrect(node)
          scope = surrounding_scope_of(node)
          indent = indentation_of(scope)

          lambda do |corrector|
            # This prevents us from inserting the same enable/disable comment
            # for a method or block that has multiple offenses.
            next if whitelisted_scopes.include?(scope)

            corrector.insert_before(
              scope.source_range,
              "# rubocop: disable #{cop_name}\n#{indent}"
            )

            corrector.insert_after(
              scope.source_range,
              "\n#{indent}# rubocop: enable #{cop_name}"
            )

            whitelisted_scopes << scope
          end
        end

        def indentation_of(node)
          ' ' * node.loc.expression.source_line[/\A */].length
        end

        def surrounding_scope_of(node)
          %i[def defs block begin].each do |type|
            if (found = node.each_ancestor(type).first)
              return found
            end
          end
        end

        def whitelisted_scopes
          @whitelisted_scopes ||= Set.new
        end
      end
    end
  end
end
