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

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

import com.google.common.collect.ImmutableSet;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.NotifyConfig.NotifyType;
import com.google.gerrit.entities.Project;
import com.google.gerrit.server.account.ProjectWatches.NotifyValue;
import com.google.gerrit.server.account.ProjectWatches.ProjectWatchKey;
import com.google.gerrit.server.git.ValidationError;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jgit.lib.Config;
import org.junit.Before;
import org.junit.Test;

public class WatchConfigTest implements ValidationError.Sink {
  private List<ValidationError> validationErrors = new ArrayList<>();

  @Before
  public void setup() {
    validationErrors.clear();
  }

  @Test
  public void parseWatchConfig() throws Exception {
    Config cfg = new Config();
    cfg.fromText(
        "[project \"myProject\"]\n"
            + "  notify = * [ALL_COMMENTS, NEW_PATCHSETS]\n"
            + "  notify = branch:master [NEW_CHANGES]\n"
            + "  notify = branch:master [NEW_PATCHSETS]\n"
            + "  notify = branch:foo []\n"
            + "[project \"otherProject\"]\n"
            + "  notify = [NEW_PATCHSETS]\n"
            + "  notify = * [NEW_PATCHSETS, ALL_COMMENTS]\n");
    Map<ProjectWatchKey, ImmutableSet<NotifyType>> projectWatches =
        ProjectWatches.parse(Account.id(1000000), cfg, this);

    assertThat(validationErrors).isEmpty();

    Project.NameKey myProject = Project.nameKey("myProject");
    Project.NameKey otherProject = Project.nameKey("otherProject");
    Map<ProjectWatchKey, Set<NotifyType>> expectedProjectWatches = new HashMap<>();
    expectedProjectWatches.put(
        ProjectWatchKey.create(myProject, null),
        EnumSet.of(NotifyType.ALL_COMMENTS, NotifyType.NEW_PATCHSETS));
    expectedProjectWatches.put(
        ProjectWatchKey.create(myProject, "branch:master"),
        EnumSet.of(NotifyType.NEW_CHANGES, NotifyType.NEW_PATCHSETS));
    expectedProjectWatches.put(
        ProjectWatchKey.create(myProject, "branch:foo"), EnumSet.noneOf(NotifyType.class));
    expectedProjectWatches.put(
        ProjectWatchKey.create(otherProject, null), EnumSet.of(NotifyType.NEW_PATCHSETS));
    expectedProjectWatches.put(
        ProjectWatchKey.create(otherProject, null),
        EnumSet.of(NotifyType.ALL_COMMENTS, NotifyType.NEW_PATCHSETS));
    assertThat(projectWatches).containsExactlyEntriesIn(expectedProjectWatches);
  }

  @Test
  public void parseInvalidWatchConfig() throws Exception {
    Config cfg = new Config();
    cfg.fromText(
        "[project \"myProject\"]\n"
            + "  notify = * [ALL_COMMENTS, NEW_PATCHSETS]\n"
            + "  notify = branch:master [INVALID, NEW_CHANGES]\n"
            + "[project \"otherProject\"]\n"
            + "  notify = [NEW_PATCHSETS]\n");

    ProjectWatches.parse(Account.id(1000000), cfg, this);
    assertThat(validationErrors).hasSize(1);
    assertThat(validationErrors.get(0).getMessage())
        .isEqualTo(
            "watch.config: Invalid notify type INVALID in project watch of"
                + " account 1000000 for project myProject: branch:master"
                + " [INVALID, NEW_CHANGES]");
  }

  @Test
  public void parseNotifyValue() throws Exception {
    assertParseNotifyValue("* []", null, EnumSet.noneOf(NotifyType.class));
    assertParseNotifyValue("* [ALL_COMMENTS]", null, EnumSet.of(NotifyType.ALL_COMMENTS));
    assertParseNotifyValue("[]", null, EnumSet.noneOf(NotifyType.class));
    assertParseNotifyValue(
        "[ALL_COMMENTS, NEW_PATCHSETS]",
        null,
        EnumSet.of(NotifyType.ALL_COMMENTS, NotifyType.NEW_PATCHSETS));
    assertParseNotifyValue("branch:master []", "branch:master", EnumSet.noneOf(NotifyType.class));
    assertParseNotifyValue(
        "branch:master || branch:stable []",
        "branch:master || branch:stable",
        EnumSet.noneOf(NotifyType.class));
    assertParseNotifyValue(
        "branch:master [ALL_COMMENTS]", "branch:master", EnumSet.of(NotifyType.ALL_COMMENTS));
    assertParseNotifyValue(
        "branch:master [ALL_COMMENTS, NEW_PATCHSETS]",
        "branch:master",
        EnumSet.of(NotifyType.ALL_COMMENTS, NotifyType.NEW_PATCHSETS));
    assertParseNotifyValue("* [ALL]", null, EnumSet.of(NotifyType.ALL));

    assertThat(validationErrors).isEmpty();
  }

  @Test
  public void parseInvalidNotifyValue() {
    assertParseNotifyValueFails("* [] illegal-characters-at-the-end");
    assertParseNotifyValueFails("* [INVALID]");
    assertParseNotifyValueFails("* [ALL_COMMENTS, UNKNOWN]");
    assertParseNotifyValueFails("* [ALL_COMMENTS NEW_CHANGES]");
    assertParseNotifyValueFails("* [ALL_COMMENTS, NEW_CHANGES");
    assertParseNotifyValueFails("* ALL_COMMENTS, NEW_CHANGES]");
  }

  @Test
  public void toNotifyValue() throws Exception {
    assertToNotifyValue(null, EnumSet.noneOf(NotifyType.class), "* []");
    assertToNotifyValue("*", EnumSet.noneOf(NotifyType.class), "* []");
    assertToNotifyValue(null, EnumSet.of(NotifyType.ALL_COMMENTS), "* [ALL_COMMENTS]");
    assertToNotifyValue("branch:master", EnumSet.noneOf(NotifyType.class), "branch:master []");
    assertToNotifyValue(
        "branch:master",
        EnumSet.of(NotifyType.ALL_COMMENTS, NotifyType.NEW_PATCHSETS),
        "branch:master [ALL_COMMENTS, NEW_PATCHSETS]");
    assertToNotifyValue(
        "branch:master",
        EnumSet.of(
            NotifyType.ABANDONED_CHANGES,
            NotifyType.ALL_COMMENTS,
            NotifyType.NEW_CHANGES,
            NotifyType.NEW_PATCHSETS,
            NotifyType.SUBMITTED_CHANGES),
        "branch:master [ABANDONED_CHANGES, ALL_COMMENTS, NEW_CHANGES,"
            + " NEW_PATCHSETS, SUBMITTED_CHANGES]");
    assertToNotifyValue("*", EnumSet.of(NotifyType.ALL), "* [ALL]");
  }

  private void assertParseNotifyValue(
      String notifyValue, String expectedFilter, Set<NotifyType> expectedNotifyTypes) {
    NotifyValue nv = parseNotifyValue(notifyValue);
    assertThat(nv.filter()).isEqualTo(expectedFilter);
    assertThat(nv.notifyTypes()).containsExactlyElementsIn(expectedNotifyTypes);
  }

  private static void assertToNotifyValue(
      String filter, Set<NotifyType> notifyTypes, String expectedNotifyValue) {
    NotifyValue nv = NotifyValue.create(filter, notifyTypes);
    assertThat(nv.toString()).isEqualTo(expectedNotifyValue);
  }

  private void assertParseNotifyValueFails(String notifyValue) {
    assertThat(validationErrors).isEmpty();
    parseNotifyValue(notifyValue);
    assertWithMessage("expected validation error for notifyValue: " + notifyValue)
        .that(validationErrors)
        .isNotEmpty();
    validationErrors.clear();
  }

  private NotifyValue parseNotifyValue(String notifyValue) {
    return NotifyValue.parse(Account.id(1000000), "project", notifyValue, this);
  }

  @Override
  public void error(ValidationError error) {
    validationErrors.add(error);
  }
}
