/*
 * Copyright 2007 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.json.client;

import com.google.gwt.core.client.JsonUtils;
import com.google.gwt.junit.client.GWTTestCase;

import java.util.Set;

/**
 * Test case for JSONValue and friends.
 */
public class JSONTest extends GWTTestCase {
  static final String menuTest = "{\"menu\": {\n" + "  \"id\": \"file\",\n"
      + "  \"value\": \"File:\",\n" + "  \"popup\": {\n"
      + "    \"menuitem\": [\n"
      + "      {\"value\": \"New\", \"onclick\": \"CreateNewDoc()\"},\n"
      + "      {\"value\": \"Open\", \"onclick\": \"OpenDoc()\"},\n"
      + "      {\"value\": \"Close\", \"onclick\": \"CloseDoc()\"}\n"
      + "    ]\n" + "  }\n" + "}}\n" + "";

  static final String widgetTest = "{\"widget\": {\n"
      + "    \"debug\": \"on\",\n"
      + "    \"window\": {\n"
      + "        \"title\": \"Sample Konfabulator Widget\",        \"name\": \"main_window\",        \"width\": 500,        \"height\": 500\n"
      + "    },    \"image\": { \n"
      + "        \"src\": \"Images/Sun.png\",\n"
      + "        \"name\": \"sun1\",        \"hOffset\": 250,        \"vOffset\": 250,        \"alignment\": \"center\"\n"
      + "    },    \"text\": {\n"
      + "        \"data\": \"Click Here\",\n"
      + "        \"size\": 36,\n"
      + "        \"style\": \"bold\",        \"name\": \"text1\",        \"hOffset\": 250,        \"vOffset\": 100,        \"alignment\": \"center\",\n"
      + "        \"onMouseUp\": \"sun1.opacity = (sun1.opacity / 100) * 90;\"\n"
      + "    }\n" + "}}    \n" + "";

  // Number of unicode characters to test at once for JSON escaping
  private static final int JSON_CHUNK_SIZE = 256;

  private static void assertJSONArrayEquals(JSONArray expected, JSONArray actual) {
    assertEquals(expected.size(), actual.size());
    for (int i = 0; i < expected.size(); ++i) {
      assertJSONValueEquals(expected.get(i), actual.get(i));
    }
  }

  private static void assertJSONObjectEquals(JSONObject expected,
      JSONObject actual) {
    Set<String> actKeys = actual.keySet();
    Set<String> expKeys = expected.keySet();
    assertEquals(expKeys.size(), actKeys.size());
    for (String key : expKeys) {
      assertTrue(actual.containsKey(key));
      JSONValue expValue = expected.get(key);
      JSONValue actValue = actual.get(key);
      assertJSONValueEquals(expValue, actValue);
    }
  }

  private static void assertJSONValueEquals(JSONValue expected, JSONValue actual) {
    if (expected.isArray() != null) {
      JSONArray expArray = expected.isArray();
      JSONArray actArray = actual.isArray();
      assertNotNull(actArray);
      assertJSONArrayEquals(expArray, actArray);
    } else if (expected.isBoolean() != null) {
      JSONBoolean expBool = expected.isBoolean();
      JSONBoolean actBool = actual.isBoolean();
      assertNotNull(actBool);
      assertEquals(expBool.booleanValue(), actBool.booleanValue());
    } else if (expected.isNull() != null) {
      assertNotNull(actual.isNull());
    } else if (expected.isNumber() != null) {
      JSONNumber expNum = expected.isNumber();
      JSONNumber actNum = actual.isNumber();
      assertNotNull(actNum);
      assertEquals(expNum.doubleValue(), actNum.doubleValue());
    } else if (expected.isObject() != null) {
      JSONObject expObj = expected.isObject();
      JSONObject actObj = actual.isObject();
      assertNotNull(actObj);
      assertJSONObjectEquals(expObj, actObj);
    } else if (expected.isString() != null) {
      JSONString expStr = expected.isString();
      JSONString actStr = actual.isString();
      assertNotNull(actStr);
      assertEquals(expStr.stringValue(), actStr.stringValue());
    } else {
      fail("Unknown JSONValue " + expected);
    }
  }

  /**
   * Returns the module name for GWT unit test running.
   */
  @Override
  public String getModuleName() {
    return "com.google.gwt.json.JSON";
  }

  public void testArrayBasics() {
    JSONArray a = new JSONArray();
    JSONString s = new JSONString("s");
    JSONBoolean b = JSONBoolean.getInstance(false);
    JSONNumber n = new JSONNumber(4);
    assertNull(a.get(3));
    assertNull(a.get(-4));
    a.set(3, s);
    assertEquals(s, a.get(3));
    a.set(-4, b);
    assertEquals(b, a.get(-4));
    a.set(2, b);
    assertEquals(b, a.get(2));
    a.set(1, n);
    assertEquals(n, a.get(1));
  }

  /**
   * Test deep recursion of arrays.
   */
  public void testArrayOfArraysOfArrays() {
    JSONArray array = populateRecursiveArray(3, 5);
    assertEquals(
        "[[[[[[],[],[]],[[],[],[]],[[],[],[]]],[[[],[],[]],[[],[],[]],[[],[],[]]],[[[],[],[]],[[],[],[]],[[],[],[]]]],[[[[],[],[]],[[],[],[]],[[],[],[]]],[[[],[],[]],[[],[],[]],[[],[],[]]],[[[],[],[]],[[],[],[]],[[],[],[]]]],[[[[],[],[]],[[],[],[]],[[],[],[]]],[[[],[],[]],[[],[],[]],[[],[],[]]],[[[],[],[]],[[],[],[]],[[],[],[]]]]],[[[[[],[],[]],[[],[],[]],[[],[],[]]],[[[],[],[]],[[],[],[]],[[],[],[]]],[[[],[],[]],[[],[],[]],[[],[],[]]]],[[[[],[],[]],[[],[],[]],[[],[],[]]],[[[],[],[]],[[],[],[]],[[],[],[]]],[[[],[],[]],[[],[],[]],[[],[],[]]]],[[[[],[],[]],[[],[],[]],[[],[],[]]],[[[],[],[]],[[],[],[]],[[],[],[]]],[[[],[],[]],[[],[],[]],[[],[],[]]]]],[[[[[],[],[]],[[],[],[]],[[],[],[]]],[[[],[],[]],[[],[],[]],[[],[],[]]],[[[],[],[]],[[],[],[]],[[],[],[]]]],[[[[],[],[]],[[],[],[]],[[],[],[]]],[[[],[],[]],[[],[],[]],[[],[],[]]],[[[],[],[]],[[],[],[]],[[],[],[]]]],[[[[],[],[]],[[],[],[]],[[],[],[]]],[[[],[],[]],[[],[],[]],[[],[],[]]],[[[],[],[]],[[],[],[]],[[],[],[]]]]]]",
        array.toString());
    // now to access
    for (int i = 0; i < 5; i++) {
      array = (JSONArray) array.get(0);
    }
  }

  /**
   * Tests an array of raw numbers, like [1,2,3].
   */
  public void testArrayOfNumbers() {
    JSONArray arr = new JSONArray();
    for (int i = 0; i < 10; i++) {
      arr.set(i, new JSONNumber(i));
    }
    String s = arr.toString();
    JSONValue v = parseStrictVsLenient(s);
    JSONArray array = v.isArray();
    assertTrue("v must be an array", array != null);
    assertEquals("Array size must be 10", 10, array.size());
    for (int i = 0; i < 10; i++) {
      assertEquals("Array value at " + i + " must be " + i,
          array.get(i).isNumber().doubleValue(), i, 0.001);
    }
  }

  public void testBooleanBasics() {
    assertTrue(JSONBoolean.getInstance(true).booleanValue());
    assertFalse(JSONBoolean.getInstance(false).booleanValue());

    JSONValue trueVal = parseStrictVsLenient("true");
    assertEquals(trueVal, trueVal.isBoolean());
    assertTrue(trueVal.isBoolean().booleanValue());

    JSONValue falseVal = parseStrictVsLenient("false");
    assertEquals(falseVal, falseVal.isBoolean());
    assertFalse(falseVal.isBoolean().booleanValue());
  }

  public void testEquals() {
    JSONArray array = parseStrictVsLenient("[]").isArray();
    assertEquals(array, new JSONArray(array.getJavaScriptObject()));

    assertEquals(JSONBoolean.getInstance(false), JSONBoolean.getInstance(false));
    assertEquals(JSONBoolean.getInstance(true), JSONBoolean.getInstance(true));

    assertEquals(JSONNull.getInstance(), JSONNull.getInstance());

    assertEquals(new JSONNumber(3.1), new JSONNumber(3.1));

    JSONObject object = parseStrictVsLenient("{}").isObject();
    assertEquals(object, new JSONObject(object.getJavaScriptObject()));

    assertEquals(new JSONString("foo"), new JSONString("foo"));
  }

  // Null characters do not work in Development Mode
  public void testEscaping() {
    JSONObject o = new JSONObject();
    char[] charsToEscape = new char[42];
    for (char i = 1; i < 32; i++) {
      charsToEscape[i] = i;
    }
    charsToEscape[32] = '"';
    charsToEscape[33] = '\\';
    charsToEscape[34] = '\b';
    charsToEscape[35] = '\f';
    charsToEscape[36] = '\n';
    charsToEscape[37] = '\r';
    charsToEscape[38] = '\t';
    charsToEscape[39] = '/';
    charsToEscape[40] = '\u2028';
    charsToEscape[41] = '\u2029';
    for (int i = 1; i < charsToEscape.length; i++) {
      o.put("c" + i, new JSONString(new Character(charsToEscape[i]).toString()));
    }
    assertEquals("{\"c1\":\"\\u0001\", \"c2\":\"\\u0002\", "
        + "\"c3\":\"\\u0003\", \"c4\":\"\\u0004\", \"c5\":\"\\u0005\", "
        + "\"c6\":\"\\u0006\", \"c7\":\"\\u0007\", \"c8\":\"\\b\", "
        + "\"c9\":\"\\t\", \"c10\":\"\\n\", \"c11\":\"\\u000B\", "
        + "\"c12\":\"\\f\", \"c13\":\"\\r\", \"c14\":\"\\u000E\", "
        + "\"c15\":\"\\u000F\", \"c16\":\"\\u0010\", \"c17\":\"\\u0011\", "
        + "\"c18\":\"\\u0012\", \"c19\":\"\\u0013\", \"c20\":\"\\u0014\", "
        + "\"c21\":\"\\u0015\", \"c22\":\"\\u0016\", \"c23\":\"\\u0017\", "
        + "\"c24\":\"\\u0018\", \"c25\":\"\\u0019\", \"c26\":\"\\u001A\", "
        + "\"c27\":\"\\u001B\", \"c28\":\"\\u001C\", \"c29\":\"\\u001D\", "
        + "\"c30\":\"\\u001E\", \"c31\":\"\\u001F\", \"c32\":\"\\\"\", "
        + "\"c33\":\"\\\\\", \"c34\":\"\\b\", \"c35\":\"\\f\", "
        + "\"c36\":\"\\n\", \"c37\":\"\\r\", \"c38\":\"\\t\", "
        + "\"c39\":\"/\", \"c40\":\"\\u2028\", \"c41\":\"\\u2029\"}", o.toString());
  }

  public void testHashCode() {
    JSONArray array = parseStrictVsLenient("[]").isArray();
    assertHashCodeEquals(array, new JSONArray(array.getJavaScriptObject()));

    assertHashCodeEquals(JSONBoolean.getInstance(false), JSONBoolean.getInstance(false));
    assertHashCodeEquals(JSONBoolean.getInstance(true), JSONBoolean.getInstance(true));

    assertHashCodeEquals(JSONNull.getInstance(), JSONNull.getInstance());

    assertHashCodeEquals(new JSONNumber(3.1), new JSONNumber(3.1));

    JSONObject object = parseStrictVsLenient("{}").isObject();
    assertHashCodeEquals(object, new JSONObject(object.getJavaScriptObject()));

    assertHashCodeEquals(new JSONString("foo"), new JSONString("foo"));
  }

  public void testLargeArrays() {
    JSONArray arr = null;
    for (int j = 1; j < 500; j *= 2) {
      arr = createLargeArray(j);
      assertEquals("size j" + j, j, arr.size());
    }
  }

  public void testLenientAndStrict() {
    String jsonString = "{ a:27, 'b': 'value' }";

    // parseLenient should succeed
    JSONValue value = JSONParser.parseLenient(jsonString);
    JSONObject object = value.isObject();
    assertEquals(27.0, object.get("a").isNumber().doubleValue());
    assertEquals("value", object.get("b").isString().stringValue());

    // parseStrict should fail
    try {
      parseStrictVsLenient(jsonString);
      fail();
    } catch (JSONException e) {
    }

    // Must fail even on browsers that lack JSON.parse()
    jsonString = "function f() { return 5; }";
    try {
      parseStrictVsLenient(jsonString);
      fail();
    } catch (JSONException e) {
    }
  }

  public void testMenu() {
    JSONObject v = (JSONObject) parseStrictVsLenient(menuTest);
    assertTrue(v.containsKey("menu"));
    JSONObject menu = ((JSONObject) v.get("menu"));
    assertEquals(3, menu.keySet().size());
  }

  public void testNested() {
    JSONObject obj = new JSONObject();
    nestedAux(obj, 3);
    String s1 = obj.toString();
    String s2 = parseStrictVsLenient(s1).toString();
    assertEquals(s1, s2);
    assertEquals(
        "{\"string3\":\"s3\", \"Number3\":3.1, \"Boolean3\":false, "
            + "\"Null3\":null, \"object3\":{\"string2\":\"s2\", \"Number2\":2.1, "
            + "\"Boolean2\":true, \"Null2\":null, \"object2\":{\"string1\":\"s1\","
            + " \"Number1\":1.1, \"Boolean1\":false, \"Null1\":null, \"object1\":{"
            + "\"string0\":\"s0\", \"Number0\":0.1, \"Boolean0\":true, "
            + "\"Null0\":null, \"object0\":{}, "
            + "\"Array0\":[\"s0\",0.1,true,null,{}]}, "
            + "\"Array1\":[\"s1\",1.1,false,null,{\"string0\":\"s0\", "
            + "\"Number0\":0.1, \"Boolean0\":true, \"Null0\":null, \"object0\":{}, "
            + "\"Array0\":[\"s0\",0.1,true,null,{}]}]}, "
            + "\"Array2\":[\"s2\",2.1,true,null,{\"string1\":\"s1\", "
            + "\"Number1\":1.1, \"Boolean1\":false, \"Null1\":null, "
            + "\"object1\":{\"string0\":\"s0\", \"Number0\":0.1, "
            + "\"Boolean0\":true, \"Null0\":null, \"object0\":{}, "
            + "\"Array0\":[\"s0\",0.1,true,null,{}]}, "
            + "\"Array1\":[\"s1\",1.1,false,null,{\"string0\":\"s0\", "
            + "\"Number0\":0.1, \"Boolean0\":true, \"Null0\":null, "
            + "\"object0\":{}, \"Array0\":[\"s0\",0.1,true,null,{}]}]}]}, "
            + "\"Array3\":[\"s3\",3.1,false,null,{\"string2\":\"s2\", "
            + "\"Number2\":2.1, \"Boolean2\":true, \"Null2\":null, "
            + "\"object2\":{\"string1\":\"s1\", \"Number1\":1.1, "
            + "\"Boolean1\":false, \"Null1\":null, \"object1\":{\"string0\":\"s0\","
            + " \"Number0\":0.1, \"Boolean0\":true, \"Null0\":null, \"object0\":{}, "
            + "\"Array0\":[\"s0\",0.1,true,null,{}]}, "
            + "\"Array1\":[\"s1\",1.1,false,null,{\"string0\":\"s0\", "
            + "\"Number0\":0.1, \"Boolean0\":true, \"Null0\":null, "
            + "\"object0\":{}, \"Array0\":[\"s0\",0.1,true,null,{}]}]}, "
            + "\"Array2\":[\"s2\",2.1,true,null,{\"string1\":\"s1\", "
            + "\"Number1\":1.1, \"Boolean1\":false, \"Null1\":null, "
            + "\"object1\":{\"string0\":\"s0\", \"Number0\":0.1, "
            + "\"Boolean0\":true, \"Null0\":null, \"object0\":{}, "
            + "\"Array0\":[\"s0\",0.1,true,null,{}]}, "
            + "\"Array1\":[\"s1\",1.1,false,null,{\"string0\":\"s0\", "
            + "\"Number0\":0.1, \"Boolean0\":true, \"Null0\":null, \"object0\":{},"
            + " \"Array0\":[\"s0\",0.1,true,null,{}]}]}]}]}", obj.toString());
  }

  public void testNumberBasics() {
    JSONNumber n0 = new JSONNumber(1000);
    assertEquals(1000, n0.doubleValue(), .000001);
    assertTrue(n0.isNumber() == n0);
    assertNull(n0.isObject());

    JSONNumber n1 = new JSONNumber(Integer.MAX_VALUE);
    assertEquals(Integer.MAX_VALUE, n1.doubleValue(), .00001);
    assertTrue(n1.isNumber() == n1);
    assertNull(n1.isObject());

    JSONNumber n2 = new JSONNumber(Integer.MIN_VALUE);
    assertEquals(Integer.MIN_VALUE, n2.doubleValue(), .00001);
    assertTrue(n2.isNumber() == n2);
    assertNull(n2.isObject());
  }

  public void testObjectBasics() {
    JSONObject s = new JSONObject();
    assertNull(s.get("buba"));
    s.put("a", new JSONString("A"));
    s.put("a", new JSONString("AA"));
    assertEquals("\"AA\"", s.get("a").toString());
  }

  /**
   * Tests an object whose keys are filled out with numbers, like {"a":1}.
   */
  public void testObjectOfNumbers() {
    JSONObject obj = new JSONObject();
    for (int i = 0; i < 10; i++) {
      obj.put("Object " + i, new JSONNumber(i));
    }
    String s = obj.toString();
    JSONValue v = parseStrictVsLenient(s);
    JSONObject objIn = v.isObject();
    assertTrue("v must be an object", objIn != null);
    assertEquals("Object size must be 10", 10, objIn.keySet().size());
    for (int i = 0; i < 10; i++) {
      assertEquals("Object value at 'Object " + i + "' must be " + i,
          objIn.get("Object " + i).isNumber().doubleValue(), i, 0.001);
    }
  }

  /**
   * Tests generic parsing.
   */
  public void testParse() {
    try {
      new JSONString(null);
      fail();
    } catch (NullPointerException t) {
    }
    try {
      parseStrictVsLenient(null);
      fail();
    } catch (NullPointerException t) {
    }
    try {
      parseStrictVsLenient("");
      fail();
    } catch (IllegalArgumentException t) {
    }
    try {
      parseStrictVsLenient("{\"menu\": {\n" + "  \"id\": \"file\",\n");
      fail();
    } catch (JSONException e) {
    }
    assertEquals("\"null\" should be null JSONValue", JSONNull.getInstance(),
        parseStrictVsLenient("null"));
    assertEquals("5 should be JSONNumber 5", 5d,
        parseStrictVsLenient("5").isNumber().doubleValue(), 0.001);
    assertEquals("\"null\" should be null JSONValue", JSONNull.getInstance(),
        parseStrictVsLenient("null"));
    JSONValue somethingHello = parseStrictVsLenient("[{\"something\":\"hello\"}]");
    JSONArray somethingHelloArray = somethingHello.isArray();
    assertTrue("somethingHello must be a JSONArray",
        somethingHelloArray != null);
    assertTrue("somethingHello size must be one",
        somethingHelloArray.size() == 1);
    JSONObject somethingHelloObject = somethingHelloArray.get(0).isObject();
    assertTrue("somethingHello element 0 must be a JSONObject",
        somethingHelloObject != null);
    assertTrue("somethingHello element 0 must have hello for key something",
        somethingHelloObject.get("something").isString().stringValue().equals(
            "hello"));
  }

  public void testParseUnescaped() {
    for (int i = 0; i <= 0xffff; i += JSON_CHUNK_SIZE) {
      doTestParseUnescaped(i, JSON_CHUNK_SIZE);
    }
  }

  public void testRoundTripEscaping() {
    JSONObject obj = new JSONObject();
    obj.put("a", new JSONNumber(42));
    obj.put("\\", new JSONNumber(43.5));
    obj.put("\"", new JSONNumber(44));

    String toString = obj.toString();
    assertEquals("{\"a\":42, \"\\\\\":43.5, \"\\\"\":44}", toString.trim());
    JSONValue parseResponse = parseStrictVsLenient(toString);
    JSONObject obj2 = parseResponse.isObject();
    assertJSONObjectEquals(obj, obj2);
  }

  public void testSimpleNested() {
    JSONObject j1 = new JSONObject();
    j1.put("test1", new JSONString(""));

    JSONObject j2 = new JSONObject();
    j2.put("test1", new JSONString(""));

    JSONObject j22 = new JSONObject();
    j22.put("test12", new JSONString(""));
    j2.put("j22", j22);

    JSONObject j3 = new JSONObject();
    j3.put("j1", j1);
    j3.put("j2", j2);

    assertEquals(
        "{\"j1\":{\"test1\":\"\"}, \"j2\":{\"test1\":\"\", \"j22\":{\"test12\":\"\"}}}",
        j3.toString());
  }

  public void testStringBasics() {
    JSONString arr = new JSONString("");
    assertEquals("\"\"", arr.toString());
    JSONString s = new JSONString(menuTest);
    assertEquals(menuTest, s.stringValue());
  }

  public void testStringEscaping() {
    checkRoundTripJsonText("\"hello\"", "hello");
    checkRoundTripJsonText("\"hel\\\"lo\"", "hel\"lo");
    checkRoundTripJsonText("\"hel\\\\lo\"", "hel\\lo");
    checkRoundTripJsonText("\"hel\\\\\\\"lo\"", "hel\\\"lo");
  }

  public void testStringTypes() {
    JSONObject object = parseStrictVsLenient("{\"a\":\"b\",\"null\":\"foo\"}").isObject();
    assertNotNull(object);

    assertEquals("b",
        object.get(stringAsPrimitive("a")).isString().stringValue());
    assertEquals("b", object.get(stringAsObject("a")).isString().stringValue());
    assertEquals("foo",
        object.get(stringAsPrimitive("null")).isString().stringValue());
    assertEquals("foo",
        object.get(stringAsObject("null")).isString().stringValue());

    try {
      assertNull(object.get(null));
      fail("Expected NullPointerException");
    } catch (NullPointerException expected) {
    }
  }

  public void testUndefined() {
    JSONObject o = parseStrictVsLenient("{\"foo\":\"foo\",\"bar\":null}").isObject();
    assertEquals(new JSONString("foo"), o.get("foo"));
    assertEquals(JSONNull.getInstance(), o.get("bar"));
    assertNull(o.get("baz"));

    o.put("foo", JSONNull.getInstance());
    assertEquals(JSONNull.getInstance(), o.get("foo"));
    o.put("foo", null);
    assertNull(o.get("foo"));

    JSONArray array = parseStrictVsLenient("[\"foo\",null]").isArray();
    assertEquals(new JSONString("foo"), array.get(0));
    assertEquals(JSONNull.getInstance(), array.get(1));
    assertNull(array.get(2));

    array.set(0, JSONNull.getInstance());
    assertEquals(JSONNull.getInstance(), array.get(0));
    array.set(0, null);
    assertNull(array.get(0));
  }

  public void testUnicodeSeparators() {
    /*
     * ECMAScript 5 allows unescaped U+2028 (line separator) and U+2029
     * (paragraph separator) characters to occur inside strings.
     */
    String jsonString = "{ \"name\": \"miles\u2028da\u2029vis\", \"ins\u2028tru\u2029ment\": \"trumpet\" }";
    try {
      JSONValue parsed = parseStrictVsLenient(jsonString);
      JSONObject result = parsed.isObject();
      assertNotNull(result);

      JSONValue nameValue = result.get("name");
      assertNotNull(nameValue);
      JSONString nameJsonString = nameValue.isString();
      assertNotNull(nameJsonString);
      String nameString = nameJsonString.stringValue();
      assertEquals("miles\u2028da\u2029vis", nameString);
      String nameStringQuoted = nameJsonString.toString();
      assertEquals("\"miles\\u2028da\\u2029vis\"", nameStringQuoted);

      JSONValue instrumentValue = result.get("ins\u2028tru\u2029ment");
      assertNotNull(instrumentValue);
      JSONValue instrumentValue2 = result.get("instrument");
      assertNull(instrumentValue2); // check no name collision
      JSONString instrumentJsonString = instrumentValue.isString();
      assertNotNull(instrumentJsonString);
      String instrumentString = instrumentJsonString.stringValue();
      assertEquals("trumpet", instrumentString);
    } catch (JSONException e) {
      fail(e.getMessage());
    }

    // U+2028 and U+2029 should not appear outside a string
    jsonString = "{ \"name\": \"miles davis\",\u2028\"instrument\": \"trumpet\" }";
    try {
      parseStrictVsLenient(jsonString);
      fail();
    } catch (JSONException e) {
    }

    jsonString = "{ \"name\":\u2029\"miles davis\", \"instrument\": \"trumpet\" }";
    try {
      parseStrictVsLenient(jsonString);
      fail();
    } catch (JSONException e) {
    }
  }

  public void testUnsafeEval() {
    JsonUtils.unsafeEval("{\"name\":\"value\"}");
    JsonUtils.unsafeEval("[0,1,2,3,4]");
  }

  public void testWidget() {
    JSONObject v = (JSONObject) parseStrictVsLenient(widgetTest);
    JSONObject widget = (JSONObject) v.get("widget");
    JSONObject window = (JSONObject) widget.get("window");
    JSONValue title = window.get("title");
    assertNotNull(title.isString());
    JSONValue hOffSet = window.get("width");
    assertNotNull(hOffSet.isNumber());
  }

  private void assertHashCodeEquals(Object expected, Object actual) {
    assertEquals("hashCodes are not equal", expected.hashCode(),
        actual.hashCode());
  }

  private void checkRoundTripJsonText(String jsonText, String normaltext) {
    JSONString parsed = parseStrictVsLenient(jsonText).isString();
    assertEquals(normaltext, parsed.stringValue());
    assertEquals(jsonText, parsed.toString());
  }

  private JSONArray createLargeArray(int size) {
    JSONArray arr = new JSONArray();
    for (int i = 0; i < size; i++) {
      arr.set(i, new JSONNumber(i));
    }
    return arr;
  }

  /**
   * Test a chunk of Unicode code points for use in JSON keys and values.
   *
   * @param start starting code point
   * @param len number of code points to test
   */
  private void doTestParseUnescaped(int start, int len) {
    StringBuilder sb = new StringBuilder();
    for (int i = start; i < start + len; i++) {
      // Skip control characters
      if (i < 0x20) {
        continue;
      }
      // Skip surrogate pairs
      if (i >= 0xD800 && i <= 0xDFFF) {
        continue;
      }
      char c = (char) i;
      // Skip quote and backslash
      if (c == '\"' || c == '\\') {
        continue;
      }
      sb.append(c);
    }
    String chars = sb.toString();
    if (chars.length() == 0) {
      return;
    }

    String key = "na" + chars + "me";
    String value = "miles" + chars + "davis";
    String jsonString = "{ \"" + key + "\": \"" + value + "\"}";
    try {
      JSONValue parsed = parseStrictVsLenient(jsonString);
      JSONObject result = parsed.isObject();
      assertNotNull("start = " + start + ", len = " + len, result);
      Set<String> keys = result.keySet();
      assertTrue("start = " + start + ", len = " + len, keys.contains(key));
      JSONValue nameValue = result.get(key);
      assertNotNull("start = " + start + ", len = " + len, nameValue);
      JSONString nameJsonString = nameValue.isString();
      assertNotNull("start = " + start + ", len = " + len, nameJsonString);
      String nameString = nameJsonString.stringValue();
      assertEquals("start = " + start + ", len = " + len, value, nameString);
    } catch (JSONException e) {
      fail("start = " + start + ", len = " + len + ", e = " + e.getMessage());
    }
  }

  private void nestedAux(JSONObject obj, int i) {
    JSONArray array = new JSONArray();
    JSONString str = new JSONString("s" + i);
    array.set(0, str);
    obj.put("string" + i, str);
    JSONNumber num = new JSONNumber(i + 0.1);
    array.set(1, num);
    obj.put("Number" + i, num);
    JSONBoolean b = JSONBoolean.getInstance((i % 2) == 0);
    array.set(2, b);
    obj.put("Boolean" + i, b);
    JSONNull nul = JSONNull.getInstance();
    array.set(3, nul);
    obj.put("Null" + i, nul);
    JSONObject newObj = new JSONObject();
    obj.put("object" + i, newObj);
    if (i != 0) {
      nestedAux(newObj, i - 1);
    }
    array.set(4, newObj);
    obj.put("Array" + i, array);
  }

  // Check that parseLenient produces the same results as parseStrict
  private JSONValue parseStrictVsLenient(String jsonString) {
    JSONValue strictValue = JSONParser.parseStrict(jsonString);
    JSONValue lenientValue = JSONParser.parseLenient(jsonString);
    assertJSONValueEquals(strictValue, lenientValue);
    return strictValue;
  }

  private JSONArray populateRecursiveArray(int numElements, int recursion) {
    JSONArray newArray = new JSONArray();
    if (recursion <= 0) {
      return newArray;
    }

    for (int i = 0; i < numElements; i++) {
      JSONArray childArray = populateRecursiveArray(numElements, recursion - 1);
      newArray.set(i, childArray);
    }
    return newArray;
  }

  private native String stringAsObject(String str) /*-{
    return new String(str);
  }-*/;

  private native String stringAsPrimitive(String str) /*-{
    return String(str);
  }-*/;
}
