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

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

import com.google.common.collect.ImmutableList;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.server.permissions.PermissionBackendException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.restapi.change.Submit;
import com.google.gerrit.server.submit.ChangeSet;
import com.google.gerrit.server.submit.MergeSuperSet;
import com.google.gerrit.testing.ConfigSuite;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Test;

@NoHttpd
public class SubmitResolvingMergeCommitIT extends AbstractDaemonTest {
  @Inject private Provider<MergeSuperSet> mergeSuperSet;

  @Inject private Submit submit;

  @ConfigSuite.Default
  public static Config submitWholeTopicEnabled() {
    return submitWholeTopicEnabledConfig();
  }

  @Test
  public void resolvingMergeCommitAtEndOfChain() throws Exception {
    /*
      A <- B <- C <------- D
      ^                    ^
      |                    |
      E <- F <- G <- H <-- M*

      G has a conflict with C and is resolved in M which is a merge
      commit of H and D.
    */

    PushOneCommit.Result a = createChange("A");
    PushOneCommit.Result b =
        createChange("B", "new.txt", "No conflict line", ImmutableList.of(a.getCommit()));
    PushOneCommit.Result c = createChange("C", ImmutableList.of(b.getCommit()));
    PushOneCommit.Result d = createChange("D", ImmutableList.of(c.getCommit()));

    PushOneCommit.Result e = createChange("E", ImmutableList.of(a.getCommit()));
    PushOneCommit.Result f = createChange("F", ImmutableList.of(e.getCommit()));
    PushOneCommit.Result g =
        createChange("G", "new.txt", "Conflicting line", ImmutableList.of(f.getCommit()));
    PushOneCommit.Result h = createChange("H", ImmutableList.of(g.getCommit()));

    approve(a.getChangeId());
    approve(b.getChangeId());
    approve(c.getChangeId());
    approve(d.getChangeId());
    submit(d.getChangeId());

    approve(e.getChangeId());
    approve(f.getChangeId());
    approve(g.getChangeId());
    approve(h.getChangeId());

    assertMergeable(e.getChange());
    assertMergeable(f.getChange());
    assertNotMergeable(g.getChange());
    assertNotMergeable(h.getChange());

    PushOneCommit.Result m =
        createChange(
            "M", "new.txt", "Resolved conflict", ImmutableList.of(d.getCommit(), h.getCommit()));
    approve(m.getChangeId());

    assertChangeSetMergeable(m.getChange(), true);

    assertMergeable(m.getChange());
    submit(m.getChangeId());

    assertMerged(e.getChangeId());
    assertMerged(f.getChangeId());
    assertMerged(g.getChangeId());
    assertMerged(h.getChangeId());
    assertMerged(m.getChangeId());
  }

  @Test
  public void resolvingMergeCommitComingBeforeConflict() throws Exception {
    /*
      A <- B <- C <- D
      ^    ^
      |    |
      E <- F* <- G

      F is a merge commit of E and B and resolves any conflict.
      However G is conflicting with C.
    */

    PushOneCommit.Result a = createChange("A");
    PushOneCommit.Result b =
        createChange("B", "new.txt", "No conflict line", ImmutableList.of(a.getCommit()));
    PushOneCommit.Result c =
        createChange("C", "new.txt", "No conflict line #2", ImmutableList.of(b.getCommit()));
    PushOneCommit.Result d = createChange("D", ImmutableList.of(c.getCommit()));
    PushOneCommit.Result e =
        createChange("E", "new.txt", "Conflicting line", ImmutableList.of(a.getCommit()));
    PushOneCommit.Result f =
        createChange(
            "F", "new.txt", "Resolved conflict", ImmutableList.of(b.getCommit(), e.getCommit()));
    PushOneCommit.Result g =
        createChange("G", "new.txt", "Conflicting line #2", ImmutableList.of(f.getCommit()));

    assertMergeable(e.getChange());

    approve(a.getChangeId());
    approve(b.getChangeId());
    submit(b.getChangeId());

    assertNotMergeable(e.getChange());
    assertMergeable(f.getChange());
    assertMergeable(g.getChange());

    approve(c.getChangeId());
    approve(d.getChangeId());
    submit(d.getChangeId());

    approve(e.getChangeId());
    approve(f.getChangeId());
    approve(g.getChangeId());

    assertNotMergeable(g.getChange());
    assertChangeSetMergeable(g.getChange(), false);
  }

  @Test
  public void resolvingMergeCommitWithTopics() throws Exception {
    /*
      Project1:
        A <- B <-- C <---
        ^    ^          |
        |    |          |
        E <- F* <- G <- L*

      G clashes with C, and F resolves the clashes between E and B.
      Later, L resolves the clashes between C and G.

      Project2:
        H <- I
        ^    ^
        |    |
        J <- K*

      J clashes with I, and K resolves all problems.
      G, K and L are in the same topic.
    */
    assume().that(isSubmitWholeTopicEnabled()).isTrue();

    String project1Name = name("Project1");
    String project2Name = name("Project2");
    gApi.projects().create(project1Name);
    gApi.projects().create(project2Name);
    TestRepository<InMemoryRepository> project1 = cloneProject(Project.nameKey(project1Name));
    TestRepository<InMemoryRepository> project2 = cloneProject(Project.nameKey(project2Name));

    PushOneCommit.Result a = createChange(project1, "A");
    PushOneCommit.Result b =
        createChange(project1, "B", "new.txt", "No conflict line", ImmutableList.of(a.getCommit()));
    PushOneCommit.Result c =
        createChange(
            project1, "C", "new.txt", "No conflict line #2", ImmutableList.of(b.getCommit()));

    approve(a.getChangeId());
    approve(b.getChangeId());
    approve(c.getChangeId());
    submit(c.getChangeId());

    PushOneCommit.Result e =
        createChange(project1, "E", "new.txt", "Conflicting line", ImmutableList.of(a.getCommit()));
    PushOneCommit.Result f =
        createChange(
            project1,
            "F",
            "new.txt",
            "Resolved conflict",
            ImmutableList.of(b.getCommit(), e.getCommit()));
    PushOneCommit.Result g =
        createChange(
            project1,
            "G",
            "new.txt",
            "Conflicting line #2",
            ImmutableList.of(f.getCommit()),
            "refs/for/master%topic=" + name("topic1"));

    PushOneCommit.Result h = createChange(project2, "H");
    PushOneCommit.Result i =
        createChange(project2, "I", "new.txt", "No conflict line", ImmutableList.of(h.getCommit()));
    PushOneCommit.Result j =
        createChange(project2, "J", "new.txt", "Conflicting line", ImmutableList.of(h.getCommit()));
    PushOneCommit.Result k =
        createChange(
            project2,
            "K",
            "new.txt",
            "Sadly conflicting topic-wise",
            ImmutableList.of(i.getCommit(), j.getCommit()),
            "refs/for/master%topic=" + name("topic1"));

    approve(h.getChangeId());
    approve(i.getChangeId());
    submit(i.getChangeId());

    approve(e.getChangeId());
    approve(f.getChangeId());
    approve(g.getChangeId());
    approve(j.getChangeId());
    approve(k.getChangeId());

    assertChangeSetMergeable(g.getChange(), false);
    assertChangeSetMergeable(k.getChange(), false);

    PushOneCommit.Result l =
        createChange(
            project1,
            "L",
            "new.txt",
            "Resolving conflicts again",
            ImmutableList.of(c.getCommit(), g.getCommit()),
            "refs/for/master%topic=" + name("topic1"));

    approve(l.getChangeId());
    assertChangeSetMergeable(l.getChange(), true);

    submit(l.getChangeId());
    assertMerged(c.getChangeId());
    assertMerged(g.getChangeId());
    assertMerged(k.getChangeId());
  }

  @Test
  public void resolvingMergeCommitAtEndOfChainAndNotUpToDate() throws Exception {
    /*
        A <-- B
         \
          C  <- D
           \   /
             E

        B is the target branch, and D should be merged with B, but one
        of C conflicts with B
    */

    PushOneCommit.Result a = createChange("A");
    PushOneCommit.Result b =
        createChange("B", "new.txt", "No conflict line", ImmutableList.of(a.getCommit()));

    approve(a.getChangeId());
    approve(b.getChangeId());
    submit(b.getChangeId());

    PushOneCommit.Result c =
        createChange("C", "new.txt", "Create conflicts", ImmutableList.of(a.getCommit()));
    PushOneCommit.Result e = createChange("E", ImmutableList.of(c.getCommit()));
    PushOneCommit.Result d =
        createChange(
            "D", "new.txt", "Resolves conflicts", ImmutableList.of(c.getCommit(), e.getCommit()));

    approve(c.getChangeId());
    approve(e.getChangeId());
    approve(d.getChangeId());
    assertNotMergeable(d.getChange());
    assertChangeSetMergeable(d.getChange(), false);
  }

  private void submit(String changeId) throws Exception {
    gApi.changes().id(changeId).current().submit();
  }

  private void assertChangeSetMergeable(ChangeData change, boolean expected)
      throws MissingObjectException, IncorrectObjectTypeException, IOException,
          PermissionBackendException {
    ChangeSet cs = mergeSuperSet.get().completeChangeSet(change.change(), user(admin));
    assertThat(submit.unmergeableChanges(cs).isEmpty()).isEqualTo(expected);
  }

  private void assertMergeable(ChangeData change) throws Exception {
    change.setMergeable(null);
    assertThat(change.isMergeable()).isTrue();
  }

  private void assertNotMergeable(ChangeData change) throws Exception {
    change.setMergeable(null);
    assertThat(change.isMergeable()).isFalse();
  }

  private void assertMerged(String changeId) throws Exception {
    assertThat(gApi.changes().id(changeId).get().status).isEqualTo(ChangeStatus.MERGED);
  }

  private PushOneCommit.Result createChange(
      TestRepository<?> repo,
      String subject,
      String fileName,
      String content,
      List<RevCommit> parents,
      String ref)
      throws Exception {
    PushOneCommit push = pushFactory.create(admin.newIdent(), repo, subject, fileName, content);

    if (!parents.isEmpty()) {
      push.setParents(parents);
    }

    PushOneCommit.Result result;
    if (fileName.isEmpty()) {
      result = push.execute(ref);
    } else {
      result = push.to(ref);
    }
    result.assertOkStatus();
    return result;
  }

  private PushOneCommit.Result createChange(TestRepository<?> repo, String subject)
      throws Exception {
    return createChange(repo, subject, "x", "x", new ArrayList<>(), "refs/for/master");
  }

  private PushOneCommit.Result createChange(
      TestRepository<?> repo,
      String subject,
      String fileName,
      String content,
      List<RevCommit> parents)
      throws Exception {
    return createChange(repo, subject, fileName, content, parents, "refs/for/master");
  }

  @Override
  protected PushOneCommit.Result createChange(String subject) throws Exception {
    return createChange(testRepo, subject, "", "", Collections.emptyList(), "refs/for/master");
  }

  private PushOneCommit.Result createChange(String subject, List<RevCommit> parents)
      throws Exception {
    return createChange(testRepo, subject, "", "", parents, "refs/for/master");
  }

  private PushOneCommit.Result createChange(
      String subject, String fileName, String content, List<RevCommit> parents) throws Exception {
    return createChange(testRepo, subject, fileName, content, parents, "refs/for/master");
  }
}
