module RuboCop
  module Cop
    module Migration
      class IncludeStringSize < RuboCop::Cop::Base
        # Postgres and MySQL have different size limits on String and TEXT fields
        # MySQL: `String` is `varchar(255)`, `String, text: true` has a max size of 16_000 for UTF8 encoded DBs
        # Postgres: `String` and `String, text: true` are `TEXT` and has a max size of ~1GB
        # This linter ensures we have a consistent size limit across all DBs which enables users to
        #   transfer from Postgres to MySQL without truncating tables.
        COLUMN_ADDING_METHODS = %i[
          add_column set_column_type String
        ].freeze
        STRING_SIZE_WARNING = 'Please specify an explicit size for String columns.' +
                              ' `size: 255` is a good size for small strings, `size: 16_000` is the maximum for UTF8 strings.'.freeze
        STRING_TEXT_WARNING = 'Considering using string with an appropriate size rather than `text: true`. The size of `text: true` depends on MySQL vs Postgres'.freeze

        def on_block(node)
          node.each_descendant(:send) do |inner_node|
            method = inner_node.method_name
            next unless adding_column?(method)

            has_string = method == :String || has_const_child(inner_node, 'String')
            next unless has_string

            has_text = node_has_hash_key?(inner_node, :text, &:truthy_literal?)

            if has_text
              add_offense(inner_node.loc.expression, message: STRING_TEXT_WARNING)
              next
            end

            has_size = node_has_hash_key?(inner_node, :size)

            add_offense(inner_node.loc.expression, message: STRING_SIZE_WARNING) unless has_size
          end
        end

        private

        def node_has_hash_key?(node, name)
          node.each_descendant(:hash).any? do |hash|
            hash.each_pair.any? do |pair|
              pair.key.children[0] == name && (!block_given? || yield(pair.value))
            end
          end
        end

        def has_const_child(node, value)
          node.each_descendant(:const).any? { |n| n.const_name == value }
        end

        def adding_column?(method)
          COLUMN_ADDING_METHODS.include?(method)
        end
      end
    end
  end
end
