# frozen_string_literal: true

require_relative File.expand_path('../../lib/gitlab/danger/commit_linter', __dir__)

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 `danger-review` job"
MAX_COMMITS_COUNT = 10

def gitlab_danger
  @gitlab_danger ||= GitlabDanger.new(helper.gitlab_helper)
end

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 squash_mr?
  gitlab_danger.ci? ? gitlab.mr_json['squash'] : false
end

def wip_mr?
  gitlab_danger.ci? ? gitlab.mr_json['work_in_progress'] : false
end

def danger_job_link
  gitlab_danger.ci? ? "[#{THE_DANGER_JOB_TEXT}](#{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::Danger::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? && 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 wip_mr? || 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 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::Danger::CommitLinter.new(commit).lint_subject("merge request title")
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) }
  failed_commit_linters = commit_linters.select { |commit_linter| commit_linter.failed? }
  warn_or_fail_commits(failed_commit_linters, default_to_fail: !squash_mr?)

  if count_non_fixup_commits(commit_linters) > MAX_COMMITS_COUNT
    level = squash_mr? ? :warn : :fail
    self.__send__(level, # rubocop:disable GitlabSecurity/PublicSend
      "This merge request includes more than #{MAX_COMMITS_COUNT} commits. " \
        'Please rebase these commits into a smaller number of commits or split ' \
        'this merge request into multiple smaller merge requests.')
  end

  if 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)
    else
      title_linter = lint_mr_title(gitlab.mr_json['title'])
      if title_linter.failed?
        warn_or_fail_commits(title_linter)
      end
    end
  end
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_above_warning
        warn_commit(linter.commit, problem_desc)
      else
        self.__send__("#{level}_commit", linter.commit, problem_desc) # rubocop:disable GitlabSecurity/PublicSend
      end
    end
  end
end

lint_commits(git.commits)