// Copyright (C) 2020 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.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_COMMIT;
import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
import static com.google.gerrit.git.ObjectIds.abbreviateName;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static java.nio.charset.StandardCharsets.UTF_8;

import com.google.common.collect.Iterables;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.ExtensionRegistry;
import com.google.gerrit.acceptance.PushOneCommit;
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.entities.BranchNameKey;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.extensions.api.accounts.AccountInput;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.extensions.common.MergeInput;
import com.google.gerrit.extensions.common.MergePatchSetInput;
import com.google.gerrit.extensions.events.WorkInProgressStateChangedListener;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.inject.Inject;
import java.io.ByteArrayOutputStream;
import java.util.List;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Before;
import org.junit.Test;

public class CreateMergePatchSetIT extends AbstractDaemonTest {

  @Inject private ProjectOperations projectOperations;
  @Inject private RequestScopeOperations requestScopeOperations;
  @Inject private ExtensionRegistry extensionRegistry;

  @Before
  public void setUp() {
    projectOperations
        .project(project)
        .forUpdate()
        .add(allow(Permission.FORGE_AUTHOR).ref("refs/*").group(REGISTERED_USERS))
        .update();
  }

  @Test
  public void createMergePatchSet() throws Exception {
    RevCommit initialHead = projectOperations.project(project).getHead("master");
    createBranch(BranchNameKey.create(project, "dev"));

    // create a change for master
    String changeId = createChange().getChangeId();

    testRepo.reset(initialHead);
    PushOneCommit.Result currentMaster = pushTo("refs/heads/master");
    currentMaster.assertOkStatus();
    String parent = currentMaster.getCommit().getName();

    // push a commit into dev branch
    testRepo.reset(initialHead);
    PushOneCommit.Result changeA =
        pushFactory
            .create(user.newIdent(), testRepo, "change A", "A.txt", "A content")
            .to("refs/heads/dev");
    changeA.assertOkStatus();
    MergeInput mergeInput = new MergeInput();
    mergeInput.source = "dev";
    MergePatchSetInput in = new MergePatchSetInput();
    in.merge = mergeInput;
    String subject = "update change by merge ps2";
    in.subject = subject;

    TestWorkInProgressStateChangedListener wipStateChangedListener =
        new TestWorkInProgressStateChangedListener();
    try (ExtensionRegistry.Registration registration =
        extensionRegistry.newRegistration().add(wipStateChangedListener)) {
      ChangeInfo changeInfo = gApi.changes().id(changeId).createMergePatchSet(in);
      assertThat(changeInfo.subject).isEqualTo(in.subject);
      assertThat(changeInfo.containsGitConflicts).isNull();
      assertThat(changeInfo.workInProgress).isNull();
    }
    assertThat(wipStateChangedListener.invoked).isFalse();

    // To get the revisions, we must retrieve the change with more change options.
    ChangeInfo changeInfo =
        gApi.changes().id(changeId).get(ALL_REVISIONS, CURRENT_COMMIT, CURRENT_REVISION);
    assertThat(changeInfo.revisions).hasSize(2);
    assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.parents.get(0).commit)
        .isEqualTo(parent);

    // Verify the message that has been posted on the change.
    List<ChangeMessageInfo> messages = gApi.changes().id(changeId).messages();
    assertThat(messages).hasSize(2);
    assertThat(Iterables.getLast(messages).message).isEqualTo("Uploaded patch set 2.");

    assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.message)
        .contains(subject);
  }

  @Test
  public void createMergePatchSet_SubjectCarriesOverByDefault() throws Exception {
    RevCommit initialHead = projectOperations.project(project).getHead("master");
    createBranch(BranchNameKey.create(project, "dev"));

    // create a change for master
    PushOneCommit.Result result = createChange();
    String changeId = result.getChangeId();
    String subject = result.getChange().change().getSubject();

    // push a commit into dev branch
    testRepo.reset(initialHead);
    PushOneCommit.Result pushResult =
        pushFactory.create(user.newIdent(), testRepo).to("refs/heads/dev");
    pushResult.assertOkStatus();
    MergeInput mergeInput = new MergeInput();
    mergeInput.source = "dev";
    MergePatchSetInput in = new MergePatchSetInput();
    in.merge = mergeInput;
    in.subject = null;

    // Ensure subject carries over
    gApi.changes().id(changeId).createMergePatchSet(in);
    ChangeInfo changeInfo = gApi.changes().id(changeId).get();
    assertThat(changeInfo.subject).isEqualTo(subject);
  }

  @Test
  public void createMergePatchSet_Conflict() throws Exception {
    RevCommit initialHead = projectOperations.project(project).getHead("master");
    createBranch(BranchNameKey.create(project, "dev"));

    // create a change for master
    String changeId = createChange().getChangeId();

    String fileName = "shared.txt";
    testRepo.reset(initialHead);
    PushOneCommit.Result currentMaster =
        pushFactory
            .create(admin.newIdent(), testRepo, "change 1", fileName, "content 1")
            .to("refs/heads/master");
    currentMaster.assertOkStatus();

    // push a commit into dev branch
    testRepo.reset(initialHead);
    PushOneCommit.Result changeA =
        pushFactory
            .create(user.newIdent(), testRepo, "change 2", fileName, "content 2")
            .to("refs/heads/dev");
    changeA.assertOkStatus();
    MergeInput mergeInput = new MergeInput();
    mergeInput.source = "dev";
    MergePatchSetInput in = new MergePatchSetInput();
    in.merge = mergeInput;
    in.subject = "update change by merge ps2";
    ResourceConflictException thrown =
        assertThrows(
            ResourceConflictException.class,
            () -> gApi.changes().id(changeId).createMergePatchSet(in));
    assertThat(thrown).hasMessageThat().isEqualTo("merge conflict(s):\n" + fileName);
  }

  @Test
  public void createMergePatchSet_ConflictAllowed() throws Exception {
    RevCommit initialHead = projectOperations.project(project).getHead("master");
    createBranch(BranchNameKey.create(project, "dev"));

    // create a change for master
    String changeId = createChange().getChangeId();

    String fileName = "shared.txt";
    String sourceSubject = "source change";
    String sourceContent = "source content";
    String targetSubject = "target change";
    String targetContent = "target content";
    testRepo.reset(initialHead);
    PushOneCommit.Result currentMaster =
        pushFactory
            .create(admin.newIdent(), testRepo, targetSubject, fileName, targetContent)
            .to("refs/heads/master");
    currentMaster.assertOkStatus();
    String parent = currentMaster.getCommit().getName();

    // push a commit into dev branch
    testRepo.reset(initialHead);
    PushOneCommit.Result changeA =
        pushFactory
            .create(user.newIdent(), testRepo, sourceSubject, fileName, sourceContent)
            .to("refs/heads/dev");
    changeA.assertOkStatus();
    MergeInput mergeInput = new MergeInput();
    mergeInput.source = "dev";
    mergeInput.allowConflicts = true;
    MergePatchSetInput in = new MergePatchSetInput();
    in.merge = mergeInput;
    in.subject = "update change by merge ps2";

    TestWorkInProgressStateChangedListener wipStateChangedListener =
        new TestWorkInProgressStateChangedListener();
    try (ExtensionRegistry.Registration registration =
        extensionRegistry.newRegistration().add(wipStateChangedListener)) {
      ChangeInfo changeInfo = gApi.changes().id(changeId).createMergePatchSet(in);
      assertThat(changeInfo.subject).isEqualTo(in.subject);
      assertThat(changeInfo.containsGitConflicts).isTrue();
      assertThat(changeInfo.workInProgress).isTrue();
    }
    assertThat(wipStateChangedListener.invoked).isTrue();
    assertThat(wipStateChangedListener.wip).isTrue();

    // To get the revisions, we must retrieve the change with more change options.
    ChangeInfo changeInfo =
        gApi.changes().id(changeId).get(ALL_REVISIONS, CURRENT_COMMIT, CURRENT_REVISION);
    assertThat(changeInfo.revisions).hasSize(2);
    assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.parents.get(0).commit)
        .isEqualTo(parent);

    // Verify that the file content in the created patch set is correct.
    // We expect that it has conflict markers to indicate the conflict.
    BinaryResult bin = gApi.changes().id(changeId).current().file(fileName).content();
    ByteArrayOutputStream os = new ByteArrayOutputStream();
    bin.writeTo(os);
    String fileContent = new String(os.toByteArray(), UTF_8);
    String sourceSha1 = abbreviateName(changeA.getCommit(), 6);
    String targetSha1 = abbreviateName(currentMaster.getCommit(), 6);
    assertThat(fileContent)
        .isEqualTo(
            "<<<<<<< TARGET BRANCH ("
                + targetSha1
                + " "
                + targetSubject
                + ")\n"
                + targetContent
                + "\n"
                + "=======\n"
                + sourceContent
                + "\n"
                + ">>>>>>> SOURCE BRANCH ("
                + sourceSha1
                + " "
                + sourceSubject
                + ")\n");

    // Verify the message that has been posted on the change.
    List<ChangeMessageInfo> messages = gApi.changes().id(changeId).messages();
    assertThat(messages).hasSize(2);
    assertThat(Iterables.getLast(messages).message)
        .isEqualTo(
            "Uploaded patch set 2.\n\n"
                + "The following files contain Git conflicts:\n"
                + "* "
                + fileName
                + "\n");
  }

  @Test
  public void createMergePatchSet_ConflictAllowedNotSupportedByMergeStrategy() throws Exception {
    RevCommit initialHead = projectOperations.project(project).getHead("master");
    createBranch(BranchNameKey.create(project, "dev"));

    // create a change for master
    String changeId = createChange().getChangeId();

    String fileName = "shared.txt";
    String sourceSubject = "source change";
    String sourceContent = "source content";
    String targetSubject = "target change";
    String targetContent = "target content";
    testRepo.reset(initialHead);
    PushOneCommit.Result currentMaster =
        pushFactory
            .create(admin.newIdent(), testRepo, targetSubject, fileName, targetContent)
            .to("refs/heads/master");
    currentMaster.assertOkStatus();

    // push a commit into dev branch
    testRepo.reset(initialHead);
    PushOneCommit.Result changeA =
        pushFactory
            .create(user.newIdent(), testRepo, sourceSubject, fileName, sourceContent)
            .to("refs/heads/dev");
    changeA.assertOkStatus();
    MergeInput mergeInput = new MergeInput();
    mergeInput.source = "dev";
    mergeInput.allowConflicts = true;
    mergeInput.strategy = "simple-two-way-in-core";
    MergePatchSetInput in = new MergePatchSetInput();
    in.merge = mergeInput;
    in.subject = "update change by merge ps2";

    BadRequestException ex =
        assertThrows(
            BadRequestException.class, () -> gApi.changes().id(changeId).createMergePatchSet(in));
    assertThat(ex)
        .hasMessageThat()
        .isEqualTo(
            "merge with conflicts is not supported with merge strategy: " + mergeInput.strategy);
  }

  @Test
  public void createMergePatchSetInheritParent() throws Exception {
    RevCommit initialHead = projectOperations.project(project).getHead("master");
    createBranch(BranchNameKey.create(project, "dev"));

    // create a change for master
    PushOneCommit.Result r = createChange();
    String changeId = r.getChangeId();
    String parent = r.getCommit().getParent(0).getName();

    // advance master branch
    testRepo.reset(initialHead);
    PushOneCommit.Result currentMaster = pushTo("refs/heads/master");
    currentMaster.assertOkStatus();

    // push a commit into dev branch
    testRepo.reset(initialHead);
    PushOneCommit.Result changeA =
        pushFactory
            .create(user.newIdent(), testRepo, "change A", "A.txt", "A content")
            .to("refs/heads/dev");
    changeA.assertOkStatus();
    MergeInput mergeInput = new MergeInput();
    mergeInput.source = "dev";
    MergePatchSetInput in = new MergePatchSetInput();
    in.merge = mergeInput;
    in.subject = "update change by merge ps2 inherit parent of ps1";
    in.inheritParent = true;
    gApi.changes().id(changeId).createMergePatchSet(in);
    ChangeInfo changeInfo =
        gApi.changes().id(changeId).get(ALL_REVISIONS, CURRENT_COMMIT, CURRENT_REVISION);

    assertThat(changeInfo.revisions).hasSize(2);
    assertThat(changeInfo.subject).isEqualTo(in.subject);
    assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.parents.get(0).commit)
        .isEqualTo(parent);
    assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.parents.get(0).commit)
        .isNotEqualTo(currentMaster.getCommit().getName());
  }

  @Test
  public void createMergePatchSetCannotBaseOnInvisibleChange() throws Exception {
    RevCommit initialHead = projectOperations.project(project).getHead("master");
    createBranch(BranchNameKey.create(project, "foo"));
    createBranch(BranchNameKey.create(project, "bar"));

    // Create a merged commit on 'foo' branch.
    merge(createChange("refs/for/foo"));

    // Create the base change on 'bar' branch.
    testRepo.reset(initialHead);
    String baseChange = createChange("refs/for/bar").getChangeId();
    gApi.changes().id(baseChange).setPrivate(true, "set private");

    // Create the destination change on 'master' branch.
    requestScopeOperations.setApiUser(user.id());
    testRepo.reset(initialHead);
    String changeId = createChange().getChangeId();

    UnprocessableEntityException thrown =
        assertThrows(
            UnprocessableEntityException.class,
            () ->
                gApi.changes()
                    .id(changeId)
                    .createMergePatchSet(createMergePatchSetInput(baseChange)));
    assertThat(thrown).hasMessageThat().contains("Read not permitted for " + baseChange);
  }

  @Test
  public void createMergePatchSetBaseOnChange() throws Exception {
    RevCommit initialHead = projectOperations.project(project).getHead("master");
    createBranch(BranchNameKey.create(project, "foo"));
    createBranch(BranchNameKey.create(project, "bar"));

    // Create a merged commit on 'foo' branch.
    merge(createChange("refs/for/foo"));

    // Create the base change on 'bar' branch.
    testRepo.reset(initialHead);
    PushOneCommit.Result result = createChange("refs/for/bar");
    String baseChange = result.getChangeId();
    String expectedParent = result.getCommit().getName();

    // Create the destination change on 'master' branch.
    testRepo.reset(initialHead);
    String changeId = createChange().getChangeId();

    gApi.changes().id(changeId).createMergePatchSet(createMergePatchSetInput(baseChange));

    ChangeInfo changeInfo =
        gApi.changes().id(changeId).get(ALL_REVISIONS, CURRENT_COMMIT, CURRENT_REVISION);
    assertThat(changeInfo.revisions).hasSize(2);
    assertThat(changeInfo.subject).isEqualTo("create ps2");
    assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.parents.get(0).commit)
        .isEqualTo(expectedParent);
  }

  @Test
  public void createMergePatchSetWithUnupportedMergeStrategy() throws Exception {
    RevCommit initialHead = projectOperations.project(project).getHead("master");
    createBranch(BranchNameKey.create(project, "dev"));

    // create a change for master
    String changeId = createChange().getChangeId();

    String fileName = "shared.txt";
    String sourceSubject = "source change";
    String sourceContent = "source content";
    String targetSubject = "target change";
    String targetContent = "target content";
    testRepo.reset(initialHead);
    PushOneCommit.Result currentMaster =
        pushFactory
            .create(admin.newIdent(), testRepo, targetSubject, fileName, targetContent)
            .to("refs/heads/master");
    currentMaster.assertOkStatus();

    // push a commit into dev branch
    testRepo.reset(initialHead);
    PushOneCommit.Result changeA =
        pushFactory
            .create(user.newIdent(), testRepo, sourceSubject, fileName, sourceContent)
            .to("refs/heads/dev");
    changeA.assertOkStatus();
    MergeInput mergeInput = new MergeInput();
    mergeInput.source = "dev";
    mergeInput.strategy = "unsupported-strategy";
    MergePatchSetInput in = new MergePatchSetInput();
    in.merge = mergeInput;
    in.subject = "update change by merge ps2";

    BadRequestException ex =
        assertThrows(
            BadRequestException.class, () -> gApi.changes().id(changeId).createMergePatchSet(in));
    assertThat(ex).hasMessageThat().isEqualTo("invalid merge strategy: " + mergeInput.strategy);
  }

  @Test
  public void createMergePatchSetWithOtherAuthor() throws Exception {
    RevCommit initialHead = projectOperations.project(project).getHead("master");
    createBranch(BranchNameKey.create(project, "dev"));

    // create a change for master
    String changeId = createChange().getChangeId();

    testRepo.reset(initialHead);
    PushOneCommit.Result currentMaster = pushTo("refs/heads/master");
    currentMaster.assertOkStatus();
    String parent = currentMaster.getCommit().getName();

    // push a commit into dev branch
    testRepo.reset(initialHead);
    PushOneCommit.Result changeA =
        pushFactory
            .create(user.newIdent(), testRepo, "change A", "A.txt", "A content")
            .to("refs/heads/dev");
    changeA.assertOkStatus();
    MergeInput mergeInput = new MergeInput();
    mergeInput.source = "dev";
    MergePatchSetInput in = new MergePatchSetInput();
    in.merge = mergeInput;
    String subject = "update change by merge ps2";
    in.subject = subject;
    in.author = new AccountInput();
    in.author.name = "Other Author";
    in.author.email = "otherauthor@example.com";
    gApi.changes().id(changeId).createMergePatchSet(in);

    // To get the revisions, we must retrieve the change with more change options.
    ChangeInfo changeInfo =
        gApi.changes().id(changeId).get(ALL_REVISIONS, CURRENT_COMMIT, CURRENT_REVISION);
    assertThat(changeInfo.revisions).hasSize(2);
    assertThat(changeInfo.revisions.get(changeInfo.currentRevision).commit.parents.get(0).commit)
        .isEqualTo(parent);

    // Verify the message that has been posted on the change.
    List<ChangeMessageInfo> messages = gApi.changes().id(changeId).messages();
    assertThat(messages).hasSize(2);
    assertThat(Iterables.getLast(messages).message).isEqualTo("Uploaded patch set 2.");

    CommitInfo commitInfo = changeInfo.revisions.get(changeInfo.currentRevision).commit;
    assertThat(commitInfo.message).contains(subject);
    assertThat(commitInfo.author.name).isEqualTo("Other Author");
    assertThat(commitInfo.author.email).isEqualTo("otherauthor@example.com");
  }

  @Test
  public void createMergePatchSetWithSpecificAuthorButNoForgeAuthorPermission() throws Exception {
    RevCommit initialHead = projectOperations.project(project).getHead("master");
    createBranch(BranchNameKey.create(project, "dev"));

    // create a change for master
    String changeId = createChange().getChangeId();

    testRepo.reset(initialHead);
    PushOneCommit.Result currentMaster = pushTo("refs/heads/master");
    currentMaster.assertOkStatus();

    // push a commit into dev branch
    testRepo.reset(initialHead);
    PushOneCommit.Result changeA =
        pushFactory
            .create(user.newIdent(), testRepo, "change A", "A.txt", "A content")
            .to("refs/heads/dev");
    changeA.assertOkStatus();
    MergeInput mergeInput = new MergeInput();
    mergeInput.source = "dev";
    MergePatchSetInput in = new MergePatchSetInput();
    in.merge = mergeInput;
    String subject = "update change by merge ps2";
    in.subject = subject;
    in.author = new AccountInput();
    in.author.name = "Foo";
    in.author.email = "foo@example.com";

    projectOperations
        .project(project)
        .forUpdate()
        .remove(
            TestProjectUpdate.permissionKey(Permission.FORGE_AUTHOR)
                .ref("refs/*")
                .group(REGISTERED_USERS))
        .add(block(Permission.FORGE_AUTHOR).ref("refs/*").group(REGISTERED_USERS))
        .update();
    AuthException ex =
        assertThrows(
            AuthException.class, () -> gApi.changes().id(changeId).createMergePatchSet(in));
    assertThat(ex).hasMessageThat().isEqualTo("not permitted: forge author on refs/heads/master");
  }

  @Test
  public void createMergePatchSetWithMissingNameFailsWithBadRequestException() throws Exception {
    RevCommit initialHead = projectOperations.project(project).getHead("master");
    createBranch(BranchNameKey.create(project, "dev"));

    // create a change for master
    String changeId = createChange().getChangeId();

    testRepo.reset(initialHead);
    PushOneCommit.Result currentMaster = pushTo("refs/heads/master");
    currentMaster.assertOkStatus();

    // push a commit into dev branch
    testRepo.reset(initialHead);
    PushOneCommit.Result changeA =
        pushFactory
            .create(user.newIdent(), testRepo, "change A", "A.txt", "A content")
            .to("refs/heads/dev");
    changeA.assertOkStatus();
    MergeInput mergeInput = new MergeInput();
    mergeInput.source = "dev";
    MergePatchSetInput in = new MergePatchSetInput();
    in.merge = mergeInput;
    String subject = "update change by merge ps2";
    in.subject = subject;
    in.author = new AccountInput();
    in.author.name = "Foo";

    projectOperations
        .project(project)
        .forUpdate()
        .add(block(Permission.FORGE_AUTHOR).ref("refs/*").group(REGISTERED_USERS))
        .update();
    BadRequestException ex =
        assertThrows(
            BadRequestException.class, () -> gApi.changes().id(changeId).createMergePatchSet(in));
    assertThat(ex).hasMessageThat().isEqualTo("Author must specify name and email");
  }

  @Test
  public void createMergePatchSetWithMissingEmailFailsWithBadRequestException() throws Exception {
    RevCommit initialHead = projectOperations.project(project).getHead("master");
    createBranch(BranchNameKey.create(project, "dev"));

    // create a change for master
    String changeId = createChange().getChangeId();

    testRepo.reset(initialHead);
    PushOneCommit.Result currentMaster = pushTo("refs/heads/master");
    currentMaster.assertOkStatus();

    // push a commit into dev branch
    testRepo.reset(initialHead);
    PushOneCommit.Result changeA =
        pushFactory
            .create(user.newIdent(), testRepo, "change A", "A.txt", "A content")
            .to("refs/heads/dev");
    changeA.assertOkStatus();
    MergeInput mergeInput = new MergeInput();
    mergeInput.source = "dev";
    MergePatchSetInput in = new MergePatchSetInput();
    in.merge = mergeInput;
    String subject = "update change by merge ps2";
    in.subject = subject;
    in.author = new AccountInput();
    in.author.email = "Foo";

    projectOperations
        .project(project)
        .forUpdate()
        .add(block(Permission.FORGE_AUTHOR).ref("refs/*").group(REGISTERED_USERS))
        .update();
    BadRequestException ex =
        assertThrows(
            BadRequestException.class, () -> gApi.changes().id(changeId).createMergePatchSet(in));
    assertThat(ex).hasMessageThat().isEqualTo("Author must specify name and email");
  }

  private MergePatchSetInput createMergePatchSetInput(String baseChange) {
    MergeInput mergeInput = new MergeInput();
    mergeInput.source = "foo";
    MergePatchSetInput in = new MergePatchSetInput();
    in.merge = mergeInput;
    in.subject = "create ps2";
    in.inheritParent = false;
    in.baseChange = baseChange;
    return in;
  }

  private static class TestWorkInProgressStateChangedListener
      implements WorkInProgressStateChangedListener {
    boolean invoked;
    Boolean wip;

    @Override
    public void onWorkInProgressStateChanged(Event event) {
      this.invoked = true;
      this.wip =
          event.getChange().workInProgress != null ? event.getChange().workInProgress : false;
    }
  }
}
