common/lib/dependabot/requirement.rb
# typed: strict
# frozen_string_literal: true

require "sorbet-runtime"

module Dependabot
  class Requirement < Gem::Requirement
    extend T::Sig
    extend T::Helpers

    # Constants for operator groups
    MINIMUM_OPERATORS = %w(>= > ~>).freeze
    MAXIMUM_OPERATORS = %w(<= < ~>).freeze

    abstract!

    # Parses requirement strings and returns an array of requirement objects.
    sig do
      abstract
        .params(requirement_string: T.nilable(String))
        .returns(T::Array[Requirement])
    end
    def self.requirements_array(requirement_string); end

    # Returns all requirement constraints as an array of strings
    sig { returns(T::Array[String]) }
    def constraints
      requirements.map { |op, version| "#{op} #{version}" }
    end

    # Returns the highest lower limit among all minimum constraints.
    sig { returns(T.nilable(Dependabot::Version)) }
    def min_version
      # Select constraints with minimum operators
      min_constraints = requirements.select { |op, _version| MINIMUM_OPERATORS.include?(op) }

      # Process each minimum constraint using the respective handler
      effective_min_versions = min_constraints.filter_map do |op, version|
        handle_min_operator(op, version.is_a?(Dependabot::Version) ? version : Dependabot::Version.new(version))
      end

      # Return the maximum among the effective minimum constraints
      Dependabot::Version.new(effective_min_versions.max) if effective_min_versions.any?
    end

    # Returns the lowest upper limit among all maximum constraints.
    sig { returns(T.nilable(Dependabot::Version)) }
    def max_version
      # Select constraints with maximum operators
      max_constraints = requirements.select { |op, _version| MAXIMUM_OPERATORS.include?(op) }

      # Process each maximum constraint using the respective handler
      effective_max_versions = max_constraints.filter_map do |op, version|
        handle_max_operator(op, version.is_a?(Dependabot::Version) ? version : Dependabot::Version.new(version))
      end

      # Return the minimum among the effective maximum constraints
      Dependabot::Version.new(effective_max_versions.min) if effective_max_versions.any?
    end

    # Dynamically handles minimum operators
    sig { params(operator: String, version: Dependabot::Version).returns(T.nilable(Dependabot::Version)) }
    def handle_min_operator(operator, version)
      case operator
      when ">=" then handle_greater_than_or_equal_for_min(version)
      when ">"  then handle_greater_than_for_min(version)
      when "~>" then handle_tilde_pessimistic_for_min(version)
      end
    end

    # Dynamically handles maximum operators
    sig { params(operator: String, version: Dependabot::Version).returns(T.nilable(Dependabot::Version)) }
    def handle_max_operator(operator, version)
      case operator
      when "<=" then handle_less_than_or_equal_for_max(version)
      when "<"  then handle_less_than_max(version)
      when "~>" then handle_tilde_pessimistic_max(version)
      end
    end

    # Methods for handling minimum constraints
    sig { params(version: Dependabot::Version).returns(Dependabot::Version) }
    def handle_greater_than_or_equal_for_min(version)
      version
    end

    sig { params(version: Dependabot::Version).returns(Dependabot::Version) }
    def handle_greater_than_for_min(version)
      version
    end

    sig { params(version: Dependabot::Version).returns(Dependabot::Version) }
    def handle_tilde_pessimistic_for_min(version)
      version
    end

    # Methods for handling maximum constraints
    sig { params(version: Dependabot::Version).returns(Dependabot::Version) }
    def handle_less_than_or_equal_for_max(version)
      version
    end

    sig { params(version: Dependabot::Version).returns(Dependabot::Version) }
    def handle_less_than_max(version)
      version
    end

    sig { params(version: Dependabot::Version).returns(Dependabot::Version) }
    def handle_tilde_pessimistic_max(version)
      case version.segments.length
      when 1
        bump_major_segment(version)
      when 2
        bump_minor_segment(version)
      else
        bump_version(version)
      end
    end

    private

    sig { params(version: Dependabot::Version).returns(Dependabot::Version) }
    def bump_major_segment(version)
      Dependabot::Version.new("#{version.segments[0].to_i + 1}.0.0")
    end

    sig { params(version: Dependabot::Version).returns(Dependabot::Version) }
    def bump_minor_segment(version)
      Dependabot::Version.new("#{version.segments[0]}.#{version.segments[1].to_i + 1}.0")
    end

    sig { params(version: Dependabot::Version).returns(Dependabot::Version) }
    def bump_version(version)
      Dependabot::Version.new(version.bump)
    end
  end
end
