# frozen_string_literal: true

module RuboCop
  module Cop
    module Jekyll
      # Checks for `assert_equal(exp, act, msg = nil)` calls containing literal values as
      # second argument. The second argument should ideally be a method called on the tested
      # instance.
      #
      # @example
      #   # bad
      #   assert_equal @foo.bar, "foobar"
      #   assert_equal @alpha.beta, { "foo" => "bar", "lorem" => "ipsum" }
      #   assert_equal @alpha.omega, ["foobar", "lipsum"]
      #
      #   # good
      #   assert_equal "foobar", @foo.bar
      #
      #   assert_equal(
      #     { "foo" => "bar", "lorem" => "ipsum" },
      #     @alpha.beta
      #   )
      #
      #   assert_equal(
      #     ["foobar", "lipsum"],
      #     @alpha.omega
      #   )
      #
      class AssertEqualLiteralActual < Base
        extend AutoCorrector

        MSG = "Provide the 'expected value' as the first argument to `assert_equal`."
        RESTRICT_ON_SEND = %i[assert_equal].freeze

        SIMPLE_LITERALS = %i(
          true
          false
          nil
          int
          float
          str
          sym
          complex
          rational
          regopt
        ).freeze

        COMPLEX_LITERALS = %i(
          array
          hash
          pair
          irange
          erange
          regexp
        ).freeze

        def_node_matcher :literal_actual?, <<-PATTERN
          (send nil? :assert_equal $(send ...) $#literal?)
        PATTERN

        def_node_matcher :literal_actual_with_msg?, <<-PATTERN
          (send nil? :assert_equal $(send ...) $#literal? $#opt_msg?)
        PATTERN

        def on_send(node)
          return unless literal_actual?(node) || literal_actual_with_msg?(node)

          range = node.loc.expression
          add_offense(range) do |corrector|
            corrector.replace(range, replacement(node))
          end
        end

        private

        def opt_msg?(node)
          node&.source
        end

        # This is not implement using a NodePattern because it seems
        # to not be able to match against an explicit (nil) sexp
        def literal?(node)
          node && (simple_literal?(node) || complex_literal?(node))
        end

        def simple_literal?(node)
          SIMPLE_LITERALS.include?(node.type)
        end

        def complex_literal?(node)
          COMPLEX_LITERALS.include?(node.type) &&
            node.each_child_node.all?(&method(:literal?))
        end

        def replacement(node)
          _, _, first_param, second_param, optional_param = *node

          replaced_text = \
            if second_param.type == :hash
              replace_hash_with_variable(first_param.source, second_param.source)
            elsif second_param.type == :array && second_param.source != "[]"
              replace_array_with_variable(first_param.source, second_param.source)
            else
              replace_based_on_line_length(first_param.source, second_param.source)
            end

          return "#{replaced_text}, #{optional_param.source}" if optional_param
          replaced_text
        end

        def replace_based_on_line_length(first_expression, second_expression)
          result = "assert_equal #{second_expression}, #{first_expression}"
          return result if result.length < 80

          # fold long lines independent of Rubocop configuration for better readability
          <<~TEXT
            assert_equal(
              #{second_expression},
              #{first_expression}
            )
          TEXT
        end

        def replace_hash_with_variable(first_expression, second_expression)
          expect_expression = if second_expression.start_with?("{")
                                second_expression
                              else
                                "{#{second_expression}}"
                              end
          <<~TEXT
            expected = #{expect_expression}
            assert_equal expected, #{first_expression}
          TEXT
        end

        def replace_array_with_variable(first_expression, second_expression)
          expect_expression = if second_expression.start_with?("%")
                                second_expression
                              else
                                Array(second_expression)
                              end
          <<~TEXT
            expected = #{expect_expression}
            assert_equal expected, #{first_expression}
          TEXT
        end
      end
    end
  end
end
