package com.monitorjbl.json;

import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.BaseEncoding;
import com.google.common.primitives.Ints;
import com.monitorjbl.json.model.CustomType;
import com.monitorjbl.json.model.CustomTypeSerializer;
import com.monitorjbl.json.model.NonReplacableKeyMap;
import com.monitorjbl.json.model.TestAutodetect.AutodetectDefault;
import com.monitorjbl.json.model.TestAutodetect.AutodetectFields;
import com.monitorjbl.json.model.TestAutodetect.AutodetectGetters;
import com.monitorjbl.json.model.TestAutodetect.AutodetectNotPresent;
import com.monitorjbl.json.model.TestBackreferenceObject;
import com.monitorjbl.json.model.TestBackreferenceObject.TestForwardReferenceObject;
import com.monitorjbl.json.model.TestChildObject;
import com.monitorjbl.json.model.TestDuplicateKeys.ClassC;
import com.monitorjbl.json.model.TestInterface;
import com.monitorjbl.json.model.TestNonNulls;
import com.monitorjbl.json.model.TestNulls;
import com.monitorjbl.json.model.TestObject;
import com.monitorjbl.json.model.TestObject.TestEnum;
import com.monitorjbl.json.model.TestSubobject;
import com.monitorjbl.json.model.TestSuperinterface;
import com.monitorjbl.json.model.TestSuperinterface.TestChildInterface;
import com.monitorjbl.json.model.TestSuperinterface.TestInterfaceObject;
import com.monitorjbl.json.model.TestUnrelatedObject;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.math.BigDecimal;
import java.net.URI;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.function.Consumer;

import static com.monitorjbl.json.Match.match;
import static java.util.Arrays.asList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

@SuppressWarnings("unchecked")
public class JsonViewSerializerTest {

  ObjectMapper sut;
  JsonViewSerializer serializer;

  @Before
  public void setup() {
    this.serializer = new JsonViewSerializer();
    sut = new ObjectMapper()
        .registerModule(new JsonViewModule(serializer))
        .registerModule(new ParameterNamesModule())
        .registerModule(new Jdk8Module())
        .registerModule(new JavaTimeModule());
  }

  @Test
  public void testJsonIgnore() throws IOException {
    TestObject ref = new TestObject();
    ref.setInt1(1);
    ref.setIgnoredDirect("ignore me");
    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);
    assertNotNull(obj.get("int1"));
    assertEquals(ref.getInt1(), obj.get("int1"));
    assertNull(obj.get("ignoredDirect"));
  }

  @Test
  public void testJsonIgnoreProperties() throws IOException {
    TestObject ref = new TestObject();
    ref.setInt1(1);
    ref.setIgnoredIndirect("ignore me");
    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);
    assertNotNull(obj.get("int1"));
    assertEquals(ref.getInt1(), obj.get("int1"));
    assertNull(obj.get("ignoredIndirect"));
  }

  @Test
  public void testBasicSerialization() throws IOException {
    TestObject ref = new TestObject();
    ref.setInt1(1);
    ref.setStr2("asdf");
    ref.setStringArray(new String[]{"apple", "banana"});
    ref.setList(asList("red", "blue", "green"));
    ref.setSub(new TestSubobject("qwerqwerqwerqw", new TestSubobject("poxcpvoxcv")));
    String serialized = sut.writeValueAsString(
        JsonView.with(ref).onClass(TestObject.class, match()
            .exclude("str2")
            .exclude("sub.val")
            .include("ignoredDirect")));

    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);
    assertNull(obj.get("str2"));
    assertNotNull(obj.get("sub"));
    assertNull(((Map) obj.get("sub")).get("val"));
  }

  @Test
  public void testInheritance() throws IOException {
    TestChildObject ref = new TestChildObject();
    ref.setChildField("green");
    ref.setIgnoredDirect("ignore me");
    ref.setIgnoredIndirect("ignore me too");
    ref.setStringArray(new String[]{"pizza", "french fry"});

    String serialized = sut.writeValueAsString(
        JsonView.with(ref).onClass(TestObject.class, match()
            .exclude("str2")
            .exclude("sub.val")
            .include("ignoredDirect")));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);
    assertNull(obj.get("ignoredIndirect"));
    assertNotNull(obj.get("ignoredDirect"));
    assertEquals(ref.getIgnoredDirect(), obj.get("ignoredDirect"));
    assertNotNull(obj.get("childField"));
    assertEquals(ref.getChildField(), obj.get("childField"));
  }

  @Test
  public void testFieldSpecificity() throws IOException {
    TestObject ref = new TestObject();
    ref.setInt1(1);
    ref.setStr2("asdf");
    ref.setSub(new TestSubobject("qwerqwerqwerqw", new TestSubobject("poxcpvoxcv")));
    String serialized = sut.writeValueAsString(
        JsonView.with(ref)
            .onClass(TestObject.class, match()
                .exclude("str2")
                .exclude("sub.val"))
            .onClass(TestSubobject.class, match()
                .exclude("sub")));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertNotNull(obj.get("sub"));
    assertEquals(ref.getSub().getVal(), ((Map) obj.get("sub")).get("val"));
  }

  @Test
  public void testListWithSingleClass() throws IOException {
    TestObject ref1 = new TestObject();
    ref1.setInt1(1);
    ref1.setStr2("asdf");
    ref1.setStringArray(new String[]{"apple", "banana"});
    ref1.setList(asList("red", "blue", "green"));
    ref1.setSub(new TestSubobject("qwerqwerqwerqw", new TestSubobject("poxcpvoxcv")));

    TestObject ref2 = new TestObject();
    ref2.setInt1(2);
    ref2.setStr2("asdf");
    ref2.setStringArray(new String[]{"orange", "kiwi"});
    ref2.setList(asList("cyan", "indigo", "violet"));
    ref2.setSub(new TestSubobject("zxcvxzcv", new TestSubobject("hjhljkljh")));

    List<TestObject> refList = asList(ref1, ref2);

    String serialized = sut.writeValueAsString(
        JsonView.with(refList).onClass(TestObject.class, match()
            .exclude("str2")
            .exclude("sub.val")
            .include("ignoredDirect")));
    List<Map<String, Object>> output = sut.readValue(serialized, ArrayList.class);

    assertEquals(refList.size(), output.size());
    for(int i = 0; i < output.size(); i++) {
      Map<String, Object> obj = output.get(i);
      TestObject ref = refList.get(i);

      assertEquals(ref.getInt1(), obj.get("int1"));
      assertNull(obj.get("str2"));
      assertNotNull(obj.get("sub"));
      assertNull(((Map) obj.get("sub")).get("val"));

      assertNotNull(obj.get("stringArray"));
      assertTrue(obj.get("stringArray") instanceof List);
      List array = (List) obj.get("stringArray");
      assertEquals(ref.getStringArray().length, array.size());
      for(int j = 0; j < array.size(); j++) {
        assertEquals(ref.getStringArray()[j], array.get(j));
      }

      assertNotNull(obj.get("list"));
      assertTrue(obj.get("list") instanceof List);
      List list = (List) obj.get("list");
      assertEquals(ref.getList().size(), list.size());
      for(int j = 0; j < list.size(); j++) {
        assertEquals(ref.getList().get(j), list.get(j));
      }

    }
  }

  @Test
  public void testListWithMultipleClasses() throws IOException {
    TestObject ref1 = new TestObject();
    ref1.setInt1(1);
    ref1.setStr2("asdf");
    ref1.setStringArray(new String[]{"apple", "banana"});
    ref1.setList(asList("red", "blue", "green"));
    ref1.setIgnoredIndirect("ignore me too");
    ref1.setSub(new TestSubobject("qwerqwerqwerqw", new TestSubobject("poxcpvoxcv")));

    TestChildObject ref2 = new TestChildObject();
    ref2.setChildField("green");
    ref2.setIgnoredDirect("ignore me");
    ref2.setStringArray(new String[]{"pizza", "french fry"});

    TestUnrelatedObject ref3 = new TestUnrelatedObject();
    ref3.setId(3L);
    ref3.setName("xxzcvxc");

    JsonViewSerializer.log = true;
    String serialized = sut.writeValueAsString(
        JsonView.with(asList(ref1, ref2, ref3))
            .onClass(TestObject.class, match()
                .exclude("str2")
                .include("ignoredIndirect"))
            .onClass(TestChildObject.class, match()
                .exclude("array")
                .include("ignoredDirect"))
            .onClass(TestUnrelatedObject.class, match()
                .exclude("name")));
    List<Map<String, Object>> output = sut.readValue(serialized, ArrayList.class);
    JsonViewSerializer.log = false;

    assertEquals(3, output.size());

    Map<String, Object> t1 = output.get(0);
    assertEquals(ref1.getInt1(), t1.get("int1"));
    assertNull(t1.get("srt2"));
    assertEquals(ref1.getIgnoredIndirect(), t1.get("ignoredIndirect"));

    Map<String, Object> t2 = output.get(1);
    assertEquals(ref2.getChildField(), t2.get("childField"));
    assertNull(t2.get("array"));
    assertEquals(ref2.getIgnoredDirect(), t2.get("ignoredDirect"));

    Map<String, Object> t3 = output.get(2);
    assertEquals(ref3.getId().longValue(), ((Integer) t3.get("id")).longValue());
    assertNull(t3.get("name"));
  }

  @Test
  public void testListOfSubobjects() throws IOException {
    TestObject ref = new TestObject();
    ref.setInt1(1);
    ref.setListOfObjects(asList(new TestSubobject("test1"), new TestSubobject("test2", new TestSubobject("test3"))));
    String serialized = sut.writeValueAsString(
        JsonView.with(ref)
            .onClass(TestObject.class, match()
                .exclude("sub.val"))
            .onClass(TestSubobject.class, match()
                .exclude("sub")));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertEquals(ref.getInt1(), obj.get("int1"));
    assertTrue(obj.get("listOfObjects") instanceof List);
    List<Map<String, Object>> list = (List<Map<String, Object>>) obj.get("listOfObjects");
    assertEquals(2, list.size());
    assertEquals("test1", list.get(0).get("val"));
    assertEquals("test2", list.get(1).get("val"));
    assertNull(list.get(1).get("sub"));
  }

  @Test
  public void testListOfSubobjects_fieldSpecific() throws IOException {
    TestObject ref = new TestObject();
    ref.setInt1(1);
    ref.setListOfObjects(asList(new TestSubobject("test1"), new TestSubobject("test2", new TestSubobject("test3"))));
    String serialized = sut.writeValueAsString(
        JsonView.with(ref)
            .onClass(TestObject.class, match()
                .exclude("listOfObjects.val")));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertEquals(ref.getInt1(), obj.get("int1"));
    assertTrue(obj.get("listOfObjects") instanceof List);
    List<Map<String, Object>> list = (List<Map<String, Object>>) obj.get("listOfObjects");
    assertEquals(2, list.size());
    assertNull(list.get(0).get("val"));
    assertNull(list.get(1).get("val"));
    assertNotNull(list.get(1).get("sub"));
  }

  @Test
  public void testMapOfSubobjects() throws IOException {
    TestObject ref = new TestObject();
    ref.setInt1(1);
    ref.setMapOfObjects(ImmutableMap.of(
        "key1", new TestSubobject("test1"),
        "key2", new TestSubobject("test2", new TestSubobject("test3"))
    ));
    String serialized = sut.writeValueAsString(
        JsonView.with(ref)
            .onClass(TestObject.class, match()
                .exclude("sub.val"))
            .onClass(TestSubobject.class, match()
                .exclude("sub")));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertEquals(ref.getInt1(), obj.get("int1"));
    assertTrue(obj.get("mapOfObjects") instanceof Map);
    Map<String, Map<String, Object>> map = (Map<String, Map<String, Object>>) obj.get("mapOfObjects");
    assertEquals(2, map.size());
    assertEquals("test1", map.get("key1").get("val"));
    assertEquals("test2", map.get("key2").get("val"));
    assertNull(map.get("key2").get("sub"));
  }

  @Test
  public void testMapWithIntKeys() throws IOException {
    TestObject ref = new TestObject();
    ref.setInt1(1);
    ref.setMapWithIntKeys(ImmutableMap.of(
        1, "red",
        2, "green"
    ));
    String serialized = sut.writeValueAsString(
        JsonView.with(ref)
            .onClass(TestObject.class, match()
                .exclude("sub.val"))
            .onClass(TestSubobject.class, match()
                .exclude("sub")));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertEquals(ref.getInt1(), obj.get("int1"));
    assertTrue(obj.get("mapWithIntKeys") instanceof Map);
    Map map = (Map) obj.get("mapWithIntKeys");
    assertEquals(2, map.size());
    assertNull(map.get(1));
    assertEquals(ref.getMapWithIntKeys().get(1), map.get("1"));
    assertNull(map.get(2));
    assertEquals(ref.getMapWithIntKeys().get(2), map.get("2"));
  }

  @Test
  public void testClassMatching() throws Exception {
    TestObject ref = new TestObject();
    ref.setStr1("str");
    TestSubobject sub = new TestSubobject();
    sub.setVal("val1");
    ref.setSub(sub);

    String serialized = sut.writeValueAsString(JsonView.with(ref).onClass(TestSubobject.class, match().exclude("val")));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);
    assertEquals(ref.getStr1(), obj.get("str1"));
    assertNotNull(obj.get("sub"));
    assertNull(((Map) obj.get("sub")).get("val"));
  }

  @Test
  public void testClassMatchingMixedWithPathMatching() throws Exception {
    TestObject ref = new TestObject();
    ref.setStr1("str");
    ref.setInt1(3);
    TestSubobject sub = new TestSubobject();
    sub.setVal("val1");
    sub.setOtherVal("val2");
    ref.setSub(sub);

    String serialized = sut.writeValueAsString(JsonView.with(ref)
        .onClass(TestObject.class, match()
            .exclude("int1", "sub.otherVal"))
        .onClass(TestSubobject.class, match()
            .exclude("val")));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);
    assertEquals(ref.getStr1(), obj.get("str1"));
    assertNotNull(obj.get("sub"));
    assertNull(((Map) obj.get("sub")).get("val"));
    assertNotNull(((Map) obj.get("sub")).get("otherVal"));
  }

  @Test
  public void testClassMatchingWithNoRootMatcher() throws Exception {
    TestObject ref = new TestObject();
    ref.setStr1("str");
    ref.setInt1(3);
    TestSubobject sub = new TestSubobject();
    sub.setVal("val1");
    sub.setOtherVal("val2");
    ref.setSub(sub);

    String serialized = sut.writeValueAsString(JsonView.with(ref).onClass(TestSubobject.class, match()
        .exclude("*")
        .include("otherVal")));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);
    assertEquals(ref.getStr1(), obj.get("str1"));
    assertEquals(ref.getInt1(), obj.get("int1"));
    assertNotNull(obj.get("sub"));
    assertNull(((Map) obj.get("sub")).get("val"));
    assertNotNull(((Map) obj.get("sub")).get("otherVal"));
  }

  @Test
  public void testBlanketExclude() throws Exception {
    TestObject ref = new TestObject();
    ref.setInt1(1);
    ref.setStr1("str1");
    ref.setStr2("str2");
    ref.setMapOfObjects(ImmutableMap.of(
        "key1", new TestSubobject("test1"),
        "key2", new TestSubobject("test2", new TestSubobject("test3"))
    ));

    String serialized = sut.writeValueAsString(
        JsonView.with(ref)
            .onClass(TestObject.class, match()
                .exclude("*")
                .include("str2")));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);
    assertEquals(ref.getStr2(), obj.get("str2"));
    assertNull(obj.get("str1"));
    assertNull(obj.get("mapWithIntKeys"));
    assertNull(obj.get("int1"));
  }

  @Test
  public void testBlanketInclude() throws Exception {
    TestObject ref = new TestObject();
    ref.setInt1(1);
    ref.setStr1("str1");
    ref.setStr2("str2");
    ref.setIgnoredDirect("ignoredDirect");
    ref.setIgnoredIndirect("ignoredIndirect");
    ref.setMapOfObjects(ImmutableMap.of(
        "key1", new TestSubobject("test1"),
        "key2", new TestSubobject("test2", new TestSubobject("test3"))
    ));

    String serialized = sut.writeValueAsString(
        JsonView.with(ref)
            .onClass(TestObject.class, match()
                .include("*")
                .exclude("str2")));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);
    assertEquals(ref.getStr1(), obj.get("str1"));
    assertNull(obj.get("str2"));
    assertEquals(ref.getIgnoredIndirect(), obj.get("ignoredIndirect"));
    assertEquals(ref.getInt1(), obj.get("int1"));
    assertNotNull(obj.get("mapOfObjects"));
  }

  @Test
  public void testDate() throws Exception {
    TestObject ref = new TestObject();
    ref.setDate(new Date());

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);
    assertEquals(ref.getDate().getTime(), obj.get("date"));
  }

  @Test
  public void testDate_withFormatter() throws Exception {
    SimpleDateFormat fmt = new SimpleDateFormat("dd-MM-yyyy");
    sut.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
    sut.setDateFormat(fmt);

    TestObject ref = new TestObject();
    ref.setDate(new Date());

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);
    assertEquals(fmt.format(ref.getDate()), obj.get("date"));
  }

  @Test
  public void testPrimitiveArrays() throws Exception {
    TestObject ref = new TestObject();
    ref.setIntArray(new int[]{1, 2, 3});
    ref.setByteArray("asdf".getBytes());
    ref.setStringArray(new String[]{"val1", "val2"});

    TestObject t1 = new TestObject();
    t1.setInt1(1);
    TestObject t2 = new TestObject();
    t2.setInt1(2);
    ref.setObjArray(new TestObject[]{t1, t2});

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);
    assertEquals(Ints.asList(ref.getIntArray()), obj.get("intArray"));
    assertEquals(Arrays.asList(ref.getStringArray()), obj.get("stringArray"));

    assertEquals("asdf", new String(BaseEncoding.base64().decode((String) obj.get("byteArray"))));

    List<Map<String, Object>> objList = (List<Map<String, Object>>) obj.get("objArray");
    assertEquals(2, objList.size());
    assertEquals(t1.getInt1(), objList.get(0).get("int1"));
    assertEquals(t2.getInt1(), objList.get(1).get("int1"));
  }

  @Test
  public void testURLs() throws Exception {
    TestObject ref = new TestObject();
    ref.setUrl(new URL("http://google.com"));

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);
    assertEquals(ref.getUrl().toString(), obj.get("url"));
  }

  @Test
  public void testURIs() throws Exception {
    TestObject ref = new TestObject();
    ref.setUri(new URI("http://google.com"));

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);
    assertEquals(ref.getUri().toString(), obj.get("uri"));
  }

  @Test
  public void testClass() throws Exception {
    TestObject ref = new TestObject();
    ref.setCls(TestSubobject.class);

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);
    assertEquals(ref.getCls().getCanonicalName(), obj.get("cls"));
  }

  @Test
  public void testEnums() throws Exception {
    TestObject ref = new TestObject();
    ref.setTestEnum(TestEnum.VALUE_A);

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);
    assertNotNull(obj.get("testEnum"));
    assertEquals(ref.getTestEnum().toString(), obj.get("testEnum"));
  }

  @Test
  public void testStaticFieldsAreIgnored() throws Exception {
    TestObject ref = new TestObject();
    ref.setStr1("val1");

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);
    assertNull(obj.get("PUBLIC_FIELD"));
    assertNull(obj.get("PRIVATE_FIELD"));
  }

  @Test
  public void testPathNotationInList() throws Exception {
    TestSubobject sub1 = new TestSubobject("a");
    TestSubobject sub2 = new TestSubobject("b");
    sub1.setSub(sub2);
    TestSubobject sub3 = new TestSubobject("c");
    sub2.setSub(sub3);
    TestObject ref = new TestObject();
    ref.setListOfObjects(Arrays.asList(sub1, sub2));

    String serialized = sut.writeValueAsString(
        JsonView.with(ref)
            .onClass(TestObject.class, match()
                .exclude("listOfObjects.sub.val")));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertNotNull(obj.get("listOfObjects"));
    List<Map<String, Map<String, Map<String, Map>>>> list = (List<Map<String, Map<String, Map<String, Map>>>>) obj.get("listOfObjects");
    assertEquals(2, list.size());
    assertNull(list.get(0).get("sub").get("val"));
    assertNotNull(list.get(0).get("sub").get("sub").get("val"));
    assertNull(list.get(1).get("sub").get("val"));
  }

  @Test
  public void testWriteNullValues_enabledGlobally() throws Exception {
    TestObject ref = new TestObject();
    sut = sut.setSerializationInclusion(Include.ALWAYS);

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertTrue(obj.containsKey("list"));
    assertNull(obj.get("list"));
  }

  @Test
  public void testWriteNullValues_disabledGlobally() throws Exception {
    TestObject ref = new TestObject();
    sut.setSerializationInclusion(Include.NON_NULL);

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertFalse(obj.containsKey("str2"));
  }

  @Test
  public void testWriteNullValues_enabledForClass() throws Exception {
    TestNulls ref = new TestNulls();
    sut.setSerializationInclusion(Include.NON_NULL);

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertTrue(obj.containsKey("val"));
    assertNull(obj.get("val"));
  }

  @Test
  public void testWriteNullValues_disabledForClass() throws Exception {
    TestNonNulls ref = new TestNonNulls();
    sut.setSerializationInclusion(Include.ALWAYS);

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertFalse(obj.containsKey("val"));
  }

  @Test
  public void testImplicitInclude() throws Exception {
    TestObject ref = new TestObject();
    TestSubobject sub = new TestSubobject("test1");
    sub.setOtherVal("otherVal1");
    sub.setVal("asdf");
    ref.setSub(sub);

    String serialized = sut.writeValueAsString(JsonView.with(ref)
        .onClass(TestObject.class, match()
            .exclude("*")
            .include("sub.otherVal")));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertNotNull(obj.get("sub"));
    Map m = (Map) obj.get("sub");
    assertEquals(sub.getOtherVal(), m.get("otherVal"));
    assertNull(m.get("val"));
  }

  @Test
  public void testIncludesForList() throws Exception {
    TestObject ref = new TestObject();
    TestSubobject testSubobject1 = new TestSubobject("test1");
    testSubobject1.setOtherVal("otherVal1");
    testSubobject1.setVal("asdf");
    TestSubobject testSubobject2 = new TestSubobject("test2");
    testSubobject2.setOtherVal("otherVal2");
    testSubobject2.setVal("asdf");
    ref.setListOfObjects(Arrays.asList(testSubobject1, testSubobject2));

    String serialized = sut.writeValueAsString(JsonView.with(ref)
        .onClass(TestObject.class,
            match().exclude("*")
                .include("listOfObjects.otherVal")));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertNotNull(obj.get("listOfObjects"));
    List<Map<String, String>> list = (List<Map<String, String>>) obj.get("listOfObjects");
    assertEquals(2, list.size());
    assertNull(list.get(0).get("val"));
    assertNull(list.get(1).get("val"));
    assertNotNull(list.get(0).get("otherVal"));
    assertNotNull(list.get(1).get("otherVal"));
  }

  @Test
  public void testMatchingOnInterfaces() throws Exception {
    TestObject ref = new TestObject();
    ref.setStr1("asdf");
    ref.setDate(new Date());

    String serialized = sut.writeValueAsString(JsonView.with(ref)
        .onClass(TestInterface.class,
            match().exclude("date")));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertNotNull(obj.get("str1"));
    assertEquals(ref.getStr1(), obj.get("str1"));
    assertNull(obj.get("date"));
  }

  @Test
  public void testIgnorePropertiesOnField() throws Exception {
    TestObject ref = new TestObject();
    TestSubobject testSubobject1 = new TestSubobject("test1");
    testSubobject1.setOtherVal("otherVal1");
    testSubobject1.setVal("asdf");
    ref.setSub(testSubobject1);
    ref.setSubWithIgnores(testSubobject1);

    String serialized = sut.writeValueAsString(JsonView.with(ref)
        .onClass(TestObject.class,
            match().exclude("sub")));
    Map<String, Map<String, Object>> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertNull(obj.get("sub"));
    assertNotNull(obj.get("subWithIgnores"));
    assertNotNull(obj.get("subWithIgnores").get("otherVal"));
    assertNull(obj.get("subWithIgnores").get("val"));
  }

  @Test
  public void testBackReferenceSupport() throws Exception {
    TestForwardReferenceObject forward = new TestForwardReferenceObject();
    TestBackreferenceObject back = new TestBackreferenceObject();

    forward.setId("forward");
    forward.setParent(back);
    back.setId("back");
    back.setChildren(asList(forward));

    String serialized = sut.writeValueAsString(JsonView.with(forward));
    Map<String, Map<String, Object>> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertNotNull(obj.get("parent"));
    assertEquals("back", obj.get("parent").get("id"));
  }

  @Test
  public void testBigDecimalSerialization() throws Exception {
    TestObject ref = new TestObject();
    ref.setBigDecimal(new BigDecimal(Math.PI));

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertNotNull(obj.get("bigDecimal"));
    assertEquals(3.141592653589793, obj.get("bigDecimal"));
  }

  @Test
  public void testCustomSerializationByDefault() throws Exception {
    TestObject ref = new TestObject();
    CustomType custom = new CustomType(5l, "hello");
    ref.setCustom(custom);

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertNotNull(obj.get("custom"));
    assertTrue(obj.get("custom") instanceof Map);
    assertTrue(((Map) obj.get("custom")).get("name").equals("hello"));
    assertTrue(((Map) obj.get("custom")).get("sid").equals(5));
  }

  @Test
  public void testCustomSerializationRegistered() throws Exception {
    TestObject ref = new TestObject();
    CustomType custom = new CustomType(5l, "hello");
    ref.setCustom(custom);

    sut = new ObjectMapper().registerModule(new JsonViewModule(serializer).registerSerializer(CustomType.class, new CustomTypeSerializer()));

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertNotNull(obj.get("custom"));
    assertTrue(obj.get("custom") instanceof String);
    assertTrue((obj.get("custom")).equals("5[hello]"));
  }

  @Test
  public void testWriteJSR310_zonedDateTime() throws Exception {
    TestObject ref = new TestObject();
    ref.setZonedDateTime(ZonedDateTime.now());

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertNotNull(obj.get("zonedDateTime"));
    assertTrue(obj.get("zonedDateTime") instanceof Number);
  }

  @Test
  public void testWriteJSR310_formattedZonedDateTime() throws Exception {
    sut.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
    TestObject ref = new TestObject();
    ref.setFormattedZonedDateTime(ZonedDateTime.now());

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertNotNull(obj.get("formattedZonedDateTime"));
    assertTrue(obj.get("formattedZonedDateTime") instanceof String);
  }

  @Test
  public void testWriteJsonProperty() throws Exception {
    TestObject ref = new TestObject();
    ref.setJsonProp("jibjab");

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertNotNull(obj.get("totallyJsonProp"));
    assertEquals(ref.getJsonProp(), obj.get("totallyJsonProp"));
  }

  @Test
  public void testWriteJsonProperty_noValue() throws Exception {
    TestObject ref = new TestObject();
    ref.setJsonPropNoValue("jibjab");

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertNotNull(obj.get("jsonPropNoValue"));
    assertEquals(ref.getJsonPropNoValue(), obj.get("jsonPropNoValue"));
  }

  @Test
  public void testDeepNestedObjects() throws Exception {
    TestSubobject subobject = new TestSubobject();
    subobject.setVal("someval");
    subobject.setOtherVal("otherval");
    TestObject ref = new TestObject();
    ref.setStr1("somestr");
    ref.setSub(subobject);

    String serialized = sut.writeValueAsString(JsonView.with(ref)
        .onClass(TestObject.class, match()
            .include("sub.*")
            .exclude("sub.otherVal")));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertNotNull(obj.get("sub"));
    assertTrue(obj.get("sub") instanceof Map);

    Map<String, Object> subMap = (Map<String, Object>) obj.get("sub");
    assertNotNull(subMap.get("val"));
    assertEquals(subobject.getVal(), subMap.get("val"));
    assertNull(subMap.get("otherVal"));
  }

  @Test
  public void testUUIDs() throws Exception {
    TestObject ref = new TestObject();
    ref.setUuid(UUID.randomUUID());

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertNotNull(obj.get("uuid"));
    assertEquals(ref.getUuid().toString(), obj.get("uuid"));
  }

  @Test
  public void testJsonSerializeAnnotation() throws Exception {
    TestObject ref = new TestObject();
    CustomType custom = new CustomType(5l, "hello");
    ref.setCustomFieldSerializer(custom);

    sut = new ObjectMapper().registerModule(new JsonViewModule(serializer));

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertNotNull(obj.get("customFieldSerializer"));
    assertTrue(obj.get("customFieldSerializer") instanceof String);
    assertEquals(obj.get("customFieldSerializer"), "5[hello]");
  }

  @Test
  public void testPathFirstMatch_defaultBehavior() throws Exception {
    TestObject ref = new TestObject();
    ref.setInt1(1);
    ref.setStr1("sdfdsf");
    ref.setStr2("erer");
    ref.setRecursion(ref);

    Consumer<Map<String, Object>> doTest = obj -> {
      assertEquals(ref.getInt1(), obj.get("int1"));
      assertEquals(ref.getStr1(), obj.get("str1"));
      assertEquals(ref.getStr2(), obj.get("str2"));
      assertNotNull(obj.get("recursion"));

      Map<String, Object> rec1 = (Map<String, Object>) obj.get("recursion");
      assertNotNull(rec1.get("recursion"));
      assertEquals(ref.getStr1(), rec1.get("str1"));
      assertNull(rec1.get("str2"));
      assertNull(rec1.get("int1"));

      Map<String, Object> rec2 = (Map<String, Object>) rec1.get("recursion");
      assertEquals(ref.getStr2(), rec2.get("str2"));
      assertNull(rec2.get("str1"));
      assertNull(rec2.get("int1"));

      assertNull(rec2.get("recursion"));
    };


    // Perform test with behavior set at default level
    sut = new ObjectMapper().registerModule(new JsonViewModule(serializer)
        .withDefaultMatcherBehavior(MatcherBehavior.PATH_FIRST));
    String serialized = sut.writeValueAsString(JsonView.with(ref)
        .onClass(TestObject.class, match()
            .include("recursion", "recursion.str1", "recursion.recursion.str2", "str1", "str2", "int1")
            .exclude("*")));
    doTest.accept(sut.readValue(serialized, NonReplacableKeyMap.class));

    // Perform test with behavior set at the JsonView level
    sut = new ObjectMapper().registerModule(new JsonViewModule(serializer));
    serialized = sut.writeValueAsString(JsonView.with(ref)
        .withMatcherBehavior(MatcherBehavior.PATH_FIRST)
        .onClass(TestObject.class, match()
            .include("recursion", "recursion.str1", "recursion.recursion.str2", "str1", "str2", "int1")
            .exclude("*")));
    doTest.accept(sut.readValue(serialized, NonReplacableKeyMap.class));
  }

  @Test
  public void testFieldTransform() throws Exception {
    TestObject ref = new TestObject();
    ref.setInt1(1);
    ref.setStr1("sdfdsf");

    sut = new ObjectMapper().registerModule(new JsonViewModule(serializer));

    String serialized = sut.writeValueAsString(JsonView.with(ref)
        .onClass(TestObject.class, match()
            .exclude("*")
            .include("str1", "str2")
            .transform("str1", (TestObject t, String f) -> f.toUpperCase())
            .transform("str2", (TestObject t, String f) -> t.getStr1())));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertNull(obj.get("int1"));
    assertNotNull(obj.get("str1"));
    assertEquals(ref.getStr1().toUpperCase(), obj.get("str1"));
    assertNotNull(obj.get("str2"));
    assertEquals(ref.getStr1(), obj.get("str2"));
  }

  @Test
  public void testMethodGetters() throws Exception {
    TestObject ref = new TestObject();

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertEquals("TEST", obj.get("staticValue"));
  }

  @Test
  public void testMethodGetters_ignored() throws Exception {
    TestObject ref = new TestObject();

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertFalse(obj.containsKey("ignoredValue"));
  }

  @Test
  public void testMethodGetters_combinesAnnotations() throws Exception {
    TestObject ref = new TestObject();

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertFalse(obj.containsKey("ignoredDirect"));
  }

  @Test
  public void testAutodetect_notPresent() throws Exception {
    AutodetectNotPresent ref = new AutodetectNotPresent();
    ref.setId("test");

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertEquals("valid", obj.get("id"));
  }

  @Test
  public void testAutodetect_default() throws Exception {
    AutodetectDefault ref = new AutodetectDefault();
    ref.setId("test");

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertEquals("valid", obj.get("id"));
  }

  @Test
  public void testAutodetect_fieldsOnly() throws Exception {
    AutodetectFields ref = new AutodetectFields();
    ref.setId("test");

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertEquals("test", obj.get("id"));
  }

  @Test
  public void testAutodetect_methodsOnly() throws Exception {
    AutodetectGetters ref = new AutodetectGetters();
    ref.setId("test");

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertEquals("valid", obj.get("id"));
  }

  @Test
  public void testSuperinterfaces() throws Exception {
    TestInterfaceObject ref = new TestInterfaceObject();
    ref.setDescription("description");

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertEquals("ID", obj.get("id"));
    assertEquals("NAME", obj.get("name"));
    assertEquals("description", obj.get("description"));
  }

  @Test
  public void testSuperinterfaces_matchers() throws Exception {
    TestInterfaceObject ref = new TestInterfaceObject();
    ref.setDescription("description");

    String serialized = sut.writeValueAsString(JsonView.with(ref)
        .onClass(TestSuperinterface.class, match()
            .exclude("id"))
        .onClass(TestChildInterface.class, match()
            .exclude("name")));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertFalse(obj.containsKey("id"));
    assertFalse(obj.containsKey("name"));
    assertEquals("description", obj.get("description"));
  }

  @Test
  public void testSerializationOptions_includeNonNullGlobally() throws Exception {
    TestObject ref = new TestObject();
    ref.setInt1(1);
    sut = sut.setSerializationInclusion(Include.NON_NULL);

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertFalse(obj.containsKey("str1"));
    assertEquals(ref.getInt1(), obj.get("int1"));
  }

  @Test
  public void testSerializationOptions_includeNonNullLocally() throws Exception {
    TestObject ref = new TestObject();
    ref.setStr1("test");

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertEquals(ref.getStr1(), obj.get("str1"));
    assertFalse(obj.containsKey("str2"));
  }

  @Test
  public void testSerializationOrder() throws Exception {
    TestObject ref = new TestObject();
    ref.setStr2("sdfsdf");

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    List<String> keys = new ArrayList<>(obj.keySet());
    assertEquals("sub", keys.get(0));
    assertEquals("subWithIgnores", keys.get(1));
    assertEquals("str1", keys.get(2));
    assertEquals("str2", keys.get(3));
    assertEquals("date", keys.get(4));
  }

  @Test
  public void testJacksonJsonNodeSupport_object() throws Exception {
    TestObject ref = new TestObject();
    ObjectNode node1 = sut.createObjectNode();
    ObjectNode node2 = sut.createObjectNode();
    node2.set("stringfield", new TextNode("hello"));
    node1.set("jacksonObject", node2);
    ref.setJsonNode(node1);

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertNotNull(obj.get("jsonNode"));
    assertTrue(obj.get("jsonNode") instanceof Map);

    Map<String, Object> jsonNode = (Map<String, Object>) obj.get("jsonNode");
    assertTrue(jsonNode.get("jacksonObject") instanceof Map);
    assertEquals("hello", ((Map) jsonNode.get("jacksonObject")).get("stringfield"));
  }

  @Test
  public void testJacksonJsonNodeSupport_textNode() throws Exception {
    TestObject ref = new TestObject();
    ref.setJsonNode(new TextNode("asdf"));

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertEquals(obj.get("jsonNode"), "asdf");
  }

  @Test
  public void testJacksonJsonNodeSupport_nullNode() throws Exception {
    TestObject ref = new TestObject();
    ref.setJsonNode(NullNode.getInstance());

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertNull(obj.get("jsonNode"));
  }

  /**
   * Verify that multiple accessible properties are not serialized more than once (issue #59)
   */
  @Test
  public void testDuplicateKeysOnInheritance() throws Exception {
    ClassC ref = new ClassC();

    String serialized = sut.writeValueAsString(JsonView.with(ref));
    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);

    assertEquals(ref.getId(), obj.get("id"));
  }
  
  @Test
  public void testFieldWithGetInName() throws IOException {
    TestObject ref = new TestObject();
    ref.setInt1(1);
    ref.setWidgetName("random name");
    String serialized = sut.writeValueAsString(
        JsonView.with(ref).onClass(TestObject.class, match()));

    Map<String, Object> obj = sut.readValue(serialized, NonReplacableKeyMap.class);
    assertNull(obj.get("widName"));
    assertNotNull(obj.get("widgetName"));
  }

}