package org.apache.helix.manager.zk.serializer;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */

import java.io.IOException;
import java.util.LinkedList;
import java.util.List;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.helix.manager.zk.ZNRecordSerializer;
import org.apache.helix.manager.zk.ZNRecordStreamingSerializer;
import org.apache.helix.zookeeper.datamodel.ZNRecord;
import org.testng.Assert;
import org.testng.annotations.Test;

public class TestJacksonPayloadSerializer {
  /**
   * Ensure that the JacksonPayloadSerializer can serialize and deserialize arbitrary objects
   */
  @Test
  public void testJacksonSerializeDeserialize() {
    final String RECORD_ID = "testJacksonSerializeDeserialize";
    SampleDeserialized sample = getSample();
    ZNRecord znRecord = new ZNRecord(RECORD_ID);
    znRecord.setPayloadSerializer(new JacksonPayloadSerializer());
    znRecord.setPayload(sample);
    SampleDeserialized duplicate = znRecord.getPayload(SampleDeserialized.class);
    Assert.assertEquals(duplicate, sample);
  }

  /**
   * Test that the payload can be deserialized after serializing and deserializing the ZNRecord
   * that encloses it. This uses ZNRecordSerializer.
   */
  @Test
  public void testFullZNRecordSerializeDeserialize() {
    final String RECORD_ID = "testFullZNRecordSerializeDeserialize";
    SampleDeserialized sample = getSample();
    ZNRecord znRecord = new ZNRecord(RECORD_ID);
    znRecord.setPayloadSerializer(new JacksonPayloadSerializer());
    znRecord.setPayload(sample);
    ZNRecordSerializer znRecordSerializer = new ZNRecordSerializer();
    byte[] serialized = znRecordSerializer.serialize(znRecord);
    ZNRecord deserialized = (ZNRecord) znRecordSerializer.deserialize(serialized);
    deserialized.setPayloadSerializer(new JacksonPayloadSerializer());
    SampleDeserialized duplicate = deserialized.getPayload(SampleDeserialized.class);
    Assert.assertEquals(duplicate, sample);
  }

  /**
   * Test that the payload can be deserialized after serializing and deserializing the ZNRecord
   * that encloses it. This uses ZNRecordStreamingSerializer.
   */
  @Test
  public void testFullZNRecordStreamingSerializeDeserialize() {
    final String RECORD_ID = "testFullZNRecordStreamingSerializeDeserialize";
    SampleDeserialized sample = getSample();
    ZNRecord znRecord = new ZNRecord(RECORD_ID);
    znRecord.setPayloadSerializer(new JacksonPayloadSerializer());
    znRecord.setPayload(sample);
    ZNRecordStreamingSerializer znRecordSerializer = new ZNRecordStreamingSerializer();
    byte[] serialized = znRecordSerializer.serialize(znRecord);
    ZNRecord deserialized = (ZNRecord) znRecordSerializer.deserialize(serialized);
    deserialized.setPayloadSerializer(new JacksonPayloadSerializer());
    SampleDeserialized duplicate = deserialized.getPayload(SampleDeserialized.class);
    Assert.assertEquals(duplicate, sample);
  }

  /**
   * Test that the payload is not included whenever it is not null. This is mainly to maintain
   * backward
   * compatibility.
   */
  @Test
  public void testRawPayloadMissingIfUnspecified() {
    final String RECORD_ID = "testRawPayloadMissingIfUnspecified";
    ZNRecord znRecord = new ZNRecord(RECORD_ID);
    ZNRecordSerializer znRecordSerializer = new ZNRecordSerializer();
    byte[] serialized = znRecordSerializer.serialize(znRecord);
    ZNRecordStreamingSerializer znRecordStreamingSerializer = new ZNRecordStreamingSerializer();
    byte[] streamingSerialized = znRecordStreamingSerializer.serialize(znRecord);
    ObjectMapper mapper = new ObjectMapper();
    try {
      JsonNode jsonNode = mapper.readTree(new String(serialized));
      Assert.assertFalse(jsonNode.has("rawPayload"));
      JsonNode streamingJsonNode = mapper.readTree(new String(streamingSerialized));
      Assert.assertFalse(streamingJsonNode.has("rawPayload"));
    } catch (JsonProcessingException e) {
      Assert.fail();
    } catch (IOException e) {
      Assert.fail();
    }
  }

  /**
   * Get an object which can be tested for serialization success or failure
   * @return Initialized SampleDeserialized object
   */
  private SampleDeserialized getSample() {
    final int INT_FIELD_VALUE = 12345;
    final int LIST_FIELD_COUNT = 5;
    List<Integer> intList = new LinkedList<Integer>();
    for (int i = 0; i < LIST_FIELD_COUNT; i++) {
      intList.add(i);
    }
    return new SampleDeserialized(INT_FIELD_VALUE, intList);
  }

  @JsonIgnoreProperties(ignoreUnknown = true)
  public static class SampleDeserialized {
    private int _intField;
    private List<Integer> _listField;

    public SampleDeserialized() {
    }

    public SampleDeserialized(int intField, List<Integer> listField) {
      _intField = intField;
      _listField = listField;
    }

    @JsonProperty
    public void setIntField(int value) {
      _intField = value;
    }

    @JsonProperty
    public int getIntField() {
      return _intField;
    }

    @JsonProperty
    public void setListField(final List<Integer> listField) {
      _listField = listField;
    }

    @JsonProperty
    public List<Integer> getListField() {
      return _listField;
    }

    @Override
    public boolean equals(Object other) {
      boolean result = true;
      if (other instanceof SampleDeserialized) {
        SampleDeserialized that = (SampleDeserialized) other;
        if (_intField != that._intField) {
          // ints must match
          result = false;
        } else if (_listField != null) {
          // lists must match if one is not null
          if (!_listField.equals(that._listField)) {
            result = false;
          }
        } else {
          // both must be null if one is null
          if (that._listField != null) {
            result = false;
          }
        }
      } else {
        // only compare objects of the same type
        result = false;
      }
      return result;
    }
  }

}
