# encoding: utf-8
require 'rails_best_practices/reviews/review'

module RailsBestPractices
  module Reviews
    # Make sure to use observer (sorry we only check the mailer deliver now).
    #
    # See the best practice details here http://rails-bestpractices.com/posts/19-use-observer.
    #
    # TODO: we need a better solution, any suggestion?
    #
    # Implementation:
    #
    # Review process:
    #   check all call nodes to see if they are callback definitions, like after_create, before_destroy,
    #   if so, remember the callback methods.
    #
    #   check all method define nodes to see
    #   if the method is a callback method,
    #   and there is a mailer deliver call,
    #   then the method should be replaced by using observer.
    class UseObserverReview < Review
      def url
        "http://rails-bestpractices.com/posts/19-use-observer"
      end

      def interesting_nodes
        [:defn, :call]
      end

      def interesting_files
        MODEL_FILES
      end

      def initialize
        super
        @callbacks = []
      end

      # check a call node.
      #
      # if it is a callback definition, like
      #
      #     after_create :send_create_notification
      #     before_destroy :send_destroy_notification
      #
      # then remember its callback methods (:send_create_notification).
      def start_call(node)
        remember_callback(node)
      end

      # check a method define node in prepare process.
      #
      # if it is callback method,
      # and there is a actionmailer deliver call in the method define node,
      # then it should be replaced by using observer.
      def start_defn(node)
        if callback_method?(node) and deliver_mailer?(node)
          add_error "use observer"
        end
      end

      private
        # check a call node, if it is a callback definition, such as after_create, before_create, like
        #
        #     s(:call, nil, :after_create,
        #       s(:arglist, s(:lit, :send_create_notification))
        #     )
        #
        # then save the callback methods in @callbacks
        #
        #     @callbacks => [:send_create_notification]
        def remember_callback(node)
          if node.message.to_s =~ /^after_|^before_/
            node.arguments[1..-1].each do |argument|
              # ignore callback like after_create Comment.new
              @callbacks << argument.to_s if :lit == argument.node_type
            end
          end
        end

        # check a defn node to see if the method name exists in the @callbacks.
        def callback_method?(node)
          @callbacks.find { |callback| equal?(callback, node.method_name) }
        end

        # check a defn node to see if it contains a actionmailer deliver call.
        #
        # for rails2
        #
        # if the message of call node is deliver_xxx,
        # and the subject of the call node exists in @callbacks, like
        #
        #     s(:call, s(:const, :ProjectMailer), :deliver_notification,
        #       s(:arglist, s(:self), s(:lvar, :member))
        #     )
        #
        # for rails3
        #
        # if the message of call node is deliver,
        # and the subject of the call node is with subject node who exists in @callbacks, like
        #
        #     s(:call,
        #       s(:call, s(:const, :ProjectMailer), :notification,
        #         s(:arglist, s(:self), s(:lvar, :member))
        #       ),
        #       :deliver,
        #       s(:arglist)
        #     )
        #
        # then the call node is actionmailer deliver call.
        def deliver_mailer?(node)
          node.grep_nodes(:node_type => :call) do |child_node|
            # rails2 actionmailer deliver
            return true if child_node.message.to_s =~ /^deliver_/ && mailer_names.include?(child_node.subject.to_s)
            # rails3 actionmailer deliver
            return true if :deliver == child_node.message && mailer_names.include?(child_node.subject.subject.to_s)
          end
          false
        end

        def mailer_names
          @mailer_names ||= Prepares.mailer_names.collect(&:to_s)
        end
    end
  end
end