// 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.revision;

import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block;
import static com.google.gerrit.extensions.common.testing.EditInfoSubject.assertThat;
import static com.google.gerrit.server.group.SystemGroupBackend.ANONYMOUS_USERS;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;

import com.google.common.collect.ImmutableMap;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.entities.Patch;
import com.google.gerrit.entities.Permission;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.changes.PublishChangeEditInput;
import com.google.gerrit.extensions.api.changes.ReviewResult;
import com.google.gerrit.extensions.client.Comment;
import com.google.gerrit.extensions.common.ApplyProvidedFixInput;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.EditInfo;
import com.google.gerrit.extensions.common.FixReplacementInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
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.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.testing.BinaryResultSubject;
import com.google.gson.stream.JsonReader;
import com.google.inject.Inject;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import org.junit.Before;
import org.junit.Test;

public class ApplyProvidedFixIT extends AbstractDaemonTest {
  @Inject private ProjectOperations projectOperations;

  private static final String FILE_NAME = "file_to_fix.txt";
  private static final String FILE_NAME2 = "another_file_to_fix.txt";
  private static final String FILE_NAME3 = "file_without_newline_at_end.txt";
  private static final String FILE_CONTENT =
      "First line\nSecond line\nThird line\nFourth line\nFifth line\nSixth line"
          + "\nSeventh line\nEighth line\nNinth line\nTenth line\n";
  private static final String FILE_CONTENT2 = "1st line\n2nd line\n3rd line\n";
  private static final String FILE_CONTENT3 = "1st line\n2nd line";

  private String changeId;
  private String commitId;

  @Before
  public void setUp() throws Exception {
    PushOneCommit push =
        pushFactory.create(
            admin.newIdent(),
            testRepo,
            "Provide files which can be used for fixes",
            ImmutableMap.of(
                FILE_NAME, FILE_CONTENT, FILE_NAME2, FILE_CONTENT2, FILE_NAME3, FILE_CONTENT3));
    PushOneCommit.Result changeResult = push.to("refs/for/master");
    changeId = changeResult.getChangeId();
    commitId = changeResult.getCommit().getName();
  }

  @Test
  public void applyProvidedFixWithinALineCanBeApplied() throws Exception {
    ApplyProvidedFixInput applyProvidedFixInput =
        createApplyProvidedFixInput(FILE_NAME, "Modified content", 3, 1, 3, 3);
    gApi.changes().id(changeId).current().applyFix(applyProvidedFixInput);

    Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
    BinaryResultSubject.assertThat(file)
        .value()
        .asString()
        .isEqualTo(
            "First line\nSecond line\nTModified contentrd line\nFourth line\nFifth line\n"
                + "Sixth line\nSeventh line\nEighth line\nNinth line\nTenth line\n");
  }

  @Test
  public void applyProvidedFixRestAPItestForASimpleFix() throws Exception {
    ApplyProvidedFixInput applyProvidedFixInput =
        createApplyProvidedFixInput(FILE_NAME, "Modified content", 3, 1, 3, 3);
    RestResponse resp =
        adminRestSession.post(
            "/changes/" + changeId + "/revisions/" + commitId + "/fix:apply",
            applyProvidedFixInput);
    readContentFromJson(resp, 200, ReviewResult.class);
    Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
    BinaryResultSubject.assertThat(file)
        .value()
        .asString()
        .isEqualTo(
            "First line\nSecond line\nTModified contentrd line\nFourth line\nFifth line\n"
                + "Sixth line\nSeventh line\nEighth line\nNinth line\nTenth line\n");
  }

  @Test
  public void applyProvidedFixSpanningMultipleLinesCanBeApplied() throws Exception {
    ApplyProvidedFixInput applyProvidedFixInput =
        createApplyProvidedFixInput(FILE_NAME, "Modified content\n5", 3, 2, 5, 3);

    gApi.changes().id(changeId).current().applyFix(applyProvidedFixInput);

    Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
    BinaryResultSubject.assertThat(file)
        .value()
        .asString()
        .isEqualTo(
            "First line\nSecond line\nThModified content\n5th line\nSixth line\nSeventh line\n"
                + "Eighth line\nNinth line\nTenth line\n");
  }

  @Test
  public void applyProvidedFixWithTwoCloseReplacementsOnSameFileCanBeApplied() throws Exception {
    FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
    fixReplacementInfo1.path = FILE_NAME;
    fixReplacementInfo1.range = createRange(2, 0, 3, 0);
    fixReplacementInfo1.replacement = "First modification\n";

    FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
    fixReplacementInfo2.path = FILE_NAME;
    fixReplacementInfo2.range = createRange(3, 0, 4, 0);
    fixReplacementInfo2.replacement = "Some other modified content\n";

    List<FixReplacementInfo> fixReplacementInfoList =
        Arrays.asList(fixReplacementInfo1, fixReplacementInfo2);
    ApplyProvidedFixInput applyProvidedFixInput = new ApplyProvidedFixInput();
    applyProvidedFixInput.fixReplacementInfos = fixReplacementInfoList;

    gApi.changes().id(changeId).current().applyFix(applyProvidedFixInput);

    Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
    BinaryResultSubject.assertThat(file)
        .value()
        .asString()
        .isEqualTo(
            "First line\nFirst modification\nSome other modified content\nFourth line\nFifth line\n"
                + "Sixth line\nSeventh line\nEighth line\nNinth line\nTenth line\n");
  }

  @Test
  public void twoApplyProvidedFixesOnSameFileCanBeApplied() throws Exception {
    FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
    fixReplacementInfo1.path = FILE_NAME;
    fixReplacementInfo1.range = createRange(2, 0, 3, 0);
    fixReplacementInfo1.replacement = "First modification\n";

    FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
    fixReplacementInfo2.path = FILE_NAME;
    fixReplacementInfo2.range = createRange(8, 0, 9, 0);
    fixReplacementInfo2.replacement = "Some other modified content\n";

    List<FixReplacementInfo> fixReplacementInfoList =
        Arrays.asList(fixReplacementInfo1, fixReplacementInfo2);
    ApplyProvidedFixInput applyProvidedFixInput = new ApplyProvidedFixInput();
    applyProvidedFixInput.fixReplacementInfos = fixReplacementInfoList;

    gApi.changes().id(changeId).current().applyFix(applyProvidedFixInput);

    Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
    BinaryResultSubject.assertThat(file)
        .value()
        .asString()
        .isEqualTo(
            "First line\nFirst modification\nThird line\nFourth line\nFifth line\nSixth line\n"
                + "Seventh line\nSome other modified content\nNinth line\nTenth line\n");
  }

  @Test
  public void twoConflictingApplyProvidedFixesOnSameFileCannotBeApplied() throws Exception {
    FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
    fixReplacementInfo1.path = FILE_NAME;
    fixReplacementInfo1.range = createRange(2, 0, 3, 1);
    fixReplacementInfo1.replacement = "First modification\n";

    FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
    fixReplacementInfo2.path = FILE_NAME;
    fixReplacementInfo2.range = createRange(3, 0, 4, 0);
    fixReplacementInfo2.replacement = "Some other modified content\n";

    List<FixReplacementInfo> fixReplacementInfoList =
        Arrays.asList(fixReplacementInfo1, fixReplacementInfo2);
    ApplyProvidedFixInput applyProvidedFixInput = new ApplyProvidedFixInput();
    applyProvidedFixInput.fixReplacementInfos = fixReplacementInfoList;

    ResourceConflictException thrown =
        assertThrows(
            ResourceConflictException.class,
            () -> gApi.changes().id(changeId).current().applyFix(applyProvidedFixInput));
    assertThat(thrown).hasMessageThat().contains("Cannot calculate fix replacement");
  }

  @Test
  public void applyProvidedFixInvolvingTwoFilesCanBeApplied() throws Exception {
    FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
    fixReplacementInfo1.path = FILE_NAME;
    fixReplacementInfo1.range = createRange(2, 0, 3, 0);
    fixReplacementInfo1.replacement = "First modification\n";

    FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
    fixReplacementInfo2.path = FILE_NAME2;
    fixReplacementInfo2.range = createRange(1, 0, 2, 0);
    fixReplacementInfo2.replacement = "Different file modification\n";

    List<FixReplacementInfo> fixReplacementInfoList =
        Arrays.asList(fixReplacementInfo1, fixReplacementInfo2);
    ApplyProvidedFixInput applyProvidedFixInput = new ApplyProvidedFixInput();
    applyProvidedFixInput.fixReplacementInfos = fixReplacementInfoList;

    gApi.changes().id(changeId).current().applyFix(applyProvidedFixInput);

    Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
    BinaryResultSubject.assertThat(file)
        .value()
        .asString()
        .isEqualTo(
            "First line\nFirst modification\nThird line\nFourth line\nFifth line\nSixth line\n"
                + "Seventh line\nEighth line\nNinth line\nTenth line\n");
    Optional<BinaryResult> file2 = gApi.changes().id(changeId).edit().getFile(FILE_NAME2);
    BinaryResultSubject.assertThat(file2)
        .value()
        .asString()
        .isEqualTo("Different file modification\n2nd line\n3rd line\n");
  }

  @Test
  public void applyProvidedFixReferringToNonExistentFileCannotBeApplied() throws Exception {
    ApplyProvidedFixInput applyProvidedFixInput =
        createApplyProvidedFixInput("a_non_existent_file.txt", "Modified content\n", 1, 0, 2, 0);

    assertThrows(
        ResourceNotFoundException.class,
        () -> gApi.changes().id(changeId).current().applyFix(applyProvidedFixInput));
  }

  @Test
  public void applyProvidedFixRestAPIcallWithoutAddPatchSetPermissionCannotBeApplied()
      throws Exception {
    ApplyProvidedFixInput applyProvidedFixInput =
        createApplyProvidedFixInput(FILE_NAME, "Modified content", 3, 1, 3, 3);

    String allRefs = RefNames.REFS + "*";
    projectOperations
        .project(project)
        .forUpdate()
        .add(block(Permission.ADD_PATCH_SET).ref(allRefs).group(ANONYMOUS_USERS))
        .add(block(Permission.ADD_PATCH_SET).ref(allRefs).group(REGISTERED_USERS))
        .update();

    RestResponse resp =
        userRestSession.post(
            "/changes/" + changeId + "/revisions/" + commitId + "/fix:apply",
            applyProvidedFixInput);
    resp.assertStatus(403);
  }

  @Test
  public void applyProvidedFixOnCurrentPatchSetWithExistingChangeEditCanBeApplied()
      throws Exception {
    FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
    fixReplacementInfo1.path = FILE_NAME;
    fixReplacementInfo1.range = createRange(2, 0, 3, 0);
    fixReplacementInfo1.replacement = "First modification\n";

    List<FixReplacementInfo> fixReplacementInfoList = Arrays.asList(fixReplacementInfo1);
    ApplyProvidedFixInput applyProvidedFixInput = new ApplyProvidedFixInput();
    applyProvidedFixInput.fixReplacementInfos = fixReplacementInfoList;

    // Create an empty change edit.
    gApi.changes().id(changeId).edit().create();

    gApi.changes().id(changeId).current().applyFix(applyProvidedFixInput);

    Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME);
    BinaryResultSubject.assertThat(file)
        .value()
        .asString()
        .isEqualTo(
            "First line\nFirst modification\nThird line\nFourth line\nFifth line\nSixth line\n"
                + "Seventh line\nEighth line\nNinth line\nTenth line\n");
  }

  @Test
  public void applyProvidedFixOnPreviousPatchSetCannotBeApplied() throws Exception {
    // Remember patch set and add another one.
    String previousRevision = gApi.changes().id(changeId).get().currentRevision;
    amendChange(changeId);
    ApplyProvidedFixInput applyProvidedFixInput =
        createApplyProvidedFixInput(FILE_NAME, "Modified content", 3, 1, 3, 3);

    ResourceConflictException thrown =
        assertThrows(
            ResourceConflictException.class,
            () ->
                gApi.changes()
                    .id(changeId)
                    .revision(previousRevision)
                    .applyFix(applyProvidedFixInput));
    assertThat(thrown)
        .hasMessageThat()
        .contains("A change edit may only be created for the current patch set");
  }

  @Test
  public void applyProvidedFixOnCurrentPatchSetWithChangeEditOnPreviousPatchSetCannotBeApplied()
      throws Exception {
    // Create an empty change edit.
    gApi.changes().id(changeId).edit().create();
    // Add another patch set.
    amendChange(changeId);
    ApplyProvidedFixInput applyProvidedFixInput =
        createApplyProvidedFixInput(FILE_NAME, "Modified content", 3, 1, 3, 3);

    ResourceConflictException thrown =
        assertThrows(
            ResourceConflictException.class,
            () -> gApi.changes().id(changeId).current().applyFix(applyProvidedFixInput));
    assertThat(thrown)
        .hasMessageThat()
        .contains("on which the existing change edit is based may be modified");
  }

  @Test
  public void applyProvidedFixOnCommitMessageCanBeApplied() throws Exception {
    // Set a dedicated commit message.
    String footer = "\nChange-Id: " + changeId + "\n";
    String originalCommitMessage = "Line 1 of commit message\nLine 2 of commit message\n" + footer;
    gApi.changes().id(changeId).edit().modifyCommitMessage(originalCommitMessage);
    gApi.changes().id(changeId).edit().publish();
    ApplyProvidedFixInput applyProvidedFixInput =
        createApplyProvidedFixInput(Patch.COMMIT_MSG, "Modified line\n", 7, 0, 8, 0);

    gApi.changes().id(changeId).current().applyFix(applyProvidedFixInput);
    String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage();
    assertThat(commitMessage).isEqualTo("Modified line\nLine 2 of commit message\n" + footer);
  }

  @Test
  public void applyProvidedFixOnHeaderPartOfCommitMessageCannotBeApplied() throws Exception {
    // Set a dedicated commit message.
    String footer = "Change-Id: " + changeId;
    String originalCommitMessage =
        "Line 1 of commit message\nLine 2 of commit message\n" + "\n" + footer + "\n";
    gApi.changes().id(changeId).edit().modifyCommitMessage(originalCommitMessage);
    gApi.changes().id(changeId).edit().publish();

    ApplyProvidedFixInput applyProvidedFixInput =
        createApplyProvidedFixInput(Patch.COMMIT_MSG, "Modified line\n", 1, 0, 2, 0);
    ResourceConflictException exception =
        assertThrows(
            ResourceConflictException.class,
            () -> gApi.changes().id(changeId).current().applyFix(applyProvidedFixInput));
    assertThat(exception).hasMessageThat().contains("header");
  }

  @Test
  public void applyProvidedFixContainingSeveralModificationsOfCommitMessageCanBeApplied()
      throws Exception {
    // Set a dedicated commit message.
    String footer = "\nChange-Id: " + changeId + "\n";
    String originalCommitMessage =
        "Line 1 of commit message\nLine 2 of commit message\nLine 3 of commit message\n" + footer;
    gApi.changes().id(changeId).edit().modifyCommitMessage(originalCommitMessage);
    gApi.changes().id(changeId).edit().publish();
    FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
    fixReplacementInfo1.path = Patch.COMMIT_MSG;
    fixReplacementInfo1.range = createRange(7, 0, 8, 0);
    fixReplacementInfo1.replacement = "Modified line 1\n";
    FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
    fixReplacementInfo2.path = Patch.COMMIT_MSG;
    fixReplacementInfo2.range = createRange(9, 0, 10, 0);
    fixReplacementInfo2.replacement = "Modified line 3\n";
    List<FixReplacementInfo> fixReplacementInfoList =
        Arrays.asList(fixReplacementInfo1, fixReplacementInfo2);
    ApplyProvidedFixInput applyProvidedFixInput = new ApplyProvidedFixInput();
    applyProvidedFixInput.fixReplacementInfos = fixReplacementInfoList;
    gApi.changes().id(changeId).current().applyFix(applyProvidedFixInput);
    String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage();
    assertThat(commitMessage)
        .isEqualTo("Modified line 1\nLine 2 of commit message\nModified line 3\n" + footer);
  }

  @Test
  public void applyProvidedFixModifyingTheCommitMessageAndAFileCanBeApplied() throws Exception {
    // Set a dedicated commit message.
    String footer = "\nChange-Id: " + changeId + "\n";
    String originalCommitMessage = "Line 1 of commit message\nLine 2 of commit message\n" + footer;
    gApi.changes().id(changeId).edit().modifyCommitMessage(originalCommitMessage);
    gApi.changes().id(changeId).edit().publish();
    FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
    fixReplacementInfo1.path = Patch.COMMIT_MSG;
    fixReplacementInfo1.range = createRange(7, 0, 8, 0);
    fixReplacementInfo1.replacement = "Modified line 1\n";
    FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
    fixReplacementInfo2.path = FILE_NAME2;
    fixReplacementInfo2.range = createRange(1, 0, 2, 0);
    fixReplacementInfo2.replacement = "File modification\n";
    List<FixReplacementInfo> fixReplacementInfoList =
        Arrays.asList(fixReplacementInfo1, fixReplacementInfo2);
    ApplyProvidedFixInput applyProvidedFixInput = new ApplyProvidedFixInput();
    applyProvidedFixInput.fixReplacementInfos = fixReplacementInfoList;
    gApi.changes().id(changeId).current().applyFix(applyProvidedFixInput);
    String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage();
    assertThat(commitMessage).isEqualTo("Modified line 1\nLine 2 of commit message\n" + footer);
    Optional<BinaryResult> file = gApi.changes().id(changeId).edit().getFile(FILE_NAME2);
    BinaryResultSubject.assertThat(file)
        .value()
        .asString()
        .isEqualTo("File modification\n2nd line\n3rd line\n");
  }

  @Test
  public void twoApplyProvidedFixesNonOverlappingOnCommitMessageCanBeAppliedSubsequently()
      throws Exception {
    // Set a dedicated commit message.
    String footer = "\nChange-Id: " + changeId + "\n";
    String originalCommitMessage =
        "Line 1 of commit message\nLine 2 of commit message\nLine 3 of commit message\n" + footer;
    gApi.changes().id(changeId).edit().modifyCommitMessage(originalCommitMessage);
    gApi.changes().id(changeId).edit().publish();
    FixReplacementInfo fixReplacementInfo1 = new FixReplacementInfo();
    fixReplacementInfo1.path = Patch.COMMIT_MSG;
    fixReplacementInfo1.range = createRange(7, 0, 8, 0);
    fixReplacementInfo1.replacement = "Modified line 1\n";
    FixReplacementInfo fixReplacementInfo2 = new FixReplacementInfo();
    fixReplacementInfo2.path = Patch.COMMIT_MSG;
    fixReplacementInfo2.range = createRange(9, 0, 10, 0);
    fixReplacementInfo2.replacement = "Modified line 3\n";
    List<FixReplacementInfo> fixReplacementInfoList1 = Arrays.asList(fixReplacementInfo1);
    ApplyProvidedFixInput applyProvidedFixInput1 = new ApplyProvidedFixInput();
    applyProvidedFixInput1.fixReplacementInfos = fixReplacementInfoList1;
    List<FixReplacementInfo> fixReplacementInfoList2 = Arrays.asList(fixReplacementInfo2);
    ApplyProvidedFixInput applyProvidedFixInput2 = new ApplyProvidedFixInput();
    applyProvidedFixInput2.fixReplacementInfos = fixReplacementInfoList2;
    gApi.changes().id(changeId).current().applyFix(applyProvidedFixInput1);
    gApi.changes().id(changeId).current().applyFix(applyProvidedFixInput2);

    String commitMessage = gApi.changes().id(changeId).edit().getCommitMessage();
    assertThat(commitMessage)
        .isEqualTo("Modified line 1\nLine 2 of commit message\nModified line 3\n" + footer);
  }

  @Test
  public void applyingStoredFixTwiceIsIdempotent() throws Exception {
    ApplyProvidedFixInput applyProvidedFixInput =
        createApplyProvidedFixInput(FILE_NAME, "Modified content", 3, 1, 3, 3);
    gApi.changes().id(changeId).current().applyFix(applyProvidedFixInput);
    String expectedEditCommit =
        gApi.changes().id(changeId).edit().get().map(edit -> edit.commit.commit).orElse("");

    // Apply the fix again.
    gApi.changes().id(changeId).current().applyFix(applyProvidedFixInput);
    Optional<EditInfo> editInfo = gApi.changes().id(changeId).edit().get();
    assertThat(editInfo).value().commit().commit().isEqualTo(expectedEditCommit);
  }

  @Test
  public void applyProvidedFixReturnsEditInfoForCreatedChangeEdit() throws Exception {
    ApplyProvidedFixInput applyProvidedFixInput =
        createApplyProvidedFixInput(FILE_NAME, "Modified content", 3, 1, 3, 3);
    EditInfo editInfo = gApi.changes().id(changeId).current().applyFix(applyProvidedFixInput);
    Optional<EditInfo> expectedEditInfo = gApi.changes().id(changeId).edit().get();
    String expectedEditCommit = expectedEditInfo.map(edit -> edit.commit.commit).orElse("");
    assertThat(editInfo).commit().commit().isEqualTo(expectedEditCommit);
    String expectedBaseRevision = expectedEditInfo.map(edit -> edit.baseRevision).orElse("");
    assertThat(editInfo).baseRevision().isEqualTo(expectedBaseRevision);
  }

  @Test
  public void createdApplyProvidedFixChangeEditIsBasedOnCurrentPatchSet() throws Exception {
    String currentRevision = gApi.changes().id(changeId).get().currentRevision;
    ApplyProvidedFixInput applyProvidedFixInput =
        createApplyProvidedFixInput(FILE_NAME, "Modified content", 3, 1, 3, 3);
    EditInfo editInfo = gApi.changes().id(changeId).current().applyFix(applyProvidedFixInput);
    assertThat(editInfo).baseRevision().isEqualTo(currentRevision);
  }

  @Test
  public void applyProvidedFixRestCallWithDifferentUserTheUserBecomesUploader() throws Exception {
    ApplyProvidedFixInput applyProvidedFixInput =
        createApplyProvidedFixInput(FILE_NAME, "Modified content", 3, 1, 3, 3);

    ChangeInfo changeInfo = gApi.changes().id(changeId).get();
    RevisionInfo rev = changeInfo.revisions.get(changeInfo.currentRevision);
    assertThat(rev.uploader.username).isEqualTo(admin.username());

    RestResponse resp =
        userRestSession.post(
            "/changes/" + changeId + "/revisions/" + commitId + "/fix:apply",
            applyProvidedFixInput);
    resp.assertStatus(200);

    PublishChangeEditInput publishInput = new PublishChangeEditInput();
    RestResponse resp2 =
        userRestSession.post("/changes/" + changeId + "/edit:publish", publishInput);
    resp2.assertStatus(204);

    changeInfo = gApi.changes().id(changeId).get();
    RevisionInfo rev2 = changeInfo.revisions.get(changeInfo.currentRevision);
    assertThat(rev2.uploader.username).isEqualTo(user.username());
  }

  @Test
  public void applyProvidedFixInputNullReturnsBadRequestException() throws Exception {
    ApplyProvidedFixInput applyProvidedFixInput = null;
    BadRequestException thrown =
        assertThrows(
            BadRequestException.class,
            () -> gApi.changes().id(changeId).current().applyFix(applyProvidedFixInput));
    assertThat(thrown).hasMessageThat().contains("applyProvidedFixInput is required");
  }

  @Test
  public void applyProvidedFixInputFixReplacementInfosNullReturnsBadRequestException()
      throws Exception {
    ApplyProvidedFixInput applyProvidedFixInput = new ApplyProvidedFixInput();
    BadRequestException thrown =
        assertThrows(
            BadRequestException.class,
            () -> gApi.changes().id(changeId).current().applyFix(applyProvidedFixInput));
    assertThat(thrown)
        .hasMessageThat()
        .contains("applyProvidedFixInput.fixReplacementInfos is required");
  }

  private ApplyProvidedFixInput createApplyProvidedFixInput(
      String file_name,
      String replacement,
      int startLine,
      int startCharacter,
      int endLine,
      int endCharacter) {
    FixReplacementInfo fixReplacementInfo = new FixReplacementInfo();
    fixReplacementInfo.path = file_name;
    fixReplacementInfo.replacement = replacement;
    fixReplacementInfo.range = createRange(startLine, startCharacter, endLine, endCharacter);

    List<FixReplacementInfo> fixReplacementInfoList = Arrays.asList(fixReplacementInfo);
    ApplyProvidedFixInput applyProvidedFixInput = new ApplyProvidedFixInput();
    applyProvidedFixInput.fixReplacementInfos = fixReplacementInfoList;

    return applyProvidedFixInput;
  }

  private static Comment.Range createRange(
      int startLine, int startCharacter, int endLine, int endCharacter) {
    Comment.Range range = new Comment.Range();
    range.startLine = startLine;
    range.startCharacter = startCharacter;
    range.endLine = endLine;
    range.endCharacter = endCharacter;
    return range;
  }

  private static <T> T readContentFromJson(RestResponse r, int expectedStatus, Class<T> clazz)
      throws Exception {
    r.assertStatus(expectedStatus);
    try (JsonReader jsonReader = new JsonReader(r.getReader())) {
      jsonReader.setLenient(true);
      return newGson().fromJson(jsonReader, clazz);
    }
  }
}
