package nxm.sys.test.junit.lib;

import static nxm.sys.lib.JsonUtilities.FILE_EXT;
import static nxm.sys.lib.JsonUtilities.MEDIA_TYPE;
import static nxm.sys.lib.JsonUtilities.fromJSON;
import static nxm.sys.lib.JsonUtilities.isMediaTypeJSON;
import static nxm.sys.lib.JsonUtilities.parseNumber;
import static nxm.sys.lib.JsonUtilities.toJSON;
import static nxm.sys.lib.JsonUtilities.toType;

import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.net.URI;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;

import nxm.sys.inc.MapKBT;
import nxm.sys.lib.BaseFile;
import nxm.sys.lib.Data;
import nxm.sys.lib.JsonUtilities;
import nxm.sys.lib.JsonUtilities.JsonEntry;
import nxm.sys.lib.JsonUtilities.JsonObject;
import nxm.sys.lib.JsonUtilities.Mode;
import nxm.sys.lib.Midas;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.Shell;
import nxm.sys.lib.Table;
import nxm.sys.lib.TextFile;
import nxm.sys.test.junit.NMTestCase;


/** This will test the JsonUtilites class.
 *
 * @author J. Czechowski, Jeanette McCall
*/
public class JsonUtilitiesTest extends NMTestCase {

  /** Tolerance: default (1e-7) */
  private static final float TOLERANCE = 0.0000001f;

  public JsonUtilitiesTest (String methodName) {
    this(methodName, null);
  }

  public JsonUtilitiesTest (String methodName, Midas midas) {
    super(methodName, midas);
  }

  /**
   * testIsMediaTypeJSON() this will test that the possible types of json are accounted for.
   * Example: application/json returns true because it is a valid json media type.
   */
  public void testIsMediaTypeJSON () {
    assertEquals("MEDIA_TYPE", "application/json", MEDIA_TYPE);
    assertEquals("FILE_EXT",   ".json",            FILE_EXT);

    assertEquals("isMediaTypeJSON('application/json')", true,  isMediaTypeJSON("application/json"));
    assertEquals("isMediaTypeJSON('text/json')",        true,  isMediaTypeJSON("text/json"));
    assertEquals("isMediaTypeJSON(MEDIA_TYPE)",         true,  isMediaTypeJSON(MEDIA_TYPE));
    assertEquals("isMediaTypeJSON('json')",             false, isMediaTypeJSON("json"));
    assertEquals("isMediaTypeJSON('application/foo')",  false, isMediaTypeJSON("application/foo"));

    try {
      isMediaTypeJSON(null);
      assertEquals("isMediaTypeJSON(null) causes NullPointerException", true, false);
    }
    catch (NullPointerException e) {
      assertEquals("isMediaTypeJSON(null) causes NullPointerException", true, true);
    }
  }

  /** Tests parseNumber(..) in error and null conditions. */
  private void _testParseNumber (String in) {
    if (in == null) {
      assertEquals("parseNumber("+in+")", null, parseNumber(in));
    }
    else {
      try {
        parseNumber(in);
        assertEquals("parseNumber("+in+") causes NumberFormatException", true, false);
      }
      catch (NumberFormatException e) {
        assertEquals("parseNumber("+in+") causes NumberFormatException", true, true);
      }
    }
  }

  /** Tests parseNumber(..) in error and null conditions. */
  private void _testParseNumber (String in, int exp) {
    Number num = parseNumber(in);
         if (num instanceof Byte   ) assertEquals("parseNumber("+in+")",       exp, num.intValue());
    else if (num instanceof Short  ) assertEquals("parseNumber("+in+")",       exp, num.intValue());
    else if (num instanceof Integer) assertEquals("parseNumber("+in+")",       exp, num.intValue());
    else if (num instanceof Long   ) assertEquals("parseNumber("+in+")", (long)exp, num.longValue());
    else if (num instanceof Float  ) assertEquals("parseNumber("+in+")",       exp, num);
    else                             assertEquals("parseNumber("+in+")",       exp, num);
    _testParseNumber(Long.toString(exp),   (long  )exp);
    _testParseNumber(Float.toString(exp),  (float )exp);
    _testParseNumber(Double.toString(exp), (double)exp);
  }

  /** Tests parseNumber(..) in error and null conditions. */
  private void _testParseNumber (String in, long exp) {
    Number num = parseNumber(in);
         if (num instanceof Byte   ) assertEquals("parseNumber("+in+")", exp, num.longValue());
    else if (num instanceof Short  ) assertEquals("parseNumber("+in+")", exp, num.longValue());
    else if (num instanceof Integer) assertEquals("parseNumber("+in+")", exp, num.longValue());
    else if (num instanceof Long   ) assertEquals("parseNumber("+in+")", exp, num.longValue());
    else if (num instanceof Float  ) assertEquals("parseNumber("+in+")", exp, num);
    else                             assertEquals("parseNumber("+in+")", exp, num);
  }

  /** Tests parseNumber(..) in error and null conditions. */
  private void _testParseNumber (String in, float exp) {
    Number num = parseNumber(in);
         if (num instanceof Byte   ) assertEquals("parseNumber("+in+")", exp, num.floatValue());
    else if (num instanceof Short  ) assertEquals("parseNumber("+in+")", exp, num.floatValue());
    else if (num instanceof Integer) assertEquals("parseNumber("+in+")", exp, num.floatValue());
    else if (num instanceof Long   ) assertEquals("parseNumber("+in+")", exp, num.floatValue());
    else if (num instanceof Float  ) assertEquals("parseNumber("+in+")", exp, num.floatValue());
    else                             assertEquals("parseNumber("+in+")", exp, num.floatValue());
  }

  /** Tests parseNumber(..) in error and null conditions. */
  private void _testParseNumber (String in, double exp) {
    Number num = parseNumber(in);
         if (num instanceof Byte   ) assertEquals("parseNumber("+in+")", exp, num.doubleValue());
    else if (num instanceof Short  ) assertEquals("parseNumber("+in+")", exp, num.doubleValue());
    else if (num instanceof Integer) assertEquals("parseNumber("+in+")", exp, num.doubleValue());
    else if (num instanceof Long   ) assertEquals("parseNumber("+in+")", exp, num.doubleValue());
    else if (num instanceof Float  ) assertEquals("parseNumber("+in+")", exp, num.doubleValue());
    else                             assertEquals("parseNumber("+in+")", exp, num.doubleValue());
  }

  /**
   * testParseNumber() will test the ability to parse a number.
   * Example: "3.14" will equal 3.14 after parsing.
   */
  public void testParseNumber () {
    _testParseNumber(null);
    _testParseNumber("ten");
    _testParseNumber("3.14.2015");
    _testParseNumber("3f14");
    _testParseNumber("3e1.14");
    _testParseNumber("0",                                   0                       );
    _testParseNumber("+0",                                 +0                       );
    _testParseNumber("-0",                                 -0                       );
    _testParseNumber("0.0",                                 0.0                     );
    _testParseNumber("+0.0",                                Double.valueOf("+0.0")  );
    _testParseNumber("-0.0",                                Double.valueOf("-0.0")  );
    _testParseNumber("42",                                  42                      );
    _testParseNumber("+42",                                +42                      );
    _testParseNumber("-42",                                -42                      );
    _testParseNumber("42.0",                                42.0                    );
    _testParseNumber("+42.0",                              +42.0                    );
    _testParseNumber("-42.0",                              -42.0                    );
    _testParseNumber("3.14",                                3.14                    );
    _testParseNumber("+3.14",                              +3.14                    );
    _testParseNumber("-3.14",                              -3.14                    );
    _testParseNumber(Integer.toString(Integer.MAX_VALUE),   Integer.MAX_VALUE       );
    _testParseNumber(Integer.toString(Integer.MIN_VALUE),   Integer.MIN_VALUE       );
    _testParseNumber(Long.toString(   Long.MAX_VALUE   ),   Long.MAX_VALUE          );
    _testParseNumber(Long.toString(   Long.MIN_VALUE   ),   Long.MIN_VALUE          );
    _testParseNumber(Float.toString( +Float.MAX_VALUE  ),  +Float.MAX_VALUE         );
    _testParseNumber(Float.toString( -Float.MAX_VALUE  ),  -Float.MAX_VALUE         );
    _testParseNumber(Float.toString(  Float.MIN_NORMAL ),   Float.MIN_NORMAL        );
    _testParseNumber(Double.toString(+Double.MAX_VALUE ),  +Double.MAX_VALUE        );
    _testParseNumber(Double.toString(-Double.MAX_VALUE ),  -Double.MAX_VALUE        );
    _testParseNumber(Double.toString( Double.MIN_NORMAL),   Double.MIN_NORMAL       );
    _testParseNumber("NaN",                                 Double.NaN              );
    _testParseNumber("Infinity",                            Double.POSITIVE_INFINITY);
    _testParseNumber("+Infinity",                           Double.POSITIVE_INFINITY);
    _testParseNumber("-Infinity",                           Double.NEGATIVE_INFINITY);

    int  j = 0x00000001;
    long k = 0x0000000000000001L;
    for (int i = -512; i < 512; i++      ) _testParseNumber(Integer.toString(i), i);
    for (int i = -512; i < 512; i++      ) _testParseNumber(Long.toString(i),    i);
    for (int i =    0; i <  32; i++,j<<=1) _testParseNumber(Integer.toString(j), j);
    for (int i =    0; i <  32; i++,j>>=1) _testParseNumber(Integer.toString(j), j);
    for (int i =    0; i <  64; i++,k<<=1) _testParseNumber(Long.toString(k),    k);
    for (int i =    0; i <  64; i++,k>>=1) _testParseNumber(Long.toString(k),    k);
  }

  private <T> void _testToType (Number num) {
    _testToType(Byte.class,    num, num.byteValue());
    _testToType(Short.class,   num, num.shortValue());
    _testToType(Integer.class, num, num.intValue());
    _testToType(Long.class,    num, num.longValue());
    _testToType(Float.class,   num, num.floatValue());
    _testToType(Double.class,  num, num.doubleValue());
  }

  private <T> void _testToType (Class<T> type, Object in, Object out) {
    _testToType(type, type, in, out);
  }

  private <T> void _testToType (Type type, Class<T> clazz, Object in, Object out) {
    Object act = toType(type,in);
    if (act != null) {
      assertTrue("toType("+type+", "+in+")",clazz.isAssignableFrom(act.getClass()));
    }
    if ((act != null) && act.getClass().isArray() && out.getClass().isArray()) {
      assertEquals(        "toType("+type+", "+in+")", Array.getLength(out), Array.getLength(act));
      for (int i = 0; i < Array.getLength(act); i++) {
        assertEquals(      "toType("+type+", "+in+")", Array.get(out,i), Array.get(act,i));
      }
    }
    else {
      assertEquals(        "toType("+type+", "+in+")", out,  act);
    }
  }

  /**
   * testToType() will test the consistency of the json utilities creation of objects from json.
   * For instance, if the value of "Foo" is sent in, it will test that it is an object of String.class that is created.
   *
   */
  public void testToType () {
    // ==== BASIC TYPES ============================================================================
    List<String> inList   = Arrays.asList("A","B","C","D");
    List<String> outList1 = new LinkedList<>(inList);
    List<String> outList2 = new ArrayList<>(inList);

    Map<String,Object> inMap  = new HashMap<>(4);
    Map<String,Object> outMap = new LinkedHashMap<>(4);
    inMap.put("one",   1);    outMap.put("one",   1);
    inMap.put("two",   2);    outMap.put("two",   2);
    inMap.put("three", 3);    outMap.put("three", 3);
    inMap.put("four",  4);    outMap.put("four",  4);

    _testToType(Object.class,        null,   null);
    _testToType(Integer.class,       42,     42  );
    _testToType(String.class,        "Foo",  "Foo");
    _testToType(outList1.getClass(), inList, outList1);
    _testToType(outList2.getClass(), inList, outList2);
    _testToType(outMap.getClass(),   inMap,  outMap);

    // ==== JSON OBJECT ============================================================================
    // tested in testToJSON()

    // ==== STRINGS ================================================================================
    BigDecimal dec  = new BigDecimal("123.456");
    URL        url;
    try {
      url = new URI("http://example.com").toURL();

      UUID       uuid = UUID.randomUUID();
      //Instant    now  = Instant.now();  // MODIFICATION Java 8 ONLY

      _testToType(Character.class,     "A",                      'A');
      _testToType(Mode.class,          "Auto",                   Mode.Auto);
      _testToType(String.class,        new StringBuilder("Foo"), "Foo");
      _testToType(BigDecimal.class,    dec.toString(),           dec);
      _testToType(URL.class,           url.toString(),           url);
      _testToType(UUID.class,          uuid.toString(),          uuid);
      //_testToType(Instant.class,       now.toString(),           now); // MODIFICATION Java 8 ONLY

      // ==== NUMBERS ================================================================================
      int  j = 0x00000001;
      long k = 0x0000000000000001L;
      for (int i = -512; i < 512; i++      ) _testToType(i);
      for (int i = -512; i < 512; i++      ) _testToType(i);
      for (int i =    0; i <  32; i++,j<<=1) _testToType(j);
      for (int i =    0; i <  32; i++,j>>=1) _testToType(j);
      for (int i =    0; i <  64; i++,k<<=1) _testToType(k);
      for (int i =    0; i <  64; i++,k>>=1) _testToType(k);
      _testToType(3.14);
      _testToType(Double.POSITIVE_INFINITY);
      _testToType(Double.NEGATIVE_INFINITY);
      _testToType(Double.NaN);

      // ==== VD DATA object ===========================================================================
      Data data = Data.fromString("(1, 2, 3.4)",'D',null);
      ArrayList<Double> expectedList = new ArrayList<>();
      expectedList.add(1.0);
      expectedList.add(2.0);
      expectedList.add(3.4);
      _testToType(expectedList.getClass(), data,  expectedList);

      // ==== GENERIC TYPES ==========================================================================
      Method sampleMethod1     = JsonUtilitiesTest.class.getDeclaredMethod("_sampleMethod1", Object.class, Object[].class, Map.class );
      Method sampleMethod2     = JsonUtilitiesTest.class.getDeclaredMethod("_sampleMethod2", Number.class, Number[].class, List.class);
      Type   T1                = sampleMethod1.getGenericReturnType();         // <T>                => T
      Type   T_extends_Number1 = sampleMethod2.getGenericReturnType();         // <T extends Number> => T
      Type   T2                = sampleMethod1.getGenericParameterTypes()[0];  // <T>                => T
      Type   T_extends_Number2 = sampleMethod2.getGenericParameterTypes()[0];  // <T extends Number> => T
      Type   TA                = sampleMethod1.getGenericParameterTypes()[1];  // <T> => T[]
      Type   T_extends_NumberA = sampleMethod2.getGenericParameterTypes()[1];  // <T extends Number> => T[]
      Type   Map_String_X      = sampleMethod1.getGenericParameterTypes()[2];  // Map<String,?>
      Type   List_X_extends_CS = sampleMethod2.getGenericParameterTypes()[2];  // List<? extends CharSequence>

      List<Object>       objList = new LinkedList<>();
      List<Number>       numList = new LinkedList<>();
      List<CharSequence> csList  = new ArrayList<>();
      objList.add(1.0);    numList.add(1.0);    csList.add("1.0");
      objList.add(2);      numList.add(2);      csList.add("2");
      objList.add("3");    numList.add(3);      csList.add("3");

      Map<Object,Object> mapIn   = new HashMap<>();
      Map<Object,Object> mapOut  = new HashMap<>();
      mapIn.put("one", 1.0);     mapOut.put("one", 1.0);
      mapIn.put(2,     2.0);     mapOut.put("2",   2.0);
      mapIn.put(3.0,   3.0);     mapOut.put("3.0", 3.0);

      _testToType(T1,                Object.class,   123,     123);
      _testToType(T_extends_Number1, Number.class,   123,     123);
      _testToType(T2,                Object.class,   123,     123);
      _testToType(T_extends_Number2, Number.class,   123,     123);
      _testToType(TA,                Object[].class, objList, objList.toArray(new Object[3]));
      _testToType(T_extends_NumberA, Number[].class, objList, numList.toArray(new Number[3]));
      _testToType(Map_String_X,      Map.class,      mapIn,   mapOut);
      _testToType(List_X_extends_CS, List.class,     objList, csList);
    }
    catch (Exception e) {

      fail( "Failed with Exception:"+e);
    }

  }

  private void _testFromToJSON (Object obj, String json) {
    _testFromToJSON(obj, json, obj);
  }

  private <T>                T _sampleMethod1 (T a, T[] b, Map<String,?>                c) { return null; }
  private <T extends Number> T _sampleMethod2 (T a, T[] b, List<? extends CharSequence> c) { return null; }

  private void _testFromToJSON (Object objIn, String json, Object objOut) {
    CharSequence jsonAuto     = toJSON(objIn, Mode.Auto);
    CharSequence jsonFastAuto = toJSON(objIn, Mode.FastAuto);
    CharSequence jsonCompact  = toJSON(objIn, Mode.Compact);
    CharSequence jsonFormal   = toJSON(objIn, Mode.Formal);
    Object       exp          = fromJSON(jsonAuto);

    assertEquals("toJSON("+objIn+",Compact)",  json,   jsonCompact.toString());
    // MODIFIED to use new local assertEqualsToleranceIfNumber rather than
    // REDHAWK TestRunner assertEquals
    assertEqualsToleranceIfNumber("toJSON("+objIn+",Auto)",     exp,    fromJSON(jsonAuto));
    assertEqualsToleranceIfNumber("toJSON("+objIn+",FastAuto)", exp,    fromJSON(jsonFastAuto));
    assertEqualsToleranceIfNumber("toJSON("+objIn+",Formal)",   exp,    fromJSON(jsonFormal));
    assertEqualsToleranceIfNumber("fromJSON("+jsonCompact+")",  objOut, fromJSON(jsonCompact));
  }

  private <T> void _testFromToJsonObj (Object objIn, String json, T objOut) {
    Class<?>     type         = objOut.getClass();
    CharSequence jsonAuto     = toJSON(objIn, Mode.Auto);
    CharSequence jsonFastAuto = toJSON(objIn, Mode.FastAuto);
    CharSequence jsonCompact  = toJSON(objIn, Mode.Compact);
    CharSequence jsonFormal   = toJSON(objIn, Mode.Formal);
    Object       exp          = fromJSON(type, jsonAuto);

    assertEquals("toJSON("+objIn+",Compact)",  json,   jsonCompact.toString());
    assertEquals("toJSON("+objIn+",Auto)",     exp,    fromJSON(type, jsonAuto));
    assertEquals("toJSON("+objIn+",FastAuto)", exp,    fromJSON(type, jsonFastAuto));
    assertEquals("toJSON("+objIn+",Formal)",   exp,    fromJSON(type, jsonFormal));
    assertEquals("fromJSON("+jsonCompact+")",  objOut, fromJSON(type, jsonCompact));
  }

  /**
   * testToJSON() will test the abiilty of JsonUtilites.class to create the proper objects.
   * Example: if the value is true then json utilities will make a boolean object with the value of true.
   *
   */
  public void testToJSON ()  {
    // ==== BASIC TYPES AND LISTS ==================================================================
    Integer[]     array   = { 1,2,3,4 };
    List<Integer> list    = Arrays.asList(array);
    String        listStr = "[1,2,3,4]";

    _testFromToJSON(null,                     "null");
    _testFromToJSON(true,                     "true");
    _testFromToJSON(false,                    "false");
    _testFromToJSON(0,                        "0" );
    _testFromToJSON(Double.NaN,               "NaN");
    _testFromToJSON(Double.POSITIVE_INFINITY, "Infinity");
    _testFromToJSON(Double.NEGATIVE_INFINITY, "-Infinity");
    _testFromToJSON('X',                      "\"X\"",      "X");  // Character => String
    _testFromToJSON(array,                    listStr,      list); // array => List
    _testFromToJSON(list,                     listStr,      list);

    // ==== NUMBERS ================================================================================
    int  j = 0x00000001;
    long k = 0x0000000000000001L;
    for (int i = -512; i < 512; i++      ) _testFromToJSON(i, Integer.toString(i));
    for (int i = -512; i < 512; i++      ) _testFromToJSON(i, Long.toString(i));
    for (int i =    0; i <  32; i++,j<<=1) _testFromToJSON(j, Integer.toString(j));
    for (int i =    0; i <  32; i++,j>>=1) _testFromToJSON(j, Integer.toString(j));
    for (int i =    0; i <  64; i++,k<<=1) _testFromToJSON(k, Long.toString(k));
    for (int i =    0; i <  64; i++,k>>=1) _testFromToJSON(k, Long.toString(k));

    // ==== MAPS ===================================================================================
    Map<String,Integer> map1 = new LinkedHashMap<>();
    Map<String,Object>  map2 = new LinkedHashMap<>();
    StringBuilder       str1 = new StringBuilder();
    StringBuilder       str2 = new StringBuilder();
    for (int i = 0; i < 10; i++) {
      map1.put(Integer.toString(i), i);
      str1.append(((i == 0)? '{' : ','));
      str1.append('\"').append(i).append("\":").append(i);
    }
    str1.append('}');

    str2.append("{\"Numbers\":").append(str1);
    str2.append(",\"String\":\"Hello World!\"}");
    map2.put("Numbers", map1);
    map2.put("String",  "Hello World!");

    _testFromToJSON(map1, str1.toString());
    _testFromToJSON(map2, str2.toString());

    // ==== JSON OBJECT ============================================================================
    TestClassAuto           auto      = new TestClassAuto();
    TestClassAltName        altName   = new TestClassAltName();
    TestClassFooBarBaz      fooBarBaz = new TestClassFooBarBaz();
    TestClassFactory        factory   = new TestClassFactory("ABC123");
    TestClassTypes          types     = new TestClassTypes();
    Map<StringBuilder,Byte> myMap     = new LinkedHashMap<>();
    auto.set("Hello World!");
    altName.setFooBar("Four score and...");
    fooBarBaz.setFoo((byte)1);
    fooBarBaz.setBar(2);
    fooBarBaz.baz = 3.14;
    factory.myValidator();
    types.setChars(new StringBuilder("ABC"));
    types.setList(Arrays.asList(new StringBuilder("ONE"), new StringBuilder("TWO")));
    types.setMap(myMap);
    myMap.put(new StringBuilder("ONE"), (byte)1);
    myMap.put(new StringBuilder("TWO"), (byte)2);

    _testFromToJsonObj(auto,      "{\"value\":\"Hello World!\"}",       auto);
    _testFromToJsonObj(altName,   "{\"foo_bar\":\"Four score and...\"}",altName);
    _testFromToJsonObj(fooBarBaz, "{\"bar\":2,\"baz\":3.14,\"foo\":1}", fooBarBaz);
    _testFromToJsonObj(factory,   "{\"length\":6,\"text\":\"ABC123\"}", factory);
    _testFromToJsonObj(types,     "{\"chars\":\"ABC\",\"list\":[\"ONE\",\"TWO\"],"+
                                   "\"map\":{\"ONE\":1,\"TWO\":2}}",    types);

    try {
      fromJSON(TestClassAuto.class, "{\"oops\":123}");
      // passed
    }
    catch (Exception e) {
      fail("fromJSON(..) Exception if strict and requires are *not* violated");
    }

    try {
      fromJSON(TestClassFooBarBaz.class, "{}");
      fail("fromJSON(..) Exception if required parameter is missing");
    }
    catch (Exception e) {
      // passed
    }

    try {
      fromJSON(TestClassFooBarBaz.class, "{\"bar\":2,\"baz\":3.14,\"foo\":1,\"oops\":123}");
      fail("fromJSON(..) Exception if strict is violated");
    }
    catch (Exception e) {
      // passed
    }

    try {
      fromJSON(TestClassFooBarBaz.class, "{}");
      fail("fromJSON(..) Exception if required parameter is missing");
    }
    catch (Exception e) {
      // passed
    }

    // Since 3.8.0 - added support NeXtMidas Data object -> JSON Array conversion
    // before Complex and Vector numeric types were converted to a String
    Data data = Data.fromString("(1, 2, 3.4)",'D',null);
    String jsonString = JsonUtilities.toJSON(data).toString();
    assertEquals('V', data.getFormatModeChar());
    assertEquals("toJson converts VD Data to JSON Array", "[1.0,2.0,3.4]",jsonString);
    //Type=Double Mode=Quad Vector
    Data data1 = Data.fromString("(1,2,3.123,4.456)",'D',null);
    String jsonString1 = JsonUtilities.toJSON(data1).toString();
    assertEquals('Q', data1.getFormatModeChar());
    assertEquals("toJson converts QF Data to JSON Array", "[1.0,2.0,3.123,4.456]",jsonString1);
    //Type=Long Mode=Complex
    Data data2 = Data.fromString("(1, 2)",'L',null);
    String jsonString2 = JsonUtilities.toJSON(data2).toString();
    assertEquals('C', data2.getFormatModeChar());
    assertEquals("toJson converts CL Data to JSON Array", "[1,2]",jsonString2);
    //Type=ASCII Mode=Scalar
    Data data3 = Data.fromString("testtesttest",'A', null);
    String jsonString3 = JsonUtilities.toJSON(data3).toString();
    assertEquals('S', data3.getFormatModeChar());
    assertEquals("toJason converts S Data to JSON", "\"testtesttest\"",jsonString3);
    //Type=Integer Short Mode=Vector
    Data data4 = Data.fromString("(1, 2, 3.4)",'I',null);
    String jsonString4 = JsonUtilities.toJSON(data4).toString();
    assertEquals('V', data4.getFormatModeChar());
    assertEquals("toJson converts VI Data to JSON Array", "[1,2,3]",jsonString4);

    // ==== PATH OBJECT ====(since 3.7.3)=======================================================
    String pathStr = "/tmp/file.txt";
    String expPath = "\""+pathStr+"\"";
    Path   path    = Paths.get(pathStr);
    assertTrue("Path to JSON expected:"+expPath+" actual:"+JsonUtilities.toJSON(path),
        expPath.equals(JsonUtilities.toJSON(path).toString()));

    // ==== Escape character testing === (since 4.0.0) ================================
    String escapeStr = "a\nb\nc\nd";
    int[] expectedChars = new int[] {'"','a','\\','n','b','\\','n','c','\\','n','d','"'};
    CharSequence escapeJSON = JsonUtilities.toJSON(escapeStr);
    int[] actualChars = escapeJSON.chars().toArray();
    assertTrue("JSON handles escapes properly",Arrays.equals(expectedChars, actualChars));

  }

  /** limited fromJSON testing*/
  @SuppressWarnings("unchecked")
  public void testFromJSON () {
    // test that nested JSON table becomes nested nxm.sys.lib.Table
    // when nxm.sys.lib.Table is specified as class type
    String tableString = "{\"Nested\":{\"One\":1,\"Two\":2,\"Inner\":{\"Alpha\":\"bet\"}},\"Name\":\"Jeremy\"}";
    Table inner = new Table();
    inner.setFlags("+CaseSensitive");
    inner.put("Alpha", "bet");
    Table nestedTable = new Table();
    Table nested = new Table();
    nested.setFlags("+CaseSensitive");
    nested.put("One", Integer.valueOf(1)); // note: if int is used table will be ONE=L:1 and fail assert
    nested.put("Two", Integer.valueOf(2));
    nested.put("Inner",inner);
    nestedTable.put("Nested",nested);
    nestedTable.put("Name","Jeremy");
    Table outTable = JsonUtilities.fromJSON(nxm.sys.lib.Table.class, tableString);
    assertEquals("fromJason(Table.class,jsonTable) preserves nested Tables",
        nxm.sys.lib.Table.class,outTable.get("Nested").getClass());
    assertEquals("fromJason(Table.class,jsonTable) preserves inner nested Tables",
        nxm.sys.lib.Table.class,((Table)outTable.get("Nested")).get("Inner").getClass());
    assertEquals("fromJason(Table.class,jsonTable) gives expected Table",
        nestedTable,outTable);

    // check that setting underlying map to MapKBT results in pure MapKBT
    Type previousClass = JsonUtilities.getUnderlyingMap();
    Class<? extends Map> clazz = LinkedHashMap.class;
    if (previousClass instanceof Class) {
      clazz = (Class<? extends Map>)previousClass;
    }
    JsonUtilities.setUnderlyingMap(MapKBT.class);
    MapKBT outMapKBT = (MapKBT)JsonUtilities.fromJSON(tableString);
    assertEquals("MapKBT mode fromJason(jsonString) has nested MapKBT",
        "class nxm.sys.inc.MapKBT$AbstractMapKBT",((MapKBT)outMapKBT.get("Nested")).get("Inner").getClass().toString());
    JsonUtilities.setUnderlyingMap(clazz);

    /*
     * Test the convert BLUE2JSON command
     * NXM-3872 NextMidas 4.1.2
     */
    // Skip the BLUE2JSON testing when in a Gradle build
    Midas M = Shell.getSharedMidasContext();
    if (Shell.isGradleBuild()) {
      M.warning("nxm.sys.junit.lib.JsonUtilitiesTest skipping testFromJSON() BLUE2JSON tests because fails in Gradle build");
      return;
    }

    // Skip the BLUE2JSON testing when in LIBNXM mode
    boolean convertCmdExists = getMidas().dictionary.isDictionaryEntry("CONVERT");
    if (!convertCmdExists) {
      info("nxm.sys.junit.lib.JsonUtilitiesTest skipping testFromJSON() BLUE2JSON tests, seem to be in LIBNXM mode without CONVERT command");
      return;
    }

    // Test the convert BLUE2JSON command with default behavior
    Shell.runCommand(M, "convert test1000 BLUE2JSON test1000jsonDefault.txt{AUX=RAM}");
    TextFile inTextFile = new TextFile(M, "test1000jsonDefault.txt");
    inTextFile.open(BaseFile.INPUT);
    String jsonObjStr = inTextFile.readAll().trim();
    inTextFile.close();
    LinkedHashMap<String,Object> jsonAsMap = JsonUtilities.fromJSON(LinkedHashMap.class,jsonObjStr);
    String jsonOuterKeyValueExpected = "{}";
    String jsonOuterKeyValueActual = jsonAsMap.get("FileKeywordsExt").toString();
    assertEquals("BLUE2JSON with default behavior - outer key", jsonOuterKeyValueExpected, jsonOuterKeyValueActual);
    String jsonInnerKeyValueExpected = "1000";
    String jsonInnerKeyValueActual = ((LinkedHashMap<String,Object>)jsonAsMap.get("FileHeader")).get("Type").toString();
    assertEquals("BLUE2JSON with default behavior - inner key", jsonInnerKeyValueExpected, jsonInnerKeyValueActual);

 // Test the convert BLUE2JSON command with a conversion table
    String nmPid = Shell.getNmPID();
    String testPlat1000Assoc6000 = "testplat1000assoc6000_"+nmPid+".cdif";
    String testPlat6000Assoc1000 = "testplat6000assoc1000_"+nmPid+".cdif";
    String temp1000json          = "temp1000json_"+nmPid;
    String temp6000json          = "temp6000json_"+nmPid;
    String blue2JsonPlatPair     = "blue2json_platinum_pair_"+nmPid+".txt";

    Shell.runCommand(M, "file copy/f nxm.sys.test.testplat4plat2blue_cov.cdif "+testPlat1000Assoc6000+"{AUX=RAM}");
    Shell.runCommand(M, "file UNPROTECT "+testPlat1000Assoc6000);
    Shell.runCommand(M, "keyword "+testPlat1000Assoc6000+" set TOA_SIGMA 0.0 FOA_SIGMA 0.0");
    Shell.runCommand(M, "keyword "+testPlat1000Assoc6000+" set ASSOC6000 "+testPlat6000Assoc1000);
    Shell.runCommand(M, "keyword "+testPlat1000Assoc6000+" set VER 2.0 /SCOPE=main");

    Shell.runCommand(M, "file copy/f test6000.prm{BLUEVER=2.0} "+testPlat6000Assoc1000+"{AUX=RAM}");
    Shell.runCommand(M, "file UNPROTECT "+testPlat6000Assoc1000);
    Shell.runCommand(M, "keyword "+testPlat6000Assoc1000+" set ASSOC1000 "+testPlat1000Assoc6000);
    Shell.runCommand(M, "keyword "+testPlat6000Assoc1000+" set VER 2.0 /SCOPE=main");

    Shell.runCommand(M, "convert "+testPlat1000Assoc6000+" BLUE2JSON "+temp1000json+"{AUX=RAM} OPTIONS=nxm.sys.test.blue2json_plat1000_test.tbl");
    Shell.runCommand(M, "convert "+testPlat6000Assoc1000+" BLUE2JSON "+temp6000json+"{AUX=RAM} OPTIONS=nxm.sys.test.blue2json_records_test.tbl");
    JsonUtilities.combineJSON(blue2JsonPlatPair+"{AUX=RAM}", temp1000json, temp6000json);

    inTextFile = new TextFile(M, blue2JsonPlatPair);
    inTextFile.open(BaseFile.INPUT);
    jsonObjStr = inTextFile.readAll().trim();
    inTextFile.close();
    jsonAsMap = JsonUtilities.fromJSON(LinkedHashMap.class,jsonObjStr);
    jsonOuterKeyValueExpected = "[{core:sample_start=0.0, core:datetime=2012-11-10T09:08:07.654321Z}]";
    jsonOuterKeyValueActual = jsonAsMap.get("captures").toString();
    assertEquals("BLUE2JSON with conversion table - outer key", jsonOuterKeyValueExpected, jsonOuterKeyValueActual);
    jsonInnerKeyValueExpected = "rf16_be";
    jsonInnerKeyValueActual = ((LinkedHashMap<String,Object>)jsonAsMap.get("global")).get("core:datatype").toString();
    assertEquals("BLUE2JSON with conversion table - inner key", jsonInnerKeyValueExpected, jsonInnerKeyValueActual);
  }

  public void testFromToJSONDotKey () {
    String jeremyJSON = "{\"hello.you\":\"world\",\"key\":{\"inner.tab\":\"6\"}}";
    String test2JSON = "{\"Nested\":{\"One.One\":1,\"Two\":2,\"Inner\":{\"Alpha\":\"bet\"}},\"Name\":\"Jeremy\"}";
    String jeremyExpected = "{\"hello_you\":\"world\",\"key\":{\"inner_tab\":\"6\"}}";
    String test2Expected = "{\"Nested\":{\"One_One\":1,\"Two\":2,\"Inner\":{\"Alpha\":\"bet\"}},\"Name\":\"Jeremy\"}";

    ///////////////////////////////////////////////////////////////////////////
    // fromJSON now substitutes '_" for '." in keys
    ///////////////////////////////////////////////////////////////////////////
    Table message = JsonUtilities.fromJSON(Table.class, jeremyJSON);
    String actual = JsonUtilities.toJSON(message).toString();
    assertTrue("FromToJSONDotKey Jeremy JSON works with modifyKeys",
        jeremyExpected.equals(actual));

    message = JsonUtilities.fromJSON(Table.class, test2JSON);
    actual = JsonUtilities.toJSON(message).toString();
    assertTrue("FromToJSONDotKey Test #2 JSON works with modifyKeys",
        test2Expected.equals(actual));

    ///////////////////////////////////////////////////////////////////////////
    // Table fromJSON does the right thing
    ///////////////////////////////////////////////////////////////////////////
    // subtitute . for _ case
    int mode = Table.getDefaultModeInt();
    int flags = Table.getDefaultFlagsInt() + Table.CASESENSITIVE;
    Table jsonTable = new Table(mode, flags);
    jsonTable.fromJSON(jeremyJSON,"_");
    actual = JsonUtilities.toJSON(jsonTable).toString();
    assertTrue("Table.fromJSON toJSON Jeremy JSON with subtitute dots in keys works",
        jeremyExpected.equals(actual));
    jsonTable.fromJSON(test2JSON,"_");
    actual = JsonUtilities.toJSON(jsonTable).toString();
    assertTrue("Table.fromJSON toJSON Test #2 JSON with subtitute dots in keys works",
        test2Expected.equals(actual));

    // no substitute case
    try {
      jsonTable.fromJSON(jeremyJSON);
      fail("Table.fromJSON did not throw exception with key with '.'s");
    }
    catch (MidasException me) {
      assertTrue("Table.fromJSON toJSON Jeremy JSON with dots in keys throws Exception",
          me.getMessage().startsWith("Copying Map entries into Table failed"));
    }

    // substitute contains '.'
    try {
      jsonTable.fromJSON(test2JSON,"_._");
      fail("Table.fromJSON did not throw exception with key with '.'s");
    }
    catch (MidasException me) {
      assertTrue("Table.fromJSON toJSON Test #2 JSON with dots in keys throws Exception",
          me.getMessage().startsWith("Copying Map entries into Table failed"));
    }

    // ALLOW_DOTS_IN_KEYS gives back identical JSON
    flags = Table.getDefaultFlagsInt() + Table.CASESENSITIVE + Table.ALLOW_DOTS_IN_KEYS;
    jsonTable.setFlags(flags);
    jsonTable.fromJSON(jeremyJSON);
    actual = JsonUtilities.toJSON(jsonTable).toString();
    assertTrue("Table.fromJSON toJSON Jeremy JSON with ALLOW_DOTS_IN_KEYS in keys works",
        jeremyJSON.equals(actual));

  }

  /**
   * This tests that the fix for NXM-3166 is valid. It tests a private method so it will need to use reflection.
   * The tests that have been performed test that the fix works. Tests that a bad value is still going to be bad.
   * It tests that an exception can still be thrown.
   */
  public void testFromString() {
    try {
      Class<JsonUtilities> jsonUtils = JsonUtilities.class;

      Method method = jsonUtils.getDeclaredMethod("_fromStr", String.class);
      method.setAccessible(true);
      String exp = "ABC";
      String exp2 = "Hello World";
      String result = (String) method.invoke(null, "\"\\u0041\\u0042\\u0043\"");
      String result2 = (String) method.invoke(null, "\"ABC\"");
      String result3 = (String) method.invoke(null, "\"This should faile\"");

      //All of the unicode needs to have 2 escape slashes in front of the 'u', otherwise java will automatically translate it and we want the code to do it instead.
      assertEquals("fromString(\"\\u0041\\u0042\\u0043\")",exp, result);
      assertEquals("fromString(\"ABC\")",exp, result2);
      assertEquals("fromString(\"\\u0048\\u0065\\u006C\\u006C\\u006F World\")",exp2, (String) method.invoke(null, "\"\\u0048\\u0065\\u006C\\u006C\\u006F World\""));
      assertEquals("fromString(\"\\u0031\\u0032\\u0033\\u0034\\u0035\")","12345", (String) method.invoke(null,  "\"\\u0031\\u0032\\u0033\\u0034\\u0035\""));
      assertEquals("fromString(\"ABC\")",exp, (String) method.invoke(null, "\"ABC\""));
      assertEquals("fromString(\"Hello World\")",exp2, (String) method.invoke(null, "\"Hello World\""));
      assertEquals("fromString(\"Hello \"World!\"\")","Hello \"World!\"", (String) method.invoke(null, "\"Hello \"World!\"\""));
      assertEquals("fromString(\"Hello 'World!'\")","Hello 'World!'", (String) method.invoke(null, "\"Hello 'World!'\""));
      assertEquals("fromString(\"Hello `World!`\")","Hello `World!`", (String) method.invoke(null, "\"Hello `World!`\""));
      assertEquals("fromString(\"Hello\n World!\")","Hello\n World!", (String) method.invoke(null, "\"Hello\n World!\""));
      assertEquals("fromString(\"\\u000d\")","\r", (String) method.invoke(null, "\"\\u000d\""));
      assertEquals("fromString(\"\\u000A\")","\n", (String) method.invoke(null,"\"\\u000A\""));

      //Test with a number
      assertEquals("fromString(\"1\")","1", (String) method.invoke(null, "\"1\""));
      assertEquals("fromString(\"\\u0031\\u0032\\u0033\\u0034\\u0035\")","12345", (String) method.invoke(null,  "\"\\u0031\\u0032\\u0033\\u0034\\u0035\""));
      assertEquals("fromString(\"\\u005b\\u0031\\u002c\\u0032\\u002c\\u0033\\u002c\\u0034\\u002c\\u0035\\u005d\")","[1,2,3,4,5]", method.invoke(null,"\"\\u005b\\u0031\\u002c\\u0032\\u002c\\u0033\\u002c\\u0034\\u002c\\u0035\\u005d\""));
      //badTest
      assertFalse(exp.equalsIgnoreCase(result3));

    }catch(Exception e){
      //fail the test and print exception
      fail("Failed test witl exception:"+e);
    }

    //Exception Test
    try {
      Class<JsonUtilities> jsonUtils = JsonUtilities.class;

      Method method = jsonUtils.getDeclaredMethod("_fromStr", String.class);
      method.setAccessible(true);

      method.invoke(null, "Single quote should throw exception because it is not a valid json string");

      //if you get here, fail the test. Should throw exception

    }catch(Exception e){
      //exception is expected, pass the test
      assertTrue(true);
    }
  }

  /**
   * Assertion that two Objects are equal, where values are compared with a
   * tolerance if both subclasses of Numbers
   * (MODIFICATION to REDHAWK code to avoid TestRunner Dependency)
   *
   * @param message  assert message
   * @param expected expected value
   * @param actual   actual value
   * @since NeXtMidas 3.7.2
   */
  private static void assertEqualsToleranceIfNumber (String message, Object expected, Object actual) {
    if ((expected instanceof Number) && (actual instanceof Number)) {
      assertNumberEquals(message,(Number)actual, (Number) expected, TOLERANCE);
    }
    else {
      assertEquals(message, expected, actual);
    }
  }

  /** Root test class. */
  private static class TestClass {
    @Override
    public boolean equals (Object obj) {
      return (obj != null) && (obj.getClass() == getClass())
                           && toString().equals(obj.toString());
    }
    @Override
    public int hashCode () {
      return toString().hashCode();
    }
  }

  @JsonObject(auto=true)
  public static class TestClassAuto extends TestClass {
    private String value = "Not Set!";
    @Override             public String toString ()           { return value; }
    /* auto @JsonEntry */ public String get      ()           { return value; }
    /* auto @JsonEntry */ public void   set      (String val) { value = val;  }
  }

  @JsonObject(auto=true)
  public static class TestClassAltName extends TestClass {
    private String value = "Not Set!";
    @Override                  public String toString  ()           { return value; }
    @JsonEntry(name="foo_bar") public String getFooBar ()           { return value; }
    @JsonEntry(name="foo_bar") public void   setFooBar (String val) { value = val;  }
  }

  @JsonObject(strict=true)
  public static class TestClassFooBarBaz extends TestClass {
               private Byte   foo; // private and not a JsonEntry
               public  int    bar; // public but not a JsonEntry
    @JsonEntry public  double baz; // public and is a JsonEntry

    @Override                 public String toString ()         { return foo+"/"+bar+"/"+baz; }
    @JsonEntry(required=true) public Byte   getFoo   ()         { return foo; }
    @JsonEntry(required=true) public int    getBar   ()         { return bar; }
    @JsonEntry(required=true) public void   setFoo   (Byte val) { foo = val;  }
    /* auto @JsonEntry */     public void   setBar   (int  val) { bar = val;  }
  }

  @JsonObject(strict=true)
  public static class TestClassTypes extends TestClass {
    private CharSequence                               chars;
    private List<? extends Appendable>                 list;
    private Map<? extends Appendable,? extends Number> map;

    @Override              public String                     toString () { return chars+"/"+list+"/"+map; }
    /* auto @JsonEntry */  public CharSequence               getChars () { return chars; }
    /* auto @JsonEntry */  public List<? extends Appendable> getList  () { return list; }
    /* auto @JsonEntry */  public Map<? extends Appendable,
                                      ? extends Number>      getMap   () { return map; }

    @JsonEntry(type=StringBuilder.class)
    public void setChars (CharSequence val) {
      chars = val;
    }
    @JsonEntry(type=ArrayList.class,valtype=StringBuilder.class)
    public void setList (List<? extends Appendable> val) {
      list = val;
    }
    @JsonEntry(type=LinkedHashMap.class,keytype=StringBuilder.class,valtype=Byte.class)
    public void setMap (Map<? extends Appendable,? extends Number> val) {
      map = val;
    }
  }

  @JsonObject(factory="myFactoryMethod",validator="myValidator")
  public static class TestClassFactory extends TestClass {
              String  text;
              int     length;
    transient boolean valid;  // transient is unnecessary here, but is good example of usage

    public TestClassFactory (String str) { // not callable via no-argument constructor
      this.text   = str;
      this.length = str.length();
      this.valid  = false; // only set to true by myValidator()
    }

    public static TestClassFactory myFactoryMethod (Map<String,Object> map) {
      return new TestClassFactory("FACTORY");
    }

    public void myValidator () {
      if (length != text.length()) {
        throw new RuntimeException("Bad length value");
      }
      valid = true;
    }

    @Override  public String toString  ()           { return text+"/"+length+"/"+valid; }
    @JsonEntry public String getText   ()           { return text;   }
    @JsonEntry public int    getLength ()           { return length; }
    @JsonEntry public void   setText   (String val) { text   = val;  }
    @JsonEntry public void   setLength (int val)    { length = val;  }

    // not a @JsonEntry
    public void setText (String txt, int len) { text = txt; length = len; }
  }

  @SuppressWarnings("unchecked")
  public void testReadJsonFile() {
    List<MapKBT.AbstractMapKBT> json = JsonUtilities.readJsonFile(MapKBT.AbstractMapKBT.class, urlDat + "states.json");
    Map<String, Object> resMap = json.get(0);
    List<LinkedHashMap<String, Object>> results1 =  (List<LinkedHashMap<String, Object>>) resMap.get("states");
    assertEquals("The state should be Montana", "Montana", results1.get(30).get("name"));

    BaseFile bf = new BaseFile(urlDat+"states.json");
    json = JsonUtilities.readJsonFile(MapKBT.AbstractMapKBT.class, bf);
    resMap = json.get(0);
    List<LinkedHashMap<String, Object>> results2 =  (List<LinkedHashMap<String, Object>>) resMap.get("states");
    assertEquals("the state should be Kentucky", "Kentucky", results2.get(20).get("name"));

    MapKBT mkbt = JsonUtilities.readJsonFile(urlDat+"states.json");
    List<LinkedHashMap<String, Object>> results3 = (List<LinkedHashMap<String, Object>>) mkbt.get("states");
    assertEquals("The state should be Virginia", "Virginia", results3.get(54).get("name"));

    mkbt = JsonUtilities.readJsonFile(bf);
    List<LinkedHashMap<String, Object>> results4 = (List<LinkedHashMap<String, Object>>) mkbt.get("states");
    assertEquals("The state should be Virginia", "Virginia", results4.get(54).get("name"));

    // Assert that all the objects are the same (all the methods should return the same value of the read in json file)
    assertEquals("the two json objects should be equal", results1, results2);
    assertEquals("the two json objects should be equal", results1, results3);
    assertEquals("the two json objects should be equal", results1, results4);
    assertEquals("the two json objects should be equal", results2, results4);

  }

  @SuppressWarnings("unchecked")
  public void testReadJsonFileMapKBT() {
    Type previousClass = JsonUtilities.getUnderlyingMap();
    Class<? extends Map> clazz = LinkedHashMap.class;
    if (previousClass instanceof Class) {
      clazz = (Class<? extends Map>)previousClass;
    }
    JsonUtilities.setUnderlyingMap(MapKBT.class);
    List<MapKBT.AbstractMapKBT> json = JsonUtilities.readJsonFile(MapKBT.AbstractMapKBT.class, urlDat + "states.json");
    Map<String, Object> resMap = json.get(0);
    List<MapKBT> results1 =  (List<MapKBT>) resMap.get("states");
    assertEquals("The state should be Montana", "Montana", results1.get(30).get("name"));

    BaseFile bf = new BaseFile(urlDat+"states.json");
    json = JsonUtilities.readJsonFile(MapKBT.AbstractMapKBT.class, bf);
    resMap = json.get(0);
    List<MapKBT> results2 =  (List<MapKBT>) resMap.get("states");
    assertEquals("the state should be Kentucky", "Kentucky", results2.get(20).get("name"));

    MapKBT mkbt = JsonUtilities.readJsonFile(urlDat+"states.json");
    List<MapKBT> results3 = (List<MapKBT>) mkbt.get("states");
    assertEquals("The state should be Virginia", "Virginia", results3.get(54).get("name"));

    mkbt = JsonUtilities.readJsonFile(bf);
    List<MapKBT> results4 = (List<MapKBT>) mkbt.get("states");
    assertEquals("The state should be Virginia", "Virginia", results4.get(54).get("name"));

    // Assert that all the objects are the same (all the methods should return the same value of the read in json file)
    assertEquals("the two json objects should be equal", results1, results2);
    assertEquals("the two json objects should be equal", results1, results3);
    assertEquals("the two json objects should be equal", results1, results4);
    assertEquals("the two json objects should be equal", results2, results4);
    JsonUtilities.setUnderlyingMap(clazz);
  }

  public void testPrettyPrint () {
    String json1 = "{\"JSON2\":\"Hello my World!\",\"JSON3\":{\"key1\":\"value1\"},\"JSON1\":\"Hello World!\"}";
    String json2 = "{\"array\":[1,2,3],\"boolean\":true,\"color\":\"#82b92c\",\"null\":null,\"number\":123,\"object\":{\"a\":\"b\",\"c\":\"d\",\"e\":\"f\"},\"string\":\"Hello World\"}";
    String expJson1 = "{\n" +
        "  \"JSON2\": \"Hello my World!\",\n" +
        "  \"JSON3\": {\n" +
        "    \"key1\": \"value1\"\n" +
        "  },\n" +
        "  \"JSON1\": \"Hello World!\"\n" +
        "}";

    String expJson2 = "{\n" +
        "  \"array\": [\n" +
        "    1,\n" +
        "    2,\n" +
        "    3\n" +
        "  ],\n" +
        "  \"boolean\": true,\n" +
        "  \"color\": \"#82b92c\",\n" +
        "  \"null\": null,\n" +
        "  \"number\": 123,\n" +
        "  \"object\": {\n" +
        "    \"a\": \"b\",\n" +
        "    \"c\": \"d\",\n" +
        "    \"e\": \"f\"\n" +
        "  },\n" +
        "  \"string\": \"Hello World\"\n" +
        "}";
    assertEquals(expJson1, JsonUtilities.prettyPrint(json1));
    assertEquals(expJson2, JsonUtilities.prettyPrint(json2));

  }
}
