// Copyright (C) 2018 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.gerrit.server.git.receive;

import static com.google.gerrit.git.ObjectIds.abbreviateName;
import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;

import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Project;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.git.validators.CommitValidationException;
import com.google.gerrit.server.git.validators.CommitValidationMessage;
import com.google.gerrit.server.git.validators.CommitValidators;
import com.google.gerrit.server.logging.TraceContext;
import com.google.gerrit.server.logging.TraceContext.TraceTimer;
import com.google.gerrit.server.permissions.PermissionBackend;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.ssh.SshInfo;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.notes.NoteMap;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.ReceiveCommand;

/** Validates single commits for a branch. */
public class BranchCommitValidator {
  private static final FluentLogger logger = FluentLogger.forEnclosingClass();

  private final CommitValidators.Factory commitValidatorsFactory;
  private final IdentifiedUser user;
  private final PermissionBackend.ForProject permissions;
  private final Project project;
  private final BranchNameKey branch;
  private final SshInfo sshInfo;

  interface Factory {
    BranchCommitValidator create(
        ProjectState projectState, BranchNameKey branch, IdentifiedUser user);
  }

  /** A boolean validation status and a list of additional messages. */
  @AutoValue
  abstract static class Result {
    static Result create(boolean isValid, ImmutableList<CommitValidationMessage> messages) {
      return new AutoValue_BranchCommitValidator_Result(isValid, messages);
    }

    /** Whether the commit is valid. */
    abstract boolean isValid();

    /**
     * A list of messages related to the validation. Messages may be present regardless of the
     * {@link #isValid()} status.
     */
    abstract ImmutableList<CommitValidationMessage> messages();
  }

  @Inject
  BranchCommitValidator(
      CommitValidators.Factory commitValidatorsFactory,
      PermissionBackend permissionBackend,
      SshInfo sshInfo,
      @Assisted ProjectState projectState,
      @Assisted BranchNameKey branch,
      @Assisted IdentifiedUser user) {
    this.sshInfo = sshInfo;
    this.user = user;
    this.branch = branch;
    this.commitValidatorsFactory = commitValidatorsFactory;
    project = projectState.getProject();
    permissions = permissionBackend.user(user).project(project.getNameKey());
  }

  /**
   * Validates a single commit. If the commit does not validate, the command is rejected.
   *
   * @param repository the repository
   * @param objectReader the object reader to use.
   * @param cmd the ReceiveCommand executing the push.
   * @param commit the commit being validated.
   * @param isMerged whether this is a merge commit created by magicBranch --merge option
   * @param change the change for which this is a new patchset.
   * @return The validation {@link Result}.
   */
  Result validateCommit(
      Repository repository,
      ObjectReader objectReader,
      ReceiveCommand cmd,
      RevCommit commit,
      ImmutableListMultimap<String, String> pushOptions,
      boolean isMerged,
      NoteMap rejectCommits,
      @Nullable Change change)
      throws IOException {
    return validateCommit(
        repository, objectReader, cmd, commit, pushOptions, isMerged, rejectCommits, change, false);
  }

  /**
   * Validates a single commit. If the commit does not validate, the command is rejected.
   *
   * @param repository the repository
   * @param objectReader the object reader to use.
   * @param cmd the ReceiveCommand executing the push.
   * @param commit the commit being validated.
   * @param isMerged whether this is a merge commit created by magicBranch --merge option
   * @param change the change for which this is a new patchset.
   * @param skipValidation whether 'skip-validation' was requested.
   * @return The validation {@link Result}.
   */
  Result validateCommit(
      Repository repository,
      ObjectReader objectReader,
      ReceiveCommand cmd,
      RevCommit commit,
      ImmutableListMultimap<String, String> pushOptions,
      boolean isMerged,
      NoteMap rejectCommits,
      @Nullable Change change,
      boolean skipValidation)
      throws IOException {
    try (TraceTimer traceTimer = TraceContext.newTimer("BranchCommitValidator#validateCommit")) {
      ImmutableList.Builder<CommitValidationMessage> messages = new ImmutableList.Builder<>();
      try (CommitReceivedEvent receiveEvent =
          new CommitReceivedEvent(
              cmd,
              project,
              branch.branch(),
              pushOptions,
              new Config(repository.getConfig()),
              objectReader,
              commit,
              user)) {
        CommitValidators validators;
        if (isMerged) {
          validators =
              commitValidatorsFactory.forMergedCommits(
                  permissions, branch, user.asIdentifiedUser());
        } else {
          validators =
              commitValidatorsFactory.forReceiveCommits(
                  permissions,
                  branch,
                  user.asIdentifiedUser(),
                  sshInfo,
                  rejectCommits,
                  receiveEvent.revWalk,
                  change,
                  skipValidation);
        }

        for (CommitValidationMessage m : validators.validate(receiveEvent)) {
          messages.add(
              new CommitValidationMessage(
                  messageForCommit(commit, m.getMessage(), objectReader), m.getType()));
        }
      } catch (CommitValidationException e) {
        logger.atFine().log("Commit validation failed on %s", commit.name());
        for (CommitValidationMessage m : e.getMessages()) {
          // The non-error messages may contain background explanation for the
          // fatal error, so have to preserve all messages.
          messages.add(
              new CommitValidationMessage(
                  messageForCommit(commit, m.getMessage(), objectReader), m.getType()));
        }
        cmd.setResult(
            REJECTED_OTHER_REASON, messageForCommit(commit, e.getMessage(), objectReader));
        return Result.create(false, messages.build());
      }
      return Result.create(true, messages.build());
    }
  }

  private String messageForCommit(RevCommit c, String msg, ObjectReader objectReader)
      throws IOException {
    return String.format("commit %s: %s", abbreviateName(c, objectReader), msg);
  }
}
