// Copyright (C) 2014 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.gerrit.acceptance.testsuite.project.TestProjectUpdate.allowLabel;
import static com.google.gerrit.entities.Permission.forLabel;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.server.project.testing.TestLabels.label;
import static com.google.gerrit.server.project.testing.TestLabels.value;
import static org.junit.Assert.assertEquals;

import com.google.common.collect.ImmutableSet;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.entities.AccessSection;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.LabelId;
import com.google.gerrit.entities.LabelType;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeInput;
import com.google.gerrit.lifecycle.LifecycleManager;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AuthRequest;
import com.google.gerrit.server.change.LabelNormalizer.Result;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.meta.MetaDataUpdate;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.schema.SchemaCreator;
import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.gerrit.testing.InMemoryModule;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import org.eclipse.jgit.lib.Repository;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

/** Unit tests for {@link LabelNormalizer}. */
public class LabelNormalizerTest {
  @Inject private AccountManager accountManager;
  @Inject private AllProjectsName allProjects;
  @Inject private GitRepositoryManager repoManager;
  @Inject private IdentifiedUser.GenericFactory userFactory;
  @Inject private LabelNormalizer norm;
  @Inject private MetaDataUpdate.User metaDataUpdateFactory;
  @Inject private ProjectCache projectCache;
  @Inject private SchemaCreator schemaCreator;
  @Inject protected ThreadLocalRequestContext requestContext;
  @Inject private ChangeNotes.Factory changeNotesFactory;
  @Inject private ProjectConfig.Factory projectConfigFactory;
  @Inject private GerritApi gApi;
  @Inject private ProjectOperations projectOperations;
  @Inject private AuthRequest.Factory authRequestFactory;

  private LifecycleManager lifecycle;
  private Account.Id userId;
  private IdentifiedUser user;
  private Change change;
  private ChangeNotes notes;

  @Before
  public void setUpInjector() throws Exception {
    Injector injector = Guice.createInjector(new InMemoryModule());
    injector.injectMembers(this);
    lifecycle = new LifecycleManager();
    lifecycle.add(injector);
    lifecycle.start();

    schemaCreator.create();
    userId = accountManager.authenticate(authRequestFactory.createForUser("user")).getAccountId();
    user = userFactory.create(userId);

    requestContext.setContext(() -> user);

    configureProject();
    setUpChange();
  }

  private void configureProject() throws Exception {
    ProjectConfig pc = loadAllProjects();

    for (AccessSection sec : pc.getAccessSections()) {
      pc.upsertAccessSection(
          sec.getName(),
          updatedSection -> {
            for (String label : pc.getLabelSections().keySet()) {
              updatedSection.removePermission(forLabel(label));
            }
          });
    }
    LabelType lt =
        label(
            LabelId.VERIFIED, value(1, LabelId.VERIFIED), value(0, "No score"), value(-1, "Fails"));
    pc.upsertLabelType(lt);
    save(pc);
  }

  private void setUpChange() throws Exception {
    ChangeInput input = new ChangeInput();
    input.project = allProjects.get();
    input.branch = "master";
    input.newBranch = true;
    input.subject = "Test change";
    ChangeInfo info = gApi.changes().create(input).get();
    notes = changeNotesFactory.createChecked(allProjects, Change.id(info._number));
    change = notes.getChange();
  }

  @After
  public void tearDown() {
    if (lifecycle != null) {
      lifecycle.stop();
    }
    requestContext.setContext(null);
  }

  @Test
  public void noNormalizeByPermission() throws Exception {
    projectOperations
        .allProjectsForUpdate()
        .add(
            allowLabel(LabelId.CODE_REVIEW)
                .ref("refs/heads/*")
                .group(REGISTERED_USERS)
                .range(-1, 1))
        .add(allowLabel(LabelId.VERIFIED).ref("refs/heads/*").group(REGISTERED_USERS).range(-1, 1))
        .update();

    PatchSetApproval cr = psa(userId, LabelId.CODE_REVIEW, 2);
    PatchSetApproval v = psa(userId, LabelId.VERIFIED, 1);
    assertEquals(Result.create(set(cr, v), set(), set()), norm.normalize(notes, set(cr, v)));
  }

  @Test
  public void normalizeByType() throws Exception {
    projectOperations
        .allProjectsForUpdate()
        .add(
            allowLabel(LabelId.CODE_REVIEW)
                .ref("refs/heads/*")
                .group(REGISTERED_USERS)
                .range(-5, 5))
        .add(allowLabel(LabelId.VERIFIED).ref("refs/heads/*").group(REGISTERED_USERS).range(-5, 5))
        .update();

    PatchSetApproval cr = psa(userId, LabelId.CODE_REVIEW, 5);
    PatchSetApproval v = psa(userId, LabelId.VERIFIED, 5);
    assertEquals(
        Result.create(set(), set(copy(cr, 2), copy(v, 1)), set()),
        norm.normalize(notes, set(cr, v)));
  }

  @Test
  public void emptyPermissionRangeKeepsResult() throws Exception {
    PatchSetApproval cr = psa(userId, LabelId.CODE_REVIEW, 1);
    PatchSetApproval v = psa(userId, LabelId.VERIFIED, 1);
    assertEquals(Result.create(set(cr, v), set(), set()), norm.normalize(notes, set(cr, v)));
  }

  @Test
  public void explicitZeroVoteOnNonEmptyRangeIsPresent() throws Exception {
    projectOperations
        .allProjectsForUpdate()
        .add(
            allowLabel(LabelId.CODE_REVIEW)
                .ref("refs/heads/*")
                .group(REGISTERED_USERS)
                .range(-1, 1))
        .update();

    PatchSetApproval cr = psa(userId, LabelId.CODE_REVIEW, 0);
    PatchSetApproval v = psa(userId, LabelId.VERIFIED, 0);
    assertEquals(Result.create(set(cr, v), set(), set()), norm.normalize(notes, set(cr, v)));
  }

  private ProjectConfig loadAllProjects() throws Exception {
    try (Repository repo = repoManager.openRepository(allProjects)) {
      ProjectConfig pc = projectConfigFactory.create(allProjects);
      pc.load(repo);
      return pc;
    }
  }

  private void save(ProjectConfig pc) throws Exception {
    try (MetaDataUpdate md = metaDataUpdateFactory.create(pc.getProject().getNameKey(), user)) {
      pc.commit(md);
      projectCache.evictAndReindex(pc.getProject().getNameKey());
    }
  }

  private PatchSetApproval psa(Account.Id accountId, String label, int value) {
    return PatchSetApproval.builder()
        .key(PatchSetApproval.key(change.currentPatchSetId(), accountId, LabelId.create(label)))
        .value(value)
        .granted(TimeUtil.now())
        .build();
  }

  private PatchSetApproval copy(PatchSetApproval src, int newValue) {
    return src.toBuilder().value(newValue).build();
  }

  private static ImmutableSet<PatchSetApproval> set(PatchSetApproval... psas) {
    return ImmutableSet.copyOf(psas);
  }
}
