# Copyright 2015 Google Inc. All rights reserved.
#
# 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.

import io
import math
import os
import sys
import unittest

from collections import OrderedDict

import json5


class TestLoads(unittest.TestCase):
    maxDiff = None

    def check(self, s, obj):
        self.assertEqual(json5.loads(s), obj)

    def check_fail(self, s, err=None):
        try:
            json5.loads(s)
            self.fail()  # pragma: no cover
        except ValueError as e:
            if err:
                self.assertEqual(err, str(e))

    def test_arrays(self):
        self.check('[]', [])
        self.check('[0]', [0])
        self.check('[0,1]', [0, 1])
        self.check('[ 0 , 1 ]', [0, 1])

        try:
            json5.loads('[ ,]')
            self.fail()
        except ValueError as e:
            self.assertIn('Unexpected "," at column 3', str(e))

    def test_bools(self):
        self.check('true', True)
        self.check('false', False)

    def test_cls_is_not_supported(self):
        self.assertRaises(AssertionError, json5.loads, '1', cls=lambda x: x)

    def test_duplicate_keys_should_be_allowed(self):
        self.assertEqual(json5.loads('{foo: 1, foo: 2}',
                                     allow_duplicate_keys=True),
                         {"foo": 2})

    def test_duplicate_keys_should_be_allowed_by_default(self):
        self.check('{foo: 1, foo: 2}', {"foo": 2})

    def test_duplicate_keys_should_not_be_allowed(self):
        self.assertRaises(ValueError, json5.loads, '{foo: 1, foo: 2}',
                          allow_duplicate_keys=False)

    def test_empty_strings_are_errors(self):
        self.check_fail('', 'Empty strings are not legal JSON5')

    def test_encoding(self):
        if sys.version_info[0] < 3:
            s = '"\xf6"'
        else:
            s = b'"\xf6"'
        self.assertEqual(json5.loads(s, encoding='iso-8859-1'),
                         u'\xf6')

    def test_numbers(self):
        # decimal literals
        self.check('1', 1)
        self.check('-1', -1)
        self.check('+1', 1)

        # hex literals
        self.check('0xf', 15)
        self.check('0xfe', 254)
        self.check('0xfff', 4095)
        self.check('0XABCD', 43981)
        self.check('0x123456', 1193046)

        # floats
        self.check('1.5', 1.5)
        self.check('1.5e3', 1500.0)
        self.check('-0.5e-2', -0.005)

        # names
        self.check('Infinity', float('inf'))
        self.check('-Infinity', float('-inf'))
        self.assertTrue(math.isnan(json5.loads('NaN')))
        self.assertTrue(math.isnan(json5.loads('-NaN')))

        # syntax errors
        self.check_fail('14d', '<string>:1 Unexpected "d" at column 3')

    def test_identifiers(self):
        self.check('{a: 1}', {'a': 1})
        self.check('{$: 1}', {'$': 1})
        self.check('{_: 1}', {'_': 1})
        self.check('{a_b: 1}', {'a_b': 1})
        self.check('{a$: 1}', {'a$': 1})

        # This valid JavaScript but not valid JSON5; keys must be identifiers
        # or strings.
        self.check_fail('{1: 1}')

    def test_identifiers_unicode(self):
        self.check(u'{\xc3: 1}', {u'\xc3': 1})

    def test_null(self):
        self.check('null', None)

    def test_object_hook(self):
        hook = lambda d: [d]
        self.assertEqual(json5.loads('{foo: 1}', object_hook=hook),
                         [{"foo": 1}])

    def test_object_pairs_hook(self):
        hook = lambda pairs: pairs
        self.assertEqual(json5.loads('{foo: 1, bar: 2}',
                                     object_pairs_hook=hook),
                         [('foo', 1), ('bar', 2)])

    def test_objects(self):
        self.check('{}', {})
        self.check('{"foo": 0}', {"foo": 0})
        self.check('{"foo":0,"bar":1}', {"foo": 0, "bar": 1})
        self.check('{ "foo" : 0 , "bar" : 1 }', {"foo": 0, "bar": 1})

    def test_parse_constant(self):
        hook = lambda x: x
        self.assertEqual(json5.loads('-Infinity', parse_constant=hook),
                         '-Infinity')
        self.assertEqual(json5.loads('NaN', parse_constant=hook),
                         'NaN')

    def test_parse_float(self):
        hook = lambda x: x
        self.assertEqual(json5.loads('1.0', parse_float=hook), '1.0')

    def test_parse_int(self):
        hook = lambda x, base=10: x
        self.assertEqual(json5.loads('1', parse_int=hook), '1')

    def test_sample_file(self):
        path = os.path.join(os.path.dirname(__file__), '..', 'sample.json5')
        with open(path) as fp:
            obj = json5.load(fp)
        self.assertEqual({
            u'oh': [
                u"we shouldn't forget",
                u"arrays can have",
                u"trailing commas too",
            ],
            u"this": u"is a multi-line string",
            u"delta": 10,
            u"hex": 3735928559,
            u"finally": "a trailing comma",
            u"here": "is another",
            u"to": float("inf"),
            u"while": True,
            u"half": 0.5,
            u"foo": u"bar"
            }, obj)

    def test_strings(self):
        self.check('"foo"', 'foo')
        self.check("'foo'", 'foo')

        # escape chars
        self.check("'\\b\\t\\f\\n\\r\\v\\\\'", '\b\t\f\n\r\v\\')
        self.check("'\\''", "'")
        self.check('"\\""', '"')

        # hex literals
        self.check('"\\x66oo"', 'foo')

        # unicode literals
        self.check('"\\u0066oo"', 'foo')

        # string literals w/ continuation markers at the end of the line.
        # These should not have spaces is the result.
        self.check('"foo\\\nbar"', 'foobar')
        self.check("'foo\\\nbar'", 'foobar')

        # unterminated string literals.
        self.check_fail('"\n')
        self.check_fail("'\n")

        # bad hex literals
        self.check_fail("'\\x0'")
        self.check_fail("'\\xj'")
        self.check_fail("'\\x0j'")

        # bad unicode literals
        self.check_fail("'\\u0'")
        self.check_fail("'\\u00'")
        self.check_fail("'\\u000'")
        self.check_fail("'\\u000j'")
        self.check_fail("'\\u00j0'")
        self.check_fail("'\\u0j00'")
        self.check_fail("'\\uj000'")

    def test_unrecognized_escape_char(self):
        self.check(r'"\/"', '/')

    def test_nul(self):
        self.check(r'"\0"', '\x00')

    def test_whitespace(self):
        self.check('\n1', 1)
        self.check('\r1', 1)
        self.check('\r\n1', 1)
        self.check('\t1', 1)
        self.check('\v1', 1)
        self.check(u'\uFEFF 1', 1)
        self.check(u'\u00A0 1', 1)
        self.check(u'\u2028 1', 1)
        self.check(u'\u2029 1', 1)


class TestDump(unittest.TestCase):
    def test_basic(self):
        sio = io.StringIO()
        json5.dump(True, sio)
        self.assertEqual('true', sio.getvalue())


class TestDumps(unittest.TestCase):
    maxDiff = None

    def check(self, obj, s):
        self.assertEqual(json5.dumps(obj), s)

    def test_allow_duplicate_keys(self):
        self.assertIn(json5.dumps({1: "foo", "1": "bar"}),
                      {'{"1": "foo", "1": "bar"}',
                       '{"1": "bar", "1": "foo"}'})

        self.assertRaises(ValueError, json5.dumps,
                          {1: "foo", "1": "bar"},
                           allow_duplicate_keys=False)

    def test_arrays(self):
        self.check([], '[]')
        self.check([1, 2, 3], '[1, 2, 3]')
        self.check([{'foo': 'bar'}, {'baz': 'quux'}],
                    '[{foo: "bar"}, {baz: "quux"}]')

    def test_bools(self):
        self.check(True, 'true')
        self.check(False, 'false')

    def test_check_circular(self):
        # This tests a trivial cycle.
        l = [1, 2, 3]
        l[2] = l
        self.assertRaises(ValueError, json5.dumps, l)

        # This checks that json5 doesn't raise an error. However,
        # the underlying Python implementation likely will.
        try:
            json5.dumps(l, check_circular=False)
            self.fail()  # pragma: no cover
        except Exception as e:
            self.assertNotIn(str(e), 'Circular reference detected')

        # This checks that repeated but non-circular references
        # are okay.
        x = [1, 2]
        y = {"foo": x, "bar": x}
        self.check(y,
                   '{foo: [1, 2], bar: [1, 2]}')

        # This tests a more complicated cycle.
        x = {}
        y = {}
        z = {}
        z['x'] = x
        z['y'] = y
        z['x']['y'] = y
        z['y']['x'] = x
        self.assertRaises(ValueError, json5.dumps, z)

    def test_default(self):

        def _custom_serializer(obj):
            del obj
            return 'something'

        self.assertRaises(TypeError, json5.dumps, set())
        self.assertEqual(json5.dumps(set(), default=_custom_serializer),
                         'something')

    def test_ensure_ascii(self):
        self.check(u'\u00fc', '"\\u00fc"')
        self.assertEqual(json5.dumps(u'\u00fc', ensure_ascii=False),
                         u'"\u00fc"')

    def test_indent(self):
        self.assertEqual(json5.dumps([1, 2, 3], indent=None),
                         u'[1, 2, 3]')
        self.assertEqual(json5.dumps([1, 2, 3], indent=-1),
                         u'[\n1,\n2,\n3,\n]')
        self.assertEqual(json5.dumps([1, 2, 3], indent=0),
                         u'[\n1,\n2,\n3,\n]')
        self.assertEqual(json5.dumps([], indent=2),
                         u'[]')
        self.assertEqual(json5.dumps([1, 2, 3], indent=2),
                         u'[\n  1,\n  2,\n  3,\n]')
        self.assertEqual(json5.dumps([1, 2, 3], indent=' '),
                         u'[\n 1,\n 2,\n 3,\n]')
        self.assertEqual(json5.dumps([1, 2, 3], indent='++'),
                         u'[\n++1,\n++2,\n++3,\n]')
        self.assertEqual(json5.dumps([[1, 2, 3]], indent=2),
                         u'[\n  [\n    1,\n    2,\n    3,\n  ],\n]')

        self.assertEqual(json5.dumps({}, indent=2),
                         u'{}')
        self.assertEqual(json5.dumps({'foo': 'bar', 'baz': 'quux'}, indent=2),
                         u'{\n  foo: "bar",\n  baz: "quux",\n}')

    def test_numbers(self):
        self.check(15, '15')
        self.check(1.0, '1.0')
        self.check(float('inf'), 'Infinity')
        self.check(float('-inf'), '-Infinity')
        self.check(float('nan'), 'NaN')

        self.assertRaises(ValueError, json5.dumps,
                          float('inf'), allow_nan=False)

    def test_null(self):
        self.check(None, 'null')

    def test_objects(self):
        self.check({'foo': 1}, '{foo: 1}')
        self.check({'foo bar': 1}, '{"foo bar": 1}')
        self.check({'1': 1}, '{"1": 1}')

    def test_reserved_words_in_object_keys_are_quoted(self):
        self.check({'new': 1}, '{"new": 1}')

    def test_identifiers_only_starting_with_reserved_words_are_not_quoted(self):
        self.check({'newbie': 1}, '{newbie: 1}')

    def test_non_string_keys(self):
        self.assertEqual(json5.dumps({False: 'a', 1: 'b', 2.0: 'c', None: 'd'}),
                         '{"false": "a", "1": "b", "2.0": "c", "null": "d"}')

    def test_quote_keys(self):
        self.assertEqual(json5.dumps({"foo": 1}, quote_keys=True),
                         '{"foo": 1}')

    def test_strings(self):
        self.check("'single'", '"\'single\'"')
        self.check('"double"', '"\\"double\\""')
        self.check("'single \\' and double \"'",
                   '"\'single \\\\\' and double \\"\'"')

    def test_string_escape_sequences(self):
        self.check(u'\u2028\u2029\b\t\f\n\r\v\\\0',
                   '"\\u2028\\u2029\\b\\t\\f\\n\\r\\v\\\\\\0"')

    def test_skip_keys(self):
        od = OrderedDict()
        od[(1, 2)] = 2
        self.assertRaises(TypeError, json5.dumps, od)
        self.assertEqual(json5.dumps(od, skipkeys=True), '{}')

        od['foo'] = 1
        self.assertEqual(json5.dumps(od, skipkeys=True), '{foo: 1}')

        # Also test that having an invalid key as the last element
        # doesn't incorrectly add a trailing comma (see
        # https://github.com/dpranke/pyjson5/issues/33).
        od = OrderedDict()
        od['foo'] = 1
        od[(1, 2)] = 2
        self.assertEqual(json5.dumps(od, skipkeys=True), '{foo: 1}')

    def test_sort_keys(self):
        od = OrderedDict()
        od['foo'] = 1
        od['bar'] = 2
        self.assertEqual(json5.dumps(od, sort_keys=True),
                         '{bar: 2, foo: 1}')

    def test_trailing_commas(self):
        # By default, multi-line dicts and lists should have trailing
        # commas after their last items.
        self.assertEqual(json5.dumps({"foo": 1}, indent=2),
                         '{\n  foo: 1,\n}')
        self.assertEqual(json5.dumps([1], indent=2),
                         '[\n  1,\n]')

        self.assertEqual(json5.dumps({"foo": 1}, indent=2,
                                     trailing_commas=False),
                         '{\n  foo: 1\n}')
        self.assertEqual(json5.dumps([1], indent=2, trailing_commas=False),
                         '[\n  1\n]')

    def test_supplemental_unicode(self):
        try:
            s = chr(0x10000)
            self.check(s, '"\\ud800\\udc00"')
        except ValueError:
            # Python2 doesn't support supplemental unicode planes, so
            # we can't test this there.
            pass


if __name__ == '__main__':  # pragma: no cover
    unittest.main()
