# frozen_string_literal: true

module RailsBestPractices
  module Reviews
    # review a controller file to make sure to use model association instead of foreign key id assignment.
    #
    # See the best practice details here https://rails-bestpractices.com/posts/2010/07/19/use-model-association/
    #
    # Implementation:
    #
    # Review process:
    #   check model define nodes in all controller files,
    #   if there is an attribute assignment node with message xxx_id=,
    #   and after it, there is a call node with message "save" or "save!",
    #   and the receivers of attribute assignment node and call node are the same,
    #   then model association should be used instead of xxx_id assignment.
    class UseModelAssociationReview < Review
      interesting_nodes :def
      interesting_files CONTROLLER_FILES
      url 'https://rails-bestpractices.com/posts/2010/07/19/use-model-association/'

      # check method define nodes to see if there are some attribute assignments that can use model association instead.
      #
      # it will check attribute assignment node with message xxx_id=, and call node with message "save" or "save!"
      #
      # 1. if there is an attribute assignment node with message xxx_id=,
      #    then remember the receiver of attribute assignment node.
      # 2. after assignment, if there is a call node with message "save" or "save!",
      #    and the receiver of call node is one of the receiver of attribute assignment node,
      #    then the attribute assignment should be replaced by using model association.
      add_callback :start_def do |node|
        @assignments = {}
        node.recursive_children do |child|
          case child.sexp_type
          when :assign
            attribute_assignment(child)
          when :call
            call_assignment(child)
          end
        end
        @assignments = nil
      end

      private

      # check an attribute assignment node, if its message is xxx_id,
      # then remember the receiver of the attribute assignment in @assignments.
      def attribute_assignment(node)
        if node.left_value.message.is_a?(Sexp) && node.left_value.message.to_s =~ /_id$/
          receiver = node.left_value.receiver.to_s
          @assignments[receiver] = true
        end
      end

      # check a call node with message "save" or "save!",
      # if the receiver of call node exists in @assignments,
      # then the attribute assignment should be replaced by using model association.
      def call_assignment(node)
        if ['save', 'save!'].include? node.message.to_s
          receiver = node.receiver.to_s
          add_error "use model association (for #{receiver})" if @assignments[receiver]
        end
      end
    end
  end
end
