require_relative 'smell_detector'
require_relative 'smell_warning'

module Reek
  module Smells
    # Checking for nil is a special kind of type check, and therefore a case of
    # SimulatedPolymorphism.
    #
    # See {file:docs/Nil-Check.md} for details.
    class NilCheck < SmellDetector
      def self.smell_category
        'SimulatedPolymorphism'
      end

      def inspect(ctx)
        call_node_finder = NodeFinder.new(ctx, :send, NilCallNodeDetector)
        case_node_finder = NodeFinder.new(ctx, :when, NilWhenNodeDetector)
        smelly_nodes = call_node_finder.smelly_nodes + case_node_finder.smelly_nodes

        smelly_nodes.map do |node|
          smell_warning(
            context: ctx,
            lines: [node.line],
            message: 'performs a nil-check')
        end
      end

      #
      # A base class that allows to work on all nodes of a certain type.
      #
      class NodeFinder
        def initialize(ctx, type, detector)
          @nodes = ctx.local_nodes(type)
          @detector = detector
        end

        def smelly_nodes
          nodes.select do |when_node|
            detector.detect(when_node)
          end
        end

        private

        private_attr_reader :detector, :nodes
      end

      # Detect 'call' nodes which perform a nil check.
      module NilCallNodeDetector
        module_function

        def detect(node)
          nil_query?(node) || nil_comparison?(node)
        end

        def nil_query?(call)
          call.method_name == :nil?
        end

        def nil_comparison?(call)
          comparison_call?(call) && involves_nil?(call)
        end

        def comparison_call?(call)
          comparison_methods.include? call.method_name
        end

        def involves_nil?(call)
          call.participants.any? { |it| it.type == :nil }
        end

        def comparison_methods
          [:==, :===]
        end
      end

      # Detect 'when' statements that perform a nil check.
      module NilWhenNodeDetector
        module_function

        def detect(node)
          node.condition_list.any? { |it| it.type == :nil }
        end
      end
    end
  end
end
