// 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.api.change;

import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
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.label;
import static com.google.gerrit.server.project.testing.TestLabels.value;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.MoreCollectors;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.ExtensionRegistry;
import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.UseTimezone;
import com.google.gerrit.acceptance.VerifyNoPiiInChangeNotes;
import com.google.gerrit.acceptance.testsuite.change.IndexOperations;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.LabelFunction;
import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.LegacySubmitRequirement;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.entities.SubmitRecord;
import com.google.gerrit.entities.SubmitRequirement;
import com.google.gerrit.entities.SubmitRequirementExpression;
import com.google.gerrit.entities.SubmitRequirementExpressionResult;
import com.google.gerrit.entities.SubmitRequirementResult;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
import com.google.gerrit.extensions.api.changes.RevisionApi;
import com.google.gerrit.extensions.api.groups.GroupInput;
import com.google.gerrit.extensions.client.ListChangesOption;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.ActionInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.LabelDefinitionInput;
import com.google.gerrit.extensions.common.LegacySubmitRequirementInfo;
import com.google.gerrit.extensions.common.SubmitRecordInfo;
import com.google.gerrit.extensions.common.SubmitRequirementExpressionInfo;
import com.google.gerrit.extensions.common.SubmitRequirementInput;
import com.google.gerrit.extensions.common.SubmitRequirementResultInfo;
import com.google.gerrit.extensions.common.SubmitRequirementResultInfo.Status;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.httpd.raw.IndexPreloadingUtil;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.testing.TestLabels;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.rules.SubmitRule;
import com.google.inject.Inject;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.IntStream;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.util.RawParseUtils;
import org.junit.Test;

@NoHttpd
@UseTimezone(timezone = "US/Eastern")
@VerifyNoPiiInChangeNotes(true)
public class SubmitRequirementIT extends AbstractDaemonTest {
  @Inject private ProjectOperations projectOperations;
  @Inject private RequestScopeOperations requestScopeOperations;
  @Inject private ExtensionRegistry extensionRegistry;
  @Inject private IndexOperations.Change changeIndexOperations;

  @Test
  public void submitRecords() throws Exception {
    PushOneCommit.Result r = createChange();
    TestSubmitRule testSubmitRule = new TestSubmitRule();
    try (Registration registration = extensionRegistry.newRegistration().add(testSubmitRule)) {
      String changeId = r.getChangeId();

      ChangeInfo change = gApi.changes().id(changeId).get();
      assertThat(change.submitRecords).hasSize(2);
      // Check the default submit record for the code-review label
      SubmitRecordInfo codeReviewRecord = Iterables.get(change.submitRecords, 0);
      assertThat(codeReviewRecord.ruleName).isEqualTo("gerrit~DefaultSubmitRule");
      assertThat(codeReviewRecord.status).isEqualTo(SubmitRecordInfo.Status.NOT_READY);
      assertThat(codeReviewRecord.labels).hasSize(1);
      SubmitRecordInfo.Label label = Iterables.getOnlyElement(codeReviewRecord.labels);
      assertThat(label.label).isEqualTo("Code-Review");
      assertThat(label.status).isEqualTo(SubmitRecordInfo.Label.Status.NEED);
      assertThat(label.appliedBy).isNull();
      // Check the custom test record created by the TestSubmitRule
      SubmitRecordInfo testRecord = Iterables.get(change.submitRecords, 1);
      assertThat(testRecord.ruleName).isEqualTo("testSubmitRule");
      assertThat(testRecord.status).isEqualTo(SubmitRecordInfo.Status.OK);
      assertThat(testRecord.requirements)
          .containsExactly(new LegacySubmitRequirementInfo("OK", "fallback text", "type"));
      assertThat(testRecord.labels).hasSize(1);
      SubmitRecordInfo.Label testLabel = Iterables.getOnlyElement(testRecord.labels);
      assertThat(testLabel.label).isEqualTo("label");
      assertThat(testLabel.status).isEqualTo(SubmitRecordInfo.Label.Status.OK);
      assertThat(testLabel.appliedBy).isNull();

      voteLabel(changeId, "Code-Review", 2);
      // Code review record is satisfied after voting +2
      change = gApi.changes().id(changeId).get();
      assertThat(change.submitRecords).hasSize(2);
      codeReviewRecord = Iterables.get(change.submitRecords, 0);
      assertThat(codeReviewRecord.ruleName).isEqualTo("gerrit~DefaultSubmitRule");
      assertThat(codeReviewRecord.status).isEqualTo(SubmitRecordInfo.Status.OK);
      assertThat(codeReviewRecord.labels).hasSize(1);
      label = Iterables.getOnlyElement(codeReviewRecord.labels);
      assertThat(label.label).isEqualTo("Code-Review");
      assertThat(label.status).isEqualTo(SubmitRecordInfo.Label.Status.OK);
      assertThat(label.appliedBy._accountId).isEqualTo(admin.id().get());
    }
  }

  @Test
  public void checkSubmitRequirement_fromRefsConfigChange_satisfied() throws Exception {
    String oldHead = projectOperations.project(project).getHead("master").name();
    projectOperations
        .project(project)
        .forUpdate()
        .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS))
        .add(allow(Permission.PUSH).ref("refs/for/refs/meta/config").group(REGISTERED_USERS))
        .add(allow(Permission.READ).ref("refs/meta/config").group(REGISTERED_USERS))
        .add(allow(Permission.SUBMIT).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS))
        .update();
    fetchRefsMetaConfig();
    PushOneCommit.Result configResult =
        createConfigChangeWithSubmitRequirement("Foo", "label:Code-Review=+2");

    // Upload a normal change. Check the SR against it.
    testRepo.reset(oldHead);
    PushOneCommit.Result r2 = createChange();
    String changeId = r2.getChangeId();
    SubmitRequirementResultInfo info =
        gApi.changes()
            .id(changeId)
            .checkSubmitRequirementRequest()
            .srName("Foo")
            .refsConfigChangeId(configResult.getChange().getId().toString())
            .get();
    assertThat(info.status).isEqualTo(SubmitRequirementResultInfo.Status.UNSATISFIED);
    voteLabel(changeId, "Code-Review", 2);
    info =
        gApi.changes()
            .id(changeId)
            .checkSubmitRequirementRequest()
            .srName("Foo")
            .refsConfigChangeId(configResult.getChange().getId().toString())
            .get();
    assertThat(info.status).isEqualTo(SubmitRequirementResultInfo.Status.SATISFIED);
  }

  @Test
  public void checkSubmitRequirement_fromRefsConfigChangeOfAnotherProject_satisfied()
      throws Exception {
    projectOperations
        .project(project)
        .forUpdate()
        .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS))
        .add(allow(Permission.PUSH).ref("refs/for/refs/meta/config").group(REGISTERED_USERS))
        .add(allow(Permission.READ).ref("refs/meta/config").group(REGISTERED_USERS))
        .add(allow(Permission.SUBMIT).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS))
        .update();
    fetchRefsMetaConfig();
    PushOneCommit.Result configResult =
        createConfigChangeWithSubmitRequirement("Foo", "label:Code-Review=+2");

    // Upload a normal change in another project. Check the SR against it.
    Project.NameKey otherProject = projectOperations.newProject().create();
    TestRepository<InMemoryRepository> otherRepo = cloneProject(otherProject, admin);
    PushOneCommit.Result r2 = createChange(otherRepo);
    String changeId = r2.getChangeId();
    SubmitRequirementResultInfo info =
        gApi.changes()
            .id(changeId)
            .checkSubmitRequirementRequest()
            .srName("Foo")
            .refsConfigChangeId(configResult.getChange().getId().toString())
            .get();
    assertThat(info.status).isEqualTo(SubmitRequirementResultInfo.Status.UNSATISFIED);
    voteLabel(changeId, "Code-Review", 2);
    info =
        gApi.changes()
            .id(changeId)
            .checkSubmitRequirementRequest()
            .srName("Foo")
            .refsConfigChangeId(configResult.getChange().getId().toString())
            .get();
    assertThat(info.status).isEqualTo(SubmitRequirementResultInfo.Status.SATISFIED);
  }

  @Test
  public void checkSubmitRequirement_fromRefsConfigChange_failsForNonExistingSR() throws Exception {
    String oldHead = projectOperations.project(project).getHead("master").name();
    projectOperations
        .project(project)
        .forUpdate()
        .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS))
        .add(allow(Permission.PUSH).ref("refs/for/refs/meta/config").group(REGISTERED_USERS))
        .add(allow(Permission.READ).ref("refs/meta/config").group(REGISTERED_USERS))
        .add(allow(Permission.SUBMIT).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS))
        .update();
    fetchRefsMetaConfig();
    PushOneCommit.Result configResult =
        createConfigChangeWithSubmitRequirement("Foo", "label:Code-Review=+2");

    // Upload a normal change. Check the SR against it.
    testRepo.reset(oldHead);
    PushOneCommit.Result r2 = createChange();
    String changeId = r2.getChangeId();
    Exception thrown =
        assertThrows(
            BadRequestException.class,
            () ->
                gApi.changes()
                    .id(changeId)
                    .checkSubmitRequirementRequest()
                    .srName("Bar")
                    .refsConfigChangeId(configResult.getChange().getId().toString())
                    .get());
    assertThat(thrown).hasMessageThat().isEqualTo("No submit requirement matching name 'Bar'");
  }

  @Test
  public void checkSubmitRequirement_notAllowedFromNonRefsConfigChange() throws Exception {
    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();
    Exception thrown =
        assertThrows(
            BadRequestException.class,
            () ->
                gApi.changes()
                    .id(changeId)
                    .checkSubmitRequirementRequest()
                    .srName("Foo")
                    .refsConfigChangeId(r.getChange().getId().toString())
                    .get());
    assertThat(thrown)
        .hasMessageThat()
        .isEqualTo(
            String.format(
                "Change '%s' is not in refs/meta/config branch.", r.getChange().getId().get()));
  }

  @Test
  public void checkSubmitRequirement_notAllowedFromNonExistingChange() throws Exception {
    String invalidChangeNumber = "2134789";
    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();
    Exception thrown =
        assertThrows(
            BadRequestException.class,
            () ->
                gApi.changes()
                    .id(changeId)
                    .checkSubmitRequirementRequest()
                    .srName("Foo")
                    .refsConfigChangeId(invalidChangeNumber)
                    .get());
    assertThat(thrown)
        .hasMessageThat()
        .isEqualTo(String.format("Change '%s' does not exist", invalidChangeNumber));
  }

  @Test
  public void checkSubmitRequirement_fromRefsConfigChange_failsIfBothParametersAreNotSet()
      throws Exception {
    String oldHead = projectOperations.project(project).getHead("master").name();
    projectOperations
        .project(project)
        .forUpdate()
        .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS))
        .add(allow(Permission.PUSH).ref("refs/for/refs/meta/config").group(REGISTERED_USERS))
        .add(allow(Permission.READ).ref("refs/meta/config").group(REGISTERED_USERS))
        .add(allow(Permission.SUBMIT).ref(RefNames.REFS_CONFIG).group(REGISTERED_USERS))
        .update();
    fetchRefsMetaConfig();
    PushOneCommit.Result configResult =
        createConfigChangeWithSubmitRequirement("Foo", "label:Code-Review=+2");

    // Upload a normal change. Check the SR against it.
    testRepo.reset(oldHead);
    PushOneCommit.Result r2 = createChange();
    String changeId = r2.getChangeId();
    Exception thrown =
        assertThrows(
            BadRequestException.class,
            () -> gApi.changes().id(changeId).checkSubmitRequirementRequest().srName("Bar").get());
    assertThat(thrown)
        .hasMessageThat()
        .isEqualTo("Both 'sr-name' and 'refs-config-change-id' parameters must be set");

    thrown =
        assertThrows(
            BadRequestException.class,
            () ->
                gApi.changes()
                    .id(changeId)
                    .checkSubmitRequirementRequest()
                    .refsConfigChangeId(configResult.getChangeId())
                    .get());
    assertThat(thrown)
        .hasMessageThat()
        .isEqualTo("Both 'sr-name' and 'refs-config-change-id' parameters must be set");
  }

  @Test
  public void checkSubmitRequirement_satisfied() throws Exception {
    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();

    SubmitRequirementInput in =
        createSubmitRequirementInput(
            "Code-Review", /* submittabilityExpression= */ "label:Code-Review=+2");

    SubmitRequirementResultInfo result = gApi.changes().id(changeId).checkSubmitRequirement(in);
    assertThat(result.status).isEqualTo(SubmitRequirementResultInfo.Status.UNSATISFIED);

    voteLabel(changeId, "Code-Review", 2);
    result = gApi.changes().id(changeId).checkSubmitRequirement(in);
    assertThat(result.status).isEqualTo(SubmitRequirementResultInfo.Status.SATISFIED);
  }

  @Test
  public void checkSubmitRequirement_notApplicable() throws Exception {
    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();

    SubmitRequirementInput in =
        createSubmitRequirementInput(
            "Code-Review",
            /* applicableIf= */ "branch:non-existent",
            /* submittableIf= */ "label:Code-Review=+2",
            /* overrideIf= */ null);

    SubmitRequirementResultInfo result = gApi.changes().id(changeId).checkSubmitRequirement(in);
    assertThat(result.status).isEqualTo(SubmitRequirementResultInfo.Status.NOT_APPLICABLE);

    voteLabel(changeId, "Code-Review", 2);
    result = gApi.changes().id(changeId).checkSubmitRequirement(in);
    assertThat(result.status).isEqualTo(SubmitRequirementResultInfo.Status.NOT_APPLICABLE);
  }

  @Test
  public void checkSubmitRequirement_overridden() throws Exception {
    configLabel("Override-Label", LabelFunction.NO_OP); // label function has no effect
    projectOperations
        .project(project)
        .forUpdate()
        .add(
            allowLabel("Override-Label")
                .ref("refs/heads/master")
                .group(REGISTERED_USERS)
                .range(-1, 1))
        .update();

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();

    SubmitRequirementInput in =
        createSubmitRequirementInput(
            "Code-Review",
            /* applicableIf= */ null,
            /* submittableIf= */ "label:Code-Review=+2",
            /* overrideIf= */ "label:Override-Label=+1");

    SubmitRequirementResultInfo result = gApi.changes().id(changeId).checkSubmitRequirement(in);
    assertThat(result.status).isEqualTo(SubmitRequirementResultInfo.Status.UNSATISFIED);

    voteLabel(changeId, "Code-Review", 2);
    result = gApi.changes().id(changeId).checkSubmitRequirement(in);
    assertThat(result.status).isEqualTo(SubmitRequirementResultInfo.Status.SATISFIED);

    voteLabel(changeId, "Override-Label", 1);
    result = gApi.changes().id(changeId).checkSubmitRequirement(in);
    assertThat(result.status).isEqualTo(SubmitRequirementResultInfo.Status.OVERRIDDEN);
  }

  @Test
  public void checkSubmitRequirement_error() throws Exception {
    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();

    SubmitRequirementInput in =
        createSubmitRequirementInput("Code-Review", /* submittabilityExpression= */ "!!!");

    SubmitRequirementResultInfo result = gApi.changes().id(changeId).checkSubmitRequirement(in);
    assertThat(result.status).isEqualTo(SubmitRequirementResultInfo.Status.ERROR);
  }

  @Test
  public void submitRequirement_withLabelEqualsMax() throws Exception {
    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(
                SubmitRequirementExpression.create("label:Code-Review=MAX"))
            .setAllowOverrideInChildProjects(false)
            .build());

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();

    ChangeInfo change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

    voteLabel(changeId, "Code-Review", 2);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
  }

  @Test
  public void submitRequirement_withLabelEqualsMax_fromNonUploader() throws Exception {
    configLabel("my-label", LabelFunction.NO_OP); // label function has no effect
    projectOperations
        .project(project)
        .forUpdate()
        .add(allowLabel("my-label").ref("refs/heads/master").group(REGISTERED_USERS).range(-1, 1))
        .update();
    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("my-label")
            .setSubmittabilityExpression(
                SubmitRequirementExpression.create("label:my-label=MAX,user=non_uploader"))
            .setAllowOverrideInChildProjects(false)
            .build());

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();
    ChangeInfo change = gApi.changes().id(changeId).get();
    // The second requirement is coming from the legacy code-review label function
    assertThat(change.submitRequirements).hasSize(2);
    assertSubmitRequirementStatus(
        change.submitRequirements, "my-label", Status.UNSATISFIED, /* isLegacy= */ false);

    // Voting with a max vote as the uploader will not satisfy the submit requirement.
    voteLabel(changeId, "my-label", 1);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(2);
    assertSubmitRequirementStatus(
        change.submitRequirements, "my-label", Status.UNSATISFIED, /* isLegacy= */ false);

    // Voting as a non-uploader will satisfy the submit requirement.
    requestScopeOperations.setApiUser(user.id());
    voteLabel(changeId, "my-label", 1);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(2);
    assertSubmitRequirementStatus(
        change.submitRequirements, "my-label", Status.SATISFIED, /* isLegacy= */ false);
  }

  @Test
  public void submitRequirement_withLabelEqualsMinBlockingSubmission() throws Exception {
    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(
                SubmitRequirementExpression.create("-label:Code-Review=MIN"))
            .setAllowOverrideInChildProjects(false)
            .build());

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();

    ChangeInfo change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(2);
    // Requirement is satisfied because there are no votes
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
    // Legacy requirement (coming from the label function definition) is not satisfied. We return
    // both legacy and non-legacy requirements in this case since their statuses are not identical.
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);

    voteLabel(changeId, "Code-Review", -1);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(2);
    // Requirement is still satisfied because -1 is not the max negative value
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);

    voteLabel(changeId, "Code-Review", -2);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    // Requirement is now unsatisfied because -2 is the max negative value
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
  }

  @Test
  public void submitRequirement_withMaxWithBlock_ignoringSelfApproval() throws Exception {
    configLabel("my-label", LabelFunction.MAX_WITH_BLOCK);
    projectOperations
        .project(project)
        .forUpdate()
        .add(allowLabel("my-label").ref("refs/heads/master").group(REGISTERED_USERS).range(-1, 1))
        .update();

    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("my-label")
            .setSubmittabilityExpression(
                SubmitRequirementExpression.create(
                    "label:my-label=MAX,user=non_uploader -label:my-label=MIN"))
            .setAllowOverrideInChildProjects(false)
            .build());

    // Create the change as admin
    requestScopeOperations.setApiUser(admin.id());
    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();

    // Admin (a.k.a uploader) adds a -1 min vote. This is going to block submission.
    voteLabel(changeId, "my-label", -1);
    ChangeInfo change = gApi.changes().id(changeId).get();
    // The other requirement is coming from the code-review label function
    assertThat(change.submitRequirements).hasSize(2);
    assertSubmitRequirementStatus(
        change.submitRequirements, "my-label", Status.UNSATISFIED, /* isLegacy= */ false);

    // user (i.e. non_uploader) votes 1. Requirement is still blocking because of -1 of uploader.
    requestScopeOperations.setApiUser(user.id());
    voteLabel(changeId, "my-label", 1);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(2);
    assertSubmitRequirementStatus(
        change.submitRequirements, "my-label", Status.UNSATISFIED, /* isLegacy= */ false);

    // Admin (a.k.a uploader) removes -1. Now requirement is fulfilled.
    requestScopeOperations.setApiUser(admin.id());
    voteLabel(changeId, "my-label", 0);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(2);
    assertSubmitRequirementStatus(
        change.submitRequirements, "my-label", Status.SATISFIED, /* isLegacy= */ false);
  }

  @Test
  public void submitRequirement_withLabelEqualsAny() throws Exception {
    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(
                SubmitRequirementExpression.create("label:Code-Review=ANY"))
            .setAllowOverrideInChildProjects(false)
            .build());

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();

    ChangeInfo change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

    voteLabel(changeId, "Code-Review", 1);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(2);
    // Legacy and non-legacy requirements have mismatching status. Both are returned from the API.
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
  }

  @Test
  public void submitRequirementIsSatisfied_whenSubmittabilityExpressionIsFulfilled()
      throws Exception {
    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
            .setAllowOverrideInChildProjects(false)
            .build());
    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("Verified")
            .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Verified=+1"))
            .setAllowOverrideInChildProjects(false)
            .build());

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();

    ChangeInfo change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(2);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Verified", Status.UNSATISFIED, /* isLegacy= */ false);

    voteLabel(changeId, "Code-Review", 2);

    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(2);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Verified", Status.UNSATISFIED, /* isLegacy= */ false);
  }

  @Test
  public void submitRequirementIsNotApplicable_whenApplicabilityExpressionIsNotFulfilled()
      throws Exception {
    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setApplicabilityExpression(SubmitRequirementExpression.of("project:foo"))
            .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
            .setAllowOverrideInChildProjects(false)
            .build());

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();

    ChangeInfo change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(2);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.NOT_APPLICABLE, /* isLegacy= */ false);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
  }

  @Test
  public void submitRequirementIsOverridden_whenOverrideExpressionIsFulfilled() throws Exception {
    configLabel("build-cop-override", LabelFunction.NO_BLOCK);
    projectOperations
        .project(project)
        .forUpdate()
        .add(
            allowLabel("build-cop-override")
                .ref("refs/heads/master")
                .group(REGISTERED_USERS)
                .range(-1, 1))
        .update();

    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
            .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
            .setAllowOverrideInChildProjects(false)
            .build());

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();
    ChangeInfo change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

    voteLabel(changeId, "build-cop-override", 1);

    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(2);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.OVERRIDDEN, /* isLegacy= */ false);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
  }

  @Test
  public void submitRequirement_overriddenInChildProjectWithStricterRequirement() throws Exception {
    configSubmitRequirement(
        allProjects,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1"))
            .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
            .setAllowOverrideInChildProjects(true)
            .build());

    // Override submit requirement in child project (requires Code-Review=+2 instead of +1)
    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
            .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
            .setAllowOverrideInChildProjects(false)
            .build());

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();
    ChangeInfo change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

    voteLabel(changeId, "Code-Review", 1);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

    voteLabel(changeId, "Code-Review", 2);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
  }

  @Test
  public void submitRequirement_overriddenInChildProjectWithLessStrictRequirement()
      throws Exception {
    configSubmitRequirement(
        allProjects,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
            .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
            .setAllowOverrideInChildProjects(true)
            .build());

    // Override submit requirement in child project (requires Code-Review=+1 instead of +2)
    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1"))
            .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
            .setAllowOverrideInChildProjects(false)
            .build());

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();
    ChangeInfo change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

    voteLabel(changeId, "Code-Review", 1);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(2);
    // +1 was enough to fulfill the requirement: override in child project applies
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
    // Legacy requirement that is coming from the label MaxWithBlock function. Still unsatisfied.
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
  }

  @Test
  public void submitRequirement_overriddenInChildProjectAsDisabled() throws Exception {
    configSubmitRequirement(
        allProjects,
        SubmitRequirement.builder()
            .setName("Custom-Requirement")
            .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
            .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
            .setAllowOverrideInChildProjects(true)
            .build());

    // Override submit requirement in child project (requires Code-Review=+1 instead of +2)
    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("Custom-Requirement")
            .setApplicabilityExpression(SubmitRequirementExpression.of("is:false"))
            .setSubmittabilityExpression(SubmitRequirementExpression.create("is:false"))
            .setAllowOverrideInChildProjects(false)
            .build());

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();
    ChangeInfo change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(2);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
    assertSubmitRequirementStatus(
        change.submitRequirements,
        "Custom-Requirement",
        Status.NOT_APPLICABLE,
        /* isLegacy= */ false);
  }

  @Test
  public void submitRequirement_inheritedFromParentProject() throws Exception {
    configSubmitRequirement(
        allProjects,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1"))
            .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
            .setAllowOverrideInChildProjects(false)
            .build());

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();
    ChangeInfo change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

    voteLabel(changeId, "Code-Review", 1);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(2);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
    // Legacy requirement is coming from the label MaxWithBlock function. Still unsatisfied.
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
  }

  @Test
  public void submitRequirement_overriddenSRInParentProjectIsInheritedByChildProject()
      throws Exception {
    // Define submit requirement in root project.
    configSubmitRequirement(
        allProjects,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1"))
            .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
            .setAllowOverrideInChildProjects(true)
            .build());

    // Override submit requirement in parent project (requires Code-Review=+2 instead of +1).
    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
            .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
            .setAllowOverrideInChildProjects(false)
            .build());

    Project.NameKey child = createProjectOverAPI("child", project, true, /* submitType= */ null);
    TestRepository<InMemoryRepository> childRepo = cloneProject(child);
    PushOneCommit.Result r = createChange(childRepo);
    String changeId = r.getChangeId();
    ChangeInfo change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

    voteLabel(changeId, "Code-Review", 1);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

    voteLabel(changeId, "Code-Review", 2);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
  }

  @Test
  public void submitRequirement_ignoredInChildProject_ifParentDoesNotAllowOverride()
      throws Exception {
    configSubmitRequirement(
        allProjects,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1"))
            .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
            .setAllowOverrideInChildProjects(false)
            .build());

    // Override submit requirement in child project (requires Code-Review=+2 instead of +1).
    // Will have no effect since parent does not allow override.
    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
            .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
            .setAllowOverrideInChildProjects(false)
            .build());

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();
    ChangeInfo change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

    voteLabel(changeId, "Code-Review", 1);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(2);
    // +1 was enough to fulfill the requirement: override in child project was ignored
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
    // Legacy requirement is coming from the label MaxWithBlock function. Still unsatisfied.
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
  }

  @Test
  public void submitRequirement_ignoredInChildProject_ifParentAddsSRThatDoesNotAllowOverride()
      throws Exception {
    // Submit requirement in child project (requires Code-Review=+1)
    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1"))
            .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
            .setAllowOverrideInChildProjects(false)
            .build());

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();
    ChangeInfo change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

    voteLabel(changeId, "Code-Review", 1);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(2);
    // +1 was enough to fulfill the requirement
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
    // Legacy requirement that is coming from the label MaxWithBlock function. Still unsatisfied.
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);

    // Add stricter non-overridable submit requirement in parent project (requires Code-Review=+2,
    // instead of Code-Review=+1)
    configSubmitRequirement(
        allProjects,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
            .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
            .setAllowOverrideInChildProjects(false)
            .build());

    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

    voteLabel(changeId, "Code-Review", 2);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
  }

  @Test
  public void submitRequirement_ignoredInChildProject_ifParentMakesSRNonOverridable()
      throws Exception {
    configSubmitRequirement(
        allProjects,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
            .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
            .setAllowOverrideInChildProjects(true)
            .build());

    // Override submit requirement in child project (requires Code-Review=+1 instead of +2)
    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1"))
            .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
            .setAllowOverrideInChildProjects(false)
            .build());

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();
    ChangeInfo change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

    voteLabel(changeId, "Code-Review", 1);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(2);
    // +1 was enough to fulfill the requirement: override in child project applies
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
    // Legacy requirement that is coming from the label MaxWithBlock function. Still unsatisfied.
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);

    // Disallow overriding the submit requirement in the parent project.
    configSubmitRequirement(
        allProjects,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
            .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
            .setAllowOverrideInChildProjects(false)
            .build());

    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

    voteLabel(changeId, "Code-Review", 2);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
  }

  @Test
  public void submitRequirement_ignoredInGrandChildProject_ifGrandParentDoesNotAllowOverride()
      throws Exception {
    configSubmitRequirement(
        allProjects,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1"))
            .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
            .setAllowOverrideInChildProjects(false)
            .build());

    Project.NameKey grandChild =
        createProjectOverAPI("grandChild", project, true, /* submitType= */ null);

    // Override submit requirement in grand child project (requires Code-Review=+2 instead of +1).
    // Will have no effect since grand parent does not allow override.
    configSubmitRequirement(
        grandChild,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
            .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
            .setAllowOverrideInChildProjects(false)
            .build());

    TestRepository<InMemoryRepository> grandChildRepo = cloneProject(grandChild);
    PushOneCommit.Result r = createChange(grandChildRepo);
    String changeId = r.getChangeId();
    ChangeInfo change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

    voteLabel(changeId, "Code-Review", 1);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(2);
    // +1 was enough to fulfill the requirement: override in grand child project was ignored
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
    // Legacy requirement is coming from the label MaxWithBlock function. Still unsatisfied.
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
  }

  @Test
  public void submitRequirement_overrideOverideExpression() throws Exception {
    // Define submit requirement in root project.
    configSubmitRequirement(
        allProjects,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1"))
            .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
            .setAllowOverrideInChildProjects(true)
            .build());

    // Create Code-Review-Override label
    LabelDefinitionInput input = new LabelDefinitionInput();
    input.function = "NoOp";
    input.values = ImmutableMap.of("+1", "Override", " 0", "No Override");
    gApi.projects().name(project.get()).label("Code-Review-Override").create(input).get();

    // Allow to vote on the Code-Review-Override label.
    projectOperations
        .project(project)
        .forUpdate()
        .add(
            TestProjectUpdate.allowLabel("Code-Review-Override")
                .range(0, 1)
                .ref("refs/*")
                .group(REGISTERED_USERS)
                .build())
        .update();

    // Override submit requirement in project (requires Code-Review-Override+1 as override instead
    // of build-cop-override+1).
    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1"))
            .setOverrideExpression(SubmitRequirementExpression.of("label:Code-Review-Override=+1"))
            .setAllowOverrideInChildProjects(false)
            .build());

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();
    ChangeInfo change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

    voteLabel(changeId, "Code-Review-Override", 1);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(2);
    // Code-Review-Override+1 was enough to fulfill the override expression of the requirement
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.OVERRIDDEN, /* isLegacy= */ false);
    // Legacy requirement is coming from the label MaxWithBlock function. Still unsatisfied.
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
  }

  @Test
  public void submitRequirementThatOverridesParentSubmitRequirementTakesEffectImmediately()
      throws Exception {
    // Define submit requirement in root project that ignores self approvals from the uploader.
    configSubmitRequirement(
        allProjects,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(
                SubmitRequirementExpression.create("label:Code-Review=MAX,user=non_uploader"))
            .setAllowOverrideInChildProjects(true)
            .build());

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();

    // Apply a self approval from the uploader.
    voteLabel(changeId, "Code-Review", 2);

    ChangeInfo change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(2);
    // Code-Review+2 is ignored since it's a self approval from the uploader
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
    // Legacy requirement is coming from the label MaxWithBlock function. Already satisfied since it
    // doesn't ignore self approvals.
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true);

    // since the change is not submittable we expect the submit action to be not returned
    assertThat(gApi.changes().id(changeId).current().actions()).doesNotContainKey("submit");

    // Override submit requirement in project (allow uploaders to self approve).
    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(
                SubmitRequirementExpression.create("label:Code-Review=MAX"))
            .setAllowOverrideInChildProjects(true)
            .build());

    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    // the self approval from the uploader is no longer ignored, hence the submit requirement is
    // satisfied now
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);

    // since the change is submittable now we expect the submit action to be returned
    Map<String, ActionInfo> actions = gApi.changes().id(changeId).current().actions();
    assertThat(actions).containsKey("submit");
    ActionInfo submitAction = actions.get("submit");
    assertThat(submitAction.enabled).isTrue();
  }

  @Test
  public void
      submitRequirementThatOverridesParentSubmitRequirementTakesEffectImmediately_staleIndex()
          throws Exception {
    // Define submit requirement in root project that ignores self approvals from the uploader.
    configSubmitRequirement(
        allProjects,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(
                SubmitRequirementExpression.create("label:Code-Review=MAX,user=non_uploader"))
            .setAllowOverrideInChildProjects(true)
            .build());

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();

    // Apply a self approval from the uploader.
    voteLabel(changeId, "Code-Review", 2);

    ChangeInfo change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(2);
    // Code-Review+2 is ignored since it's a self approval from the uploader
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);
    // Legacy requirement is coming from the label MaxWithBlock function. Already satisfied since it
    // doesn't ignore self approvals.
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true);

    // since the change is not submittable we expect the submit action to be not returned
    assertThat(gApi.changes().id(changeId).current().actions()).doesNotContainKey("submit");

    // disable change index writes so that the change in the index gets stale when the new submit
    // requirement is added
    try (AutoCloseable ignored = changeIndexOperations.disableWrites()) {
      // Override submit requirement in project (allow uploaders to self approve).
      configSubmitRequirement(
          project,
          SubmitRequirement.builder()
              .setName("Code-Review")
              .setSubmittabilityExpression(
                  SubmitRequirementExpression.create("label:Code-Review=MAX"))
              .setAllowOverrideInChildProjects(true)
              .build());

      change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).hasSize(1);
      // the self approval from the uploader is no longer ignored, hence the submit requirement is
      // satisfied now
      assertSubmitRequirementStatus(
          change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);

      // since the change is submittable now we expect the submit action to be returned
      Map<String, ActionInfo> actions = gApi.changes().id(changeId).current().actions();
      assertThat(actions).containsKey("submit");
      ActionInfo submitAction = actions.get("submit");
      assertThat(submitAction.enabled).isTrue();
    }
  }

  @Test
  public void submitRequirement_partiallyOverriddenSRIsIgnored() throws Exception {
    // Create build-cop-override label
    LabelDefinitionInput input = new LabelDefinitionInput();
    input.function = "NoOp";
    input.values = ImmutableMap.of("+1", "Override", " 0", "No Override");
    gApi.projects().name(project.get()).label("build-cop-override").create(input).get();

    // Allow to vote on the build-cop-override label.
    projectOperations
        .project(project)
        .forUpdate()
        .add(
            TestProjectUpdate.allowLabel("build-cop-override")
                .range(0, 1)
                .ref("refs/*")
                .group(REGISTERED_USERS)
                .build())
        .update();

    // Define submit requirement in root project.
    configSubmitRequirement(
        allProjects,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1"))
            .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
            .setAllowOverrideInChildProjects(true)
            .build());

    // Create Code-Review-Override label
    gApi.projects().name(project.get()).label("Code-Review-Override").create(input).get();

    // Allow to vote on the Code-Review-Override label.
    projectOperations
        .project(project)
        .forUpdate()
        .add(
            TestProjectUpdate.allowLabel("Code-Review-Override")
                .range(0, 1)
                .ref("refs/*")
                .group(REGISTERED_USERS)
                .build())
        .update();

    // Override submit requirement in project (requires Code-Review-Override+1 as override instead
    // of build-cop-override+1), but do not set all required properties (submittability expression
    // is missing). We update the project.config file directly in the remote repository, since
    // trying to push such a submit requirement would be rejected by the commit validation.
    projectOperations
        .project(project)
        .forInvalidation()
        .addProjectConfigUpdater(
            config ->
                config.setString(
                    ProjectConfig.SUBMIT_REQUIREMENT,
                    "Code-Review",
                    ProjectConfig.KEY_SR_OVERRIDE_EXPRESSION,
                    "label:Code-Review-Override=+1"))
        .invalidate();

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();
    ChangeInfo change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

    voteLabel(changeId, "Code-Review-Override", 1);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    // The override expression in the project is satisfied, but it's ignored since the SR is
    // incomplete.
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

    voteLabel(changeId, "build-cop-override", 1);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(2);
    // The submit requirement is overridden now (the override expression in the child project is
    // ignored)
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.OVERRIDDEN, /* isLegacy= */ false);
    // Legacy requirement is coming from the label MaxWithBlock function. Still unsatisfied.
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
  }

  @Test
  public void submitRequirement_storedForClosedChanges() throws Exception {
    for (SubmitType submitType : SubmitType.values()) {
      Project.NameKey project = createProjectForPush(submitType);
      TestRepository<InMemoryRepository> repo = cloneProject(project);
      configSubmitRequirement(
          project,
          SubmitRequirement.builder()
              .setName("Code-Review")
              .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
              .setAllowOverrideInChildProjects(false)
              .build());

      PushOneCommit.Result r =
          createChange(repo, "master", "Add a file", "foo", "content", "topic");
      String changeId = r.getChangeId();

      voteLabel(changeId, "Code-Review", 2);

      ChangeInfo change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).hasSize(1);
      assertSubmitRequirementStatus(
          change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);

      RevisionApi revision = gApi.changes().id(r.getChangeId()).current();
      revision.review(ReviewInput.approve());
      revision.submit();

      ChangeNotes notes = notesFactory.create(project, r.getChange().getId());

      SubmitRequirementResult result =
          notes.getSubmitRequirementsResult().stream().collect(MoreCollectors.onlyElement());
      assertSubmitRequirementResult(
          result,
          "Code-Review",
          SubmitRequirementResult.Status.SATISFIED,
          /* submitExpr= */ "label:Code-Review=MAX",
          SubmitRequirementExpressionResult.Status.PASS);

      // Adding comments does not affect the stored SRs.
      addComment(r.getChangeId(), /* file= */ "foo");
      notes = notesFactory.create(project, r.getChange().getId());
      result = notes.getSubmitRequirementsResult().stream().collect(MoreCollectors.onlyElement());
      assertSubmitRequirementResult(
          result,
          "Code-Review",
          SubmitRequirementResult.Status.SATISFIED,
          /* submitExpr= */ "label:Code-Review=MAX",
          SubmitRequirementExpressionResult.Status.PASS);
      assertThat(notes.getHumanComments()).hasSize(1);
    }
  }

  @Test
  public void submitRequirement_storedForAbandonedChanges() throws Exception {
    for (SubmitType submitType : SubmitType.values()) {
      Project.NameKey project = createProjectForPush(submitType);
      TestRepository<InMemoryRepository> repo = cloneProject(project);
      configSubmitRequirement(
          project,
          SubmitRequirement.builder()
              .setName("Code-Review")
              .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
              .setAllowOverrideInChildProjects(false)
              .build());

      PushOneCommit.Result r =
          createChange(repo, "master", "Add a file", "foo", "content", "topic");
      String changeId = r.getChangeId();

      voteLabel(changeId, "Code-Review", 2);
      ChangeInfo change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).hasSize(1);
      assertSubmitRequirementStatus(
          change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);

      gApi.changes().id(r.getChangeId()).abandon();
      ChangeNotes notes = notesFactory.create(project, r.getChange().getId());
      SubmitRequirementResult result =
          notes.getSubmitRequirementsResult().stream().collect(MoreCollectors.onlyElement());
      assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.SATISFIED);
      assertThat(result.submittabilityExpressionResult().get().status())
          .isEqualTo(SubmitRequirementExpressionResult.Status.PASS);
      assertThat(result.submittabilityExpressionResult().get().expression().expressionString())
          .isEqualTo("label:Code-Review=MAX");
    }
  }

  @Test
  public void submitRequirement_loadedFromTheLatestRevisionNoteForClosedChanges() throws Exception {
    for (SubmitType submitType : SubmitType.values()) {
      Project.NameKey project = createProjectForPush(submitType);
      TestRepository<InMemoryRepository> repo = cloneProject(project);
      configSubmitRequirement(
          project,
          SubmitRequirement.builder()
              .setName("Code-Review")
              .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
              .setAllowOverrideInChildProjects(false)
              .build());

      PushOneCommit.Result r =
          createChange(repo, "master", "Add a file", "foo", "content", "topic");
      String changeId = r.getChangeId();

      // Abandon change. Submit requirements get stored in the revision note of patch-set 1.
      gApi.changes().id(changeId).abandon();
      ChangeInfo change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).hasSize(1);
      assertSubmitRequirementStatus(
          change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

      // Restore the change.
      gApi.changes().id(changeId).restore();
      change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).hasSize(1);
      assertSubmitRequirementStatus(
          change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

      // Upload a second patch-set, fulfill the CR submit requirement.
      amendChange(changeId, "refs/for/master", user, repo);
      change = gApi.changes().id(changeId).get();
      assertThat(change.revisions).hasSize(2);
      voteLabel(changeId, "Code-Review", 2);
      change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).hasSize(1);
      assertSubmitRequirementStatus(
          change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);

      // Abandon the change.
      gApi.changes().id(changeId).abandon();
      change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).hasSize(1);
      assertSubmitRequirementStatus(
          change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
    }
  }

  @Test
  public void submitRequirement_abandonRestoreUpdateMerge() throws Exception {
    for (SubmitType submitType : SubmitType.values()) {
      Project.NameKey project = createProjectForPush(submitType);
      TestRepository<InMemoryRepository> repo = cloneProject(project);
      configSubmitRequirement(
          project,
          SubmitRequirement.builder()
              .setName("Code-Review")
              .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
              .setAllowOverrideInChildProjects(false)
              .build());

      PushOneCommit.Result r =
          createChange(repo, "master", "Add a file", "foo", "content", "topic");
      String changeId = r.getChangeId();

      // Abandon change. Submit requirements get stored in the revision note of patch-set 1.
      gApi.changes().id(changeId).abandon();
      ChangeInfo change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).hasSize(1);
      assertSubmitRequirementStatus(
          change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

      // Restore the change.
      gApi.changes().id(changeId).restore();
      change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).hasSize(1);
      assertSubmitRequirementStatus(
          change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

      // Update the change.
      amendChange(changeId, "refs/for/master", user, repo);
      change = gApi.changes().id(changeId).get();
      assertThat(change.revisions).hasSize(2);
      voteLabel(changeId, "Code-Review", 2);
      change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).hasSize(1);
      assertSubmitRequirementStatus(
          change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);

      // Merge the change.
      gApi.changes().id(changeId).current().submit();
      change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).hasSize(1);
      assertSubmitRequirementStatus(
          change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
    }
  }

  @Test
  public void submitRequirement_returnsEmpty_forAbandonedChangeWithPreviouslyStoredSRs()
      throws Exception {
    for (SubmitType submitType : SubmitType.values()) {
      Project.NameKey project = createProjectForPush(submitType);
      TestRepository<InMemoryRepository> repo = cloneProject(project);
      configSubmitRequirement(
          project,
          SubmitRequirement.builder()
              .setName("Code-Review")
              .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
              .setAllowOverrideInChildProjects(false)
              .build());

      PushOneCommit.Result r =
          createChange(repo, "master", "Add a file", "foo", "content", "topic");
      String changeId = r.getChangeId();

      // Abandon change. Submit requirements get stored in the revision note of patch-set 1.
      gApi.changes().id(changeId).abandon();
      ChangeInfo change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).hasSize(1);
      assertSubmitRequirementStatus(
          change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

      // Clear SRs for the project and update code-review label to be non-blocking.
      clearSubmitRequirements(project);
      LabelType cr =
          TestLabels.codeReview().toBuilder().setFunction(LabelFunction.NO_BLOCK).build();
      try (ProjectConfigUpdate u = updateProject(project)) {
        u.getConfig().upsertLabelType(cr);
        u.save();
      }

      // Restore the change. No SRs apply.
      gApi.changes().id(changeId).restore();
      change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).isEmpty();

      // Abandon the change. Still, no SRs apply.
      gApi.changes().id(changeId).abandon();
      change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).isEmpty();
    }
  }

  @Test
  public void submitRequirement_returnsEmpty_forMergedChangeWithPreviouslyStoredSRs()
      throws Exception {
    for (SubmitType submitType : SubmitType.values()) {
      Project.NameKey project = createProjectForPush(submitType);
      TestRepository<InMemoryRepository> repo = cloneProject(project);
      configSubmitRequirement(
          project,
          SubmitRequirement.builder()
              .setName("Code-Review")
              .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
              .setAllowOverrideInChildProjects(false)
              .build());

      PushOneCommit.Result r =
          createChange(repo, "master", "Add a file", "foo", "content", "topic");
      String changeId = r.getChangeId();

      // Abandon change. Submit requirements get stored in the revision note of patch-set 1.
      gApi.changes().id(changeId).abandon();
      ChangeInfo change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).hasSize(1);
      assertSubmitRequirementStatus(
          change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

      // Clear SRs for the project and update code-review label to be non-blocking.
      clearSubmitRequirements(project);
      LabelType cr =
          TestLabels.codeReview().toBuilder().setFunction(LabelFunction.NO_BLOCK).build();
      try (ProjectConfigUpdate u = updateProject(project)) {
        u.getConfig().upsertLabelType(cr);
        u.save();
      }

      // Restore the change. No SRs apply.
      gApi.changes().id(changeId).restore();
      change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).isEmpty();

      // Merge the change. Still, no SRs apply.
      gApi.changes().id(changeId).current().submit();
      change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).isEmpty();
    }
  }

  @Test
  public void submitRequirement_withMultipleAbandonAndRestore() throws Exception {
    for (SubmitType submitType : SubmitType.values()) {
      Project.NameKey project = createProjectForPush(submitType);
      TestRepository<InMemoryRepository> repo = cloneProject(project);
      configSubmitRequirement(
          project,
          SubmitRequirement.builder()
              .setName("Code-Review")
              .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
              .setAllowOverrideInChildProjects(false)
              .build());

      PushOneCommit.Result r =
          createChange(repo, "master", "Add a file", "foo", "content", "topic");
      String changeId = r.getChangeId();

      // Abandon change. Submit requirements get stored in the revision note of patch-set 1.
      gApi.changes().id(changeId).abandon();
      ChangeInfo change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).hasSize(1);
      assertSubmitRequirementStatus(
          change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

      // Restore the change.
      gApi.changes().id(changeId).restore();
      change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).hasSize(1);
      assertSubmitRequirementStatus(
          change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

      // Abandon the change again.
      gApi.changes().id(changeId).abandon();
      change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).hasSize(1);
      assertSubmitRequirementStatus(
          change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

      // Restore, vote CR=+2, and abandon again. Make sure the requirement is now satisfied.
      gApi.changes().id(changeId).restore();
      voteLabel(changeId, "Code-Review", 2);
      gApi.changes().id(changeId).abandon();
      change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).hasSize(1);
      assertSubmitRequirementStatus(
          change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
    }
  }

  @Test
  public void submitRequirement_retrievedFromNoteDbForAbandonedChanges() throws Exception {
    for (SubmitType submitType : SubmitType.values()) {
      Project.NameKey project = createProjectForPush(submitType);
      TestRepository<InMemoryRepository> repo = cloneProject(project);
      configSubmitRequirement(
          project,
          SubmitRequirement.builder()
              .setName("Code-Review")
              .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
              .setAllowOverrideInChildProjects(false)
              .build());

      PushOneCommit.Result r =
          createChange(repo, "master", "Add a file", "foo", "content", "topic");
      String changeId = r.getChangeId();
      voteLabel(changeId, "Code-Review", 2);
      gApi.changes().id(changeId).abandon();

      // Add another submit requirement. This will not get returned for the abandoned change, since
      // we return the state of the SR results when the change was abandoned.
      configSubmitRequirement(
          project,
          SubmitRequirement.builder()
              .setName("New-Requirement")
              .setSubmittabilityExpression(SubmitRequirementExpression.create("-has:unresolved"))
              .setAllowOverrideInChildProjects(false)
              .build());
      ChangeInfo changeInfo =
          gApi.changes().id(changeId).get(ListChangesOption.SUBMIT_REQUIREMENTS);
      assertThat(changeInfo.submitRequirements).hasSize(1);
      assertSubmitRequirementStatus(
          changeInfo.submitRequirements,
          "Code-Review",
          Status.SATISFIED,
          /* isLegacy= */ false,
          /* submittabilityCondition= */ "label:Code-Review=MAX");

      // Restore the change, the new requirement will show up
      gApi.changes().id(changeId).restore();
      changeInfo = gApi.changes().id(changeId).get(ListChangesOption.SUBMIT_REQUIREMENTS);
      assertThat(changeInfo.submitRequirements).hasSize(2);
      assertSubmitRequirementStatus(
          changeInfo.submitRequirements,
          "Code-Review",
          Status.SATISFIED,
          /* isLegacy= */ false,
          /* submittabilityCondition= */ "label:Code-Review=MAX");
      assertSubmitRequirementStatus(
          changeInfo.submitRequirements,
          "New-Requirement",
          Status.SATISFIED,
          /* isLegacy= */ false,
          /* submittabilityCondition= */ "-has:unresolved");

      // Abandon again, make sure the new requirement was persisted
      gApi.changes().id(changeId).abandon();
      changeInfo = gApi.changes().id(changeId).get(ListChangesOption.SUBMIT_REQUIREMENTS);
      assertThat(changeInfo.submitRequirements).hasSize(2);
      assertSubmitRequirementStatus(
          changeInfo.submitRequirements,
          "Code-Review",
          Status.SATISFIED,
          /* isLegacy= */ false,
          /* submittabilityCondition= */ "label:Code-Review=MAX");
      assertSubmitRequirementStatus(
          changeInfo.submitRequirements,
          "New-Requirement",
          Status.SATISFIED,
          /* isLegacy= */ false,
          /* submittabilityCondition= */ "-has:unresolved");
    }
  }

  @Test
  public void submitRequirement_retrievedFromNoteDbForClosedChanges() throws Exception {
    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
            .setAllowOverrideInChildProjects(false)
            .build());

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();

    ChangeInfo change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

    voteLabel(changeId, "Code-Review", 2);

    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);

    gApi.changes().id(changeId).current().submit();

    // Add new submit requirement
    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("Verified")
            .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Verified=+1"))
            .setAllowOverrideInChildProjects(false)
            .build());

    // The new "Verified" submit requirement is not returned, since this change is closed
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
  }

  @Test
  public void
      submitRequirements_returnOneEntryForMatchingLegacyAndNonLegacyResultsWithTheSameName_ifLegacySubmitRecordsAreEnabled()
          throws Exception {
    // Configure a legacy submit requirement: label with a max with block function
    configLabel("build-cop-override", LabelFunction.MAX_WITH_BLOCK);
    projectOperations
        .project(project)
        .forUpdate()
        .add(
            allowLabel("build-cop-override")
                .ref("refs/heads/master")
                .group(REGISTERED_USERS)
                .range(-1, 1))
        .update();

    // Configure a submit requirement with the same name.
    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("build-cop-override")
            .setSubmittabilityExpression(
                SubmitRequirementExpression.create(
                    "label:build-cop-override=MAX -label:build-cop-override=MIN"))
            .setAllowOverrideInChildProjects(false)
            .build());

    // Create a change. Vote to fulfill all requirements.
    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();
    voteLabel(changeId, "build-cop-override", 1);
    voteLabel(changeId, "Code-Review", 2);

    // Project has two legacy requirements: Code-Review and bco, and a non-legacy requirement: bco.
    // Only non-legacy bco is returned.
    ChangeInfo change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(2);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true);
    assertSubmitRequirementStatus(
        change.submitRequirements,
        "build-cop-override",
        Status.SATISFIED,
        /* isLegacy= */ false,
        /* submittabilityCondition= */ "label:build-cop-override=MAX -label:build-cop-override=MIN");
    assertThat(change.submittable).isTrue();

    // Merge the change. Submit requirements are still the same.
    gApi.changes().id(changeId).current().submit();
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(2);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true);
    assertSubmitRequirementStatus(
        change.submitRequirements,
        "build-cop-override",
        Status.SATISFIED,
        /* isLegacy= */ false,
        /* submittabilityCondition= */ "label:build-cop-override=MAX -label:build-cop-override=MIN");
  }

  @Test
  public void
      submitRequirements_returnTwoEntriesForMismatchingLegacyAndNonLegacyResultsWithTheSameName_ifLegacySubmitRecordsAreEnabled()
          throws Exception {
    // Configure a legacy submit requirement: label with a max with block function
    configLabel("build-cop-override", LabelFunction.MAX_WITH_BLOCK);
    projectOperations
        .project(project)
        .forUpdate()
        .add(
            allowLabel("build-cop-override")
                .ref("refs/heads/master")
                .group(REGISTERED_USERS)
                .range(-1, 1))
        .update();

    // Configure a submit requirement with the same name.
    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("build-cop-override")
            .setSubmittabilityExpression(
                SubmitRequirementExpression.create("label:build-cop-override=MIN"))
            .setAllowOverrideInChildProjects(false)
            .build());

    // Create a change
    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();
    voteLabel(changeId, "build-cop-override", 1);
    voteLabel(changeId, "Code-Review", 2);

    // Project has two legacy requirements: Code-Review and bco, and a non-legacy requirement: bco.
    // Two instances of bco will be returned since their status is not matching.
    ChangeInfo change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(3);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true);
    assertSubmitRequirementStatus(
        change.submitRequirements,
        "build-cop-override",
        Status.SATISFIED,
        /* isLegacy= */ true,
        // MAX_WITH_BLOCK function was translated to a submittability expression.
        /* submittabilityCondition= */ "label:build-cop-override=MAX -label:build-cop-override=MIN");
    assertSubmitRequirementStatus(
        change.submitRequirements,
        "build-cop-override",
        Status.UNSATISFIED,
        /* isLegacy= */ false,
        /* submittabilityCondition= */ "label:build-cop-override=MIN");
    assertThat(change.submittable).isFalse();
  }

  @Test
  public void submitRequirements_skippedIfLegacySRIsBasedOnOptionalLabel() throws Exception {
    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();
    SubmitRule r1 =
        createSubmitRule("r1", SubmitRecord.Status.OK, "CR", SubmitRecord.Label.Status.MAY);
    try (Registration registration = extensionRegistry.newRegistration().add(r1)) {
      ChangeInfo change = gApi.changes().id(changeId).get();
      Collection<SubmitRequirementResultInfo> submitRequirements = change.submitRequirements;
      assertThat(submitRequirements).hasSize(1);
      assertSubmitRequirementStatus(
          submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
    }
  }

  @Test
  public void submitRequirement_notSkippedIfLegacySRIsBasedOnNonOptionalLabel() throws Exception {
    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();
    SubmitRule r1 =
        createSubmitRule("r1", SubmitRecord.Status.OK, "CR", SubmitRecord.Label.Status.OK);
    try (Registration registration = extensionRegistry.newRegistration().add(r1)) {
      ChangeInfo change = gApi.changes().id(changeId).get();
      Collection<SubmitRequirementResultInfo> submitRequirements = change.submitRequirements;
      assertThat(submitRequirements).hasSize(2);
      assertSubmitRequirementStatus(
          submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
      assertSubmitRequirementStatus(
          submitRequirements, "CR", Status.SATISFIED, /* isLegacy= */ true);
    }
  }

  @Test
  public void submitRequirements_returnForLegacySubmitRecords_ifEnabled() throws Exception {
    configLabel("build-cop-override", LabelFunction.MAX_WITH_BLOCK);
    projectOperations
        .project(project)
        .forUpdate()
        .add(
            allowLabel("build-cop-override")
                .ref("refs/heads/master")
                .group(REGISTERED_USERS)
                .range(-1, 1))
        .update();

    // 1. Project has two legacy requirements: Code-Review and bco. Both unsatisfied.
    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();
    ChangeInfo change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(2);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
    assertSubmitRequirementStatus(
        change.submitRequirements, "build-cop-override", Status.UNSATISFIED, /* isLegacy= */ true);

    // 2. Vote +1 on bco. bco becomes satisfied
    voteLabel(changeId, "build-cop-override", 1);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(2);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
    assertSubmitRequirementStatus(
        change.submitRequirements, "build-cop-override", Status.SATISFIED, /* isLegacy= */ true);

    // 3. Vote +1 on Code-Review. Code-Review becomes satisfied
    voteLabel(changeId, "Code-Review", 2);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(2);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true);
    assertSubmitRequirementStatus(
        change.submitRequirements, "build-cop-override", Status.SATISFIED, /* isLegacy= */ true);

    // 4. Merge the change. Submit requirements status is presented from NoteDb.
    gApi.changes().id(changeId).current().submit();
    change = gApi.changes().id(changeId).get();
    // Legacy submit records are returned as submit requirements.
    assertThat(change.submitRequirements).hasSize(2);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true);
    assertSubmitRequirementStatus(
        change.submitRequirements, "build-cop-override", Status.SATISFIED, /* isLegacy= */ true);
  }

  @Test
  public void submitRequirement_backFilledFromIndexForActiveChanges() throws Exception {
    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
            .setAllowOverrideInChildProjects(false)
            .build());

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();

    voteLabel(changeId, "Code-Review", 2);

    // Query the change. ChangeInfo is back-filled from the change index.
    List<ChangeInfo> changeInfos =
        gApi.changes()
            .query()
            .withQuery("project:{" + project.get() + "} (status:open OR status:closed)")
            .withOptions(ImmutableSet.of(ListChangesOption.SUBMIT_REQUIREMENTS))
            .get();
    assertThat(changeInfos).hasSize(1);
    assertSubmitRequirementStatus(
        changeInfos.get(0).submitRequirements,
        "Code-Review",
        Status.SATISFIED,
        /* isLegacy= */ false);
  }

  @Test
  public void submitRequirement_backFilledFromIndexForClosedChanges() throws Exception {
    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
            .setAllowOverrideInChildProjects(false)
            .build());

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();

    voteLabel(changeId, "Code-Review", 2);
    gApi.changes().id(changeId).current().submit();

    // Query the change. ChangeInfo is back-filled from the change index.
    List<ChangeInfo> changeInfos =
        gApi.changes()
            .query()
            .withQuery("project:{" + project.get() + "} (status:open OR status:closed)")
            .withOptions(ImmutableSet.of(ListChangesOption.SUBMIT_REQUIREMENTS))
            .get();
    assertThat(changeInfos).hasSize(1);
    assertSubmitRequirementStatus(
        changeInfos.get(0).submitRequirements,
        "Code-Review",
        Status.SATISFIED,
        /* isLegacy= */ false);
  }

  @Test
  public void submitRequirement_applicabilityExpressionIsAlwaysHidden() throws Exception {
    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setApplicabilityExpression(SubmitRequirementExpression.of("branch:refs/heads/master"))
            .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
            .setAllowOverrideInChildProjects(false)
            .build());

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();

    voteLabel(changeId, "Code-Review", 2);
    ChangeInfo changeInfo = gApi.changes().id(changeId).get();
    SubmitRequirementResultInfo requirement =
        changeInfo.submitRequirements.stream().collect(MoreCollectors.onlyElement());
    assertSubmitRequirementExpression(
        requirement.applicabilityExpressionResult,
        /* expression= */ null,
        /* passingAtoms= */ null,
        /* failingAtoms= */ null,
        /* status= */ SubmitRequirementExpressionInfo.Status.PASS,
        /* fulfilled= */ true);
    assertThat(requirement.submittabilityExpressionResult).isNotNull();
  }

  @Test
  public void submitRequirement_nonApplicable_submittabilityAndOverrideNotEvaluated()
      throws Exception {
    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setApplicabilityExpression(
                SubmitRequirementExpression.of("branch:refs/heads/non-existent"))
            .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
            .setOverrideExpression(SubmitRequirementExpression.of("project:" + project.get()))
            .setAllowOverrideInChildProjects(false)
            .build());

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();

    voteLabel(changeId, "Code-Review", 2);

    ChangeInfo changeInfo = gApi.changes().id(changeId).get();
    assertSubmitRequirementStatus(
        changeInfo.submitRequirements, "Code-Review", Status.NOT_APPLICABLE, /* isLegacy= */ false);
    SubmitRequirementResultInfo requirement =
        changeInfo.submitRequirements.stream().collect(MoreCollectors.onlyElement());
    assertSubmitRequirementExpression(
        requirement.applicabilityExpressionResult,
        /* expression= */ null,
        /* passingAtoms= */ null,
        /* failingAtoms= */ null,
        /* status= */ SubmitRequirementExpressionInfo.Status.FAIL,
        /* fulfilled= */ false);
    assertThat(requirement.submittabilityExpressionResult.status)
        .isEqualTo(SubmitRequirementExpressionInfo.Status.NOT_EVALUATED);
    assertThat(requirement.submittabilityExpressionResult.expression)
        .isEqualTo(SubmitRequirementExpression.maxCodeReview().expressionString());
    assertThat(requirement.overrideExpressionResult.status)
        .isEqualTo(SubmitRequirementExpressionInfo.Status.NOT_EVALUATED);
    assertThat(requirement.overrideExpressionResult.expression)
        .isEqualTo("project:" + project.get());
  }

  @Test
  public void submitRequirement_emptyApplicable_submittabilityAndOverrideEvaluated()
      throws Exception {
    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setApplicabilityExpression(Optional.empty())
            .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
            .setOverrideExpression(SubmitRequirementExpression.of("project:non-existent"))
            .setAllowOverrideInChildProjects(false)
            .build());

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();

    voteLabel(changeId, "Code-Review", 2);

    ChangeInfo changeInfo = gApi.changes().id(changeId).get();
    assertSubmitRequirementStatus(
        changeInfo.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
    SubmitRequirementResultInfo requirement =
        changeInfo.submitRequirements.stream().collect(MoreCollectors.onlyElement());
    assertThat(requirement.applicabilityExpressionResult).isNull();
    assertSubmitRequirementExpression(
        requirement.submittabilityExpressionResult,
        /* expression= */ SubmitRequirementExpression.maxCodeReview().expressionString(),
        /* passingAtoms= */ ImmutableList.of(
            SubmitRequirementExpression.maxCodeReview().expressionString()),
        /* failingAtoms= */ ImmutableList.of(),
        /* status= */ SubmitRequirementExpressionInfo.Status.PASS,
        /* fulfilled= */ true);
    assertSubmitRequirementExpression(
        requirement.overrideExpressionResult,
        /* expression= */ "project:non-existent",
        /* passingAtoms= */ ImmutableList.of(),
        /* failingAtoms= */ ImmutableList.of("project:non-existent"),
        /* status= */ SubmitRequirementExpressionInfo.Status.FAIL,
        /* fulfilled= */ false);
  }

  @Test
  public void submitRequirement_overriden_submittabilityEvaluated() throws Exception {
    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setApplicabilityExpression(Optional.empty())
            .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
            .setOverrideExpression(SubmitRequirementExpression.of("project:" + project.get()))
            .setAllowOverrideInChildProjects(false)
            .build());

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();

    voteLabel(changeId, "Code-Review", 1);

    ChangeInfo changeInfo = gApi.changes().id(changeId).get();
    assertSubmitRequirementStatus(
        changeInfo.submitRequirements, "Code-Review", Status.OVERRIDDEN, /* isLegacy= */ false);
    SubmitRequirementResultInfo requirement =
        changeInfo.submitRequirements.stream()
            .filter(sr -> !sr.isLegacy)
            .collect(MoreCollectors.onlyElement());
    assertThat(requirement.applicabilityExpressionResult).isNull();
    assertSubmitRequirementExpression(
        requirement.submittabilityExpressionResult,
        /* expression= */ SubmitRequirementExpression.maxCodeReview().expressionString(),
        /* passingAtoms= */ ImmutableList.of(),
        /* failingAtoms= */ ImmutableList.of(
            SubmitRequirementExpression.maxCodeReview().expressionString()),
        /* status= */ SubmitRequirementExpressionInfo.Status.FAIL,
        /* fulfilled= */ false);
    assertSubmitRequirementExpression(
        requirement.overrideExpressionResult,
        /* expression= */ "project:" + project.get(),
        /* passingAtoms= */ ImmutableList.of("project:" + project.get()),
        /* failingAtoms= */ ImmutableList.of(),
        /* status= */ SubmitRequirementExpressionInfo.Status.PASS,
        /* fulfilled= */ true);
  }

  @Test
  public void submitRequirements_eliminatesDuplicatesForLegacyNonMatchingSRs() throws Exception {
    // If a custom/prolog submit rule emits the same label name multiple times, we merge these into
    // a single submit requirement result: in this test, we have two different submit rules that
    // return the same label name, one as "OK" and the other as "NEED". The submit requirements
    // API favours the blocking entry and returns one SR result with status=UNSATISFIED.
    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();
    SubmitRule r1 =
        createSubmitRule("r1", SubmitRecord.Status.OK, "CR", SubmitRecord.Label.Status.OK);
    SubmitRule r2 =
        createSubmitRule("r2", SubmitRecord.Status.NOT_READY, "CR", SubmitRecord.Label.Status.NEED);
    try (Registration registration = extensionRegistry.newRegistration().add(r1).add(r2)) {
      ChangeInfo change = gApi.changes().id(changeId).get();
      Collection<SubmitRequirementResultInfo> submitRequirements = change.submitRequirements;
      assertThat(submitRequirements).hasSize(2);
      assertSubmitRequirementStatus(
          submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
      assertSubmitRequirementStatus(
          submitRequirements, "CR", Status.UNSATISFIED, /* isLegacy= */ true);
    }
  }

  @Test
  public void submitRequirements_eliminatesDuplicatesForLegacyMatchingSRs() throws Exception {
    // If a custom/prolog submit rule emits the same label name multiple times, we merge these into
    // a single submit requirement result: in this test, we have two different submit rules that
    // return the same label name, but both are fulfilled (i.e. they both allow submission). The
    // submit requirements API returns one SR result with status=SATISFIED.
    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();
    SubmitRule r1 =
        createSubmitRule("r1", SubmitRecord.Status.OK, "CR", SubmitRecord.Label.Status.OK);
    SubmitRule r2 =
        createSubmitRule("r2", SubmitRecord.Status.OK, "CR", SubmitRecord.Label.Status.MAY);
    try (Registration registration = extensionRegistry.newRegistration().add(r1).add(r2)) {
      ChangeInfo change = gApi.changes().id(changeId).get();
      Collection<SubmitRequirementResultInfo> submitRequirements = change.submitRequirements;
      assertThat(submitRequirements).hasSize(2);
      assertSubmitRequirementStatus(
          submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
      assertSubmitRequirementStatus(
          submitRequirements, "CR", Status.SATISFIED, /* isLegacy= */ true);
    }
  }

  @Test
  public void submitRequirements_eliminatesMultipleDuplicatesForLegacyMatchingSRs()
      throws Exception {
    // If a custom/prolog submit rule emits the same label name multiple times, we merge these into
    // a single submit requirement result: in this test, we have five different submit rules that
    // return the same label name, all with an "OK" status. The submit requirements API returns
    // a single SR result with status=SATISFIED.
    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();
    try (Registration registration = extensionRegistry.newRegistration()) {
      IntStream.range(0, 5)
          .forEach(
              i ->
                  registration.add(
                      createSubmitRule(
                          "r" + i, SubmitRecord.Status.OK, "CR", SubmitRecord.Label.Status.OK)));
      ChangeInfo change = gApi.changes().id(changeId).get();
      Collection<SubmitRequirementResultInfo> submitRequirements = change.submitRequirements;
      assertThat(submitRequirements).hasSize(2);
      assertSubmitRequirementStatus(
          submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
      assertSubmitRequirementStatus(
          submitRequirements, "CR", Status.SATISFIED, /* isLegacy= */ true);
    }
  }

  @Test
  public void submitRequirement_duplicateSubmitRequirement_sameCase() throws Exception {
    // Define 2 submit requirements with exact same name but different submittability expression.
    try (TestRepository<Repository> repo =
        new TestRepository<>(repoManager.openRepository(project))) {
      Ref ref = repo.getRepository().exactRef(RefNames.REFS_CONFIG);
      RevCommit head = repo.getRevWalk().parseCommit(ref.getObjectId());
      RevObject blob = repo.get(head.getTree(), ProjectConfig.PROJECT_CONFIG);
      byte[] data = repo.getRepository().open(blob).getCachedBytes(Integer.MAX_VALUE);
      String projectConfig = RawParseUtils.decode(data);

      repo.update(
          RefNames.REFS_CONFIG,
          repo.commit()
              .parent(head)
              .message("Set project config")
              .add(
                  ProjectConfig.PROJECT_CONFIG,
                  projectConfig
                      // JGit parses this as a list value:
                      // submit-requirement.Code-Review.submittableIf =
                      //     [label:Code-Review=+2, label:Code-Review=+1]
                      // if getString is used to read submittableIf JGit returns the last value
                      // (label:Code-Review=+1)
                      + "[submit-requirement \"Code-Review\"]\n"
                      + "    submittableIf = label:Code-Review=+2\n"
                      + "[submit-requirement \"Code-Review\"]\n"
                      + "    submittableIf = label:Code-Review=+1\n"));
    }
    projectCache.evict(project);

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();
    ChangeInfo change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

    voteLabel(changeId, "Code-Review", 1);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(2);
    // The submit requirement is fulfilled now, since label:Code-Review=+1 applies as submittability
    // expression (see comment above)
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
    // Legacy requirement is coming from the label MaxWithBlock function. Still unsatisfied.
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
  }

  @Test
  public void submitRequirement_duplicateSubmitRequirement_differentCase() throws Exception {
    // Define 2 submit requirements with same name but different case and different submittability
    // expression.
    try (TestRepository<Repository> repo =
        new TestRepository<>(repoManager.openRepository(project))) {
      Ref ref = repo.getRepository().exactRef(RefNames.REFS_CONFIG);
      RevCommit head = repo.getRevWalk().parseCommit(ref.getObjectId());
      RevObject blob = repo.get(head.getTree(), ProjectConfig.PROJECT_CONFIG);
      byte[] data = repo.getRepository().open(blob).getCachedBytes(Integer.MAX_VALUE);
      String projectConfig = RawParseUtils.decode(data);

      repo.update(
          RefNames.REFS_CONFIG,
          repo.commit()
              .parent(head)
              .message("Set project config")
              .add(
                  ProjectConfig.PROJECT_CONFIG,
                  projectConfig
                      // ProjectConfig processes the submit requirements in the order in which they
                      // appear (1. Code-Review, 2. code-review) and ignores any further submit
                      // requirement if its name case-insensitively matches the name of a submit
                      // requirement that has already been seen. This means the Code-Review submit
                      // requirement applies and the code-review submit requirement is ignored.
                      + "[submit-requirement \"Code-Review\"]\n"
                      + "    submittableIf = label:Code-Review=+2\n"
                      + "[submit-requirement \"code-review\"]\n"
                      + "    submittableIf = label:Code-Review=+1\n"));
    }
    projectCache.evict(project);

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();
    ChangeInfo change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

    voteLabel(changeId, "Code-Review", 1);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    // Still not satisfied since the Code-Review submit requirement with label:Code-Review=+2 as
    // submittability expression applies (see comment above).
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

    voteLabel(changeId, "Code-Review", 2);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    // The submit requirement is fulfilled now, since label:Code-Review=+2 applies as submittability
    // expression (see comment above)
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
  }

  @Test
  public void submitRequirement_overrideInheritedSRWithDifferentNameCasing() throws Exception {
    // Define submit requirement in root project and allow override.
    configSubmitRequirement(
        allProjects,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
            .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
            .setAllowOverrideInChildProjects(true)
            .build());

    // Define a submit requirement with the same name in the child project that differs by case and
    // has a different submittability expression (requires Code-Review=+1 instead of +2).
    // This overrides the inherited submit requirement with the same name, although the case is
    // different.
    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("code-review")
            .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1"))
            .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
            .setAllowOverrideInChildProjects(false)
            .build());

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();
    ChangeInfo change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "code-review", Status.UNSATISFIED, /* isLegacy= */ false);

    voteLabel(changeId, "Code-Review", 1);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(2);
    // +1 was enough to fulfill the requirement since the override applies
    assertSubmitRequirementStatus(
        change.submitRequirements, "code-review", Status.SATISFIED, /* isLegacy= */ false);
    // Legacy requirement is coming from the label MaxWithBlock function. Still unsatisfied.
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
  }

  @Test
  public void submitRequirement_cannotOverrideNonOverridableInheritedSRWithDifferentNameCasing()
      throws Exception {
    // Define submit requirement in root project and disallow override.
    configSubmitRequirement(
        allProjects,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
            .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
            .setAllowOverrideInChildProjects(false)
            .build());

    // Define a submit requirement with the same name in the child project that differs by case and
    // has a different submittability expression (requires Code-Review=+1 instead of +2).
    // This is ignored since the inherited submit requirement with the same name (different case)
    // disallows overriding.
    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("code-review")
            .setSubmittabilityExpression(SubmitRequirementExpression.create("label:Code-Review=+1"))
            .setOverrideExpression(SubmitRequirementExpression.of("label:build-cop-override=+1"))
            .setAllowOverrideInChildProjects(false)
            .build());

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();
    ChangeInfo change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

    voteLabel(changeId, "Code-Review", 1);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    // Still not satisfied since the override is ignored.
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

    voteLabel(changeId, "Code-Review", 2);
    change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
  }

  @Test
  public void globalSubmitRequirement_storedForClosedChanges() throws Exception {
    SubmitRequirement globalSubmitRequirement =
        SubmitRequirement.builder()
            .setName("global-submit-requirement")
            .setSubmittabilityExpression(SubmitRequirementExpression.create("topic:test"))
            .setAllowOverrideInChildProjects(false)
            .build();
    try (Registration registration =
        extensionRegistry.newRegistration().add(globalSubmitRequirement)) {
      PushOneCommit.Result r = createChange();
      String changeId = r.getChangeId();

      ChangeInfo change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).hasSize(2);
      assertSubmitRequirementStatus(
          change.submitRequirements,
          "global-submit-requirement",
          Status.UNSATISFIED,
          /* isLegacy= */ false);
      assertSubmitRequirementStatus(
          change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);

      voteLabel(changeId, "Code-Review", 2);
      gApi.changes().id(changeId).topic("test");

      change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).hasSize(2);
      assertSubmitRequirementStatus(
          change.submitRequirements,
          "global-submit-requirement",
          Status.SATISFIED,
          /* isLegacy= */ false);
      assertSubmitRequirementStatus(
          change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true);

      gApi.changes().id(changeId).current().submit();

      ChangeNotes notes = notesFactory.create(project, r.getChange().getId());
      SubmitRequirementResult result =
          notes.getSubmitRequirementsResult().stream().collect(MoreCollectors.onlyElement());
      assertThat(result.status()).isEqualTo(SubmitRequirementResult.Status.SATISFIED);
      assertThat(result.submittabilityExpressionResult().get().status())
          .isEqualTo(SubmitRequirementExpressionResult.Status.PASS);
      assertThat(result.submittabilityExpressionResult().get().expression().expressionString())
          .isEqualTo("topic:test");
    }
  }

  @Test
  public void projectSubmitRequirementDuplicatesGlobal_overrideNotAllowed_globalEvaluated()
      throws Exception {
    SubmitRequirement globalSubmitRequirement =
        SubmitRequirement.builder()
            .setName("CoDe-reView")
            .setSubmittabilityExpression(SubmitRequirementExpression.create("topic:test"))
            .setAllowOverrideInChildProjects(false)
            .build();
    try (Registration registration =
        extensionRegistry.newRegistration().add(globalSubmitRequirement)) {
      configSubmitRequirement(
          project,
          SubmitRequirement.builder()
              .setName("Code-Review")
              .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
              .setAllowOverrideInChildProjects(false)
              .build());
      PushOneCommit.Result r = createChange();
      String changeId = r.getChangeId();

      // Vote does not satisfy submit requirement, because the global definition is evaluated.
      voteLabel(changeId, "CoDe-reView", 2);

      ChangeInfo change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).hasSize(2);
      assertSubmitRequirementStatus(
          change.submitRequirements, "CoDe-reView", Status.UNSATISFIED, /* isLegacy= */ false);
      // In addition, the legacy submit requirement is emitted, since the status mismatch
      assertSubmitRequirementStatus(
          change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true);

      // Setting the topic satisfies the global definition.
      gApi.changes().id(changeId).topic("test");

      change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).hasSize(1);
      assertSubmitRequirementStatus(
          change.submitRequirements, "CoDe-reView", Status.SATISFIED, /* isLegacy= */ false);
    }
  }

  @Test
  public void projectSubmitRequirementDuplicatesGlobal_overrideAllowed_projectRequirementEvaluated()
      throws Exception {
    SubmitRequirement globalSubmitRequirement =
        SubmitRequirement.builder()
            .setName("CoDe-reView")
            .setSubmittabilityExpression(SubmitRequirementExpression.create("topic:test"))
            .setAllowOverrideInChildProjects(true)
            .build();
    try (Registration registration =
        extensionRegistry.newRegistration().add(globalSubmitRequirement)) {
      configSubmitRequirement(
          project,
          SubmitRequirement.builder()
              .setName("Code-Review")
              .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
              .setAllowOverrideInChildProjects(false)
              .build());
      PushOneCommit.Result r = createChange();
      String changeId = r.getChangeId();

      // Setting the topic does not satisfy submit requirement, because the project definition is
      // evaluated.
      gApi.changes().id(changeId).topic("test");

      ChangeInfo change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).hasSize(1);
      // There is no mismatch with legacy submit requirement, so the single result is emitted.
      assertSubmitRequirementStatus(
          change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

      // Voting satisfies the project definition.
      voteLabel(changeId, "Code-Review", 2);
      change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).hasSize(1);
      assertSubmitRequirementStatus(
          change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ false);
    }
  }

  @Test
  public void legacySubmitRequirementDuplicatesGlobal_statusMatches_globalReturned()
      throws Exception {
    // The behaviour does not depend on AllowOverrideInChildProject in global submit requirement.
    testLegacySubmitRequirementDuplicatesGlobalStatusMatches(/*allowOverrideInChildProject=*/ true);
    testLegacySubmitRequirementDuplicatesGlobalStatusMatches(
        /*allowOverrideInChildProject=*/ false);
  }

  private void testLegacySubmitRequirementDuplicatesGlobalStatusMatches(
      boolean allowOverrideInChildProject) throws Exception {
    SubmitRequirement globalSubmitRequirement =
        SubmitRequirement.builder()
            .setName("CoDe-reView")
            .setSubmittabilityExpression(SubmitRequirementExpression.create("topic:test"))
            .setAllowOverrideInChildProjects(allowOverrideInChildProject)
            .build();
    try (Registration registration =
        extensionRegistry.newRegistration().add(globalSubmitRequirement)) {
      PushOneCommit.Result r = createChange();
      String changeId = r.getChangeId();

      // Both are evaluated, but only the global is returned, since both are unsatisfied
      ChangeInfo change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).hasSize(1);
      assertSubmitRequirementStatus(
          change.submitRequirements, "CoDe-reView", Status.UNSATISFIED, /* isLegacy= */ false);

      // Both are evaluated, but only the global is returned, since both are satisfied
      voteLabel(changeId, "Code-Review", 2);
      gApi.changes().id(changeId).topic("test");

      change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).hasSize(1);
      assertSubmitRequirementStatus(
          change.submitRequirements, "CoDe-reView", Status.SATISFIED, /* isLegacy= */ false);
    }
  }

  @Test
  public void legacySubmitRequirementWithIgnoreSelfApproval() throws Exception {
    LabelType verified =
        label(LabelId.VERIFIED, value(1, "Passes"), value(0, "No score"), value(-1, "Failed"));
    verified = verified.toBuilder().setIgnoreSelfApproval(true).build();
    try (ProjectConfigUpdate u = updateProject(project)) {
      u.getConfig().upsertLabelType(verified);
      u.save();
    }
    projectOperations
        .project(project)
        .forUpdate()
        .add(
            allowLabel(verified.getName())
                .ref(RefNames.REFS_HEADS + "*")
                .group(REGISTERED_USERS)
                .range(-1, 1))
        .update();

    // The DefaultSubmitRule emits an "OK" submit record for Verified, while the
    // ignoreSelfApprovalRule emits a "NEED" submit record. The "submit requirements" adapter merges
    // both results and returns the blocking one only.
    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();
    gApi.changes().id(changeId).addReviewer(user.id().toString());

    voteLabel(changeId, verified.getName(), +1);
    ChangeInfo changeInfo = gApi.changes().id(changeId).get();
    Collection<SubmitRequirementResultInfo> submitRequirements = changeInfo.submitRequirements;
    assertSubmitRequirementStatus(
        submitRequirements, "Verified", Status.UNSATISFIED, /* isLegacy= */ true);
  }

  @Test
  public void legacySubmitRequirementDuplicatesGlobal_statusDoesNotMatch_bothRecordsReturned()
      throws Exception {
    // The behaviour does not depend on AllowOverrideInChildProject in global submit requirement.
    testLegacySubmitRequirementDuplicatesGlobalStatusDoesNotMatch(
        /*allowOverrideInChildProject=*/ true);
    testLegacySubmitRequirementDuplicatesGlobalStatusDoesNotMatch(
        /*allowOverrideInChildProject=*/ false);
  }

  private void testLegacySubmitRequirementDuplicatesGlobalStatusDoesNotMatch(
      boolean allowOverrideInChildProject) throws Exception {
    SubmitRequirement globalSubmitRequirement =
        SubmitRequirement.builder()
            .setName("CoDe-reView")
            .setSubmittabilityExpression(SubmitRequirementExpression.create("topic:test"))
            .setAllowOverrideInChildProjects(allowOverrideInChildProject)
            .build();
    try (Registration registration =
        extensionRegistry.newRegistration().add(globalSubmitRequirement)) {
      PushOneCommit.Result r = createChange();
      String changeId = r.getChangeId();

      // Both are evaluated, but only the global is returned, since both are unsatisfied
      ChangeInfo change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).hasSize(1);
      assertSubmitRequirementStatus(
          change.submitRequirements, "CoDe-reView", Status.UNSATISFIED, /* isLegacy= */ false);

      // Both are evaluated and both are returned, since result mismatch
      voteLabel(changeId, "Code-Review", 2);

      change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).hasSize(2);
      assertSubmitRequirementStatus(
          change.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy= */ true);
      assertSubmitRequirementStatus(
          change.submitRequirements, "CoDe-reView", Status.UNSATISFIED, /* isLegacy= */ false);

      gApi.changes().id(changeId).topic("test");
      gApi.changes().id(changeId).reviewer(admin.id().toString()).deleteVote(LabelId.CODE_REVIEW);

      change = gApi.changes().id(changeId).get();
      assertThat(change.submitRequirements).hasSize(2);
      assertThat(change.submitRequirements).hasSize(2);
      assertSubmitRequirementStatus(
          change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ true);
      assertSubmitRequirementStatus(
          change.submitRequirements, "CoDe-reView", Status.SATISFIED, /* isLegacy= */ false);
    }
  }

  @Test
  public void submitRequirements_disallowsTheIsSubmittableOperator() throws Exception {
    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();

    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("Wrong-Req")
            .setSubmittabilityExpression(SubmitRequirementExpression.create("is:submittable"))
            .setAllowOverrideInChildProjects(false)
            .build());

    ChangeInfo change = gApi.changes().id(changeId).get();
    SubmitRequirementResultInfo srResult =
        change.submitRequirements.stream()
            .filter(sr -> sr.name.equals("Wrong-Req"))
            .collect(MoreCollectors.onlyElement());
    assertThat(srResult.status).isEqualTo(Status.ERROR);
    assertThat(srResult.submittabilityExpressionResult.errorMessage)
        .isEqualTo("Operator 'is:submittable' cannot be used in submit requirement expressions.");
  }

  @Test
  public void submitRequirements_forcedByDirectSubmission() throws Exception {
    projectOperations
        .project(project)
        .forUpdate()
        .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/master").group(REGISTERED_USERS))
        .update();

    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("My-Requirement")
            // Submit requirement is always unsatisfied, but we are going to bypass it.
            .setSubmittabilityExpression(SubmitRequirementExpression.create("is:false"))
            .setAllowOverrideInChildProjects(false)
            .build());

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();
    pushFactory.create(admin.newIdent(), testRepo, changeId).to("refs/for/master%submit");

    ChangeInfo change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(2);
    assertSubmitRequirementStatus(
        change.submitRequirements, "My-Requirement", Status.FORCED, /* isLegacy= */ false);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.FORCED, /* isLegacy= */ true);
  }

  @Test
  public void submitRequirement_evaluatedWithInternalUserCredentials() throws Exception {
    GroupInput in = new GroupInput();
    in.name = "invisible-group";
    in.visibleToAll = false;
    in.ownerId = adminGroupUuid().get();
    gApi.groups().create(in);

    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("My-Requirement")
            .setApplicabilityExpression(SubmitRequirementExpression.of("ownerin:invisible-group"))
            .setSubmittabilityExpression(SubmitRequirementExpression.create("is:true"))
            .setAllowOverrideInChildProjects(false)
            .build());

    requestScopeOperations.setApiUser(user.id());
    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();

    ChangeInfo change = gApi.changes().id(changeId).get();
    SubmitRequirementResultInfo srResult =
        change.submitRequirements.stream()
            .filter(sr -> sr.name.equals("My-Requirement"))
            .collect(MoreCollectors.onlyElement());
    assertThat(srResult.status).isEqualTo(Status.NOT_APPLICABLE);
  }

  @Test
  public void submitRequirements_submittedTogetherWithoutLegacySubmitRequirements()
      throws Exception {
    // Add a code review submit requirement and mark the 'Code-Review' label function to be
    // non-blocking.
    configSubmitRequirement(
        allProjects,
        SubmitRequirement.builder()
            .setName("Code-Review")
            .setSubmittabilityExpression(SubmitRequirementExpression.maxCodeReview())
            .setAllowOverrideInChildProjects(true)
            .build());

    LabelType cr = TestLabels.codeReview().toBuilder().setFunction(LabelFunction.NO_BLOCK).build();
    try (ProjectConfigUpdate u = updateProject(project)) {
      u.getConfig().upsertLabelType(cr);
      u.save();
    }

    // Create two changes in a chain.
    createChange();
    PushOneCommit.Result r2 = createChange();

    // Make sure the CR requirement is unsatisfied.
    String changeId = r2.getChangeId();
    ChangeInfo change = gApi.changes().id(changeId).get();
    assertThat(change.submitRequirements).hasSize(1);
    assertSubmitRequirementStatus(
        change.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy= */ false);

    List<ChangeInfo> changeInfos = gApi.changes().id(changeId).submittedTogether();
    assertThat(changeInfos).hasSize(2);
    assertThat(
            changeInfos.stream()
                .map(c -> c.submittable)
                .distinct()
                .collect(MoreCollectors.onlyElement()))
        .isFalse();
  }

  @Test
  public void queryChangesBySubmitRequirementResultUsingTheLabelPredicate() throws Exception {
    // Create a non-blocking label and a submit-requirement that necessitates voting on this label.
    configLabel("LC", LabelFunction.NO_OP);
    projectOperations
        .project(project)
        .forUpdate()
        .add(allowLabel("LC").ref("refs/heads/master").group(REGISTERED_USERS).range(-1, 1))
        .update();
    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("LC")
            .setSubmittabilityExpression(SubmitRequirementExpression.create("label:LC=MAX"))
            .setAllowOverrideInChildProjects(false)
            .build());

    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();

    List<ChangeInfo> changeInfos = gApi.changes().query("label:LC=NEED").get();
    assertThat(changeInfos).hasSize(1);
    assertThat(changeInfos.get(0).changeId).isEqualTo(changeId);
    assertThat(gApi.changes().query("label:LC=OK").get()).isEmpty();
    // case does not matter
    changeInfos = gApi.changes().query("label:lc=NEED").get();
    assertThat(changeInfos).hasSize(1);
    assertThat(changeInfos.get(0).changeId).isEqualTo(changeId);

    voteLabel(r.getChangeId(), "LC", +1);
    changeInfos = gApi.changes().query("label:LC=OK").get();
    assertThat(changeInfos.get(0).changeId).isEqualTo(changeId);
    assertThat(gApi.changes().query("label:LC=NEED").get()).isEmpty();
  }

  @Test
  public void queryingChangesWithSubmitRequirementOptionDoesNotTouchDatabase() throws Exception {
    configSubmitRequirement(
        project,
        SubmitRequirement.builder()
            .setName("Code-Review")
            // Always not submittable
            .setSubmittabilityExpression(SubmitRequirementExpression.create("is:false"))
            .setAllowOverrideInChildProjects(false)
            .build());

    requestScopeOperations.setApiUser(admin.id());
    PushOneCommit.Result r1 = createChange();
    gApi.changes()
        .id(r1.getChangeId())
        .revision(r1.getCommit().name())
        .review(ReviewInput.approve());

    ChangeInfo changeInfo = gApi.changes().id(r1.getChangeId()).get();
    assertThat(changeInfo.submitRequirements).hasSize(2);
    assertSubmitRequirementStatus(
        changeInfo.submitRequirements, "Code-Review", Status.UNSATISFIED, /* isLegacy = */ false);
    assertSubmitRequirementStatus(
        changeInfo.submitRequirements, "Code-Review", Status.SATISFIED, /* isLegacy = */ true);

    requestScopeOperations.setApiUser(user.id());
    try (AutoCloseable ignored = disableNoteDb()) {
      List<ChangeInfo> changeInfos =
          gApi.changes()
              .query()
              .withQuery("project:{" + project.get() + "} (status:open OR status:closed)")
              .withOptions(
                  new ImmutableSet.Builder<ListChangesOption>()
                      .addAll(IndexPreloadingUtil.DASHBOARD_OPTIONS)
                      .add(ListChangesOption.SUBMIT_REQUIREMENTS)
                      .build())
              .get();
      assertThat(changeInfos).hasSize(1);
      assertSubmitRequirementStatus(
          changeInfos.get(0).submitRequirements,
          "Code-Review",
          Status.UNSATISFIED,
          /* isLegacy = */ false);
      assertSubmitRequirementStatus(
          changeInfos.get(0).submitRequirements,
          "Code-Review",
          Status.SATISFIED,
          /* isLegacy = */ true);
    }
  }

  private void voteLabel(String changeId, String labelName, int score) throws RestApiException {
    gApi.changes().id(changeId).current().review(new ReviewInput().label(labelName, score));
  }

  private void assertSubmitRequirementResult(
      SubmitRequirementResult result,
      String srName,
      SubmitRequirementResult.Status status,
      String submitExpr,
      SubmitRequirementExpressionResult.Status submitStatus) {
    assertThat(result.submitRequirement().name()).isEqualTo(srName);
    assertThat(result.status()).isEqualTo(status);
    assertThat(result.submittabilityExpressionResult().get().expression().expressionString())
        .isEqualTo(submitExpr);
    assertThat(result.submittabilityExpressionResult().get().status()).isEqualTo(submitStatus);
  }

  private void assertSubmitRequirementStatus(
      Collection<SubmitRequirementResultInfo> results,
      String requirementName,
      SubmitRequirementResultInfo.Status status,
      boolean isLegacy,
      String submittabilityCondition) {
    for (SubmitRequirementResultInfo result : results) {
      if (result.name.equals(requirementName)
          && result.status == status
          && result.isLegacy == isLegacy
          && result.submittabilityExpressionResult.expression.equals(submittabilityCondition)) {
        return;
      }
    }
    throw new AssertionError(
        String.format(
            "Could not find submit requirement %s with status %s (results = %s)",
            requirementName,
            status,
            results.stream()
                .map(r -> String.format("%s=%s", r.name, r.status))
                .collect(toImmutableList())));
  }

  private void assertSubmitRequirementStatus(
      Collection<SubmitRequirementResultInfo> results,
      String requirementName,
      SubmitRequirementResultInfo.Status status,
      boolean isLegacy) {
    for (SubmitRequirementResultInfo result : results) {
      if (result.name.equals(requirementName)
          && result.status == status
          && result.isLegacy == isLegacy) {
        return;
      }
    }
    throw new AssertionError(
        String.format(
            "Could not find submit requirement %s with status %s (results = %s)",
            requirementName,
            status,
            results.stream()
                .map(r -> String.format("%s=%s, legacy=%s", r.name, r.status, r.isLegacy))
                .collect(toImmutableList())));
  }

  private void assertSubmitRequirementExpression(
      SubmitRequirementExpressionInfo result,
      @Nullable String expression,
      @Nullable List<String> passingAtoms,
      @Nullable List<String> failingAtoms,
      SubmitRequirementExpressionInfo.Status status,
      boolean fulfilled) {
    assertThat(result.expression).isEqualTo(expression);
    if (passingAtoms == null) {
      assertThat(result.passingAtoms).isNull();
    } else {
      assertThat(result.passingAtoms).containsExactlyElementsIn(passingAtoms);
    }
    if (failingAtoms == null) {
      assertThat(result.failingAtoms).isNull();
    } else {
      assertThat(result.failingAtoms).containsExactlyElementsIn(failingAtoms);
    }
    assertThat(result.status).isEqualTo(status);
    assertThat(result.fulfilled).isEqualTo(fulfilled);
  }

  private Project.NameKey createProjectForPush(SubmitType submitType) throws Exception {
    Project.NameKey project = projectOperations.newProject().submitType(submitType).create();
    projectOperations
        .project(project)
        .forUpdate()
        .add(allow(Permission.PUSH).ref("refs/heads/*").group(adminGroupUuid()))
        .add(allow(Permission.SUBMIT).ref("refs/for/refs/heads/*").group(adminGroupUuid()))
        .update();
    return project;
  }

  private static SubmitRule createSubmitRule(
      String ruleName,
      SubmitRecord.Status srStatus,
      String labelName,
      SubmitRecord.Label.Status labelStatus) {
    return changeData -> {
      SubmitRecord r = new SubmitRecord();
      r.ruleName = ruleName;
      r.status = srStatus;
      SubmitRecord.Label label = new SubmitRecord.Label();
      label.label = labelName;
      label.status = labelStatus;
      r.labels = Arrays.asList(label);
      return Optional.of(r);
    };
  }

  /** Returns a hard-coded submit record containing all fields. */
  private static class TestSubmitRule implements SubmitRule {
    @Override
    public Optional<SubmitRecord> evaluate(ChangeData changeData) {
      SubmitRecord record = new SubmitRecord();
      record.ruleName = "testSubmitRule";
      record.status = SubmitRecord.Status.OK;
      SubmitRecord.Label label = new SubmitRecord.Label();
      label.label = "label";
      label.status = SubmitRecord.Label.Status.OK;
      record.labels = Arrays.asList(label);
      record.requirements =
          Arrays.asList(
              LegacySubmitRequirement.builder()
                  .setType("type")
                  .setFallbackText("fallback text")
                  .build());
      return Optional.of(record);
    }
  }

  private static SubmitRequirementInput createSubmitRequirementInput(
      String name, String submittabilityExpression) {
    SubmitRequirementInput input = new SubmitRequirementInput();
    input.name = name;
    input.submittabilityExpression = submittabilityExpression;
    return input;
  }

  private static SubmitRequirementInput createSubmitRequirementInput(
      String name, String applicableIf, String submittableIf, String overrideIf) {
    SubmitRequirementInput input = new SubmitRequirementInput();
    input.name = name;
    input.applicabilityExpression = applicableIf;
    input.submittabilityExpression = submittableIf;
    input.overrideExpression = overrideIf;
    return input;
  }

  private void addComment(String changeId, String file) throws Exception {
    ReviewInput in = new ReviewInput();
    CommentInput ci = new CommentInput();
    ci.path = file;
    ci.message = "message";
    ci.line = 1;
    in.comments = ImmutableMap.of("foo", ImmutableList.of(ci));
    gApi.changes().id(changeId).current().review(in);
  }

  private void fetchRefsMetaConfig() throws Exception {
    git().fetch().setRefSpecs(new RefSpec("refs/meta/config:refs/meta/config")).call();
    testRepo.reset(RefNames.REFS_CONFIG);
  }

  private PushOneCommit.Result createConfigChangeWithSubmitRequirement(
      String srName, String submitExpression) throws Exception {
    Config cfg = projectOperations.project(project).getConfig();
    cfg.setString(
        ProjectConfig.SUBMIT_REQUIREMENT,
        srName,
        ProjectConfig.KEY_SR_SUBMITTABILITY_EXPRESSION,
        submitExpression);
    return createConfigChange(cfg);
  }

  private PushOneCommit.Result createConfigChange(Config cfg) throws Exception {
    PushOneCommit.Result r =
        pushFactory
            .create(
                admin.newIdent(), testRepo, "Update project config", "project.config", cfg.toText())
            .to("refs/for/refs/meta/config");
    r.assertOkStatus();
    return r;
  }
}
