// 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.ApprovalDataSubject.assertThat;
import static com.google.gerrit.acceptance.server.change.ApprovalCopierIT.ApprovalDataSubject.assertThatList;
import static com.google.gerrit.acceptance.server.change.ApprovalCopierIT.ApprovalDataSubject.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 static com.google.gerrit.truth.ListSubject.elements;

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.StandardSubjectBuilder;
import com.google.common.truth.StringSubject;
import com.google.common.truth.Subject;
import com.google.common.truth.Truth8;
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.client.ChangeKind;
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.Optional;
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 {
    // Overwrite "Code-Review" label that is inherited from All-Projects.
    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(
                  String.format(
                      "changekind:%s OR changekind:%s OR is:MIN",
                      ChangeKind.NO_CHANGE, ChangeKind.TRIVIAL_REBASE.name()));
      u.getConfig().upsertLabelType(codeReview.build());
      u.save();
    }

    // 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));

    ApprovalDataSubject codeReviewApprovalSubject =
        assertThat(approvalCopierResult.outdatedApprovals(), LabelId.CODE_REVIEW, admin.id());
    codeReviewApprovalSubject.hasPassingAtomsThat().isEmpty();
    codeReviewApprovalSubject
        .hasFailingAtomsThat()
        .containsExactly("changekind:NO_CHANGE", "changekind:TRIVIAL_REBASE", "is:MIN");

    ApprovalDataSubject verifiedApprovalSubject =
        assertThat(approvalCopierResult.outdatedApprovals(), LabelId.VERIFIED, user.id());
    verifiedApprovalSubject.hasPassingAtomsThat().isEmpty();
    verifiedApprovalSubject.hasFailingAtomsThat().containsExactly("is:MIN");
  }

  @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();

    ApprovalDataSubject codeReviewApprovalSubject =
        assertThat(approvalCopierResult.copiedApprovals(), LabelId.CODE_REVIEW, admin.id());
    codeReviewApprovalSubject.hasPassingAtomsThat().containsExactly("is:MIN");
    codeReviewApprovalSubject
        .hasFailingAtomsThat()
        .containsExactly("changekind:NO_CHANGE", "changekind:TRIVIAL_REBASE");

    ApprovalDataSubject verifiedApprovalSubject =
        assertThat(approvalCopierResult.copiedApprovals(), LabelId.VERIFIED, user.id());
    verifiedApprovalSubject.hasPassingAtomsThat().containsExactly("is:MIN");
    verifiedApprovalSubject.hasFailingAtomsThat().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));

    ApprovalDataSubject copiedCodeReviewApprovalSubject =
        assertThat(approvalCopierResult.copiedApprovals(), LabelId.CODE_REVIEW, admin.id());
    copiedCodeReviewApprovalSubject.hasPassingAtomsThat().containsExactly("is:MIN");
    copiedCodeReviewApprovalSubject
        .hasFailingAtomsThat()
        .containsExactly("changekind:NO_CHANGE", "changekind:TRIVIAL_REBASE");

    ApprovalDataSubject copiedVerifiedApprovalSubject =
        assertThat(approvalCopierResult.copiedApprovals(), LabelId.VERIFIED, user.id());
    copiedVerifiedApprovalSubject.hasPassingAtomsThat().containsExactly("is:MIN");
    copiedVerifiedApprovalSubject.hasFailingAtomsThat().isEmpty();

    ApprovalDataSubject outdatedCodeReviewApprovalSubject1 =
        assertThat(approvalCopierResult.outdatedApprovals(), LabelId.CODE_REVIEW, user.id());
    outdatedCodeReviewApprovalSubject1.hasPassingAtomsThat().isEmpty();
    outdatedCodeReviewApprovalSubject1
        .hasFailingAtomsThat()
        .containsExactly("changekind:NO_CHANGE", "changekind:TRIVIAL_REBASE", "is:MIN");

    ApprovalDataSubject outdatedVerifiedApprovalSubject1 =
        assertThat(approvalCopierResult.outdatedApprovals(), LabelId.VERIFIED, admin.id());
    outdatedVerifiedApprovalSubject1.hasPassingAtomsThat().isEmpty();
    outdatedVerifiedApprovalSubject1.hasFailingAtomsThat().containsExactly("is:MIN");
  }

  @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));

    ApprovalDataSubject codeReviewApprovalSubject1 =
        assertThat(approvalCopierResult.outdatedApprovals(), LabelId.CODE_REVIEW, admin.id());
    codeReviewApprovalSubject1.hasPassingAtomsThat().isEmpty();
    codeReviewApprovalSubject1.hasFailingAtomsThat().isEmpty();
  }

  @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<ApprovalCopier.Result.PatchSetApprovalData> copiedApprovals =
        approvalCopierResult.copiedApprovals();
    assertThatList(filter(copiedApprovals, approval -> approval.patchSetApproval().copied()))
        .comparingElementsUsing(hasTestId())
        .containsExactly(
            PatchSetApprovalTestId.create(patchSet2Id, user.id(), LabelId.VERIFIED, -1));
    assertThatList(filter(copiedApprovals, approval -> !approval.patchSetApproval().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<ApprovalCopier.Result.PatchSetApprovalData> filter(
      Set<ApprovalCopier.Result.PatchSetApprovalData> approvals,
      Predicate<ApprovalCopier.Result.PatchSetApprovalData> 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 ApprovalDataSubject extends Subject {
    public static Correspondence<ApprovalCopier.Result.PatchSetApprovalData, PatchSetApprovalTestId>
        hasTestId() {
      return NullAwareCorrespondence.transforming(
          approvalData -> PatchSetApprovalTestId.create(approvalData.patchSetApproval()),
          "has test ID");
    }

    public static ApprovalDataSubject assertThat(
        ApprovalCopier.Result.PatchSetApprovalData approvalData) {
      return assertAbout(approvalDatas()).that(approvalData);
    }

    public static ApprovalDataSubject assertThat(
        ImmutableSet<ApprovalCopier.Result.PatchSetApprovalData> approvalDatas,
        String labelId,
        Account.Id accountId) {
      Optional<ApprovalCopier.Result.PatchSetApprovalData> approvalDataForLabelAndAccount =
          approvalDatas.stream()
              .filter(
                  approvalData ->
                      approvalData.patchSetApproval().label().equals(labelId)
                          && approvalData.patchSetApproval().accountId().equals(accountId))
              .findAny();
      Truth8.assertThat(approvalDataForLabelAndAccount).isPresent();
      return assertAbout(approvalDatas()).that(approvalDataForLabelAndAccount.get());
    }

    public static ListSubject<ApprovalDataSubject, ApprovalCopier.Result.PatchSetApprovalData>
        assertThatList(ImmutableSet<ApprovalCopier.Result.PatchSetApprovalData> approvalDatas) {
      return ListSubject.assertThat(approvalDatas.asList(), approvalDatas());
    }

    private static Factory<ApprovalDataSubject, ApprovalCopier.Result.PatchSetApprovalData>
        approvalDatas() {
      return ApprovalDataSubject::new;
    }

    private final ApprovalCopier.Result.PatchSetApprovalData approvalData;

    private ApprovalDataSubject(
        FailureMetadata metadata, ApprovalCopier.Result.PatchSetApprovalData approvalData) {
      super(metadata, approvalData);
      this.approvalData = approvalData;
    }

    public ListSubject<StringSubject, String> hasPassingAtomsThat() {
      return check("passingAtoms()")
          .about(elements())
          .that(approvalData().passingAtoms().asList(), StandardSubjectBuilder::that);
    }

    public ListSubject<StringSubject, String> hasFailingAtomsThat() {
      return check("failingAtoms()")
          .about(elements())
          .that(approvalData().failingAtoms().asList(), StandardSubjectBuilder::that);
    }

    private ApprovalCopier.Result.PatchSetApprovalData approvalData() {
      isNotNull();
      return approvalData;
    }
  }

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

    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);
    }
  }
}
