# frozen_string_literal: true

require_relative "../../../gitlab/dangerfiles/commit_linter"
require_relative "../../../gitlab/dangerfiles/merge_request_linter"

COMMIT_MESSAGE_GUIDELINES = "https://docs.gitlab.com/ee/development/contributing/merge_request_workflow.html#commit-messages-guidelines"
MORE_INFO = "For more information, take a look at our [Commit message guidelines](#{COMMIT_MESSAGE_GUIDELINES})."
THE_DANGER_JOB_TEXT = "the `%<job_name>s` job"
MAX_COMMITS_COUNT = helper.config.max_commits_count
MAX_COMMITS_COUNT_EXCEEDED_MESSAGE = <<~MSG
This merge request includes more than %<max_commits_count>d commits. Each commit should meet the following criteria:

1. Have a well-written commit message.
1. Has all tests passing when used on its own (e.g. when using git checkout SHA).
1. Can be reverted on its own without also requiring the revert of commit that came before it.
1. Is small enough that it can be reviewed in isolation in under 30 minutes or so.

If this merge request contains commits that do not meet this criteria and/or contains intermediate work, please rebase these commits into a smaller number of commits or split this merge request into multiple smaller merge requests.
MSG

def fail_commit(commit, message, more_info: true)
  self.fail(build_message(commit, message, more_info: more_info))
end

def warn_commit(commit, message, more_info: true)
  self.warn(build_message(commit, message, more_info: more_info))
end

def build_message(commit, message, more_info: true)
  [message].tap do |full_message|
    full_message << ". #{MORE_INFO}" if more_info
    full_message.unshift("#{commit.sha}: ") if commit.sha
  end.join
end

def danger_job_link
  helper.ci? ? "[#{format(THE_DANGER_JOB_TEXT, job_name: ENV["CI_JOB_NAME"])}](#{ENV['CI_JOB_URL']})" : THE_DANGER_JOB_TEXT
end

# Perform various checks against commits. We're not using
# https://github.com/jonallured/danger-commit_lint because its output is not
# very helpful, and it doesn't offer the means of ignoring merge commits.
def lint_commit(commit)
  linter = Gitlab::Dangerfiles::CommitLinter.new(commit)

  # For now we'll ignore merge commits, as getting rid of those is a problem
  # separate from enforcing good commit messages.
  return linter if linter.merge?

  # We ignore revert commits as they are well structured by Git already
  return linter if linter.revert?

  # If MR is set to squash, we ignore fixup commits
  return linter if linter.fixup? && helper.squash_mr?

  if linter.fixup?
    msg = "Squash or fixup commits must be squashed before merge, or enable squash merge option and re-run #{danger_job_link}."
    if helper.draft_mr? || helper.squash_mr?
      warn_commit(commit, msg, more_info: false)
    else
      fail_commit(commit, msg, more_info: false)
    end

    # Makes no sense to process other rules for fixup commits, they trigger just more noise
    return linter
  end

  # Fail if a suggestion commit is used and squash is not enabled
  if linter.suggestion?
    unless helper.squash_mr?
      fail_commit(commit, "If you are applying suggestions, enable squash in the merge request and re-run #{danger_job_link}.", more_info: false)
    end

    return linter
  end

  linter.lint
end

def lint_mr_title(mr_title)
  commit = Struct.new(:message, :sha).new(mr_title)

  Gitlab::Dangerfiles::MergeRequestLinter.new(commit).lint
end

def count_non_fixup_commits(commit_linters)
  commit_linters.count { |commit_linter| !commit_linter.fixup? }
end

def lint_commits(commits)
  commit_linters = commits.map { |commit| lint_commit(commit) }

  if helper.squash_mr?
    multi_line_commit_linter = commit_linters.detect { |commit_linter| !commit_linter.merge? && commit_linter.multi_line? }

    if multi_line_commit_linter && multi_line_commit_linter.failed?
      warn_or_fail_commits(multi_line_commit_linter)
      commit_linters.delete(multi_line_commit_linter) # Don't show an error (here) and a warning (below)
    elsif helper.ci? # We don't have access to the MR title locally
      title_linter = lint_mr_title(gitlab.mr_json['title'])
      if title_linter.failed?
        warn_or_fail_commits(title_linter)
      end
    end
  else
    if count_non_fixup_commits(commit_linters) > MAX_COMMITS_COUNT
      self.warn(format(MAX_COMMITS_COUNT_EXCEEDED_MESSAGE, max_commits_count: MAX_COMMITS_COUNT))
    end
  end

  failed_commit_linters = commit_linters.select { |commit_linter| commit_linter.failed? }
  warn_or_fail_commits(failed_commit_linters, default_to_fail: !helper.squash_mr?)
end

def warn_or_fail_commits(failed_linters, default_to_fail: true)
  level = default_to_fail ? :fail : :warn

  Array(failed_linters).each do |linter|
    linter.problems.each do |problem_key, problem_desc|
      case problem_key
      when :subject_too_short, :subject_above_warning, :details_too_many_changes, :details_line_too_long
        warn_commit(linter.commit, problem_desc)
      else
        self.__send__("#{level}_commit", linter.commit, problem_desc) # rubocop:disable GitlabSecurity/PublicSend
      end
    end
  end
end

# As part of https://gitlab.com/groups/gitlab-org/-/epics/4826 we are
# vendoring workhorse commits from the stand-alone gitlab-workhorse
# repo. There is no point in linting commits that we want to vendor as
# is.
def workhorse_changes?
  git.diff.any? { |file| file.path.start_with?('workhorse/') }
end

lint_commits(git.commits) unless workhorse_changes?