// Copyright (C) 2015 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.change;

import static com.google.common.collect.Collections2.permutations;
import static com.google.common.truth.Truth.assertThat;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
import com.google.gerrit.server.change.WalkSorter.PatchSetData;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.testing.InMemoryRepositoryManager;
import com.google.gerrit.testing.InMemoryRepositoryManager.Repo;
import com.google.gerrit.testing.TestChanges;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.junit.Before;
import org.junit.Test;

public class WalkSorterTest {
  private Account.Id userId;
  private InMemoryRepositoryManager repoManager;

  @Before
  public void setUp() {
    userId = Account.id(1);
    repoManager = new InMemoryRepositoryManager();
  }

  @Test
  public void seriesOfChanges() throws Exception {
    TestRepository<Repo> p = newRepo("p");
    RevCommit c1_1 = p.commit().create();
    RevCommit c2_1 = p.commit().parent(c1_1).create();
    RevCommit c3_1 = p.commit().parent(c2_1).create();

    ChangeData cd1 = newChange(p, c1_1);
    ChangeData cd2 = newChange(p, c2_1);
    ChangeData cd3 = newChange(p, c3_1);

    List<ChangeData> changes = ImmutableList.of(cd1, cd2, cd3);
    WalkSorter sorter = new WalkSorter(repoManager);

    assertSorted(
        sorter,
        changes,
        ImmutableList.of(
            patchSetData(cd3, c3_1), patchSetData(cd2, c2_1), patchSetData(cd1, c1_1)));

    // Add new patch sets whose commits are in reverse order, so output is in
    // reverse order.
    RevCommit c3_2 = p.commit().create();
    RevCommit c2_2 = p.commit().parent(c3_2).create();
    RevCommit c1_2 = p.commit().parent(c2_2).create();

    addPatchSet(cd1, c1_2);
    addPatchSet(cd2, c2_2);
    addPatchSet(cd3, c3_2);

    assertSorted(
        sorter,
        changes,
        ImmutableList.of(
            patchSetData(cd1, c1_2), patchSetData(cd2, c2_2), patchSetData(cd3, c3_2)));
  }

  @Test
  public void subsetOfSeriesOfChanges() throws Exception {
    TestRepository<Repo> p = newRepo("p");
    RevCommit c1_1 = p.commit().create();
    RevCommit c2_1 = p.commit().parent(c1_1).create();
    RevCommit c3_1 = p.commit().parent(c2_1).create();

    ChangeData cd1 = newChange(p, c1_1);
    ChangeData cd3 = newChange(p, c3_1);

    List<ChangeData> changes = ImmutableList.of(cd1, cd3);
    WalkSorter sorter = new WalkSorter(repoManager);

    assertSorted(
        sorter, changes, ImmutableList.of(patchSetData(cd3, c3_1), patchSetData(cd1, c1_1)));
  }

  @Test
  public void seriesOfChangesAtSameTimestamp() throws Exception {
    TestRepository<Repo> p = newRepo("p");
    RevCommit c0 = p.commit().tick(0).create();
    RevCommit c1 = p.commit().tick(0).parent(c0).create();
    RevCommit c2 = p.commit().tick(0).parent(c1).create();
    RevCommit c3 = p.commit().tick(0).parent(c2).create();
    RevCommit c4 = p.commit().tick(0).parent(c3).create();

    RevWalk rw = p.getRevWalk();
    rw.parseCommit(c1);
    assertThat(rw.parseCommit(c2).getCommitTime()).isEqualTo(c1.getCommitTime());
    assertThat(rw.parseCommit(c3).getCommitTime()).isEqualTo(c1.getCommitTime());
    assertThat(rw.parseCommit(c4).getCommitTime()).isEqualTo(c1.getCommitTime());

    ChangeData cd1 = newChange(p, c1);
    ChangeData cd2 = newChange(p, c2);
    ChangeData cd3 = newChange(p, c3);
    ChangeData cd4 = newChange(p, c4);

    List<ChangeData> changes = ImmutableList.of(cd1, cd2, cd3, cd4);
    WalkSorter sorter = new WalkSorter(repoManager);

    assertSorted(
        sorter,
        changes,
        ImmutableList.of(
            patchSetData(cd4, c4),
            patchSetData(cd3, c3),
            patchSetData(cd2, c2),
            patchSetData(cd1, c1)));
  }

  @Test
  public void seriesOfChangesWithReverseTimestamps() throws Exception {
    TestRepository<Repo> p = newRepo("p");
    RevCommit c0 = p.commit().tick(-1).create();
    RevCommit c1 = p.commit().tick(-1).parent(c0).create();
    RevCommit c2 = p.commit().tick(-1).parent(c1).create();
    RevCommit c3 = p.commit().tick(-1).parent(c2).create();
    RevCommit c4 = p.commit().tick(-1).parent(c3).create();

    RevWalk rw = p.getRevWalk();
    rw.parseCommit(c1);
    assertThat(rw.parseCommit(c2).getCommitTime()).isLessThan(c1.getCommitTime());
    assertThat(rw.parseCommit(c3).getCommitTime()).isLessThan(c2.getCommitTime());
    assertThat(rw.parseCommit(c4).getCommitTime()).isLessThan(c3.getCommitTime());

    ChangeData cd1 = newChange(p, c1);
    ChangeData cd2 = newChange(p, c2);
    ChangeData cd3 = newChange(p, c3);
    ChangeData cd4 = newChange(p, c4);

    List<ChangeData> changes = ImmutableList.of(cd1, cd2, cd3, cd4);
    WalkSorter sorter = new WalkSorter(repoManager);

    assertSorted(
        sorter,
        changes,
        ImmutableList.of(
            patchSetData(cd4, c4),
            patchSetData(cd3, c3),
            patchSetData(cd2, c2),
            patchSetData(cd1, c1)));
  }

  @Test
  public void subsetOfSeriesOfChangesWithReverseTimestamps() throws Exception {
    TestRepository<Repo> p = newRepo("p");
    RevCommit c0 = p.commit().tick(-1).create();
    RevCommit c1 = p.commit().tick(-1).parent(c0).create();
    RevCommit c2 = p.commit().tick(-1).parent(c1).create();
    RevCommit c3 = p.commit().tick(-1).parent(c2).create();
    RevCommit c4 = p.commit().tick(-1).parent(c3).create();

    RevWalk rw = p.getRevWalk();
    rw.parseCommit(c1);
    assertThat(rw.parseCommit(c2).getCommitTime()).isLessThan(c1.getCommitTime());
    assertThat(rw.parseCommit(c3).getCommitTime()).isLessThan(c2.getCommitTime());
    assertThat(rw.parseCommit(c4).getCommitTime()).isLessThan(c3.getCommitTime());

    ChangeData cd1 = newChange(p, c1);
    ChangeData cd2 = newChange(p, c2);
    ChangeData cd4 = newChange(p, c4);

    List<ChangeData> changes = ImmutableList.of(cd1, cd2, cd4);
    WalkSorter sorter = new WalkSorter(repoManager);
    List<PatchSetData> expected =
        ImmutableList.of(patchSetData(cd4, c4), patchSetData(cd2, c2), patchSetData(cd1, c1));

    for (List<ChangeData> list : permutations(changes)) {
      // Not inOrder(); since child of c2 is missing, partial topo sort isn't
      // guaranteed to work.
      assertThat(sorter.sort(list)).containsExactlyElementsIn(expected);
    }
  }

  @Test
  public void seriesOfChangesAtSameTimestampWithRootCommit() throws Exception {
    TestRepository<Repo> p = newRepo("p");
    RevCommit c1 = p.commit().tick(0).create();
    RevCommit c2 = p.commit().tick(0).parent(c1).create();
    RevCommit c3 = p.commit().tick(0).parent(c2).create();
    RevCommit c4 = p.commit().tick(0).parent(c3).create();

    RevWalk rw = p.getRevWalk();
    rw.parseCommit(c1);
    assertThat(rw.parseCommit(c2).getCommitTime()).isEqualTo(c1.getCommitTime());
    assertThat(rw.parseCommit(c3).getCommitTime()).isEqualTo(c1.getCommitTime());
    assertThat(rw.parseCommit(c4).getCommitTime()).isEqualTo(c1.getCommitTime());

    ChangeData cd1 = newChange(p, c1);
    ChangeData cd2 = newChange(p, c2);
    ChangeData cd3 = newChange(p, c3);
    ChangeData cd4 = newChange(p, c4);

    List<ChangeData> changes = ImmutableList.of(cd1, cd2, cd3, cd4);
    WalkSorter sorter = new WalkSorter(repoManager);

    assertSorted(
        sorter,
        changes,
        ImmutableList.of(
            patchSetData(cd4, c4),
            patchSetData(cd3, c3),
            patchSetData(cd2, c2),
            patchSetData(cd1, c1)));
  }

  @Test
  public void projectsSortedByName() throws Exception {
    TestRepository<Repo> pa = newRepo("a");
    TestRepository<Repo> pb = newRepo("b");
    RevCommit c1 = pa.commit().create();
    RevCommit c2 = pb.commit().create();
    RevCommit c3 = pa.commit().parent(c1).create();
    RevCommit c4 = pb.commit().parent(c2).create();

    ChangeData cd1 = newChange(pa, c1);
    ChangeData cd2 = newChange(pb, c2);
    ChangeData cd3 = newChange(pa, c3);
    ChangeData cd4 = newChange(pb, c4);

    assertSorted(
        new WalkSorter(repoManager),
        ImmutableList.of(cd1, cd2, cd3, cd4),
        ImmutableList.of(
            patchSetData(cd3, c3),
            patchSetData(cd1, c1),
            patchSetData(cd4, c4),
            patchSetData(cd2, c2)));
  }

  @Test
  public void restrictToPatchSets() throws Exception {
    TestRepository<Repo> p = newRepo("p");
    RevCommit c1_1 = p.commit().create();
    RevCommit c2_1 = p.commit().parent(c1_1).create();

    ChangeData cd1 = newChange(p, c1_1);
    ChangeData cd2 = newChange(p, c2_1);

    // Add new patch sets whose commits are in reverse order.
    RevCommit c2_2 = p.commit().create();
    RevCommit c1_2 = p.commit().parent(c2_2).create();

    addPatchSet(cd1, c1_2);
    addPatchSet(cd2, c2_2);

    List<ChangeData> changes = ImmutableList.of(cd1, cd2);
    WalkSorter sorter = new WalkSorter(repoManager);

    assertSorted(
        sorter, changes, ImmutableList.of(patchSetData(cd1, c1_2), patchSetData(cd2, c2_2)));

    // If we restrict to PS1 of each change, the sorter uses that commit.
    sorter.includePatchSets(
        ImmutableSet.of(PatchSet.id(cd1.getId(), 1), PatchSet.id(cd2.getId(), 1)));
    assertSorted(
        sorter, changes, ImmutableList.of(patchSetData(cd2, 1, c2_1), patchSetData(cd1, 1, c1_1)));
  }

  @Test
  public void restrictToPatchSetsOmittingWholeProject() throws Exception {
    TestRepository<Repo> pa = newRepo("a");
    TestRepository<Repo> pb = newRepo("b");
    RevCommit c1 = pa.commit().create();
    RevCommit c2 = pa.commit().create();

    ChangeData cd1 = newChange(pa, c1);
    ChangeData cd2 = newChange(pb, c2);

    List<ChangeData> changes = ImmutableList.of(cd1, cd2);
    WalkSorter sorter =
        new WalkSorter(repoManager).includePatchSets(ImmutableSet.of(cd1.currentPatchSet().id()));

    assertSorted(sorter, changes, ImmutableList.of(patchSetData(cd1, c1)));
  }

  @Test
  public void retainBody() throws Exception {
    TestRepository<Repo> p = newRepo("p");
    RevCommit c = p.commit().message("message").create();
    ChangeData cd = newChange(p, c);

    List<ChangeData> changes = ImmutableList.of(cd);
    RevCommit actual =
        new WalkSorter(repoManager).setRetainBody(true).sort(changes).iterator().next().commit();
    assertThat(actual.getRawBuffer()).isNotNull();
    assertThat(actual.getShortMessage()).isEqualTo("message");

    actual =
        new WalkSorter(repoManager).setRetainBody(false).sort(changes).iterator().next().commit();
    assertThat(actual.getRawBuffer()).isNull();
  }

  @Test
  public void oneChange() throws Exception {
    TestRepository<Repo> p = newRepo("p");
    RevCommit c = p.commit().create();
    ChangeData cd = newChange(p, c);

    List<ChangeData> changes = ImmutableList.of(cd);
    WalkSorter sorter = new WalkSorter(repoManager);

    assertSorted(sorter, changes, ImmutableList.of(patchSetData(cd, c)));
  }

  private ChangeData newChange(TestRepository<Repo> tr, ObjectId id) throws Exception {
    Project.NameKey project = tr.getRepository().getDescription().getProject();
    Change c = TestChanges.newChange(project, userId);
    ChangeData cd = ChangeData.createForTest(project, c.getId(), 1, id);
    cd.setChange(c);
    cd.setPatchSets(ImmutableList.of(cd.currentPatchSet()));
    return cd;
  }

  private PatchSet addPatchSet(ChangeData cd, ObjectId id) throws Exception {
    TestChanges.incrementPatchSet(cd.change());
    PatchSet ps = TestChanges.newPatchSet(cd.change().currentPatchSetId(), id.name(), userId);
    List<PatchSet> patchSets = new ArrayList<>(cd.patchSets());
    patchSets.add(ps);
    cd.setPatchSets(patchSets);
    return ps;
  }

  private TestRepository<Repo> newRepo(String name) throws Exception {
    return new TestRepository<>(repoManager.createRepository(Project.nameKey(name)));
  }

  private static PatchSetData patchSetData(ChangeData cd, RevCommit commit) throws Exception {
    return PatchSetData.create(cd, cd.currentPatchSet(), commit);
  }

  private static PatchSetData patchSetData(ChangeData cd, int psId, RevCommit commit)
      throws Exception {
    return PatchSetData.create(cd, cd.patchSet(PatchSet.id(cd.getId(), psId)), commit);
  }

  private static void assertSorted(
      WalkSorter sorter, List<ChangeData> changes, List<PatchSetData> expected) throws Exception {
    for (List<ChangeData> list : permutations(changes)) {
      assertThat(sorter.sort(list)).containsExactlyElementsIn(expected).inOrder();
    }
  }
}
