// Copyright (C) 2018 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.entities.converter;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.extensions.proto.ProtoTruth.assertThat;
import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;

import com.google.common.collect.ImmutableMap;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
import com.google.gerrit.proto.Entities;
import com.google.gerrit.proto.testing.SerializedClassSubject;
import java.lang.reflect.Type;
import java.time.Instant;
import org.junit.Test;

public class ChangeProtoConverterTest {
  private final ChangeProtoConverter changeProtoConverter = ChangeProtoConverter.INSTANCE;

  @Test
  public void allValuesConvertedToProto() {
    Change change =
        new Change(
            Change.key("change 1"),
            Change.id(14),
            Account.id(35),
            BranchNameKey.create(Project.nameKey("project 67"), "branch 74"),
            Instant.ofEpochMilli(987654L));
    change.setLastUpdatedOn(Instant.ofEpochMilli(1234567L));
    change.setStatus(Change.Status.MERGED);
    change.setCurrentPatchSet(
        PatchSet.id(Change.id(14), 23), "subject XYZ", "original subject ABC");
    change.setTopic("my topic");
    change.setSubmissionId("submission ID 234");
    change.setPrivate(true);
    change.setWorkInProgress(true);
    change.setReviewStarted(true);
    change.setRevertOf(Change.id(180));

    Entities.Change proto = changeProtoConverter.toProto(change);

    Entities.Change expectedProto =
        Entities.Change.newBuilder()
            .setChangeId(Entities.Change_Id.newBuilder().setId(14))
            .setChangeKey(Entities.Change_Key.newBuilder().setId("change 1"))
            .setCreatedOn(987654L)
            .setLastUpdatedOn(1234567L)
            .setOwnerAccountId(Entities.Account_Id.newBuilder().setId(35))
            .setDest(
                Entities.Branch_NameKey.newBuilder()
                    .setProject(Entities.Project_NameKey.newBuilder().setName("project 67"))
                    .setBranch("refs/heads/branch 74"))
            .setStatus(Change.STATUS_MERGED)
            .setCurrentPatchSetId(23)
            .setSubject("subject XYZ")
            .setTopic("my topic")
            .setOriginalSubject("original subject ABC")
            .setSubmissionId("submission ID 234")
            .setIsPrivate(true)
            .setWorkInProgress(true)
            .setReviewStarted(true)
            .setRevertOf(Entities.Change_Id.newBuilder().setId(180))
            .build();
    assertThat(proto).isEqualTo(expectedProto);
  }

  @Test
  public void mandatoryValuesConvertedToProto() {
    Change change =
        new Change(
            Change.key("change 1"),
            Change.id(14),
            Account.id(35),
            BranchNameKey.create(Project.nameKey("project 67"), "branch-74"),
            Instant.ofEpochMilli(987654L));

    Entities.Change proto = changeProtoConverter.toProto(change);

    Entities.Change expectedProto =
        Entities.Change.newBuilder()
            .setChangeId(Entities.Change_Id.newBuilder().setId(14))
            .setChangeKey(Entities.Change_Key.newBuilder().setId("change 1"))
            .setCreatedOn(987654L)
            // Defaults to createdOn if not set.
            .setLastUpdatedOn(987654L)
            .setOwnerAccountId(Entities.Account_Id.newBuilder().setId(35))
            .setDest(
                Entities.Branch_NameKey.newBuilder()
                    .setProject(Entities.Project_NameKey.newBuilder().setName("project 67"))
                    .setBranch("refs/heads/branch-74"))
            // Default values which can't be unset.
            .setCurrentPatchSetId(0)
            .setStatus(Change.STATUS_NEW)
            .setIsPrivate(false)
            .setWorkInProgress(false)
            .setReviewStarted(false)
            .build();
    assertThat(proto).isEqualTo(expectedProto);
  }

  // This test documents a special behavior which is necessary to ensure binary compatibility.
  @Test
  public void currentPatchSetIsAlwaysSetWhenConvertedToProto() {
    Change change =
        new Change(
            Change.key("change 1"),
            Change.id(14),
            Account.id(35),
            BranchNameKey.create(Project.nameKey("project 67"), "branch-74"),
            Instant.ofEpochMilli(987654L));
    // O as ID actually means that no current patch set is present.
    change.setCurrentPatchSet(PatchSet.id(Change.id(14), 0), null, null);

    Entities.Change proto = changeProtoConverter.toProto(change);

    Entities.Change expectedProto =
        Entities.Change.newBuilder()
            .setChangeId(Entities.Change_Id.newBuilder().setId(14))
            .setChangeKey(Entities.Change_Key.newBuilder().setId("change 1"))
            .setCreatedOn(987654L)
            // Defaults to createdOn if not set.
            .setLastUpdatedOn(987654L)
            .setOwnerAccountId(Entities.Account_Id.newBuilder().setId(35))
            .setDest(
                Entities.Branch_NameKey.newBuilder()
                    .setProject(Entities.Project_NameKey.newBuilder().setName("project 67"))
                    .setBranch("refs/heads/branch-74"))
            .setCurrentPatchSetId(0)
            // Default values which can't be unset.
            .setStatus(Change.STATUS_NEW)
            .setIsPrivate(false)
            .setWorkInProgress(false)
            .setReviewStarted(false)
            .build();
    assertThat(proto).isEqualTo(expectedProto);
  }

  // This test documents a special behavior which is necessary to ensure binary compatibility.
  @Test
  public void originalSubjectIsNotAutomaticallySetToSubjectWhenConvertedToProto() {
    Change change =
        new Change(
            Change.key("change 1"),
            Change.id(14),
            Account.id(35),
            BranchNameKey.create(Project.nameKey("project 67"), "branch-74"),
            Instant.ofEpochMilli(987654L));
    change.setCurrentPatchSet(PatchSet.id(Change.id(14), 23), "subject ABC", null);

    Entities.Change proto = changeProtoConverter.toProto(change);

    Entities.Change expectedProto =
        Entities.Change.newBuilder()
            .setChangeId(Entities.Change_Id.newBuilder().setId(14))
            .setChangeKey(Entities.Change_Key.newBuilder().setId("change 1"))
            .setCreatedOn(987654L)
            // Defaults to createdOn if not set.
            .setLastUpdatedOn(987654L)
            .setOwnerAccountId(Entities.Account_Id.newBuilder().setId(35))
            .setDest(
                Entities.Branch_NameKey.newBuilder()
                    .setProject(Entities.Project_NameKey.newBuilder().setName("project 67"))
                    .setBranch("refs/heads/branch-74"))
            .setCurrentPatchSetId(23)
            .setSubject("subject ABC")
            // Default values which can't be unset.
            .setStatus(Change.STATUS_NEW)
            .setIsPrivate(false)
            .setWorkInProgress(false)
            .setReviewStarted(false)
            .build();
    assertThat(proto).isEqualTo(expectedProto);
  }

  @Test
  public void allValuesConvertedToProtoAndBackAgain() {
    Change change =
        new Change(
            Change.key("change 1"),
            Change.id(14),
            Account.id(35),
            BranchNameKey.create(Project.nameKey("project 67"), "branch-74"),
            Instant.ofEpochMilli(987654L));
    change.setLastUpdatedOn(Instant.ofEpochMilli(1234567L));
    change.setStatus(Change.Status.MERGED);
    change.setCurrentPatchSet(
        PatchSet.id(Change.id(14), 23), "subject XYZ", "original subject ABC");
    change.setTopic("my topic");
    change.setSubmissionId("submission ID 234");
    change.setPrivate(true);
    change.setWorkInProgress(true);
    change.setReviewStarted(true);
    change.setRevertOf(Change.id(180));

    Change convertedChange = changeProtoConverter.fromProto(changeProtoConverter.toProto(change));
    assertEqualChange(convertedChange, change);
  }

  @Test
  public void mandatoryValuesConvertedToProtoAndBackAgain() {
    Change change =
        new Change(
            Change.key("change 1"),
            Change.id(14),
            Account.id(35),
            BranchNameKey.create(Project.nameKey("project 67"), "branch-74"),
            Instant.ofEpochMilli(987654L));

    Change convertedChange = changeProtoConverter.fromProto(changeProtoConverter.toProto(change));
    assertEqualChange(convertedChange, change);
  }

  // We need this special test as some values are only optional in the protobuf definition but can
  // never be unset in our entity object.
  @Test
  public void protoWithOnlyRequiredValuesCanBeConvertedBack() {
    Entities.Change proto =
        Entities.Change.newBuilder().setChangeId(Entities.Change_Id.newBuilder().setId(14)).build();
    Change change = changeProtoConverter.fromProto(proto);

    assertThat(change.getChangeId()).isEqualTo(14);
    // Values which can't be null according to ReviewDb's column definition but which are optional.
    assertThat(change.getKey()).isNull();
    assertThat(change.getOwner()).isNull();
    assertThat(change.getDest()).isNull();
    assertThat(change.getCreatedOn()).isEqualTo(Instant.EPOCH);
    assertThat(change.getLastUpdatedOn()).isEqualTo(Instant.EPOCH);
    assertThat(change.getSubject()).isNull();
    assertThat(change.currentPatchSetId()).isNull();
    // Default values for unset protobuf fields which can't be unset in the entity object.
    assertThat(change.isNew()).isTrue();
    assertThat(change.isPrivate()).isFalse();
    assertThat(change.isWorkInProgress()).isFalse();
    assertThat(change.hasReviewStarted()).isFalse();
  }

  @Test
  public void unsetLastUpdatedOnIsAutomaticallySetToCreatedOnWhenConvertedBack() {
    Entities.Change proto =
        Entities.Change.newBuilder()
            .setChangeId(Entities.Change_Id.newBuilder().setId(14))
            .setChangeKey(Entities.Change_Key.newBuilder().setId("change 1"))
            .setCreatedOn(987654L)
            .setOwnerAccountId(Entities.Account_Id.newBuilder().setId(35))
            .setDest(
                Entities.Branch_NameKey.newBuilder()
                    .setProject(Entities.Project_NameKey.newBuilder().setName("project 67"))
                    .setBranch("branch 74"))
            .build();
    Change change = changeProtoConverter.fromProto(proto);

    assertThat(change.getLastUpdatedOn()).isEqualTo(Instant.ofEpochMilli(987654L));
  }

  /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
  @Test
  public void fieldsExistAsExpected() {
    assertThatSerializedClass(Change.class)
        .hasFields(
            ImmutableMap.<String, Type>builder()
                .put("changeId", Change.Id.class)
                .put("changeKey", Change.Key.class)
                .put("createdOn", Instant.class)
                .put("lastUpdatedOn", Instant.class)
                .put("owner", Account.Id.class)
                .put("dest", BranchNameKey.class)
                .put("status", char.class)
                .put("currentPatchSetId", int.class)
                .put("subject", String.class)
                .put("topic", String.class)
                .put("originalSubject", String.class)
                .put("submissionId", String.class)
                .put("isPrivate", boolean.class)
                .put("workInProgress", boolean.class)
                .put("reviewStarted", boolean.class)
                .put("revertOf", Change.Id.class)
                .put("cherryPickOf", PatchSet.Id.class)
                .build());
  }

  // Unfortunately, Change doesn't implement equals(). Remove this method when we switch Change to
  // an AutoValue.
  private static void assertEqualChange(Change change, Change expectedChange) {
    assertThat(change.getChangeId()).isEqualTo(expectedChange.getChangeId());
    assertThat(change.getKey()).isEqualTo(expectedChange.getKey());
    assertThat(change.getCreatedOn()).isEqualTo(expectedChange.getCreatedOn());
    assertThat(change.getLastUpdatedOn()).isEqualTo(expectedChange.getLastUpdatedOn());
    assertThat(change.getOwner()).isEqualTo(expectedChange.getOwner());
    assertThat(change.getDest()).isEqualTo(expectedChange.getDest());
    assertThat(change.getStatus()).isEqualTo(expectedChange.getStatus());
    assertThat(change.currentPatchSetId()).isEqualTo(expectedChange.currentPatchSetId());
    assertThat(change.getSubject()).isEqualTo(expectedChange.getSubject());
    assertThat(change.getTopic()).isEqualTo(expectedChange.getTopic());
    assertThat(change.getOriginalSubject()).isEqualTo(expectedChange.getOriginalSubject());
    assertThat(change.getSubmissionId()).isEqualTo(expectedChange.getSubmissionId());
    assertThat(change.isPrivate()).isEqualTo(expectedChange.isPrivate());
    assertThat(change.isWorkInProgress()).isEqualTo(expectedChange.isWorkInProgress());
    assertThat(change.hasReviewStarted()).isEqualTo(expectedChange.hasReviewStarted());
    assertThat(change.getRevertOf()).isEqualTo(expectedChange.getRevertOf());
  }
}
