package com.hubspot.jackson.datatype.protobuf.proto3;

import static com.hubspot.jackson.datatype.protobuf.util.ObjectMapperHelper.camelCase;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;

import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;

import org.junit.Test;

import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.protobuf.Duration;
import com.hubspot.jackson.datatype.protobuf.util.BuiltInProtobufs.HasOneof;
import com.hubspot.jackson.datatype.protobuf.util.TestProtobuf.AllFields;
import com.hubspot.jackson.datatype.protobuf.util.TestProtobuf3.AllFieldsProto3;
import com.hubspot.jackson.datatype.protobuf.util.TestProtobuf3.EnumProto3;

public class OneofTest {
  private static final HasOneof EMPTY = HasOneof.newBuilder().build();
  private static final HasOneof STRING = HasOneof.newBuilder().setString("test").build();
  private static final HasOneof DEFAULT_DURATION = HasOneof
          .newBuilder()
          .setDuration(Duration.getDefaultInstance())
          .build();
  private static final HasOneof DURATION = HasOneof
          .newBuilder()
          .setDuration(Duration.newBuilder().setSeconds(30).build())
          .build();
  private static final HasOneof DEFAULT_ENUM = HasOneof.newBuilder().setEnum(EnumProto3.DEFAULT).build();
  private static final HasOneof ENUM = HasOneof.newBuilder().setEnum(EnumProto3.FIRST).build();
  private static final HasOneof DEFAULT_PROTO2_MESSAGE = HasOneof
          .newBuilder()
          .setProto2Message(AllFields.getDefaultInstance())
          .build();
  private static final HasOneof PROTO2_MESSAGE = HasOneof
          .newBuilder()
          .setProto2Message(AllFields.newBuilder().setString("proto2").build())
          .build();
  private static final HasOneof DEFAULT_PROTO3_MESSAGE = HasOneof
          .newBuilder()
          .setProto3Message(AllFieldsProto3.getDefaultInstance())
          .build();
  private static final HasOneof PROTO3_MESSAGE = HasOneof
          .newBuilder()
          .setProto3Message(AllFieldsProto3.newBuilder().setString("proto3").build())
          .build();

  @Test
  public void itOmitsOneofWhenNotSetWithDefaultInclusion() throws IOException {
    String json = camelCase().writeValueAsString(EMPTY);
    assertThat(json).isEqualTo("{}");
  }

  @Test
  public void itWritesOneofWhenSetToStringWithDefaultInclusion() throws IOException {
    String json = camelCase().writeValueAsString(STRING);
    assertThat(json).isEqualTo("{\"string\":\"test\"}");
  }

  @Test
  public void itWritesOneofWhenSetToDefaultDurationWithDefaultInclusion() throws IOException {
    String json = camelCase().writeValueAsString(DEFAULT_DURATION);
    assertThat(json).isEqualTo("{\"duration\":\"0s\"}");
  }

  @Test
  public void itWritesOneofWhenSetToDurationWithDefaultInclusion() throws IOException {
    String json = camelCase().writeValueAsString(DURATION);
    assertThat(json).isEqualTo("{\"duration\":\"30s\"}");
  }

  @Test
  public void itWritesOneofWhenSetToDefaultEnumWithDefaultInclusion() throws IOException {
    String json = camelCase().writeValueAsString(DEFAULT_ENUM);
    assertThat(json).isEqualTo("{\"enum\":\"DEFAULT\"}");
  }

  @Test
  public void itWritesOneofWhenSetToEnumWithDefaultInclusion() throws IOException {
    String json = camelCase().writeValueAsString(ENUM);
    assertThat(json).isEqualTo("{\"enum\":\"FIRST\"}");
  }

  @Test
  public void itWritesOneofWhenSetToDefaultProto2MessageWithDefaultInclusion() throws IOException {
    String json = camelCase().writeValueAsString(DEFAULT_PROTO2_MESSAGE);
    assertThat(json).isEqualTo("{\"proto2Message\":{}}");
  }

  @Test
  public void itWritesOneofWhenSetToProto2MessageWithDefaultInclusion() throws IOException {
    String json = camelCase().writeValueAsString(PROTO2_MESSAGE);
    assertThat(json).isEqualTo("{\"proto2Message\":{\"string\":\"proto2\"}}");
  }

  @Test
  public void itWritesOneofWhenSetToDefaultProto3MessageWithDefaultInclusion() throws IOException {
    String json = camelCase().writeValueAsString(DEFAULT_PROTO3_MESSAGE);
    JsonNode node = camelCase().readTree(json).get("proto3Message");
    assertThat(node).isEqualTo(nullProto3Message().without("nested"));
  }

  @Test
  public void itWritesOneofWhenSetToProto3MessageWithDefaultInclusion() throws IOException {
    String json = camelCase().writeValueAsString(PROTO3_MESSAGE);
    JsonNode node = camelCase().readTree(json).get("proto3Message");
    assertThat(node).isEqualTo(nullProto3Message().put("string", "proto3").without("nested"));
  }

  @Test
  public void itOmitsOneofWhenNotSetWithAlwaysInclusion() throws IOException {
    String json = camelCase(Include.ALWAYS).writeValueAsString(EMPTY);
    assertThat(json).isEqualTo("{}");
  }

  @Test
  public void itWritesOneofWhenSetToStringWithAlwaysInclusion() throws IOException {
    String json = camelCase(Include.ALWAYS).writeValueAsString(STRING);
    assertThat(json).isEqualTo("{\"string\":\"test\"}");
  }

  @Test
  public void itWritesOneofWhenSetToDefaultDurationWithAlwaysInclusion() throws IOException {
    String json = camelCase(Include.ALWAYS).writeValueAsString(DEFAULT_DURATION);
    assertThat(json).isEqualTo("{\"duration\":\"0s\"}");
  }

  @Test
  public void itWritesOneofWhenSetToDurationWithAlwaysInclusion() throws IOException {
    String json = camelCase(Include.ALWAYS).writeValueAsString(DURATION);
    assertThat(json).isEqualTo("{\"duration\":\"30s\"}");
  }

  @Test
  public void itWritesOneofWhenSetToDefaultEnumWithAlwaysInclusion() throws IOException {
    String json = camelCase(Include.ALWAYS).writeValueAsString(DEFAULT_ENUM);
    assertThat(json).isEqualTo("{\"enum\":\"DEFAULT\"}");
  }

  @Test
  public void itWritesOneofWhenSetToEnumWithAlwaysInclusion() throws IOException {
    String json = camelCase(Include.ALWAYS).writeValueAsString(ENUM);
    assertThat(json).isEqualTo("{\"enum\":\"FIRST\"}");
  }

  @Test
  public void itWritesOneofWhenSetToDefaultProto2MessageWithAlwaysInclusion() throws IOException {
    String json = camelCase(Include.ALWAYS).writeValueAsString(DEFAULT_PROTO2_MESSAGE);
    JsonNode node = camelCase(Include.ALWAYS).readTree(json).get("proto2Message");
    assertThat(node).isEqualTo(nullProto2Message());
  }

  @Test
  public void itWritesOneofWhenSetToProto2MessageWithAlwaysInclusion() throws IOException {
    String json = camelCase(Include.ALWAYS).writeValueAsString(PROTO2_MESSAGE);
    JsonNode node = camelCase(Include.ALWAYS).readTree(json).get("proto2Message");
    assertThat(node).isEqualTo(nullProto2Message().put("string", "proto2"));
  }

  @Test
  public void itWritesOneofWhenSetToDefaultProto3MessageWithAlwaysInclusion() throws IOException {
    String json = camelCase(Include.ALWAYS).writeValueAsString(DEFAULT_PROTO3_MESSAGE);
    JsonNode node = camelCase(Include.ALWAYS).readTree(json).get("proto3Message");
    assertThat(node).isEqualTo(nullProto3Message());
  }

  @Test
  public void itWritesOneofWhenSetToProto3MessageWithAlwaysInclusion() throws IOException {
    String json = camelCase(Include.ALWAYS).writeValueAsString(PROTO3_MESSAGE);
    JsonNode node = camelCase(Include.ALWAYS).readTree(json).get("proto3Message");
    assertThat(node).isEqualTo(nullProto3Message().put("string", "proto3"));
  }

  @Test
  public void itDoesntSetAnythingWhenJsonIsEmpty() throws IOException {
    String json = "{}";
    HasOneof message = camelCase().readValue(json, HasOneof.class);
    switch (message.getOneofCase()) {
      case ONEOF_NOT_SET:
        assertThat(message).isEqualTo(HasOneof.getDefaultInstance());
        break;
      default:
        fail("Unexpected oneof set: " + message.getOneofCase());
    }
  }

  @Test
  public void itSetsStringWhenPresentInJson() throws IOException {
    String json = "{\"string\":\"test\"}";
    HasOneof message = camelCase().readValue(json, HasOneof.class);
    switch (message.getOneofCase()) {
      case STRING:
        assertThat(message.getString()).isEqualTo("test");
        break;
      default:
        fail("Unexpected oneof set: " + message.getOneofCase());
    }
  }

  @Test
  public void itDoesntSetStringWhenNullInJson() throws IOException {
    String json = "{\"string\":null}";
    HasOneof message = camelCase().readValue(json, HasOneof.class);
    switch (message.getOneofCase()) {
      case ONEOF_NOT_SET:
        assertThat(message).isEqualTo(HasOneof.getDefaultInstance());
        break;
      default:
        fail("Unexpected oneof set: " + message.getOneofCase());
    }
  }

  @Test
  public void itSetsDurationWhenPresentInJson() throws IOException {
    String json = "{\"duration\":\"30s\"}";
    HasOneof message = camelCase().readValue(json, HasOneof.class);
    switch (message.getOneofCase()) {
      case DURATION:
        assertThat(message.getDuration()).isEqualTo(Duration.newBuilder().setSeconds(30).build());
        break;
      default:
        fail("Unexpected oneof set: " + message.getOneofCase());
    }
  }

  @Test
  public void itDoesntSetDurationWhenNullInJson() throws IOException {
    String json = "{\"duration\":null}";
    HasOneof message = camelCase().readValue(json, HasOneof.class);
    switch (message.getOneofCase()) {
      case ONEOF_NOT_SET:
        assertThat(message).isEqualTo(HasOneof.getDefaultInstance());
        break;
      default:
        fail("Unexpected oneof set: " + message.getOneofCase());
    }
  }

  @Test
  public void itSetsEnumWhenPresentInJson() throws IOException {
    String json = "{\"enum\":\"FIRST\"}";
    HasOneof message = camelCase().readValue(json, HasOneof.class);
    switch (message.getOneofCase()) {
      case ENUM:
        assertThat(message.getEnum()).isEqualTo(EnumProto3.FIRST);
        break;
      default:
        fail("Unexpected oneof set: " + message.getOneofCase());
    }
  }

  @Test
  public void itSetsEnumWhenSetToDefaultInJson() throws IOException {
    String json = "{\"enum\":\"DEFAULT\"}";
    HasOneof message = camelCase().readValue(json, HasOneof.class);
    switch (message.getOneofCase()) {
      case ENUM:
        assertThat(message.getEnum()).isEqualTo(EnumProto3.DEFAULT);
        break;
      default:
        fail("Unexpected oneof set: " + message.getOneofCase());
    }
  }

  @Test
  public void itDoesntSetEnumWhenNullInJson() throws IOException {
    String json = "{\"enum\":null}";
    HasOneof message = camelCase().readValue(json, HasOneof.class);
    switch (message.getOneofCase()) {
      case ONEOF_NOT_SET:
        assertThat(message).isEqualTo(HasOneof.getDefaultInstance());
        break;
      default:
        fail("Unexpected oneof set: " + message.getOneofCase());
    }
  }

  @Test
  public void itSetsProto2MessageWhenPresentInJson() throws IOException {
    String json = "{\"proto2Message\":{\"string\":\"test\"}}";
    HasOneof message = camelCase().readValue(json, HasOneof.class);
    switch (message.getOneofCase()) {
      case PROTO2_MESSAGE:
        assertThat(message.getProto2Message()).isEqualTo(AllFields.newBuilder().setString("test").build());
        break;
      default:
        fail("Unexpected oneof set: " + message.getOneofCase());
    }
  }

  @Test
  public void itSetsProto2MessageWhenEmptyInJson() throws IOException {
    String json = "{\"proto2Message\":{}}";
    HasOneof message = camelCase().readValue(json, HasOneof.class);
    switch (message.getOneofCase()) {
      case PROTO2_MESSAGE:
        assertThat(message.getProto2Message()).isEqualTo(AllFields.getDefaultInstance());
        break;
      default:
        fail("Unexpected oneof set: " + message.getOneofCase());
    }
  }

  @Test
  public void itDoesntSetProto2MessageWhenNullInJson() throws IOException {
    String json = "{\"proto2Message\":null}";
    HasOneof message = camelCase().readValue(json, HasOneof.class);
    switch (message.getOneofCase()) {
      case ONEOF_NOT_SET:
        assertThat(message).isEqualTo(HasOneof.getDefaultInstance());
        break;
      default:
        fail("Unexpected oneof set: " + message.getOneofCase());
    }
  }

  @Test
  public void itSetsProto3MessageWhenPresentInJson() throws IOException {
    String json = "{\"proto3Message\":{\"string\":\"test\"}}";
    HasOneof message = camelCase().readValue(json, HasOneof.class);
    switch (message.getOneofCase()) {
      case PROTO3_MESSAGE:
        assertThat(message.getProto3Message()).isEqualTo(AllFieldsProto3.newBuilder().setString("test").build());
        break;
      default:
        fail("Unexpected oneof set: " + message.getOneofCase());
    }
  }

  @Test
  public void itSetsProto3MessageWhenEmptyInJson() throws IOException {
    String json = "{\"proto3Message\":{}}";
    HasOneof message = camelCase().readValue(json, HasOneof.class);
    switch (message.getOneofCase()) {
      case PROTO3_MESSAGE:
        assertThat(message.getProto3Message()).isEqualTo(AllFieldsProto3.getDefaultInstance());
        break;
      default:
        fail("Unexpected oneof set: " + message.getOneofCase());
    }
  }

  @Test
  public void itDoesntSetProto3MessageWhenNullInJson() throws IOException {
    String json = "{\"proto3Message\":null}";
    HasOneof message = camelCase().readValue(json, HasOneof.class);
    switch (message.getOneofCase()) {
      case ONEOF_NOT_SET:
        assertThat(message).isEqualTo(HasOneof.getDefaultInstance());
        break;
      default:
        fail("Unexpected oneof set: " + message.getOneofCase());
    }
  }

  private ObjectNode nullProto2Message() {
    Map<String, JsonNode> fields = new LinkedHashMap<>();
    fields.put("double", NullNode.getInstance());
    fields.put("float", NullNode.getInstance());
    fields.put("int32", NullNode.getInstance());
    fields.put("int64", NullNode.getInstance());
    fields.put("uint32", NullNode.getInstance());
    fields.put("uint64", NullNode.getInstance());
    fields.put("sint32", NullNode.getInstance());
    fields.put("sint64", NullNode.getInstance());
    fields.put("fixed32", NullNode.getInstance());
    fields.put("fixed64", NullNode.getInstance());
    fields.put("sfixed32", NullNode.getInstance());
    fields.put("sfixed64", NullNode.getInstance());
    fields.put("bool", NullNode.getInstance());
    fields.put("string", NullNode.getInstance());
    fields.put("bytes", NullNode.getInstance());
    fields.put("enum", NullNode.getInstance());
    fields.put("nested", NullNode.getInstance());
    ObjectNode node = camelCase().createObjectNode();
    node.setAll(fields);
    return node;
  }

  private ObjectNode nullProto3Message() {
    ObjectNode node = camelCase().createObjectNode();
    node
            .put("double", 0.0d)
            .put("float", 0.0d)
            .put("int32", 0)
            .put("int64", 0)
            .put("uint32", 0)
            .put("uint64", 0)
            .put("sint32", 0)
            .put("sint64", 0)
            .put("fixed32", 0)
            .put("fixed64", 0)
            .put("sfixed32", 0)
            .put("sfixed64", 0)
            .put("bool", false)
            .put("string", "")
            .put("bytes", "")
            .put("enum", "DEFAULT")
            .set("nested", NullNode.getInstance());
    return node;
  }
}