class Dependabot::Uv::UpdateChecker

def build_system_details

def build_system_details
  @build_system_details ||= toml_content["build-system"]
end

def current_requirement_string

def current_requirement_string
  reqs = requirements
  return if reqs.none?
  requirement = reqs.find do |r|
    file = r[:file]
    file == "Pipfile" || file == "pyproject.toml" || file.end_with?(".in") || file.end_with?(".txt")
  end
  requirement&.fetch(:requirement)
end

def exact_requirement?(reqs)

def exact_requirement?(reqs)
  reqs = reqs.map { |r| r.fetch(:requirement) }
  reqs = reqs.compact
  reqs = reqs.flat_map { |r| r.split(",").map(&:strip) }
  reqs.any? { |r| Uv::Requirement.new(r).exact? }
end

def fetch_latest_version

def fetch_latest_version
  latest_version_finder.latest_version
end

def fetch_lowest_resolvable_security_fix_version

def fetch_lowest_resolvable_security_fix_version
  fix_version = lowest_security_fix_version
  return latest_resolvable_version if fix_version.nil?
  return resolver.lowest_resolvable_security_fix_version if resolver_type == :requirements
  resolver.resolvable?(version: fix_version) ? fix_version : nil
end

def latest_resolvable_version

def latest_resolvable_version
  @latest_resolvable_version ||=
    if resolver_type == :requirements
      resolver.latest_resolvable_version
    elsif resolver_type == :pip_compile && resolver.resolvable?(version: latest_version)
      latest_version
    else
      resolver.latest_resolvable_version(
        requirement: unlocked_requirement_string
      )
    end
end

def latest_resolvable_version_with_no_unlock

def latest_resolvable_version_with_no_unlock
  @latest_resolvable_version_with_no_unlock ||=
    if resolver_type == :requirements
      resolver.latest_resolvable_version_with_no_unlock
    else
      resolver.latest_resolvable_version(
        requirement: current_requirement_string
      )
    end
end

def latest_version

def latest_version
  @latest_version ||= fetch_latest_version
end

def latest_version_finder

def latest_version_finder
  @latest_version_finder ||= LatestVersionFinder.new(
    dependency: dependency,
    dependency_files: dependency_files,
    credentials: credentials,
    ignored_versions: ignored_versions,
    raise_on_ignored: @raise_on_ignored,
    security_advisories: security_advisories
  )
end

def latest_version_resolvable_with_full_unlock?

def latest_version_resolvable_with_full_unlock?
  # Full unlock checks aren't implemented for Python (yet)
  false
end

def library?

def library?
  return false unless updating_pyproject?
  return false if library_details["name"].nil?
  # Hit PyPi and check whether there are details for a library with a
  # matching name and description
  index_response = Dependabot::RegistryClient.get(
    url: "https://pypi.org/pypi/#{normalised_name(library_details['name'])}/json/"
  )
  return false unless index_response.status == 200
  pypi_info = JSON.parse(index_response.body)["info"] || {}
  pypi_info["summary"] == library_details["description"]
rescue Excon::Error::Timeout, Excon::Error::Socket
  false
rescue URI::InvalidURIError
  false
end

def library_details

def library_details
  @library_details ||= standard_details || build_system_details
end

def lowest_resolvable_security_fix_version

def lowest_resolvable_security_fix_version
  raise "Dependency not vulnerable!" unless vulnerable?
  return @lowest_resolvable_security_fix_version if defined?(@lowest_resolvable_security_fix_version)
  @lowest_resolvable_security_fix_version =
    fetch_lowest_resolvable_security_fix_version
end

def lowest_security_fix_version

def lowest_security_fix_version
  latest_version_finder.lowest_security_fix_version
end

def normalised_name(name)

def normalised_name(name)
  NameNormaliser.normalise(name)
end

def pip_compile_files

def pip_compile_files
  dependency_files.select { |f| f.name.end_with?(".in") }
end

def pip_compile_version_resolver

def pip_compile_version_resolver
  @pip_compile_version_resolver ||=
    PipCompileVersionResolver.new(**resolver_args)
end

def pip_version_resolver

def pip_version_resolver
  @pip_version_resolver ||= PipVersionResolver.new(
    dependency: dependency,
    dependency_files: dependency_files,
    credentials: credentials,
    ignored_versions: ignored_versions,
    raise_on_ignored: @raise_on_ignored,
    security_advisories: security_advisories
  )
end

def pyproject

def pyproject
  dependency_files.find { |f| f.name == "pyproject.toml" }
end

def requirement_files

def requirement_files
  requirements.map { |r| r.fetch(:file) }
end

def requirements

def requirements
  dependency.requirements
end

def requirements_text_file?

def requirements_text_file?
  requirement_files.any? { |f| f.end_with?("requirements.txt") }
end

def requirements_unlocked_or_can_be?

def requirements_unlocked_or_can_be?
  !requirements_update_strategy.lockfile_only?
end

def requirements_update_strategy

def requirements_update_strategy
  # If passed in as an option (in the base class) honour that option
  return @requirements_update_strategy if @requirements_update_strategy
  # Otherwise, check if this is a library or not
  library? ? RequirementsUpdateStrategy::WidenRanges : RequirementsUpdateStrategy::BumpVersions
end

def resolver

def resolver
  if Dependabot::Experiments.enabled?(:enable_file_parser_python_local)
    Dependabot.logger.info("Python package resolver : #{resolver_type}")
  end
  case resolver_type
  when :pip_compile then pip_compile_version_resolver
  when :requirements then pip_version_resolver
  else raise "Unexpected resolver type #{resolver_type}"
  end
end

def resolver_args

def resolver_args
  {
    dependency: dependency,
    dependency_files: dependency_files,
    credentials: credentials,
    repo_contents_path: repo_contents_path
  }
end

def resolver_type

def resolver_type
  reqs = requirements
  # If there are no requirements then this is a sub-dependency. It
  # must come from one of Pipenv, Poetry or pip-tools, and can't come
  # from the first two unless they have a lockfile.
  return subdependency_resolver if reqs.none?
  # Otherwise, this is a top-level dependency, and we can figure out
  # which resolver to use based on the filename of its requirements
  return :requirements if updating_pyproject?
  return :pip_compile if updating_in_file?
  if dependency.version && !exact_requirement?(reqs)
    subdependency_resolver
  else
    :requirements
  end
end

def standard_details

def standard_details
  @standard_details ||= toml_content["project"]
end

def subdependency_resolver

def subdependency_resolver
  return :pip_compile if pip_compile_files.any?
  raise "Claimed to be a sub-dependency, but no lockfile exists!"
end

def toml_content

def toml_content
  @toml_content ||= TomlRB.parse(pyproject.content)
end

def unlocked_requirement_string

def unlocked_requirement_string
  lower_bound_req = updated_version_req_lower_bound
  # Add the latest_version as an upper bound. This means
  # ignore conditions are considered when checking for the latest
  # resolvable version.
  #
  # NOTE: This isn't perfect. If v2.x is ignored and v3 is out but
  # unresolvable then the `latest_version` will be v3, and
  # we won't be ignoring v2.x releases like we should be.
  return lower_bound_req if latest_version.nil?
  return lower_bound_req unless Uv::Version.correct?(latest_version)
  lower_bound_req + ",<=#{latest_version}"
end

def updated_dependencies_after_full_unlock

def updated_dependencies_after_full_unlock
  raise NotImplementedError
end

def updated_requirements

def updated_requirements
  RequirementsUpdater.new(
    requirements: requirements,
    latest_resolvable_version: preferred_resolvable_version&.to_s,
    update_strategy: requirements_update_strategy,
    has_lockfile: requirements_text_file?
  ).updated_requirements
end

def updated_version_req_lower_bound

def updated_version_req_lower_bound
  return ">=#{dependency.version}" if dependency.version
  version_for_requirement =
    requirements.filter_map { |r| r[:requirement] }
                .reject { |req_string| req_string.start_with?("<") }
                .select { |req_string| req_string.match?(VERSION_REGEX) }
                .map { |req_string| req_string.match(VERSION_REGEX).to_s }
                .select { |version| Uv::Version.correct?(version) }
                .max_by { |version| Uv::Version.new(version) }
  ">=#{version_for_requirement || 0}"
end

def updating_in_file?

def updating_in_file?
  requirement_files.any? { |f| f.end_with?(".in") }
end

def updating_pyproject?

def updating_pyproject?
  requirement_files.any?("pyproject.toml")
end

def updating_requirements_file?

def updating_requirements_file?
  requirement_files.any? { |f| f =~ /\.txt$|\.in$/ }
end