// Copyright 2022 Google LLC
//
// 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.crypto.tink.internal;

import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.NotSerializableException;
import java.io.ObjectOutputStream;
import java.math.BigInteger;
import org.junit.Test;
import org.junit.experimental.theories.DataPoints;
import org.junit.experimental.theories.FromDataPoints;
import org.junit.experimental.theories.Theories;
import org.junit.experimental.theories.Theory;
import org.junit.runner.RunWith;

/**
 * Tests for Tink's internal {@link JsonParser}.
 */
@RunWith(Theories.class)
public final class JsonParserTest {

  public static final class TestCase {
    public final String name;
    public final String input;
    public final JsonElement expected;

    public TestCase(String name, String input, JsonElement expected) {
      this.name = name;
      this.input = input;
      this.expected = expected;
    }

    public TestCase(String name, String input) {
      this.name = name;
      this.input = input;
      this.expected = null;
    }

    @Override
    public String toString() {
      return name;
    }
  }

  public static JsonArray jsonArray(JsonElement... elements) {
    JsonArray output = new JsonArray();
    for (JsonElement element : elements) {
      output.add(element);
    }
    return output;
  }

  public static JsonObject jsonObject(String name, JsonElement value) {
    JsonObject output = new JsonObject();
    output.add(name, value);
    return output;
  }

  @DataPoints("testCasesSuccess")
  public static final TestCase[] TEST_CASES_SUCCESS = {
    new TestCase("string", "\"xyz\"", new JsonPrimitive("xyz")),
    new TestCase("number", "42", new JsonPrimitive(42)),
    new TestCase("negative_number", "-42", new JsonPrimitive(-42)),
    new TestCase("float", "42.42", new JsonPrimitive(42.42)),
    new TestCase("negative_float", "-42.42", new JsonPrimitive(-42.42)),
    new TestCase("true", "true", new JsonPrimitive(true)),
    new TestCase("false", "false", new JsonPrimitive(false)),
    new TestCase("null", "null", JsonNull.INSTANCE),
    new TestCase(
        "array", "[\"a\",\"b\"]", jsonArray(new JsonPrimitive("a"), new JsonPrimitive("b"))),
    new TestCase("map", "{\"a\":\"b\"}", jsonObject("a", new JsonPrimitive("b"))),
    new TestCase("empty_string", "\"\"", new JsonPrimitive("")),
    new TestCase("empty_array", "[]", new JsonArray()),
    new TestCase("array_with_newline", "[\n]", new JsonArray()),
    new TestCase("array_with_tab", "[\t]", new JsonArray()),
    new TestCase("empty_map", "{}", new JsonObject()),
    new TestCase("map_with_empty_key", "{\"\":\"a\"}", jsonObject("", new JsonPrimitive("a"))),
    new TestCase(
        "nested_arrays",
        "[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]",
        jsonArray(
            jsonArray(
                jsonArray(
                    jsonArray(
                        jsonArray(
                            jsonArray(
                                jsonArray(
                                    jsonArray(
                                        jsonArray(
                                            jsonArray(
                                                jsonArray(
                                                    jsonArray(
                                                        jsonArray(
                                                            jsonArray(
                                                                jsonArray(
                                                                    jsonArray())))))))))))))))),
    new TestCase(
        "nested_maps",
        "{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{\"a\":{}}}}}}}}}}}",
        jsonObject(
            "a",
            jsonObject(
                "a",
                jsonObject(
                    "a",
                    jsonObject(
                        "a",
                        jsonObject(
                            "a",
                            jsonObject(
                                "a",
                                jsonObject(
                                    "a",
                                    jsonObject(
                                        "a",
                                        jsonObject("a", jsonObject("a", new JsonObject()))))))))))),
    new TestCase("tRuE", "tRuE", new JsonPrimitive(true)),
    new TestCase("fAlSe", "fAlSe", new JsonPrimitive(false)),
    new TestCase("nUlL", "nUlL", JsonNull.INSTANCE),
    new TestCase(
        "mixed_array",
        "[\"a\", null, 1, 0.1, true, {\"a\":0}, [4]]",
        jsonArray(
            new JsonPrimitive("a"),
            JsonNull.INSTANCE,
            new JsonPrimitive(1),
            new JsonPrimitive(0.1),
            new JsonPrimitive(true),
            jsonObject("a", new JsonPrimitive(0)),
            jsonArray(new JsonPrimitive(4)))),
    new TestCase("tailing_newline", "\"a\"\n", new JsonPrimitive("a")),
    new TestCase(
        "whitespace", " { \"a\"\n: \n\"b\" \n } \n ", jsonObject("a", new JsonPrimitive("b"))),
    new TestCase("string_with_comment", "\"a/*b*/c\"", new JsonPrimitive("a/*b*/c")),
    new TestCase("string_with_excaped_unicode", "\"\\uA66D\"", new JsonPrimitive("ꙭ")),
    new TestCase("valid_utf16", "\"\\uD83D\\uDC69\"", new JsonPrimitive("👩")),
    new TestCase("valid_UTF8_1", "\"\\u002c\"", new JsonPrimitive(",")),
    new TestCase("valid_UTF8_3", "\"\\u0123\"", new JsonPrimitive("ģ")),
    new TestCase("escapes", "\"\\\"\\\\\\/\\b\\f\\n\\r\\t\"", new JsonPrimitive("\"\\/\b\f\n\r\t")),
    new TestCase("newline", "\"a\\u000Ab\"", new JsonPrimitive("a\nb")),
    new TestCase("backslash", "\"\\u005C\"", new JsonPrimitive("\\")),
    new TestCase("double_quote", "\"\\u0022\"", new JsonPrimitive("\"")),
    new TestCase(
        "escaped_double_quote_in_key",
        "{\"\\\"\\\"\": 42}",
        jsonObject("\"\"", new JsonPrimitive(42))),
    new TestCase("escaped_null", "\"\\u0000\"", new JsonPrimitive("" + (char) 0x00)),
    new TestCase(
        "escaped_null_in_key",
        "{\"a\\u0000b\": 42}",
        jsonObject("a\u0000b", new JsonPrimitive(42))),
    new TestCase("invalid_UTF8", "\"日ш\"", new JsonPrimitive("日ш")),

    new TestCase("long_max_value", "9223372036854775807", new JsonPrimitive(9223372036854775807L)),
    new TestCase("big_float", "60911552482230981.0", new JsonPrimitive(6.0911552482230984e16)),
    new TestCase("exp", "4e+42", new JsonPrimitive(4e+42)),
    new TestCase("exp2", "4e42", new JsonPrimitive(4e+42)),
    new TestCase("Exp", "4E42", new JsonPrimitive(4e+42)),
    new TestCase("-exp", "-4e-42", new JsonPrimitive(-4e-42)),
    new TestCase("number_tailing_space", "42 ", new JsonPrimitive(42)),
    new TestCase("number_tailing_newline", "42\n", new JsonPrimitive(42)),
    new TestCase("number_tailing_formfeed", "42\f", new JsonPrimitive(42)),
    new TestCase(
        "float_close_to_zero",
        "0.000000000000000000000000000000001",
        new JsonPrimitive(0.000000000000000000000000000000001)),
    new TestCase(
        "-float_close_to_zero",
        "-0.000000000000000000000000000000001",
        new JsonPrimitive(-0.000000000000000000000000000000001)),
    new TestCase(
        "huge_number",
        "999999999999999999999999999999999999999999999999999999999999999999999999999999999999999",
        new JsonPrimitive(1e87)),
    new TestCase(
        "-huge_number",
        "-999999999999999999999999999999999999999999999999999999999999999999999999999999999999999",
        new JsonPrimitive(-1e87)),
    new TestCase("string_with_tailing_comma", "\"a\",", new JsonPrimitive("a")),
    new TestCase("number_with_tailing_comma", "42,", new JsonPrimitive(42)),
    new TestCase("true_with_tailing_comma", "true,", new JsonPrimitive(true)),
    new TestCase("string_with_tailing_comment", "\"a\"/*comment*/", new JsonPrimitive("a")),
    new TestCase("map_with_tailing_comma", "{\"a\":1},", jsonObject("a", new JsonPrimitive(1))),
    new TestCase(
        "map_with_tailing_comment", "{\"a\":1}/*comment*/", jsonObject("a", new JsonPrimitive(1))),
    new TestCase(
        "map_with_tailing_open_comment",
        "{\"a\":1}/*comment",
        jsonObject("a", new JsonPrimitive(1))),
    new TestCase("map_with_tailing_#", "{\"a\":1}#", jsonObject("a", new JsonPrimitive(1))),
    new TestCase("map_with_tailing_]", "{\"a\":1}]", jsonObject("a", new JsonPrimitive(1))),
    new TestCase("map_with_tailing_}", "{\"a\":1}}", jsonObject("a", new JsonPrimitive(1))),
    new TestCase("array_with_tailing_comma", "[\"a\"],", jsonArray(new JsonPrimitive("a"))),
    new TestCase(
        "array_with_tailing_comment", "[\"a\"]/*comment*/", jsonArray(new JsonPrimitive("a"))),
    new TestCase(
        "array_with_tailing_open_comment", "[\"a\"]/*comment", jsonArray(new JsonPrimitive("a"))),
    new TestCase("array_with_tailing_#", "[\"a\"]#", jsonArray(new JsonPrimitive("a"))),
    new TestCase("array_with_tailing_]", "[\"a\"]]", jsonArray(new JsonPrimitive("a"))),
    new TestCase("array_with_tailing_}", "[\"a\"]}", jsonArray(new JsonPrimitive("a"))),
    new TestCase("double_array", "[][]", new JsonArray()),
    new TestCase("number_with_space", "42 000", new JsonPrimitive(42)),
    new TestCase("float_with_space", "42 000.0", new JsonPrimitive(42)),
  };

  @Theory
  public void parse_asExpected(
      @FromDataPoints("testCasesSuccess") TestCase testCase) throws Exception {
    JsonElement output = JsonParser.parse(testCase.input);

    assertThat(output).isEqualTo(testCase.expected);
  }

  @Test
  public void parsedElementWithNumberToString_doesNotLoosePrecision() throws Exception {
    JsonElement element = JsonParser.parse("{ \"a\": 9223372036854775807 }");
    assertThat(element.toString()).isEqualTo("{\"a\":9223372036854775807}");
  }

  @Test
  public void parsedNumberSerializeDeserialize_returnsBigDecimal() throws Exception {
    JsonElement numElement = JsonParser.parse("42");
    Number num = numElement.getAsNumber();
    ByteArrayOutputStream bytes = new ByteArrayOutputStream();
    ObjectOutputStream out = new ObjectOutputStream(bytes);
    assertThrows(NotSerializableException.class, () -> out.writeObject(num));
  }

  @Test
  public void parsedNumberGetValue() throws Exception {
    JsonElement numElement = JsonParser.parse("42.42");
    assertThat(numElement.getAsInt()).isEqualTo(42);
    assertThat(numElement.getAsLong()).isEqualTo(42);
    assertThat(numElement.getAsFloat()).isEqualTo(42.42f);
    assertThat(numElement.getAsDouble()).isEqualTo(42.42);
    Number number = numElement.getAsNumber();
    assertThat(number.intValue()).isEqualTo(42);
    assertThat(number.longValue()).isEqualTo(42);
    assertThat(number.floatValue()).isEqualTo(42.42f);
    assertThat(number.doubleValue()).isEqualTo(42.42);
  }

  @Test
  public void parsedNumberGetAsLong_discardsAllBut64LowestOrderBits() throws Exception {
    // It would be preferable if JsonElement.getAsLong would throw a NumberFormatException exception
    // if the number it contains does not fit into a long, similar to what Long.parseLong does.

    // Instead, the method never throws an exception, and follows the "narrowing primitive
    // conversion" of the Java Language Specification section 5.1.3, which means that all but the 32
    // lowest order bits are discarded.

    JsonElement numElement = JsonParser.parse("9223372036854775809"); // 2^63 + 1
    assertThat(numElement.getAsLong()).isEqualTo(-9223372036854775807L);
  }

  @Test
  public void parsedNumberGetAsInt_discardsAllBut32LowestOrderBits() throws Exception {
    // It would be preferable if JsonElement.getAsInt would throw a NumberFormatException exception
    // if the number it contains does not fit into a long, similar to what Int.parseInt does.

    // Instead, the method never throws an exception, and follows the "narrowing primitive
    // conversion" of the Java Language Specification section 5.1.3, which means that all but the 32
    // lowest order bits are discarded.

    JsonElement numElement = JsonParser.parse("2147483649"); // 2^31 + 1
    assertThat(numElement.getAsInt()).isEqualTo(-2147483647);
  }

  @DataPoints("longs")
  public static final String[] LONGS =
      new String[] {
        "0",
        "42",
        "-42",
        "2147483647", // 2^31 - 1
        "-2147483648", // - 2^31
        "2147483649", // 2^31 + 1
        "44444444444444444",
        "9223372036854775807",  // 2^63 - 1
        "-9223372036854775808",  // - 2^63
      };

  @DataPoints("biggerThanLongs")
  public static final String[] BIGGER_THAN_LONGS =
      new String[] {
        "9223372036854775809",  // 2^63 + 1
        "18446744073709551658",  // 2^64 + 42
        "9999999999999999999999999999999999999999999999999999999999999999",
        "-9999999999999999999999999999999999999999999999999999999999999999",
      };

  @Theory
  public void parsedNumberGetAsLong_validLong_sameAsParseLong(
      @FromDataPoints("longs") String numString) throws Exception {
    JsonElement parsed = JsonParser.parse(numString);
    assertThat(parsed.getAsLong()).isEqualTo(Long.parseLong(numString));
  }

  @Theory
  public void parsedNumberGetAsLong_biggerThanLong_sameAsBigIntegerLongValue(
      @FromDataPoints("biggerThanLongs") String numString) throws Exception {
    JsonElement parsed = JsonParser.parse(numString);
    assertThat(parsed.getAsLong()).isEqualTo(new BigInteger(numString).longValue());
  }

  @Theory
  public void parsedNumberGetAsInt_validLong_sameAsBigIntegerIntValue(
      @FromDataPoints("longs") String numString) throws Exception {
    JsonElement parsed = JsonParser.parse(numString);
    assertThat(parsed.getAsInt()).isEqualTo(new BigInteger(numString).intValue());
  }

  @Theory
  public void parsedNumberGetAsInt_biggerThanLong_sameAsBigIntegerIntValue(
      @FromDataPoints("biggerThanLongs") String numString) throws Exception {
    JsonElement parsed = JsonParser.parse(numString);
    assertThat(parsed.getAsInt()).isEqualTo(new BigInteger(numString).intValue());
  }

  @Theory
  public void getParsedNumberAsLongOrThrow_validLong_sameAsParseLong(
      @FromDataPoints("longs") String numString) throws Exception {
    Number parsed = JsonParser.parse(numString).getAsNumber();
    assertThat(JsonParser.getParsedNumberAsLongOrThrow(parsed))
        .isEqualTo(Long.parseLong(numString));
  }

  @Theory
  public void getParsedNumberAsLongOrThrow_validLong_sameAsLongValue(
      @FromDataPoints("longs") String numString) throws Exception {
    Number parsed = JsonParser.parse(numString).getAsNumber();
    assertThat(JsonParser.getParsedNumberAsLongOrThrow(parsed)).isEqualTo(parsed.longValue());
  }

  @Theory
  public void getParsedNumberAsLongOrThrow_biggerThanLong_throws(
      @FromDataPoints("biggerThanLongs") String numString) throws Exception {
    Number parsed = JsonParser.parse(numString).getAsNumber();
    assertThrows(
        NumberFormatException.class, () -> JsonParser.getParsedNumberAsLongOrThrow(parsed));
  }

  @Theory
  public void getParsedNumberAsLongOrThrow_nestedValue_success() throws Exception {
    JsonElement parsed = JsonParser.parse("{\"a\":{\"b\":9223372036854775807}}");
    Number parsedNumber =
        parsed.getAsJsonObject().get("a").getAsJsonObject().get("b").getAsNumber();
    long output = JsonParser.getParsedNumberAsLongOrThrow(parsedNumber);
    assertThat(output).isEqualTo(9223372036854775807L);
  }

  @Theory
  public void getParsedNumberAsLongOrThrow_notParsed_throws() throws Exception {
    Number notParsedJsonElementWithNumber = new JsonPrimitive(42).getAsNumber();
    assertThrows(
        IllegalArgumentException.class,
        () -> JsonParser.getParsedNumberAsLongOrThrow(notParsedJsonElementWithNumber));
  }

  @Theory
  public void floatNumbersGetAsLong_getsTruncated() throws Exception {
    assertThat(JsonParser.parse("42.0").getAsLong()).isEqualTo(42);
    assertThat(JsonParser.parse("2.1e1").getAsLong()).isEqualTo(21);

    assertThat(JsonParser.parse("42.1").getAsLong()).isEqualTo(42);
    assertThat(JsonParser.parse("42.9999").getAsLong()).isEqualTo(42);

    // 2^63 - 1 as float
    assertThat(JsonParser.parse("9.223372036854775807e18").getAsLong())
        .isEqualTo(9223372036854775807L);

    // - 2^63 as float
    assertThat(JsonParser.parse("-9.223372036854775808e18").getAsLong())
        .isEqualTo(-9223372036854775808L);
  }

  @Theory
  public void floatNumbersGetAsInt_getsTruncated() throws Exception {
    assertThat(JsonParser.parse("42.0").getAsInt()).isEqualTo(42);
    assertThat(JsonParser.parse("2.1e1").getAsInt()).isEqualTo(21);

    assertThat(JsonParser.parse("42.1").getAsInt()).isEqualTo(42);
    assertThat(JsonParser.parse("42.9999").getAsInt()).isEqualTo(42);

    // 2^31 - 1 as float
    assertThat(JsonParser.parse("2.147483647e9").getAsInt()).isEqualTo(2147483647);

    // - 2^31 as float
    assertThat(JsonParser.parse("-2.147483648e9").getAsInt()).isEqualTo(-2147483648);
  }

  @Theory
  public void testNumbersToDouble() throws Exception {
    assertThat(JsonParser.parse("60911552482230981.0").getAsDouble())
        .isEqualTo(6.0911552482230984e16);
    assertThat(JsonParser.parse("4e+42").getAsDouble()).isEqualTo(4e42);
    assertThat(JsonParser.parse("4e42").getAsDouble()).isEqualTo(4e42);
    assertThat(JsonParser.parse("4E42").getAsDouble()).isEqualTo(4e42);
    assertThat(JsonParser.parse("-4e-42").getAsDouble()).isEqualTo(-4e-42);
    assertThat(
            JsonParser.parse(
                    "9999999999999999999999999999999999999999999999999999999999999999999999999999"
                        + "99999999999")
                .getAsDouble())
        .isEqualTo(1.0e87);
    assertThat(
            JsonParser.parse(
                    "-999999999999999999999999999999999999999999999999999999999999999999999999999"
                        + "999999999999")
                .getAsDouble())
        .isEqualTo(-1.0e87);
    assertThat(
            JsonParser.parse("99999999999999999999999999.99e+99999999999999999999999999")
                .getAsDouble())
        .isPositiveInfinity();
    assertThat(
            JsonParser.parse("-99999999999999999999999999.99e+99999999999999999999999999")
                .getAsDouble())
        .isNegativeInfinity();
    assertThat(
            JsonParser.parse("99999999999999999999999999.99e-99999999999999999999999999")
                .getAsDouble())
        .isEqualTo(0.0);
    assertThat(JsonParser.parse("0.000000000000000000000000000000001").getAsDouble())
        .isEqualTo(0.000000000000000000000000000000001);
    assertThat(JsonParser.parse("-0.000000000000000000000000000000001").getAsDouble())
        .isEqualTo(-0.000000000000000000000000000000001);
    assertThat(JsonParser.parse("42").getAsInt()).isEqualTo(42);
    assertThat(JsonParser.parse("42\n").getAsInt()).isEqualTo(42);
    assertThat(JsonParser.parse("42\f").getAsInt()).isEqualTo(42);
  }

  @DataPoints("testCasesFail")
  public static final TestCase[] TEST_CASES_FAIL = {
    new TestCase("nested_empty_maps", "{{}}"),
    new TestCase("open_map", "{\"\":{\"\":{\"\":{\"\":{\"\":{\"\":{\"\":"),
    new TestCase("open_array_map", "[{\"\":[{\"\":[{\"\":[{\"\":[{\"\":[{\"\":[{\"\":["),
    new TestCase("open_array", "["),
    new TestCase("open_array_1", "[1"),
    new TestCase("open_array_2", "[1,"),
    new TestCase("open_arrays", "[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[["),
    new TestCase("open_array_with_huge_negative_int", "[-2374623746732768942798327498324234"),
    new TestCase("map_missing_value", "{\"a\":"),
    new TestCase("string_not_closed", "\"a"),
    new TestCase("empty_string_not_closed", "\""),
    new TestCase("string_with_backslash_not_closed", "\"\\"),
    new TestCase("number_dot", "42."),
    new TestCase("-number_dot", "-42."),
    new TestCase("number_dot_with_e1", "42.e1"),
    new TestCase("number_dot_with_+e1", "42.e+1"),
    new TestCase("number_dot_with_-e1", "42.e-1"),
    new TestCase("number_with_e", "42e"),
    new TestCase("number_with_e+", "42e+"),
    new TestCase("number_with_E", "42E"),
    new TestCase("number_with_E+", "42E+"),
    new TestCase("float_with_e", "42.42e"),
    new TestCase("float_with_e+", "42.42e+"),
    new TestCase("float_with_E", "42.42E"),
    new TestCase("float_with_E+", "42.42E+"),
    new TestCase("+number", "+42"),
    new TestCase("++number", "++42"),
    new TestCase("Inf", "Inf"),
    new TestCase("+Inf", "+Inf"),
    new TestCase("-Inf", "-Inf"),
    new TestCase("Infinity", "Infinity"),
    new TestCase("+Infinity", "+Infinity"),
    new TestCase("-Infinity", "-Infinity"),
    new TestCase("NaN", "NaN"),
    new TestCase("+NaN", "+NaN"),
    new TestCase("-NaN", "-NaN"),
    new TestCase("number_dot_minus_number", "42.-42"),
    new TestCase("dot_minus_number", ".-42"),
    new TestCase("dot_number", ".42"),
    new TestCase("minus_dot_number", "-.42"),
    new TestCase("number_with_leading_zero", "042"),
    new TestCase("-number_with_leading_zero", "-042"),
    new TestCase("number_two_dots", "42.43.44"),
    new TestCase("number_two_dots_2", ".42.43"),
    new TestCase("number_two_dots_3", "42.43."),
    new TestCase("number_ee", "42ee42"),
    new TestCase("number_eE", "42eE42"),
    new TestCase("number_e_plus_minus", "42e+-42"),
    new TestCase("number_with_trailing_garbage", "2@"),
    new TestCase("number_with_tailing_comment", "42/*comment*/"),
    new TestCase("number_garbage_after_e", "1ea"),
    new TestCase("number_with_a", "1.2a-3"),
    new TestCase("number_with_h", "1.8011670033376514H-308"),
    new TestCase("hex1", "0x1"),
    new TestCase("hex2", "0x42"),
    new TestCase("number_with_tailing_a", "42a"),
    new TestCase("float_with_tailing_a", "42.42a"),
    new TestCase("minus_number_with_tailing_a", "-42a"),
    new TestCase("number_tailing_excaped_newline", "42\\n"),
    new TestCase("minus_a", "-a"),
    new TestCase("minus", "-"),
    new TestCase("addition", "1+2"),
    new TestCase("subtraction", "2-1"),
    new TestCase("multiplication", "2*1"),
    new TestCase("array_with_number_with_space", "[1 000]"),
    new TestCase("array_with_float_with_space", "[1 000.0]"),
    new TestCase("array_with_minus_space_number", "[- 42]"),
    new TestCase("key_without_quotes", "{a:0}"),
    new TestCase("key_single_quote", "{'a':0}"),
    new TestCase("array_element_without_quotes", "[a,0]"),
    new TestCase("array_element_single_quotes", "['a',0]"),
    new TestCase("map_with_trailing_comma", "{\"a\":0,}"),
    new TestCase("map_with_two_commas", "{\"a\":0,,\"b\":1}"),
    new TestCase("array_with_trailing_comma", "[\"a\",]"),
    new TestCase("map_with_comment", "{\"a\":/*comment*/\"b\"}"),
    new TestCase("map_with_null_key", "{null:0}"),
    new TestCase("map_with_number_key", "{1:1}"),
    new TestCase("map_with_huge_float_key", "{9999E9999:1}"),
    new TestCase("map_missing_colon", "{\"a\" \"b\"}"),
    new TestCase("map_missing_key", "{:\"b\"}"),
    new TestCase("map_with_comma", "{\"a\", \"b\"}"),
    new TestCase("map_double_colon", "{\"x\"::\"b\"}"),
    new TestCase("map_with_garbage", "{\"a\":\"b\" c}"),
    new TestCase("map_with_single_string", "{ \"a\" : \"b\", \"c\" }"),
    new TestCase("array_leading_comma", "[,1]"),
    new TestCase("array_double_comma", "[1,,2]"),
    new TestCase("array_double_tailing_comma", "[1,,]"),
    new TestCase("array_comma", "[,]"),
    new TestCase("nested_arrays_no_comma", "[3[4]]"),
    new TestCase("array_without_comma", "[1 2]"),
    new TestCase("array__with_colon", "[\"a\": 1]"),
    new TestCase("incomplete_false", "fals"),
    new TestCase("incomplete_null", "nul"),
    new TestCase("incomplete_true", "tru"),
    new TestCase("unquoted_string", "a"),
    new TestCase("star", "*"),
    new TestCase("angle_bracket_dot", "<.>"),
    new TestCase("string_escape_x", "\"\\x00\""),
    new TestCase("escaped_emoji", "\"\\👩\""),
    new TestCase("invalid_backslash_escape", "\"\\a\""),
    new TestCase("unicode_with_capital_u", "\"\\UA66D\""),
    new TestCase("invalid_unicode_escape", "\"\\uqqqq\""),
    new TestCase("incomplete_surrogate", "\"\\uD834\\uDd\""),
    new TestCase("1_surrogate_then_escape_u", "[\"\\uD800\\u\"]"),
    new TestCase("2_incomplete_surrogate_escape_invalid", "[\"\\uD800\\uD800\\x\"]"),
    new TestCase("array_with_formfeed", "[\f]"),
    new TestCase("array_with_tailing_formfeed", "[\"a\"\f]"),
    new TestCase("array_with_leading_uescaped_thinspace", "[\\u0020\"a\"]"),
    new TestCase("array_with_escaped_new_line", "[\\n]"),
    new TestCase("array_with_escaped_tab", "[\\t]"),

    new TestCase("duplicated_key", "{\"a\":\"b\",\"a\":\"c\"}"),
    new TestCase("duplicated_key_and_value", "{\"a\":\"b\",\"a\":\"b\"}"),
    new TestCase("empty", ""),
    new TestCase("single_space", " "),
    new TestCase("nested_with_duplicated_key", "{\"x\":{\"a\":\"b\",\"a\":\"c\"}}"),
    new TestCase("split_array", "{ \"a\" : [1,2,3], \"b\" : 0, \"a\" : [4,5,6]}"),

    // invalid characters in strings
    new TestCase("invalid_utf16", "\"\\uD834\"", null),
    new TestCase("invalid_utf16_in_key", "{\"\\ud800\\ud800key\":\"value\"}", null),
    new TestCase(
         "invalid_utf16_in_key_2", "{\"key\":\"value1\",\"\\ud800\\ud800key\":\"value2\"}", null),
    new TestCase("invalid_utf16_in_value", "{\"key\":\"value\\ud800\\ud800\"}", null),
    new TestCase("invalid_surrogate_1", "\"\\uDADA\"", null),
    new TestCase("invalid_surrogate_2", "\"\\ud800\"", null),
    new TestCase("invalid_surrogate_3", "\"\\uDd1ea\"", null),
    new TestCase("invalid_surrogate_4", "\"\\uDFAA\"", null),
    new TestCase("invalid_surrogate_5", "\"\\uD888\\u1234\"", null),
    new TestCase("invalid_surrogate_6", "\"\\uD800\\uD800\\n\"", null),
    new TestCase("invalid_surrogate_7", "\"\\uDd1e\\uD834\"", null),
    new TestCase("invalid_surrogate_in_map_key", "{\"\\uDFAA\":0}", null),
    new TestCase("invalid_surrogate_in_map_value", "{\"a\": \"\\uDFAA\"}", null),
  };

  @Test
  public void tooManyRecursions_fail() throws Exception {
    int recursionNum = 150;
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < recursionNum; i++) {
      sb.append("{\"a\":");
    }
    sb.append("1");
    for (int i = 0; i < recursionNum; i++) {
      sb.append("}");
    }
    assertThrows(IOException.class, () -> JsonParser.parse(sb.toString()));
  }

  @Theory
  public void parse_fail(
      @FromDataPoints("testCasesFail") TestCase testCase) throws Exception {
    assertThrows(IOException.class, () -> JsonParser.parse(testCase.input));
  }

}
