// 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.acceptance.api.change;

import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.extensions.client.SubmitType.CHERRY_PICK;
import static com.google.gerrit.extensions.client.SubmitType.FAST_FORWARD_ONLY;
import static com.google.gerrit.extensions.client.SubmitType.MERGE_ALWAYS;
import static com.google.gerrit.extensions.client.SubmitType.MERGE_IF_NECESSARY;
import static com.google.gerrit.extensions.client.SubmitType.REBASE_ALWAYS;
import static com.google.gerrit.extensions.client.SubmitType.REBASE_IF_NECESSARY;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;

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.Change;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.TestSubmitRuleInfo;
import com.google.gerrit.extensions.common.TestSubmitRuleInput;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
import com.google.gerrit.server.git.meta.VersionedMetaData;
import com.google.gerrit.testing.ConfigSuite;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.Before;
import org.junit.Test;

@NoHttpd
public class SubmitTypeRuleIT extends AbstractDaemonTest {
  @ConfigSuite.Default
  public static Config submitWholeTopicEnabled() {
    return submitWholeTopicEnabledConfig();
  }

  private class RulesPl extends VersionedMetaData {
    private static final String FILENAME = "rules.pl";

    private String rule;

    @Override
    protected String getRefName() {
      return RefNames.REFS_CONFIG;
    }

    @Override
    protected void onLoad() throws IOException, ConfigInvalidException {
      rule = readUTF8(FILENAME);
    }

    @Override
    protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
      TestSubmitRuleInput in = new TestSubmitRuleInput();
      in.rule = rule;
      try {
        gApi.changes().id(testChangeId.get()).current().testSubmitType(in);
      } catch (RestApiException e) {
        throw new ConfigInvalidException("Invalid submit type rule", e);
      }

      saveUTF8(FILENAME, rule);
      return true;
    }
  }

  private AtomicInteger fileCounter;
  private Change.Id testChangeId;

  @Before
  public void setUp() throws Exception {
    fileCounter = new AtomicInteger();
    gApi.projects().name(project.get()).branch("test").create(new BranchInput());
    testChangeId = createChange("test", "test change").getChange().getId();
  }

  private void setRulesPl(String rule) throws Exception {
    try (MetaDataUpdate md = metaDataUpdateFactory.create(project)) {
      RulesPl r = new RulesPl();
      r.load(md);
      r.rule = rule;
      r.commit(md);
    }
    projectCache.evict(project);
  }

  private static final String SUBMIT_TYPE_FROM_SUBJECT =
      "submit_type(fast_forward_only) :-"
          + "gerrit:commit_message(M),"
          + "regex_matches('.*FAST_FORWARD_ONLY.*', M),"
          + "!.\n"
          + "submit_type(merge_if_necessary) :-"
          + "gerrit:commit_message(M),"
          + "regex_matches('.*MERGE_IF_NECESSARY.*', M),"
          + "!.\n"
          + "submit_type(rebase_if_necessary) :-"
          + "gerrit:commit_message(M),"
          + "regex_matches('.*REBASE_IF_NECESSARY.*', M),"
          + "!.\n"
          + "submit_type(rebase_always) :-"
          + "gerrit:commit_message(M),"
          + "regex_matches('.*REBASE_ALWAYS.*', M),"
          + "!.\n"
          + "submit_type(merge_always) :-"
          + "gerrit:commit_message(M),"
          + "regex_matches('.*MERGE_ALWAYS.*', M),"
          + "!.\n"
          + "submit_type(cherry_pick) :-"
          + "gerrit:commit_message(M),"
          + "regex_matches('.*CHERRY_PICK.*', M),"
          + "!.\n"
          + "submit_type(T) :- gerrit:project_default_submit_type(T).";

  private PushOneCommit.Result createChange(String dest, String subject) throws Exception {
    PushOneCommit push =
        pushFactory.create(
            admin.newIdent(),
            testRepo,
            subject,
            "file" + fileCounter.incrementAndGet(),
            PushOneCommit.FILE_CONTENT);
    PushOneCommit.Result r = push.to("refs/for/" + dest);
    r.assertOkStatus();
    return r;
  }

  @Test
  public void unconditionalCherryPick() throws Exception {
    PushOneCommit.Result r = createChange();
    assertSubmitType(MERGE_IF_NECESSARY, r.getChangeId());
    setRulesPl("submit_type(cherry_pick).");
    assertSubmitType(CHERRY_PICK, r.getChangeId());
  }

  @Test
  public void submitTypeFromSubject() throws Exception {
    PushOneCommit.Result r1 = createChange("master", "Default 1");
    PushOneCommit.Result r2 = createChange("master", "FAST_FORWARD_ONLY 2");
    PushOneCommit.Result r3 = createChange("master", "MERGE_IF_NECESSARY 3");
    PushOneCommit.Result r4 = createChange("master", "REBASE_IF_NECESSARY 4");
    PushOneCommit.Result r5 = createChange("master", "REBASE_ALWAYS 5");
    PushOneCommit.Result r6 = createChange("master", "MERGE_ALWAYS 6");
    PushOneCommit.Result r7 = createChange("master", "CHERRY_PICK 7");

    assertSubmitType(MERGE_IF_NECESSARY, r1.getChangeId());
    assertSubmitType(MERGE_IF_NECESSARY, r2.getChangeId());
    assertSubmitType(MERGE_IF_NECESSARY, r3.getChangeId());
    assertSubmitType(MERGE_IF_NECESSARY, r4.getChangeId());
    assertSubmitType(MERGE_IF_NECESSARY, r5.getChangeId());
    assertSubmitType(MERGE_IF_NECESSARY, r6.getChangeId());
    assertSubmitType(MERGE_IF_NECESSARY, r7.getChangeId());

    setRulesPl(SUBMIT_TYPE_FROM_SUBJECT);

    assertSubmitType(MERGE_IF_NECESSARY, r1.getChangeId());
    assertSubmitType(FAST_FORWARD_ONLY, r2.getChangeId());
    assertSubmitType(MERGE_IF_NECESSARY, r3.getChangeId());
    assertSubmitType(REBASE_IF_NECESSARY, r4.getChangeId());
    assertSubmitType(REBASE_ALWAYS, r5.getChangeId());
    assertSubmitType(MERGE_ALWAYS, r6.getChangeId());
    assertSubmitType(CHERRY_PICK, r7.getChangeId());
  }

  @Test
  public void submitTypeIsUsedForSubmit() throws Exception {
    setRulesPl(SUBMIT_TYPE_FROM_SUBJECT);

    PushOneCommit.Result r = createChange("master", "CHERRY_PICK 1");

    gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
    gApi.changes().id(r.getChangeId()).current().submit();

    List<RevCommit> log = log("master", 1);
    assertThat(log.get(0).getShortMessage()).isEqualTo("CHERRY_PICK 1");
    assertThat(log.get(0).name()).isNotEqualTo(r.getCommit().name());
    assertThat(log.get(0).getFullMessage()).contains("Change-Id: " + r.getChangeId());
    assertThat(log.get(0).getFullMessage()).contains("Reviewed-on: ");
  }

  @Test
  public void mixingSubmitTypesAcrossBranchesSucceeds() throws Exception {
    setRulesPl(SUBMIT_TYPE_FROM_SUBJECT);

    PushOneCommit.Result r1 = createChange("master", "MERGE_IF_NECESSARY 1");

    RevCommit initialCommit = r1.getCommit().getParent(0);
    BranchInput bin = new BranchInput();
    bin.revision = initialCommit.name();
    gApi.projects().name(project.get()).branch("branch").create(bin);

    testRepo.reset(initialCommit);
    PushOneCommit.Result r2 = createChange("branch", "MERGE_ALWAYS 1");

    gApi.changes().id(r1.getChangeId()).topic(name("topic"));
    gApi.changes().id(r1.getChangeId()).current().review(ReviewInput.approve());
    gApi.changes().id(r2.getChangeId()).topic(name("topic"));
    gApi.changes().id(r2.getChangeId()).current().review(ReviewInput.approve());
    gApi.changes().id(r2.getChangeId()).current().submit();

    assertThat(log("master", 1).get(0).name()).isEqualTo(r1.getCommit().name());

    List<RevCommit> branchLog = log("branch", 1);
    assertThat(branchLog.get(0).getParents()).hasLength(2);
    assertThat(branchLog.get(0).getParent(1).name()).isEqualTo(r2.getCommit().name());
  }

  @Test
  public void mixingSubmitTypesOnOneBranchFails() throws Exception {
    setRulesPl(SUBMIT_TYPE_FROM_SUBJECT);

    PushOneCommit.Result r1 = createChange("master", "CHERRY_PICK 1");
    PushOneCommit.Result r2 = createChange("master", "MERGE_IF_NECESSARY 2");

    gApi.changes().id(r1.getChangeId()).current().review(ReviewInput.approve());
    gApi.changes().id(r2.getChangeId()).current().review(ReviewInput.approve());

    ResourceConflictException thrown =
        assertThrows(
            ResourceConflictException.class,
            () -> gApi.changes().id(r2.getChangeId()).current().submit());
    assertThat(thrown)
        .hasMessageThat()
        .isEqualTo(
            "Failed to submit 2 changes due to the following problems:\n"
                + "Change "
                + r1.getChange().getId()
                + ": Change has submit type "
                + "CHERRY_PICK, but previously chose submit type MERGE_IF_NECESSARY "
                + "from change "
                + r2.getChange().getId()
                + " in the same batch");
  }

  @Test
  public void invalidSubmitRuleWithNoRulesInProject() throws Exception {
    String changeId = createChange("master", "change 1").getChangeId();

    TestSubmitRuleInput in = new TestSubmitRuleInput();
    in.rule = "invalid prolog rule";
    // We have no rules.pl by default. The fact that the default rules are showing up here is a bug.
    TestSubmitRuleInfo response = gApi.changes().id(changeId).current().testSubmitRule(in);
    assertThat(response).isEqualTo(invalidPrologRuleInfo());
  }

  @Test
  public void invalidSubmitRuleWithRulesInProject() throws Exception {
    setRulesPl(SUBMIT_TYPE_FROM_SUBJECT);

    String changeId = createChange("master", "change 1").getChangeId();

    TestSubmitRuleInput in = new TestSubmitRuleInput();
    in.rule = "invalid prolog rule";
    TestSubmitRuleInfo response = gApi.changes().id(changeId).current().testSubmitRule(in);
    assertThat(response).isEqualTo(invalidPrologRuleInfo());
  }

  private static TestSubmitRuleInfo invalidPrologRuleInfo() {
    TestSubmitRuleInfo info = new TestSubmitRuleInfo();
    info.status = "RULE_ERROR";
    info.errorMessage = "operator expected after expression at: invalid prolog rule end_of_file.";
    return info;
  }

  private List<RevCommit> log(String commitish, int n) throws Exception {
    try (Repository repo = repoManager.openRepository(project);
        Git git = new Git(repo)) {
      ObjectId id = repo.resolve(commitish);
      assertThat(id).isNotNull();
      return ImmutableList.copyOf(git.log().add(id).setMaxCount(n).call());
    }
  }

  private void assertSubmitType(SubmitType expected, String id) throws Exception {
    assertThat(gApi.changes().id(id).current().submitType()).isEqualTo(expected);
  }
}
