// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// Disable background compilation so that the Issue 24908 can be reproduced.
// VMOptions=--no-background-compilation

import 'dart:convert' as convert;

import 'package:jsonc/jsonc.dart';
import "package:test/test.dart";

void testJson(jsonText, expected) {
  expect(jsonc.decode(jsonText), equals(expected));
  expect(jsoncDecode(jsonText), equals(expected));
}

String escape(String s) {
  var sb = StringBuffer();
  for (int i = 0; i < s.length; i++) {
    int code = s.codeUnitAt(i);
    if (code == '\\'.codeUnitAt(0)) {
      sb.write(r'\\');
    } else if (code == '"'.codeUnitAt(0)) {
      sb.write(r'\"');
    } else if (code >= 32 && code < 127) {
      sb.writeCharCode(code);
    } else {
      String hex = '000${code.toRadixString(16)}';
      sb.write(r'\u' '${hex.substring(hex.length - 4)}');
    }
  }
  return '$sb';
}

void testThrows(jsonText) {
  expect(() => jsonc.decode(jsonText), throwsFormatException);
  expect(() => jsoncDecode(jsonText), throwsFormatException);
  expect(() => jsonc.decoder.convert(jsonText), throwsFormatException);
  expect(
      () => convert.utf8.decoder
          .fuse(jsonc.decoder)
          .convert(convert.utf8.encode(jsonText)),
      throwsFormatException);
}

void main() {
  test('numbers', () {
    // Positive tests for number formats.
    var integerList = ["0", "9", "9999"];
    var signList = ["", "-"];
    var fractionList = ["", ".0", ".1", ".99999"];
    var exponentList = [""];
    for (var exphead in ["e", "E", "e-", "E-", "e+", "E+"]) {
      for (var expval in ["0", "1", "200"]) {
        exponentList.add("$exphead$expval");
      }
    }

    for (var integer in integerList) {
      for (var sign in signList) {
        for (var fraction in fractionList) {
          for (var exp in exponentList) {
            for (var ws in ["", " ", "\t"]) {
              var literal = "$ws$sign$integer$fraction$exp$ws";
              var expectedValue = num.parse(literal);
              testJson(literal, expectedValue);
            }
          }
        }
      }
    }

    // Regression test.
    // Detect and handle overflow on integer literals by making them doubles
    // (work like `num.parse`).
    testJson("9223372036854774784", 9223372036854774784);
    testJson("-9223372036854775808", -9223372036854775808);
    testJson("9223372036854775808", 9223372036854775808.0);
    testJson("-9223372036854775809", -9223372036854775809.0);
    testJson("9223372036854775808.0", 9223372036854775808.0);
    testJson("9223372036854775810", 9223372036854775810.0);
    testJson("18446744073709551616.0", 18446744073709551616.0);
    testJson("1e309", double.infinity);
    testJson("-1e309", double.negativeInfinity);
    testJson("1e-325", 0.0);
    testJson("-1e-325", -0.0);
    // No overflow on exponent.
    testJson("1e18446744073709551616", double.infinity);
    testJson("-1e18446744073709551616", double.negativeInfinity);
    testJson("1e-18446744073709551616", 0.0);
    testJson("-1e-18446744073709551616", -0.0);

    // (Wrapping numbers in list because the chunked parsing handles top-level
    // numbers by buffering and then parsing using platform parser).
    testJson("[9223372036854774784]", [9223372036854774784]);
    testJson("[-9223372036854775808]", [-9223372036854775808]);
    testJson("[9223372036854775808]", [9223372036854775808.0]);
    testJson("[-9223372036854775809]", [-9223372036854775809.0]);
    testJson("[9223372036854775808.0]", [9223372036854775808.0]);
    testJson("[9223372036854775810]", [9223372036854775810.0]);
    testJson("[18446744073709551616.0]", [18446744073709551616.0]);
    testJson("[1e309]", [double.infinity]);
    testJson("[-1e309]", [double.negativeInfinity]);
    testJson("[1e-325]", [0.0]);
    testJson("[-1e-325]", [-0.0]);
    // No overflow on exponent.
    testJson("[1e18446744073709551616]", [double.infinity]);
    testJson("[-1e18446744073709551616]", [double.negativeInfinity]);
    testJson("[1e-18446744073709551616]", [0.0]);
    testJson("[-1e-18446744073709551616]", [-0.0]);

    // Negative tests (syntax error).
    // testError thoroughly tests the given parts with a lot of valid
    // values for the other parts.
    testError({signs, integers, fractions, exponents}) {
      def(value, defaultValue) {
        if (value == null) return defaultValue;
        if (value is List) return value;
        return [value];
      }

      signs = def(signs, signList);
      integers = def(integers, integerList);
      fractions = def(fractions, fractionList);
      exponents = def(exponents, exponentList);
      for (var integer in integers) {
        for (var sign in signs) {
          for (var fraction in fractions) {
            for (var exponent in exponents) {
              var literal = "$sign$integer$fraction$exponent";
              testThrows(literal);
            }
          }
        }
      }
    }

    // Doubles overflow to Infinity.
    testJson("1e+400", double.infinity);
    // (Integers do not, but we don't have those on dart2js).

    // Integer part cannot be omitted:
    testError(integers: "");

    // Test for "Initial zero only allowed for zero integer part" moved to
    // json_strict_test.dart because IE's jsonDecode accepts additional initial
    // zeros.

    // Requires digits after decimal point.
    testError(fractions: ".");
    // Requires exponent digts, and only digits.
    testError(exponents: ["e", "e+", "e-", "e.0"]);

    // No whitespace inside numbers.
    // Additional case "- 2.2e+2" in json_strict_test.dart.
    testThrows("-2 .2e+2");
    testThrows("-2. 2e+2");
    testThrows("-2.2 e+2");
    testThrows("-2.2e +2");
    testThrows("-2.2e+ 2");
    testThrows("01");
    testThrows("0.");
    testThrows(".0");
    testThrows("0.e1");

    testThrows("[2.,2]");
    testThrows("{2.:2}");

    testThrows("NaN");
    testThrows("Infinity");
    testThrows("-Infinity");
  });

  test('strings', () {
    // String parser accepts and understands escapes.
    var input = r'"\u0000\uffff\n\r\f\t\b\/\\\"' '\x20\ufffd\uffff"';
    var expected = "\u0000\uffff\n\r\f\t\b/\\\"\x20\ufffd\uffff";
    testJson(input, expected);
    // Empty string.
    testJson(r'""', "");
    // Escape first.
    var escapes = {
      "f": "\f",
      "b": "\b",
      "n": "\n",
      "r": "\r",
      "t": "\t",
      r"\": r"\",
      '"': '"',
      "/": "/",
    };
    escapes.forEach((esc, lit) {
      testJson('"\\$esc........"', "$lit........");
      // Escape last.
      testJson('"........\\$esc"', "........$lit");
      // Escape middle.
      testJson('"....\\$esc...."', "....$lit....");
    });

    // Does not accept single quotes.
    testThrows(r"''");
    // Throws on unterminated strings.
    testThrows(r'"......\"');
    // Throws on unterminated escapes.
    testThrows(r'"\'); // ' is not escaped.
    testThrows(r'"\a"');
    testThrows(r'"\u"');
    testThrows(r'"\u1"');
    testThrows(r'"\u12"');
    testThrows(r'"\u123"');
    testThrows(r'"\ux"');
    testThrows(r'"\u1x"');
    testThrows(r'"\u12x"');
    testThrows(r'"\u123x"');
    // Throws on bad escapes.
    testThrows(r'"\a"');
    testThrows(r'"\x00"');
    testThrows(r'"\c2"');
    testThrows(r'"\000"');
    testThrows(r'"\u{0}"');
    testThrows(r'"\%"');
    testThrows('"\\\x00"'); // Not raw string!
    // Throws on control characters.
    for (int i = 0; i < 32; i++) {
      var string = String.fromCharCodes([0x22, i, 0x22]); // '"\x00"' etc.
      testThrows(string);
    }
  });

  test('objects', () {
    testJson(r'{}', {});
    testJson(r'{"x":42}', {"x": 42});
    testJson(r'{"x":{"x":{"x":42}}}', {
      "x": {
        "x": {"x": 42}
      }
    });
    testJson(r'{"x":10,"x":42}', {"x": 42});
    testJson(r'{"":42}', {"": 42});

    // Keys must be strings.
    testThrows(r'{x:10}');
    testThrows(r'{true:10}');
    testThrows(r'{false:10}');
    testThrows(r'{null:10}');
    testThrows(r'{42:10}');
    testThrows(r'{42e1:10}');
    testThrows(r'{-42:10}');
    testThrows(r'{["text"]:10}');
    testThrows(r'{:10}');
  });

  test('arrays', () {
    testJson(r'[]', []);
    testJson(r'[1.1e1,"string",true,false,null,{}]',
        [1.1e1, "string", true, false, null, {}]);
    testJson(r'[[[[[[]]]],[[[]]],[[]]]]', [
      [
        [
          [
            [[]]
          ]
        ],
        [
          [[]]
        ],
        [[]]
      ]
    ]);
    testJson(r'[{},[{}],{"x":[]}]', [
      {},
      [{}],
      {"x": []}
    ]);

    testThrows(r'[1,,2]');
    testThrows(r'[,2]');
  });

  test('words', () {
    testJson(r'true', true);
    testJson(r'false', false);
    testJson(r'null', null);
    testJson(r'[true]', [true]);
    testJson(r'{"true":true}', {"true": true});

    testThrows(r'truefalse');
    testThrows(r'trues');
    testThrows(r'nulll');
    testThrows(r'full');
    testThrows(r'nul');
    testThrows(r'tru');
    testThrows(r'fals');
    testThrows(r'\null');
    testThrows(r't\rue');
    testThrows(r't\rue');
  });

  test('whitespace', () {
    // Valid white-space characters.
    var v = '\t\r\n ';
    // Invalid white-space and non-recognized characters.
    var invalids = ['\x00', '\f', '\x08', '\\', '\xa0', '\u2028', '\u2029'];

    // Valid whitespace accepted "everywhere".
    testJson('$v[$v-2.2e2$v,$v{$v"key"$v:${v}true$v}$v,$v"ab"$v]$v', [
      -2.2e2,
      {"key": true},
      "ab"
    ]);

    // IE9 accepts invalid characters at the end, so some of these tests have been
    // moved to json_strict_test.dart.
    for (var i in invalids) {
      testThrows('$i"s"');
      testThrows('42$i');
      testThrows('$i[]');
      testThrows('[$i]');
      testThrows('[$i"s"]');
      testThrows('["s"$i]');
      testThrows('$i{"k":"v"}');
      testThrows('{$i"k":"v"}');
      testThrows('{"k"$i:"v"}');
      testThrows('{"k":$i"v"}');
      testThrows('{"k":"v"$i}');
    }
  });
}
