// Copyright (C) 2022 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.acceptance.server.change;

import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.truth.Truth.assertAbout;
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.server.change.ApprovalCopierIT.PatchSetApprovalSubject.assertThatList;
import static com.google.gerrit.acceptance.server.change.ApprovalCopierIT.PatchSetApprovalSubject.hasTestId;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.server.project.testing.TestLabels.labelBuilder;
import static com.google.gerrit.server.project.testing.TestLabels.value;

import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableSet;
import com.google.common.truth.Correspondence;
import com.google.common.truth.FailureMetadata;
import com.google.common.truth.Subject;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.approval.ApprovalCopier;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.truth.ListSubject;
import com.google.gerrit.truth.NullAwareCorrespondence;
import com.google.inject.Inject;
import java.io.IOException;
import java.util.Set;
import java.util.function.Predicate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevWalk;
import org.junit.Before;
import org.junit.Test;

/**
 * Tests of the {@link ApprovalCopier} API.
 *
 * <p>This class doesn't verify the copy condition predicates, as they are already covered by {@code
 * StickyApprovalsIT}.
 */
@NoHttpd
public class ApprovalCopierIT extends AbstractDaemonTest {
  @Inject private ApprovalCopier approvalCopier;
  @Inject private ProjectOperations projectOperations;
  @Inject private RequestScopeOperations requestScopeOperations;

  @Before
  public void setup() throws Exception {
    // Add Verified label.
    try (ProjectConfigUpdate u = updateProject(project)) {
      LabelType.Builder verified =
          labelBuilder(
                  LabelId.VERIFIED, value(1, "Passes"), value(0, "No score"), value(-1, "Failed"))
              .setCopyCondition("is:MIN");
      u.getConfig().upsertLabelType(verified.build());
      u.save();
    }

    // Grant permissions to vote on the verified label.
    projectOperations
        .project(project)
        .forUpdate()
        .add(
            allowLabel(LabelId.VERIFIED)
                .ref(RefNames.REFS_HEADS + "*")
                .group(REGISTERED_USERS)
                .range(-1, 1))
        .update();
  }

  @Test
  public void forInitialPatchSet_noApprovals() throws Exception {
    ChangeData changeData = createChange().getChange();
    try (Repository repo = repoManager.openRepository(project);
        RevWalk revWalk = new RevWalk(repo)) {
      ApprovalCopier.Result approvalCopierResult =
          approvalCopier.forPatchSet(
              changeData.notes(), changeData.currentPatchSet(), revWalk, repo.getConfig());
      assertThat(approvalCopierResult.copiedApprovals()).isEmpty();
      assertThat(approvalCopierResult.outdatedApprovals()).isEmpty();
    }
  }

  @Test
  public void forInitialPatchSet_currentApprovals() throws Exception {
    PushOneCommit.Result r = createChange();

    // Add some current approvals.
    vote(r.getChangeId(), admin, LabelId.CODE_REVIEW, 2);
    vote(r.getChangeId(), admin, LabelId.VERIFIED, 1);
    vote(r.getChangeId(), user, LabelId.CODE_REVIEW, -1);
    vote(r.getChangeId(), user, LabelId.VERIFIED, -1);

    ApprovalCopier.Result approvalCopierResult =
        invokeApprovalCopierForCurrentPatchSet(
            r.getChange().getId(), /* expectedCurrentPatchSetNum= */ 1);
    assertThatList(approvalCopierResult.copiedApprovals()).isEmpty();
    assertThatList(approvalCopierResult.outdatedApprovals()).isEmpty();
  }

  @Test
  public void forPatchSet_noApprovals() throws Exception {
    PushOneCommit.Result r = createChange();
    amendChange(r.getChangeId(), "refs/for/master", admin, testRepo).assertOkStatus();
    ApprovalCopier.Result approvalCopierResult =
        invokeApprovalCopierForCurrentPatchSet(
            r.getChange().getId(), /* expectedCurrentPatchSetNum= */ 2);
    assertThatList(approvalCopierResult.copiedApprovals()).isEmpty();
    assertThatList(approvalCopierResult.outdatedApprovals()).isEmpty();
  }

  @Test
  public void forPatchSet_outdatedApprovals() throws Exception {
    PushOneCommit.Result r = createChange();
    PatchSet.Id patchSet1Id = r.getPatchSetId();

    // Add some approvals that are not copied.
    vote(r.getChangeId(), admin, LabelId.CODE_REVIEW, 2);
    vote(r.getChangeId(), user, LabelId.VERIFIED, 1);

    amendChange(r.getChangeId(), "refs/for/master", admin, testRepo).assertOkStatus();
    ApprovalCopier.Result approvalCopierResult =
        invokeApprovalCopierForCurrentPatchSet(
            r.getChange().getId(), /* expectedCurrentPatchSetNum= */ 2);
    assertThat(approvalCopierResult.copiedApprovals()).isEmpty();
    assertThat(approvalCopierResult.outdatedApprovals())
        .comparingElementsUsing(hasTestId())
        .containsExactly(
            PatchSetApprovalTestId.create(patchSet1Id, admin.id(), LabelId.CODE_REVIEW, 2),
            PatchSetApprovalTestId.create(patchSet1Id, user.id(), LabelId.VERIFIED, 1));
  }

  @Test
  public void forPatchSet_copiedApprovals() throws Exception {
    PushOneCommit.Result r = createChange();

    // Add some approvals that are copied.
    vote(r.getChangeId(), admin, LabelId.CODE_REVIEW, -2);
    vote(r.getChangeId(), user, LabelId.VERIFIED, -1);

    r = amendChange(r.getChangeId(), "refs/for/master", admin, testRepo);
    r.assertOkStatus();
    PatchSet.Id patchSet2Id = r.getPatchSetId();

    ApprovalCopier.Result approvalCopierResult =
        invokeApprovalCopierForCurrentPatchSet(
            r.getChange().getId(), /* expectedCurrentPatchSetNum= */ 2);
    assertThatList(approvalCopierResult.copiedApprovals())
        .comparingElementsUsing(hasTestId())
        .containsExactly(
            PatchSetApprovalTestId.create(patchSet2Id, admin.id(), LabelId.CODE_REVIEW, -2),
            PatchSetApprovalTestId.create(patchSet2Id, user.id(), LabelId.VERIFIED, -1));
    assertThatList(approvalCopierResult.outdatedApprovals()).isEmpty();
  }

  @Test
  public void forPatchSet_currentApprovals() throws Exception {
    PushOneCommit.Result r = createChange();
    amendChange(r.getChangeId(), "refs/for/master", admin, testRepo).assertOkStatus();

    // Add some current approvals.
    vote(r.getChangeId(), admin, LabelId.CODE_REVIEW, 2);
    vote(r.getChangeId(), admin, LabelId.VERIFIED, 1);
    vote(r.getChangeId(), user, LabelId.CODE_REVIEW, -1);
    vote(r.getChangeId(), user, LabelId.VERIFIED, -1);

    ApprovalCopier.Result approvalCopierResult =
        invokeApprovalCopierForCurrentPatchSet(
            r.getChange().getId(), /* expectedCurrentPatchSetNum= */ 2);
    assertThatList(approvalCopierResult.copiedApprovals()).isEmpty();
    assertThatList(approvalCopierResult.outdatedApprovals()).isEmpty();
  }

  @Test
  public void forPatchSet_allKindOfApprovals() throws Exception {
    PushOneCommit.Result r = createChange();
    PatchSet.Id patchSet1Id = r.getPatchSetId();

    // Add some approvals that are copied.
    vote(r.getChangeId(), admin, LabelId.CODE_REVIEW, -2);
    vote(r.getChangeId(), user, LabelId.VERIFIED, -1);

    // Add some approvals that are not copied.
    vote(r.getChangeId(), user, LabelId.CODE_REVIEW, 1);
    vote(r.getChangeId(), admin, LabelId.VERIFIED, 1);

    r = amendChange(r.getChangeId(), "refs/for/master", admin, testRepo);
    r.assertOkStatus();
    PatchSet.Id patchSet2Id = r.getPatchSetId();

    // Add some current approvals.
    vote(r.getChangeId(), user, LabelId.CODE_REVIEW, -1);
    vote(r.getChangeId(), admin, LabelId.VERIFIED, -1);

    ApprovalCopier.Result approvalCopierResult =
        invokeApprovalCopierForCurrentPatchSet(
            r.getChange().getId(), /* expectedCurrentPatchSetNum= */ 2);
    assertThatList(approvalCopierResult.copiedApprovals())
        .comparingElementsUsing(hasTestId())
        .containsExactly(
            PatchSetApprovalTestId.create(patchSet2Id, admin.id(), LabelId.CODE_REVIEW, -2),
            PatchSetApprovalTestId.create(patchSet2Id, user.id(), LabelId.VERIFIED, -1));
    assertThatList(approvalCopierResult.outdatedApprovals())
        .comparingElementsUsing(hasTestId())
        .containsExactly(
            PatchSetApprovalTestId.create(patchSet1Id, user.id(), LabelId.CODE_REVIEW, 1),
            PatchSetApprovalTestId.create(patchSet1Id, admin.id(), LabelId.VERIFIED, 1));
  }

  @Test
  public void forPatchSet_copiedApprovalOverriddenByCurrentApproval() throws Exception {
    PushOneCommit.Result r = createChange();

    // Add approval that is copied.
    vote(r.getChangeId(), admin, LabelId.CODE_REVIEW, -2);

    amendChange(r.getChangeId(), "refs/for/master", admin, testRepo).assertOkStatus();

    // Override the copied approval.
    vote(r.getChangeId(), admin, LabelId.CODE_REVIEW, 1);

    ApprovalCopier.Result approvalCopierResult =
        invokeApprovalCopierForCurrentPatchSet(
            r.getChange().getId(), /* expectedCurrentPatchSetNum= */ 2);
    assertThatList(approvalCopierResult.copiedApprovals()).isEmpty();
    assertThatList(approvalCopierResult.outdatedApprovals()).isEmpty();
  }

  @Test
  public void forPatchSet_approvalForNonExistingLabel() throws Exception {
    PushOneCommit.Result r = createChange();
    PatchSet.Id patchSet1Id = r.getPatchSetId();

    // Add approval that could be copied.
    vote(r.getChangeId(), admin, LabelId.CODE_REVIEW, -2);

    // Delete the Code-Review label (override it with an empty label definition).
    try (ProjectConfigUpdate u = updateProject(project)) {
      u.getConfig().upsertLabelType(labelBuilder(LabelId.CODE_REVIEW).build());
      u.save();
    }

    amendChange(r.getChangeId(), "refs/for/master", admin, testRepo).assertOkStatus();

    ApprovalCopier.Result approvalCopierResult =
        invokeApprovalCopierForCurrentPatchSet(
            r.getChange().getId(), /* expectedCurrentPatchSetNum= */ 2);
    assertThatList(approvalCopierResult.copiedApprovals()).isEmpty();
    assertThatList(approvalCopierResult.outdatedApprovals())
        .comparingElementsUsing(hasTestId())
        .containsExactly(
            PatchSetApprovalTestId.create(patchSet1Id, admin.id(), LabelId.CODE_REVIEW, -2));
  }

  @Test
  public void forPatchSet_copyableZeroApproval() throws Exception {
    PushOneCommit.Result r = createChange();

    // Override the inherited Code-Review label to make all votes copyable, including zero votes.
    try (ProjectConfigUpdate u = updateProject(project)) {
      LabelType.Builder codeReview =
          labelBuilder(
                  LabelId.CODE_REVIEW,
                  value(2, "Looks good to me, approved"),
                  value(1, "Looks good to me, but someone else must approve"),
                  value(0, "No score"),
                  value(-1, "I would prefer this is not submitted as is"),
                  value(-2, "This shall not be submitted"))
              .setCopyCondition("is:ANY");
      u.getConfig().upsertLabelType(codeReview.build());
      u.save();
    }

    // Create a zero approval that is copyable, by adding an approval and removing it again.
    vote(r.getChangeId(), admin, LabelId.CODE_REVIEW, 2);
    vote(r.getChangeId(), admin, LabelId.CODE_REVIEW, 0);

    amendChange(r.getChangeId(), "refs/for/master", admin, testRepo).assertOkStatus();

    ApprovalCopier.Result approvalCopierResult =
        invokeApprovalCopierForCurrentPatchSet(
            r.getChange().getId(), /* expectedCurrentPatchSetNum= */ 2);
    assertThatList(approvalCopierResult.copiedApprovals()).isEmpty();
    assertThatList(approvalCopierResult.outdatedApprovals()).isEmpty();
  }

  @Test
  public void forPatchSet_nonCopyableZeroApproval() throws Exception {
    PushOneCommit.Result r = createChange();

    // Create a zero approval that is non-copyable, by adding an approval and removing it again.
    vote(r.getChangeId(), admin, LabelId.CODE_REVIEW, 2);
    vote(r.getChangeId(), admin, LabelId.CODE_REVIEW, 0);

    amendChange(r.getChangeId(), "refs/for/master", admin, testRepo).assertOkStatus();

    ApprovalCopier.Result approvalCopierResult =
        invokeApprovalCopierForCurrentPatchSet(
            r.getChange().getId(), /* expectedCurrentPatchSetNum= */ 2);
    assertThatList(approvalCopierResult.copiedApprovals()).isEmpty();
    assertThatList(approvalCopierResult.outdatedApprovals()).isEmpty();
  }

  @Test
  public void copiedFlagSetOnCopiedApprovals() throws Exception {
    PushOneCommit.Result r = createChange();

    // Add approvals that are copied.
    vote(r.getChangeId(), admin, LabelId.CODE_REVIEW, -2);
    vote(r.getChangeId(), user, LabelId.VERIFIED, -1);

    r = amendChange(r.getChangeId(), "refs/for/master", admin, testRepo);
    r.assertOkStatus();
    PatchSet.Id patchSet2Id = r.getPatchSetId();

    // Override copied approval.
    vote(r.getChangeId(), admin, LabelId.CODE_REVIEW, 1);

    // Add new current approval.
    vote(r.getChangeId(), admin, LabelId.VERIFIED, 1);

    ApprovalCopier.Result approvalCopierResult =
        invokeApprovalCopierForCurrentPatchSet(
            r.getChange().getId(), /* expectedCurrentPatchSetNum= */ 2);
    ImmutableSet<PatchSetApproval> copiedApprovals = approvalCopierResult.copiedApprovals();
    assertThatList(filter(copiedApprovals, PatchSetApproval::copied))
        .comparingElementsUsing(hasTestId())
        .containsExactly(
            PatchSetApprovalTestId.create(patchSet2Id, user.id(), LabelId.VERIFIED, -1));
    assertThatList(filter(copiedApprovals, psa -> !psa.copied())).isEmpty();
  }

  private void vote(String changeId, TestAccount testAccount, String label, int value)
      throws RestApiException {
    requestScopeOperations.setApiUser(testAccount.id());
    gApi.changes().id(changeId).current().review(new ReviewInput().label(label, value));
    requestScopeOperations.setApiUser(admin.id());
  }

  private ImmutableSet<PatchSetApproval> filter(
      Set<PatchSetApproval> approvals, Predicate<PatchSetApproval> filter) {
    return approvals.stream().filter(filter).collect(toImmutableSet());
  }

  private ApprovalCopier.Result invokeApprovalCopierForCurrentPatchSet(
      Change.Id changeId, int expectedCurrentPatchSetNum) throws IOException {
    ChangeData changeData = changeDataFactory.create(project, changeId);
    assertThat(changeData.currentPatchSet().id().get()).isEqualTo(expectedCurrentPatchSetNum);
    try (Repository repo = repoManager.openRepository(project);
        RevWalk revWalk = new RevWalk(repo)) {
      return approvalCopier.forPatchSet(
          changeData.notes(), changeData.currentPatchSet(), revWalk, repo.getConfig());
    }
  }

  public static class PatchSetApprovalSubject extends Subject {
    public static Correspondence<PatchSetApproval, PatchSetApprovalTestId> hasTestId() {
      return NullAwareCorrespondence.transforming(PatchSetApprovalTestId::create, "has test ID");
    }

    public static PatchSetApprovalSubject assertThat(PatchSetApproval patchSetApproval) {
      return assertAbout(patchSetApprovals()).that(patchSetApproval);
    }

    public static ListSubject<PatchSetApprovalSubject, PatchSetApproval> assertThatList(
        ImmutableSet<PatchSetApproval> patchSetApprovals) {
      return ListSubject.assertThat(patchSetApprovals.asList(), patchSetApprovals());
    }

    private static Factory<PatchSetApprovalSubject, PatchSetApproval> patchSetApprovals() {
      return PatchSetApprovalSubject::new;
    }

    private PatchSetApprovalSubject(FailureMetadata metadata, PatchSetApproval patchSetApproval) {
      super(metadata, patchSetApproval);
    }
  }

  /**
   * AutoValue class that contains all properties of a PatchSetApproval that are relevant to do
   * assertions in tests (patch set ID, account ID, label name, voting value).
   */
  @AutoValue
  public abstract static class PatchSetApprovalTestId {
    public abstract PatchSet.Id patchSetId();

    public abstract Account.Id accountId();

    public abstract LabelId labelId();

    public abstract short value();

    public static PatchSetApprovalTestId create(PatchSetApproval patchSetApproval) {
      return new AutoValue_ApprovalCopierIT_PatchSetApprovalTestId(
          patchSetApproval.patchSetId(),
          patchSetApproval.accountId(),
          patchSetApproval.labelId(),
          patchSetApproval.value());
    }

    public static PatchSetApprovalTestId create(
        PatchSet.Id patchSetId, Account.Id accountId, String labelId, int value) {
      return new AutoValue_ApprovalCopierIT_PatchSetApprovalTestId(
          patchSetId, accountId, LabelId.create(labelId), (short) value);
    }
  }
}
