// 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.server.patch;

import static com.google.common.truth.Truth.assertThat;

import com.google.gerrit.entities.Project;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.testing.InMemoryRepositoryManager;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.Month;
import java.time.ZoneOffset;
import java.util.Date;
import java.util.TimeZone;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Test;

public class MagicFileTest {

  private final GitRepositoryManager repositoryManager = new InMemoryRepositoryManager();

  @Test
  public void magicFileContentIsBuiltCorrectly() {
    MagicFile magicFile =
        MagicFile.builder()
            .generatedContent("Generated 1\n")
            .modifiableContent("Modifiable 1\n")
            .build();

    assertThat(magicFile.getFileContent()).isEqualTo("Generated 1\nModifiable 1\n");
  }

  @Test
  public void generatedContentMayBeEmpty() {
    MagicFile magicFile = MagicFile.builder().modifiableContent("Modifiable 1\n").build();

    assertThat(magicFile.getFileContent()).isEqualTo("Modifiable 1\n");
  }

  @Test
  public void modifiableContentMayBeEmpty() {
    MagicFile magicFile = MagicFile.builder().generatedContent("Generated 1\n").build();

    assertThat(magicFile.getFileContent()).isEqualTo("Generated 1\n");
  }

  @Test
  public void generatedContentAlwaysHasNewlineAtEnd() {
    MagicFile magicFile = MagicFile.builder().generatedContent("Generated 1").build();

    assertThat(magicFile.generatedContent()).isEqualTo("Generated 1\n");
  }

  @Test
  public void modifiableContentAlwaysHasNewlineAtEnd() {
    MagicFile magicFile = MagicFile.builder().modifiableContent("Modifiable 1").build();

    assertThat(magicFile.modifiableContent()).isEqualTo("Modifiable 1\n");
  }

  @Test
  public void startOfModifiableContentIsIndicatedCorrectlyWhenGeneratedContentIsPresent() {
    MagicFile magicFile =
        MagicFile.builder()
            .generatedContent("Line 1\nLine2\n")
            .modifiableContent("Line 3\n")
            .build();

    // Generated content. -> Modifiable content starts in line 3.
    assertThat(magicFile.getStartLineOfModifiableContent()).isEqualTo(3);
  }

  @Test
  public void startOfModifiableContentIsIndicatedCorrectlyWhenGeneratedContentIsEmpty() {
    MagicFile magicFile = MagicFile.builder().modifiableContent("Line 1\n").build();

    assertThat(magicFile.getStartLineOfModifiableContent()).isEqualTo(1);
  }

  @Test
  public void commitMessageFileOfRootCommitContainsCorrectContent() throws Exception {
    try (Repository repository = repositoryManager.createRepository(Project.nameKey("repo1"));
        TestRepository<Repository> testRepo = new TestRepository<>(repository);
        ObjectReader objectReader = repository.newObjectReader()) {

      Instant authorTime =
          LocalDateTime.of(2020, Month.APRIL, 23, 19, 30, 27).atZone(ZoneOffset.UTC).toInstant();
      PersonIdent author =
          new PersonIdent(
              "Alfred",
              "alfred@example.com",
              Date.from(authorTime),
              TimeZone.getTimeZone(ZoneOffset.UTC));

      Instant committerTime =
          LocalDateTime.of(2021, Month.JANUARY, 6, 5, 12, 55).atZone(ZoneOffset.UTC).toInstant();
      PersonIdent committer =
          new PersonIdent(
              "Luise",
              "luise@example.com",
              Date.from(committerTime),
              TimeZone.getTimeZone(ZoneOffset.UTC));

      ObjectId commit =
          testRepo
              .commit()
              .message("Subject line\n\nFurther explanations.\n")
              .author(author)
              .committer(committer)
              .noParents()
              .create();

      MagicFile commitMessageFile = MagicFile.forCommitMessage(objectReader, commit);

      // The content of the commit message file must not change over time as existing comments
      // would otherwise refer to different content than when they were originally left.
      // -> Keep this format stable over time.
      assertThat(commitMessageFile.getFileContent())
          .isEqualTo(
              "Author:     Alfred <alfred@example.com>\n"
                  + "AuthorDate: 2020-04-23 19:30:27 +0000\n"
                  + "Commit:     Luise <luise@example.com>\n"
                  + "CommitDate: 2021-01-06 05:12:55 +0000\n"
                  + "\n"
                  + "Subject line\n"
                  + "\n"
                  + "Further explanations.\n");
    }
  }

  @Test
  public void commitMessageFileOfNonMergeCommitContainsCorrectContent() throws Exception {
    try (Repository repository = repositoryManager.createRepository(Project.nameKey("repo1"));
        TestRepository<Repository> testRepo = new TestRepository<>(repository);
        ObjectReader objectReader = repository.newObjectReader()) {

      Instant authorTime =
          LocalDateTime.of(2020, Month.APRIL, 23, 19, 30, 27).atZone(ZoneOffset.UTC).toInstant();
      PersonIdent author =
          new PersonIdent(
              "Alfred",
              "alfred@example.com",
              Date.from(authorTime),
              TimeZone.getTimeZone(ZoneOffset.UTC));

      Instant committerTime =
          LocalDateTime.of(2021, Month.JANUARY, 6, 5, 12, 55).atZone(ZoneOffset.UTC).toInstant();
      PersonIdent committer =
          new PersonIdent(
              "Luise",
              "luise@example.com",
              Date.from(committerTime),
              TimeZone.getTimeZone(ZoneOffset.UTC));

      RevCommit parent =
          testRepo.commit().message("Parent subject\n\nParent further details.").create();
      ObjectId commit =
          testRepo
              .commit()
              .message("Subject line\n\nFurther explanations.\n")
              .author(author)
              .committer(committer)
              .parent(parent)
              .create();

      MagicFile commitMessageFile = MagicFile.forCommitMessage(objectReader, commit);

      // The content of the commit message file must not change over time as existing comments
      // would otherwise refer to different content than when they were originally left.
      // -> Keep this format stable over time.
      assertThat(commitMessageFile.getFileContent())
          .isEqualTo(
              String.format(
                  "Parent:     %s (Parent subject)\n"
                      + "Author:     Alfred <alfred@example.com>\n"
                      + "AuthorDate: 2020-04-23 19:30:27 +0000\n"
                      + "Commit:     Luise <luise@example.com>\n"
                      + "CommitDate: 2021-01-06 05:12:55 +0000\n"
                      + "\n"
                      + "Subject line\n"
                      + "\n"
                      + "Further explanations.\n",
                  parent.name().substring(0, 8)));
    }
  }

  @Test
  public void commitMessageFileOfMergeCommitContainsCorrectContent() throws Exception {
    try (Repository repository = repositoryManager.createRepository(Project.nameKey("repo1"));
        TestRepository<Repository> testRepo = new TestRepository<>(repository);
        ObjectReader objectReader = repository.newObjectReader()) {

      Instant authorTime =
          LocalDateTime.of(2020, Month.APRIL, 23, 19, 30, 27).atZone(ZoneOffset.UTC).toInstant();
      PersonIdent author =
          new PersonIdent(
              "Alfred",
              "alfred@example.com",
              Date.from(authorTime),
              TimeZone.getTimeZone(ZoneOffset.UTC));

      Instant committerTime =
          LocalDateTime.of(2021, Month.JANUARY, 6, 5, 12, 55).atZone(ZoneOffset.UTC).toInstant();
      PersonIdent committer =
          new PersonIdent(
              "Luise",
              "luise@example.com",
              Date.from(committerTime),
              TimeZone.getTimeZone(ZoneOffset.UTC));

      RevCommit parent1 = testRepo.commit().message("Parent 1\n\nExplanation 1.").create();
      RevCommit parent2 = testRepo.commit().message("Parent 2\n\nExplanation 2.").create();
      ObjectId commit =
          testRepo
              .commit()
              .message("Subject line\n\nFurther explanations.\n")
              .author(author)
              .committer(committer)
              .parent(parent1)
              .parent(parent2)
              .create();

      MagicFile commitMessageFile = MagicFile.forCommitMessage(objectReader, commit);

      // The content of the commit message file must not change over time as existing comments
      // would otherwise refer to different content than when they were originally left.
      // -> Keep this format stable over time.
      String expectedContent =
          String.format(
              "Merge Of:   %s (Parent 1)\n"
                  + "            %s (Parent 2)\n"
                  + "Author:     Alfred <alfred@example.com>\n"
                  + "AuthorDate: 2020-04-23 19:30:27 +0000\n"
                  + "Commit:     Luise <luise@example.com>\n"
                  + "CommitDate: 2021-01-06 05:12:55 +0000\n"
                  + "\n"
                  + "Subject line\n"
                  + "\n"
                  + "Further explanations.\n",
              parent1.name().substring(0, 8), parent2.name().substring(0, 8));
      assertThat(commitMessageFile.getFileContent()).isEqualTo(expectedContent);
    }
  }

  @Test
  public void commitMessageFileEndsWithEmptyLineIfCommitMessageIsEmpty() throws Exception {
    try (Repository repository = repositoryManager.createRepository(Project.nameKey("myRepo"));
        TestRepository<Repository> testRepo = new TestRepository<>(repository);
        ObjectReader objectReader = repository.newObjectReader()) {
      RevCommit commit = testRepo.commit().message("").create();

      MagicFile commitMessageFile = MagicFile.forCommitMessage(objectReader, commit);
      assertThat(commitMessageFile.getFileContent()).endsWith("\n\n");
    }
  }

  @Test
  public void commitMessageFileContainsFullCommitMessageAsModifiablePart() throws Exception {
    try (Repository repository = repositoryManager.createRepository(Project.nameKey("myRepo"));
        TestRepository<Repository> testRepo = new TestRepository<>(repository);
        ObjectReader objectReader = repository.newObjectReader()) {
      RevCommit commit =
          testRepo.commit().message("Subject line\n\nFurther explanations.\n").create();

      MagicFile commitMessageFile = MagicFile.forCommitMessage(objectReader, commit);
      assertThat(commitMessageFile.modifiableContent())
          .isEqualTo("Subject line\n\nFurther explanations.\n");
    }
  }

  @Test
  public void mergeListFileContainsCorrectContentForDiffAgainstFirstParent() throws Exception {
    try (Repository repository = repositoryManager.createRepository(Project.nameKey("myRepo"));
        TestRepository<Repository> testRepo = new TestRepository<>(repository);
        ObjectReader objectReader = repository.newObjectReader()) {
      RevCommit parent1 = testRepo.commit().message("Parent 1\n\nExplanation 1.").create();
      RevCommit parent2 = testRepo.commit().message("Parent 2\n\nExplanation 2.").create();
      ObjectId commit = testRepo.commit().parent(parent1).parent(parent2).create();

      MagicFile mergeListFile =
          MagicFile.forMergeList(ComparisonType.againstParent(1), objectReader, commit);

      // The content of the merge list file must not change over time as existing comments
      // would otherwise refer to different content than when they were originally left.
      // -> Keep this format stable over time.
      String expectedContent =
          String.format("Merge List:\n\n* %s Parent 2\n", parent2.name().substring(0, 8));
      assertThat(mergeListFile.getFileContent()).isEqualTo(expectedContent);
    }
  }

  @Test
  public void mergeListFileContainsCorrectContentForDiffAgainstSecondParent() throws Exception {
    try (Repository repository = repositoryManager.createRepository(Project.nameKey("myRepo"));
        TestRepository<Repository> testRepo = new TestRepository<>(repository);
        ObjectReader objectReader = repository.newObjectReader()) {
      RevCommit parent1 = testRepo.commit().message("Parent 1\n\nExplanation 1.").create();
      RevCommit parent2 = testRepo.commit().message("Parent 2\n\nExplanation 2.").create();
      ObjectId commit = testRepo.commit().parent(parent1).parent(parent2).create();

      MagicFile mergeListFile =
          MagicFile.forMergeList(ComparisonType.againstParent(2), objectReader, commit);

      // The content of the merge list file must not change over time as existing comments
      // would otherwise refer to different content than when they were originally left.
      // -> Keep this format stable over time.
      String expectedContent =
          String.format("Merge List:\n\n* %s Parent 1\n", parent1.name().substring(0, 8));
      assertThat(mergeListFile.getFileContent()).isEqualTo(expectedContent);
    }
  }

  @Test
  public void mergeListFileContainsCorrectContentForDiffAgainstAutoMerge() throws Exception {
    try (Repository repository = repositoryManager.createRepository(Project.nameKey("myRepo"));
        TestRepository<Repository> testRepo = new TestRepository<>(repository);
        ObjectReader objectReader = repository.newObjectReader()) {
      RevCommit parent1 = testRepo.commit().message("Parent 1\n\nExplanation 1.").create();
      RevCommit parent2 = testRepo.commit().message("Parent 2\n\nExplanation 2.").create();
      ObjectId commit = testRepo.commit().parent(parent1).parent(parent2).create();

      MagicFile mergeListFile =
          MagicFile.forMergeList(ComparisonType.againstAutoMerge(), objectReader, commit);

      // When auto-merge is chosen, we fall back to the diff against the first parent.
      String expectedContent =
          String.format("Merge List:\n\n* %s Parent 2\n", parent2.name().substring(0, 8));
      assertThat(mergeListFile.getFileContent()).isEqualTo(expectedContent);
    }
  }

  @Test
  public void mergeListFileIsEmptyForRootCommit() throws Exception {
    try (Repository repository = repositoryManager.createRepository(Project.nameKey("myRepo"));
        TestRepository<Repository> testRepo = new TestRepository<>(repository);
        ObjectReader objectReader = repository.newObjectReader()) {
      ObjectId commit = testRepo.commit().noParents().create();

      MagicFile mergeListFile =
          MagicFile.forMergeList(ComparisonType.againstParent(1), objectReader, commit);

      assertThat(mergeListFile.getFileContent()).isEmpty();
    }
  }

  @Test
  public void mergeListFileIsEmptyForNonMergeCommit() throws Exception {
    try (Repository repository = repositoryManager.createRepository(Project.nameKey("myRepo"));
        TestRepository<Repository> testRepo = new TestRepository<>(repository);
        ObjectReader objectReader = repository.newObjectReader()) {
      RevCommit parent = testRepo.commit().message("Parent 1\n").create();
      ObjectId commit = testRepo.commit().parent(parent).create();

      MagicFile mergeListFile =
          MagicFile.forMergeList(ComparisonType.againstParent(1), objectReader, commit);

      assertThat(mergeListFile.getFileContent()).isEmpty();
    }
  }

  @Test
  public void mergeListFileDoesNotHaveAModifiablePart() throws Exception {
    try (Repository repository = repositoryManager.createRepository(Project.nameKey("myRepo"));
        TestRepository<Repository> testRepo = new TestRepository<>(repository);
        ObjectReader objectReader = repository.newObjectReader()) {
      RevCommit parent1 = testRepo.commit().message("Parent 1\n").create();
      RevCommit parent2 = testRepo.commit().message("Parent 2\n").create();
      ObjectId commit = testRepo.commit().parent(parent1).parent(parent2).create();

      MagicFile mergeListFile =
          MagicFile.forMergeList(ComparisonType.againstParent(1), objectReader, commit);

      // Nothing in the merge list file represents something users may modify.
      assertThat(mergeListFile.modifiableContent()).isEmpty();
    }
  }
}
