// Copyright (C) 2019 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.events;

import static com.google.common.truth.Truth.assertThat;
import static com.google.gerrit.entities.Change.Status.NEW;

import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.truth.MapSubject;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.Project;
import com.google.gerrit.server.data.AccountAttribute;
import com.google.gerrit.server.data.ChangeAttribute;
import com.google.gerrit.server.data.RefUpdateAttribute;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.gerrit.testing.TestTimeUtil;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.junit.Before;
import org.junit.Test;

public class EventJsonTest {
  private static final String BRANCH = "mybranch";
  private static final String CHANGE_ID = "Ideadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
  private static final int CHANGE_NUM = 1000;
  private static final double CHANGE_NUM_DOUBLE = CHANGE_NUM;
  private static final String COMMIT_MESSAGE = "This is a test commit message";
  private static final String PROJECT = "myproject";
  private static final String REF = "refs/heads/" + BRANCH;
  private static final double TS1 = 1.2543444E9;
  private static final double TS2 = 1.254344401E9;
  private static final String URL = "http://somewhere.com";

  private final Gson gson = new EventGsonProvider().get();

  static class CustomEvent extends Event {
    static String TYPE = "custom-type";

    public String customField;

    protected CustomEvent() {
      super(TYPE);
    }
  }

  @Before
  public void setTimeForTesting() {
    TestTimeUtil.resetWithClockStep(1, TimeUnit.SECONDS);
  }

  @Test
  public void customEvent() {
    CustomEvent event = new CustomEvent();
    event.customField = "customValue";
    String json = gson.toJson(event);
    CustomEvent resullt = gson.fromJson(json, CustomEvent.class);
    assertThat(resullt.type).isEqualTo(CustomEvent.TYPE);
    assertThat(resullt.customField).isEqualTo(event.customField);
  }

  @Test
  public void customEventSimulateClassloaderIssue() {
    EventTypes.register(CustomEvent.TYPE, CustomEvent.class);
    CustomEvent event = new CustomEvent();
    event.customField = "customValue";
    // Need to serialise using the Event interface instead of json.getClass()
    // for simulating the serialisation of an object owned by another class loader
    String json = gson.toJson(event, Event.class);
    CustomEvent resullt = gson.fromJson(json, CustomEvent.class);
    assertThat(resullt.type).isEqualTo(CustomEvent.TYPE);
    assertThat(resullt.customField).isEqualTo(event.customField);
  }

  @Test
  public void refUpdatedEvent() {
    RefUpdatedEvent event = new RefUpdatedEvent();

    RefUpdateAttribute refUpdatedAttribute = new RefUpdateAttribute();
    refUpdatedAttribute.refName = REF;
    event.refUpdate = createSupplier(refUpdatedAttribute);
    event.submitter = newAccount("submitter");

    assertThatJsonMap(event)
        .isEqualTo(
            ImmutableMap.builder()
                .put(
                    "submitter",
                    ImmutableMap.builder()
                        .put("name", event.submitter.get().name)
                        .put("email", event.submitter.get().email)
                        .put("username", event.submitter.get().username)
                        .build())
                .put("refUpdate", ImmutableMap.of("refName", REF))
                .put("type", "ref-updated")
                .put("eventCreatedOn", TS1)
                .build());
  }

  @Test
  public void batchRefUpdatedEvent() {
    BatchRefUpdateEvent event = new BatchRefUpdateEvent();
    String patchsetRefName = "refs/changes/01/1";
    String metaRefName = "refs/changes/01/meta";

    RefUpdateAttribute refUpdatedAttribute = new RefUpdateAttribute();
    refUpdatedAttribute.refName = patchsetRefName;
    RefUpdateAttribute metaRefUpdatedAttribute = new RefUpdateAttribute();
    metaRefUpdatedAttribute.refName = metaRefName;
    event.refUpdates =
        createSupplier(ImmutableList.of(refUpdatedAttribute, metaRefUpdatedAttribute));
    event.submitter = newAccount("submitter");

    assertThatJsonMap(event)
        .isEqualTo(
            ImmutableMap.builder()
                .put(
                    "submitter",
                    ImmutableMap.builder()
                        .put("name", event.submitter.get().name)
                        .put("email", event.submitter.get().email)
                        .put("username", event.submitter.get().username)
                        .build())
                .put(
                    "refUpdates",
                    ImmutableList.of(
                        ImmutableMap.of("refName", patchsetRefName),
                        ImmutableMap.of("refName", metaRefName)))
                .put("type", "batch-ref-updated")
                .put("eventCreatedOn", TS1)
                .build());
  }

  @Test
  public void patchSetCreatedEvent() {
    Change change = newChange();
    PatchSetCreatedEvent event = new PatchSetCreatedEvent(change);
    event.change = asChangeAttribute(change);
    event.uploader = newAccount("uploader");

    assertThatJsonMap(event)
        .isEqualTo(
            ImmutableMap.builder()
                .put(
                    "uploader",
                    ImmutableMap.builder()
                        .put("name", event.uploader.get().name)
                        .put("email", event.uploader.get().email)
                        .put("username", event.uploader.get().username)
                        .build())
                .put(
                    "change",
                    ImmutableMap.builder()
                        .put("project", PROJECT)
                        .put("branch", BRANCH)
                        .put("id", CHANGE_ID)
                        .put("number", CHANGE_NUM_DOUBLE)
                        .put("url", URL)
                        .put("commitMessage", COMMIT_MESSAGE)
                        .put("createdOn", TS1)
                        .put("status", NEW.name())
                        .build())
                .put("project", PROJECT)
                .put("refName", REF)
                .put("changeKey", map("id", CHANGE_ID))
                .put("type", "patchset-created")
                .put("eventCreatedOn", TS2)
                .build());
  }

  @Test
  public void changeDeletedEvent() {
    Change change = newChange();
    ChangeDeletedEvent event = new ChangeDeletedEvent(change);
    event.change = asChangeAttribute(change);
    event.deleter = newAccount("deleter");

    assertThatJsonMap(event)
        .isEqualTo(
            ImmutableMap.builder()
                .put(
                    "deleter",
                    ImmutableMap.builder()
                        .put("name", event.deleter.get().name)
                        .put("email", event.deleter.get().email)
                        .put("username", event.deleter.get().username)
                        .build())
                .put(
                    "change",
                    ImmutableMap.builder()
                        .put("project", PROJECT)
                        .put("branch", BRANCH)
                        .put("id", CHANGE_ID)
                        .put("number", CHANGE_NUM_DOUBLE)
                        .put("url", URL)
                        .put("commitMessage", COMMIT_MESSAGE)
                        .put("createdOn", TS1)
                        .put("status", NEW.name())
                        .build())
                .put("project", PROJECT)
                .put("refName", REF)
                .put("changeKey", map("id", CHANGE_ID))
                .put("type", "change-deleted")
                .put("eventCreatedOn", TS2)
                .build());
  }

  @Test
  public void hashtagsChangedEvent() {
    Change change = newChange();
    HashtagsChangedEvent event = new HashtagsChangedEvent(change);
    event.change = asChangeAttribute(change);
    event.editor = newAccount("editor");
    event.added = new String[] {"added"};
    event.removed = new String[] {"removed"};
    event.hashtags = new String[] {"hashtags"};

    assertThatJsonMap(event)
        .isEqualTo(
            ImmutableMap.builder()
                .put(
                    "editor",
                    ImmutableMap.builder()
                        .put("name", event.editor.get().name)
                        .put("email", event.editor.get().email)
                        .put("username", event.editor.get().username)
                        .build())
                .put("added", list("added"))
                .put("removed", list("removed"))
                .put("hashtags", list("hashtags"))
                .put(
                    "change",
                    ImmutableMap.builder()
                        .put("project", PROJECT)
                        .put("branch", BRANCH)
                        .put("id", CHANGE_ID)
                        .put("number", CHANGE_NUM_DOUBLE)
                        .put("url", URL)
                        .put("commitMessage", COMMIT_MESSAGE)
                        .put("createdOn", TS1)
                        .put("status", NEW.name())
                        .build())
                .put("project", PROJECT)
                .put("refName", REF)
                .put("changeKey", map("id", CHANGE_ID))
                .put("type", "hashtags-changed")
                .put("eventCreatedOn", TS2)
                .build());
  }

  @Test
  public void changeAbandonedEvent() {
    Change change = newChange();
    ChangeAbandonedEvent event = new ChangeAbandonedEvent(change);
    event.change = asChangeAttribute(change);
    event.abandoner = newAccount("abandoner");
    event.reason = "some reason";

    assertThatJsonMap(event)
        .isEqualTo(
            ImmutableMap.builder()
                .put(
                    "abandoner",
                    ImmutableMap.builder()
                        .put("name", event.abandoner.get().name)
                        .put("email", event.abandoner.get().email)
                        .put("username", event.abandoner.get().username)
                        .build())
                .put("reason", "some reason")
                .put(
                    "change",
                    ImmutableMap.builder()
                        .put("project", PROJECT)
                        .put("branch", BRANCH)
                        .put("id", CHANGE_ID)
                        .put("number", CHANGE_NUM_DOUBLE)
                        .put("url", URL)
                        .put("commitMessage", COMMIT_MESSAGE)
                        .put("createdOn", TS1)
                        .put("status", NEW.name())
                        .build())
                .put("project", PROJECT)
                .put("refName", REF)
                .put("changeKey", map("id", CHANGE_ID))
                .put("type", "change-abandoned")
                .put("eventCreatedOn", TS2)
                .build());
  }

  @Test
  public void changeMergedEvent() {
    Change change = newChange();
    ChangeMergedEvent event = new ChangeMergedEvent(change);
    event.change = asChangeAttribute(change);

    assertThatJsonMap(event)
        .isEqualTo(
            ImmutableMap.builder()
                .put(
                    "change",
                    ImmutableMap.builder()
                        .put("project", PROJECT)
                        .put("branch", BRANCH)
                        .put("id", CHANGE_ID)
                        .put("number", CHANGE_NUM_DOUBLE)
                        .put("url", URL)
                        .put("commitMessage", COMMIT_MESSAGE)
                        .put("createdOn", TS1)
                        .put("status", NEW.name())
                        .build())
                .put("project", PROJECT)
                .put("refName", REF)
                .put("changeKey", map("id", CHANGE_ID))
                .put("type", "change-merged")
                .put("eventCreatedOn", TS2)
                .build());
  }

  @Test
  public void changeRestoredEvent() {
    Change change = newChange();
    ChangeRestoredEvent event = new ChangeRestoredEvent(change);
    event.change = asChangeAttribute(change);

    assertThatJsonMap(event)
        .isEqualTo(
            ImmutableMap.builder()
                .put(
                    "change",
                    ImmutableMap.builder()
                        .put("project", PROJECT)
                        .put("branch", BRANCH)
                        .put("id", CHANGE_ID)
                        .put("number", CHANGE_NUM_DOUBLE)
                        .put("url", URL)
                        .put("commitMessage", COMMIT_MESSAGE)
                        .put("createdOn", TS1)
                        .put("status", NEW.name())
                        .build())
                .put("project", PROJECT)
                .put("refName", REF)
                .put("changeKey", map("id", CHANGE_ID))
                .put("type", "change-restored")
                .put("eventCreatedOn", TS2)
                .build());
  }

  @Test
  public void commentAddedEvent() {
    Change change = newChange();
    CommentAddedEvent event = new CommentAddedEvent(change);
    event.change = asChangeAttribute(change);

    assertThatJsonMap(event)
        .isEqualTo(
            ImmutableMap.builder()
                .put(
                    "change",
                    ImmutableMap.builder()
                        .put("project", PROJECT)
                        .put("branch", BRANCH)
                        .put("id", CHANGE_ID)
                        .put("number", CHANGE_NUM_DOUBLE)
                        .put("url", URL)
                        .put("commitMessage", COMMIT_MESSAGE)
                        .put("createdOn", TS1)
                        .put("status", NEW.name())
                        .build())
                .put("project", PROJECT)
                .put("refName", REF)
                .put("changeKey", map("id", CHANGE_ID))
                .put("type", "comment-added")
                .put("eventCreatedOn", TS2)
                .build());
  }

  @Test
  public void privateStateChangedEvent() {
    Change change = newChange();
    PrivateStateChangedEvent event = new PrivateStateChangedEvent(change);
    event.change = asChangeAttribute(change);

    assertThatJsonMap(event)
        .isEqualTo(
            ImmutableMap.builder()
                .put(
                    "change",
                    ImmutableMap.builder()
                        .put("project", PROJECT)
                        .put("branch", BRANCH)
                        .put("id", CHANGE_ID)
                        .put("number", CHANGE_NUM_DOUBLE)
                        .put("url", URL)
                        .put("commitMessage", COMMIT_MESSAGE)
                        .put("createdOn", TS1)
                        .put("status", NEW.name())
                        .build())
                .put("project", PROJECT)
                .put("refName", REF)
                .put("changeKey", map("id", CHANGE_ID))
                .put("type", "private-state-changed")
                .put("eventCreatedOn", TS2)
                .build());
  }

  @Test
  public void reviewerAddedEvent() {
    Change change = newChange();
    ReviewerAddedEvent event = new ReviewerAddedEvent(change);
    event.change = asChangeAttribute(change);

    assertThatJsonMap(event)
        .isEqualTo(
            ImmutableMap.builder()
                .put(
                    "change",
                    ImmutableMap.builder()
                        .put("project", PROJECT)
                        .put("branch", BRANCH)
                        .put("id", CHANGE_ID)
                        .put("number", CHANGE_NUM_DOUBLE)
                        .put("url", URL)
                        .put("commitMessage", COMMIT_MESSAGE)
                        .put("createdOn", TS1)
                        .put("status", NEW.name())
                        .build())
                .put("project", PROJECT)
                .put("refName", REF)
                .put("changeKey", map("id", CHANGE_ID))
                .put("type", "reviewer-added")
                .put("eventCreatedOn", TS2)
                .build());
  }

  @Test
  public void reviewerDeletedEvent() {
    Change change = newChange();
    ReviewerDeletedEvent event = new ReviewerDeletedEvent(change);
    event.change = asChangeAttribute(change);

    assertThatJsonMap(event)
        .isEqualTo(
            ImmutableMap.builder()
                .put(
                    "change",
                    ImmutableMap.builder()
                        .put("project", PROJECT)
                        .put("branch", BRANCH)
                        .put("id", CHANGE_ID)
                        .put("number", CHANGE_NUM_DOUBLE)
                        .put("url", URL)
                        .put("commitMessage", COMMIT_MESSAGE)
                        .put("createdOn", TS1)
                        .put("status", NEW.name())
                        .build())
                .put("project", PROJECT)
                .put("refName", REF)
                .put("changeKey", map("id", CHANGE_ID))
                .put("type", "reviewer-deleted")
                .put("eventCreatedOn", TS2)
                .build());
  }

  @Test
  public void voteDeletedEvent() {
    Change change = newChange();
    VoteDeletedEvent event = new VoteDeletedEvent(change);
    event.change = asChangeAttribute(change);

    assertThatJsonMap(event)
        .isEqualTo(
            ImmutableMap.builder()
                .put(
                    "change",
                    ImmutableMap.builder()
                        .put("project", PROJECT)
                        .put("branch", BRANCH)
                        .put("id", CHANGE_ID)
                        .put("number", CHANGE_NUM_DOUBLE)
                        .put("url", URL)
                        .put("commitMessage", COMMIT_MESSAGE)
                        .put("createdOn", TS1)
                        .put("status", NEW.name())
                        .build())
                .put("project", PROJECT)
                .put("refName", REF)
                .put("changeKey", map("id", CHANGE_ID))
                .put("type", "vote-deleted")
                .put("eventCreatedOn", TS2)
                .build());
  }

  @Test
  public void workInProgressStateChangedEvent() {
    Change change = newChange();
    WorkInProgressStateChangedEvent event = new WorkInProgressStateChangedEvent(change);
    event.change = asChangeAttribute(change);

    assertThatJsonMap(event)
        .isEqualTo(
            ImmutableMap.builder()
                .put(
                    "change",
                    ImmutableMap.builder()
                        .put("project", PROJECT)
                        .put("branch", BRANCH)
                        .put("id", CHANGE_ID)
                        .put("number", CHANGE_NUM_DOUBLE)
                        .put("url", URL)
                        .put("commitMessage", COMMIT_MESSAGE)
                        .put("createdOn", TS1)
                        .put("status", NEW.name())
                        .build())
                .put("project", PROJECT)
                .put("refName", REF)
                .put("changeKey", map("id", CHANGE_ID))
                .put("type", "wip-state-changed")
                .put("eventCreatedOn", TS2)
                .build());
  }

  @Test
  public void topicChangedEvent() {
    Change change = newChange();
    TopicChangedEvent event = new TopicChangedEvent(change);
    event.change = asChangeAttribute(change);

    assertThatJsonMap(event)
        .isEqualTo(
            ImmutableMap.builder()
                .put(
                    "change",
                    ImmutableMap.builder()
                        .put("project", PROJECT)
                        .put("branch", BRANCH)
                        .put("id", CHANGE_ID)
                        .put("number", CHANGE_NUM_DOUBLE)
                        .put("url", URL)
                        .put("commitMessage", COMMIT_MESSAGE)
                        .put("createdOn", TS1)
                        .put("status", NEW.name())
                        .build())
                .put("project", PROJECT)
                .put("refName", REF)
                .put("changeKey", map("id", CHANGE_ID))
                .put("type", "topic-changed")
                .put("eventCreatedOn", TS2)
                .build());
  }

  @Test
  public void projectCreatedEvent() {
    ProjectCreatedEvent event = new ProjectCreatedEvent();
    event.projectName = PROJECT;
    event.headName = REF;

    assertThatJsonMap(event)
        .isEqualTo(
            ImmutableMap.builder()
                .put("projectName", PROJECT)
                .put("headName", REF)
                .put("type", "project-created")
                .put("eventCreatedOn", TS1)
                .build());
  }

  @Test
  public void projectHeadUpdatedEvent() {
    ProjectHeadUpdatedEvent event = new ProjectHeadUpdatedEvent();
    event.projectName = PROJECT;
    event.oldHead = "refs/heads/master";
    event.newHead = REF;

    assertThatJsonMap(event)
        .isEqualTo(
            ImmutableMap.builder()
                .put("projectName", PROJECT)
                .put("oldHead", "refs/heads/master")
                .put("newHead", REF)
                .put("type", "project-head-updated")
                .put("eventCreatedOn", TS1)
                .build());
  }

  private Supplier<AccountAttribute> newAccount(String name) {
    AccountAttribute account = new AccountAttribute();
    account.name = name;
    account.email = name + "@somewhere.com";
    account.username = name;
    return Suppliers.ofInstance(account);
  }

  private Change newChange() {
    return new Change(
        Change.key(CHANGE_ID),
        Change.id(CHANGE_NUM),
        Account.id(9999),
        BranchNameKey.create(Project.nameKey(PROJECT), BRANCH),
        TimeUtil.now());
  }

  private <T> Supplier<T> createSupplier(T value) {
    return Suppliers.memoize(() -> value);
  }

  private Supplier<ChangeAttribute> asChangeAttribute(Change change) {
    ChangeAttribute a = new ChangeAttribute();
    a.project = change.getProject().get();
    a.branch = change.getDest().shortName();
    a.topic = change.getTopic();
    a.id = change.getKey().get();
    a.number = change.getId().get();
    a.subject = change.getSubject();
    a.commitMessage = COMMIT_MESSAGE;
    a.url = URL;
    a.status = change.getStatus();
    a.createdOn = change.getCreatedOn().getEpochSecond();
    a.wip = change.isWorkInProgress() ? true : null;
    a.isPrivate = change.isPrivate() ? true : null;
    return Suppliers.ofInstance(a);
  }

  private MapSubject assertThatJsonMap(Object src) {
    // Parse JSON into a raw Java map:
    //  * Doesn't depend on field iteration order.
    //  * Avoids excessively long string literals in asserts.
    String json = gson.toJson(src);
    Map<Object, Object> map =
        gson.fromJson(json, new TypeToken<Map<Object, Object>>() {}.getType());
    return assertThat(map);
  }

  private static ImmutableMap<Object, Object> map(Object k, Object v) {
    return ImmutableMap.of(k, v);
  }

  private static ImmutableList<Object> list(Object... es) {
    return ImmutableList.copyOf(es);
  }
}
