import calendar
import datetime
import decimal
import json
import locale
import math
import re
import sys
import time
import tracemalloc
import weakref

import dateutil
import numpy as np
import pytest

import pandas._libs._ujson as ujson
from pandas.compat import IS64

import pandas as pd
import pandas._testing as tm


def _clean_dict(d):
    """
    Sanitize dictionary for JSON by converting all keys to strings.

    Parameters
    ----------
    d : dict
        The dictionary to convert.

    Returns
    -------
    cleaned_dict : dict
    """
    return {str(k): v for k, v in d.items()}


@pytest.fixture(
    params=[None, "split", "records", "values", "index"]  # Column indexed by default.
)
def orient(request):
    return request.param


class TestUltraJSONTests:
    @pytest.mark.skipif(not IS64, reason="not compliant on 32-bit, xref #15865")
    @pytest.mark.parametrize(
        "value, double_precision",
        [
            ("1337.1337", 15),
            ("0.95", 1),
            ("0.94", 1),
            ("1.95", 1),
            ("-1.95", 1),
            ("0.995", 2),
            ("0.9995", 3),
            ("0.99999999999999944", 15),
        ],
    )
    def test_encode_decimal(self, value, double_precision):
        sut = decimal.Decimal(value)
        encoded = ujson.ujson_dumps(sut, double_precision=double_precision)
        decoded = ujson.ujson_loads(encoded)
        assert decoded == value

    @pytest.mark.parametrize("ensure_ascii", [True, False])
    def test_encode_string_conversion(self, ensure_ascii):
        string_input = "A string \\ / \b \f \n \r \t </script> &"
        not_html_encoded = '"A string \\\\ \\/ \\b \\f \\n \\r \\t <\\/script> &"'
        html_encoded = (
            '"A string \\\\ \\/ \\b \\f \\n \\r \\t \\u003c\\/script\\u003e \\u0026"'
        )

        def helper(expected_output, **encode_kwargs):
            output = ujson.ujson_dumps(
                string_input, ensure_ascii=ensure_ascii, **encode_kwargs
            )

            assert output == expected_output
            assert string_input == json.loads(output)
            assert string_input == ujson.ujson_loads(output)

        # Default behavior assumes encode_html_chars=False.
        helper(not_html_encoded)

        # Make sure explicit encode_html_chars=False works.
        helper(not_html_encoded, encode_html_chars=False)

        # Make sure explicit encode_html_chars=True does the encoding.
        helper(html_encoded, encode_html_chars=True)

    @pytest.mark.parametrize(
        "long_number", [-4342969734183514, -12345678901234.56789012, -528656961.4399388]
    )
    def test_double_long_numbers(self, long_number):
        sut = {"a": long_number}
        encoded = ujson.ujson_dumps(sut, double_precision=15)

        decoded = ujson.ujson_loads(encoded)
        assert sut == decoded

    def test_encode_non_c_locale(self):
        lc_category = locale.LC_NUMERIC

        # We just need one of these locales to work.
        for new_locale in ("it_IT.UTF-8", "Italian_Italy"):
            if tm.can_set_locale(new_locale, lc_category):
                with tm.set_locale(new_locale, lc_category):
                    assert ujson.ujson_loads(ujson.ujson_dumps(4.78e60)) == 4.78e60
                    assert ujson.ujson_loads("4.78", precise_float=True) == 4.78
                break

    def test_decimal_decode_test_precise(self):
        sut = {"a": 4.56}
        encoded = ujson.ujson_dumps(sut)
        decoded = ujson.ujson_loads(encoded, precise_float=True)
        assert sut == decoded

    def test_encode_double_tiny_exponential(self):
        num = 1e-40
        assert num == ujson.ujson_loads(ujson.ujson_dumps(num))
        num = 1e-100
        assert num == ujson.ujson_loads(ujson.ujson_dumps(num))
        num = -1e-45
        assert num == ujson.ujson_loads(ujson.ujson_dumps(num))
        num = -1e-145
        assert np.allclose(num, ujson.ujson_loads(ujson.ujson_dumps(num)))

    @pytest.mark.parametrize("unicode_key", ["key1", "بن"])
    def test_encode_dict_with_unicode_keys(self, unicode_key):
        unicode_dict = {unicode_key: "value1"}
        assert unicode_dict == ujson.ujson_loads(ujson.ujson_dumps(unicode_dict))

    @pytest.mark.parametrize("double_input", [math.pi, -math.pi])
    def test_encode_double_conversion(self, double_input):
        output = ujson.ujson_dumps(double_input)
        assert round(double_input, 5) == round(json.loads(output), 5)
        assert round(double_input, 5) == round(ujson.ujson_loads(output), 5)

    def test_encode_with_decimal(self):
        decimal_input = 1.0
        output = ujson.ujson_dumps(decimal_input)

        assert output == "1.0"

    def test_encode_array_of_nested_arrays(self):
        nested_input = [[[[]]]] * 20
        output = ujson.ujson_dumps(nested_input)

        assert nested_input == json.loads(output)
        assert nested_input == ujson.ujson_loads(output)

    def test_encode_array_of_doubles(self):
        doubles_input = [31337.31337, 31337.31337, 31337.31337, 31337.31337] * 10
        output = ujson.ujson_dumps(doubles_input)

        assert doubles_input == json.loads(output)
        assert doubles_input == ujson.ujson_loads(output)

    def test_double_precision(self):
        double_input = 30.012345678901234
        output = ujson.ujson_dumps(double_input, double_precision=15)

        assert double_input == json.loads(output)
        assert double_input == ujson.ujson_loads(output)

        for double_precision in (3, 9):
            output = ujson.ujson_dumps(double_input, double_precision=double_precision)
            rounded_input = round(double_input, double_precision)

            assert rounded_input == json.loads(output)
            assert rounded_input == ujson.ujson_loads(output)

    @pytest.mark.parametrize(
        "invalid_val",
        [
            20,
            -1,
            "9",
            None,
        ],
    )
    def test_invalid_double_precision(self, invalid_val):
        double_input = 30.12345678901234567890
        expected_exception = ValueError if isinstance(invalid_val, int) else TypeError
        msg = "|".join(
            [
                "Invalid value '.*' for option 'double_precision', max is '15'",
                r"an integer is required \(got type ",
                "object cannot be interpreted as an integer",
            ]
        )
        with pytest.raises(expected_exception, match=msg):
            ujson.ujson_dumps(double_input, double_precision=invalid_val)

    def test_encode_string_conversion2(self):
        string_input = "A string \\ / \b \f \n \r \t"
        output = ujson.ujson_dumps(string_input)

        assert string_input == json.loads(output)
        assert string_input == ujson.ujson_loads(output)
        assert output == '"A string \\\\ \\/ \\b \\f \\n \\r \\t"'

    @pytest.mark.parametrize(
        "unicode_input",
        ["Räksmörgås اسامة بن محمد بن عوض بن لادن", "\xe6\x97\xa5\xd1\x88"],
    )
    def test_encode_unicode_conversion(self, unicode_input):
        enc = ujson.ujson_dumps(unicode_input)
        dec = ujson.ujson_loads(enc)

        assert enc == json.dumps(unicode_input)
        assert dec == json.loads(enc)

    def test_encode_control_escaping(self):
        escaped_input = "\x19"
        enc = ujson.ujson_dumps(escaped_input)
        dec = ujson.ujson_loads(enc)

        assert escaped_input == dec
        assert enc == json.dumps(escaped_input)

    def test_encode_unicode_surrogate_pair(self):
        surrogate_input = "\xf0\x90\x8d\x86"
        enc = ujson.ujson_dumps(surrogate_input)
        dec = ujson.ujson_loads(enc)

        assert enc == json.dumps(surrogate_input)
        assert dec == json.loads(enc)

    def test_encode_unicode_4bytes_utf8(self):
        four_bytes_input = "\xf0\x91\x80\xb0TRAILINGNORMAL"
        enc = ujson.ujson_dumps(four_bytes_input)
        dec = ujson.ujson_loads(enc)

        assert enc == json.dumps(four_bytes_input)
        assert dec == json.loads(enc)

    def test_encode_unicode_4bytes_utf8highest(self):
        four_bytes_input = "\xf3\xbf\xbf\xbfTRAILINGNORMAL"
        enc = ujson.ujson_dumps(four_bytes_input)

        dec = ujson.ujson_loads(enc)

        assert enc == json.dumps(four_bytes_input)
        assert dec == json.loads(enc)

    def test_encode_unicode_error(self):
        string = "'\udac0'"
        msg = (
            r"'utf-8' codec can't encode character '\\udac0' "
            r"in position 1: surrogates not allowed"
        )
        with pytest.raises(UnicodeEncodeError, match=msg):
            ujson.ujson_dumps([string])

    def test_encode_array_in_array(self):
        arr_in_arr_input = [[[[]]]]
        output = ujson.ujson_dumps(arr_in_arr_input)

        assert arr_in_arr_input == json.loads(output)
        assert output == json.dumps(arr_in_arr_input)
        assert arr_in_arr_input == ujson.ujson_loads(output)

    @pytest.mark.parametrize(
        "num_input",
        [
            31337,
            -31337,  # Negative number.
            -9223372036854775808,  # Large negative number.
        ],
    )
    def test_encode_num_conversion(self, num_input):
        output = ujson.ujson_dumps(num_input)
        assert num_input == json.loads(output)
        assert output == json.dumps(num_input)
        assert num_input == ujson.ujson_loads(output)

    def test_encode_list_conversion(self):
        list_input = [1, 2, 3, 4]
        output = ujson.ujson_dumps(list_input)

        assert list_input == json.loads(output)
        assert list_input == ujson.ujson_loads(output)

    def test_encode_dict_conversion(self):
        dict_input = {"k1": 1, "k2": 2, "k3": 3, "k4": 4}
        output = ujson.ujson_dumps(dict_input)

        assert dict_input == json.loads(output)
        assert dict_input == ujson.ujson_loads(output)

    @pytest.mark.parametrize("builtin_value", [None, True, False])
    def test_encode_builtin_values_conversion(self, builtin_value):
        output = ujson.ujson_dumps(builtin_value)
        assert builtin_value == json.loads(output)
        assert output == json.dumps(builtin_value)
        assert builtin_value == ujson.ujson_loads(output)

    def test_encode_datetime_conversion(self):
        datetime_input = datetime.datetime.fromtimestamp(time.time())
        output = ujson.ujson_dumps(datetime_input, date_unit="s")
        expected = calendar.timegm(datetime_input.utctimetuple())

        assert int(expected) == json.loads(output)
        assert int(expected) == ujson.ujson_loads(output)

    def test_encode_date_conversion(self):
        date_input = datetime.date.fromtimestamp(time.time())
        output = ujson.ujson_dumps(date_input, date_unit="s")

        tup = (date_input.year, date_input.month, date_input.day, 0, 0, 0)
        expected = calendar.timegm(tup)

        assert int(expected) == json.loads(output)
        assert int(expected) == ujson.ujson_loads(output)

    @pytest.mark.parametrize(
        "test",
        [datetime.time(), datetime.time(1, 2, 3), datetime.time(10, 12, 15, 343243)],
    )
    def test_encode_time_conversion_basic(self, test):
        output = ujson.ujson_dumps(test)
        expected = f'"{test.isoformat()}"'
        assert expected == output

    def test_encode_time_conversion_pytz(self):
        # see gh-11473: to_json segfaults with timezone-aware datetimes
        pytz = pytest.importorskip("pytz")
        test = datetime.time(10, 12, 15, 343243, pytz.utc)
        output = ujson.ujson_dumps(test)
        expected = f'"{test.isoformat()}"'
        assert expected == output

    def test_encode_time_conversion_dateutil(self):
        # see gh-11473: to_json segfaults with timezone-aware datetimes
        test = datetime.time(10, 12, 15, 343243, dateutil.tz.tzutc())
        output = ujson.ujson_dumps(test)
        expected = f'"{test.isoformat()}"'
        assert expected == output

    @pytest.mark.parametrize(
        "decoded_input", [pd.NaT, np.datetime64("NaT", "ns"), np.nan, np.inf, -np.inf]
    )
    def test_encode_as_null(self, decoded_input):
        assert ujson.ujson_dumps(decoded_input) == "null", "Expected null"

    def test_datetime_units(self):
        val = datetime.datetime(2013, 8, 17, 21, 17, 12, 215504)
        stamp = pd.Timestamp(val).as_unit("ns")

        roundtrip = ujson.ujson_loads(ujson.ujson_dumps(val, date_unit="s"))
        assert roundtrip == stamp._value // 10**9

        roundtrip = ujson.ujson_loads(ujson.ujson_dumps(val, date_unit="ms"))
        assert roundtrip == stamp._value // 10**6

        roundtrip = ujson.ujson_loads(ujson.ujson_dumps(val, date_unit="us"))
        assert roundtrip == stamp._value // 10**3

        roundtrip = ujson.ujson_loads(ujson.ujson_dumps(val, date_unit="ns"))
        assert roundtrip == stamp._value

        msg = "Invalid value 'foo' for option 'date_unit'"
        with pytest.raises(ValueError, match=msg):
            ujson.ujson_dumps(val, date_unit="foo")

    def test_encode_time_invalid_subclass_surrogate(self):
        class InvalidTime(datetime.time):
            def isoformat(self):
                return "\ud800"

        with pytest.raises(UnicodeEncodeError, match="surrogates not allowed"):
            ujson.ujson_dumps(InvalidTime())

    def test_encode_time_invalid_subclass_non_str(self):
        class InvalidTime(datetime.time):
            def isoformat(self):
                return 1

        with pytest.raises(TypeError, match=re.escape("isoformat() must return str")):
            ujson.ujson_dumps(InvalidTime())

    def test_encode_to_utf8(self):
        unencoded = "\xe6\x97\xa5\xd1\x88"

        enc = ujson.ujson_dumps(unencoded, ensure_ascii=False)
        dec = ujson.ujson_loads(enc)

        assert enc == json.dumps(unencoded, ensure_ascii=False)
        assert dec == json.loads(enc)

    def test_decode_from_unicode(self):
        unicode_input = '{"obj": 31337}'

        dec1 = ujson.ujson_loads(unicode_input)
        dec2 = ujson.ujson_loads(str(unicode_input))

        assert dec1 == dec2

    def test_encode_recursion_max(self):
        # 8 is the max recursion depth

        class O2:
            member = 0

        class O1:
            member = 0

        decoded_input = O1()
        decoded_input.member = O2()
        decoded_input.member.member = decoded_input

        with pytest.raises(OverflowError, match="Maximum recursion level reached"):
            ujson.ujson_dumps(decoded_input)

    def test_decode_jibberish(self):
        jibberish = "fdsa sda v9sa fdsa"
        msg = "Unexpected character found when decoding 'false'"
        with pytest.raises(ValueError, match=msg):
            ujson.ujson_loads(jibberish)

    @pytest.mark.parametrize(
        "bad_input, expected_pos",
        [
            ("[1, 2,", 5),
            ('{"a": fzz}', 5),
            ("[[[true", 7),
        ],
    )
    def test_decode_error_includes_position(self, bad_input, expected_pos):
        with pytest.raises(ValueError, match=f"at position {expected_pos}$"):
            ujson.ujson_loads(bad_input)

    @pytest.mark.parametrize(
        "broken_json",
        [
            "[",  # Broken array start.
            "{",  # Broken object start.
            "]",  # Broken array end.
            "}",  # Broken object end.
        ],
    )
    def test_decode_broken_json(self, broken_json):
        msg = "Expected object or value"
        with pytest.raises(ValueError, match=msg):
            ujson.ujson_loads(broken_json)

    @pytest.mark.parametrize("too_big_char", ["[", "{"])
    def test_decode_depth_too_big(self, too_big_char):
        with pytest.raises(ValueError, match="Reached object decoding depth limit"):
            ujson.ujson_loads(too_big_char * (1024 * 1024))

    @pytest.mark.parametrize(
        "bad_string",
        [
            '"TESTING',  # Unterminated.
            '"TESTING\\"',  # Unterminated escape.
            "tru",  # Broken True.
            "fa",  # Broken False.
            "n",  # Broken None.
        ],
    )
    def test_decode_bad_string(self, bad_string):
        msg = "|".join(
            [
                "Unexpected character found when decoding",
                "Unmatched ''\"' when when decoding 'string'",
            ]
        )
        with pytest.raises(ValueError, match=msg):
            ujson.ujson_loads(bad_string)

    @pytest.mark.parametrize(
        "broken_json, err_msg",
        [
            (
                '{{1337:""}}',
                "Key name of object must be 'string' when decoding 'object'",
            ),
            ('{{"key":"}', "Unmatched ''\"' when when decoding 'string'"),
            ("[[[true", "Unexpected character found when decoding array value (2)"),
        ],
    )
    def test_decode_broken_json_leak(self, broken_json, err_msg):
        for _ in range(1000):
            with pytest.raises(ValueError, match=re.escape(err_msg)):
                ujson.ujson_loads(broken_json)

    @pytest.mark.parametrize(
        "invalid_dict",
        [
            "{{{{31337}}}}",  # No key.
            '{{{{"key":}}}}',  # No value.
            '{{{{"key"}}}}',  # No colon or value.
        ],
    )
    def test_decode_invalid_dict(self, invalid_dict):
        msg = "|".join(
            [
                "Key name of object must be 'string' when decoding 'object'",
                "No ':' found when decoding object value",
                "Expected object or value",
            ]
        )
        with pytest.raises(ValueError, match=msg):
            ujson.ujson_loads(invalid_dict)

    @pytest.mark.parametrize("numeric_int_as_str", ["31337", "-31337"])
    def test_decode_numeric_int(self, numeric_int_as_str):
        assert int(numeric_int_as_str) == ujson.ujson_loads(numeric_int_as_str)

    def test_encode_null_character(self):
        wrapped_input = "31337 \x00 1337"
        output = ujson.ujson_dumps(wrapped_input)

        assert wrapped_input == json.loads(output)
        assert output == json.dumps(wrapped_input)
        assert wrapped_input == ujson.ujson_loads(output)

        alone_input = "\x00"
        output = ujson.ujson_dumps(alone_input)

        assert alone_input == json.loads(output)
        assert output == json.dumps(alone_input)
        assert alone_input == ujson.ujson_loads(output)
        assert '"  \\u0000\\r\\n "' == ujson.ujson_dumps("  \u0000\r\n ")

    def test_decode_null_character(self):
        wrapped_input = '"31337 \\u0000 31337"'
        assert ujson.ujson_loads(wrapped_input) == json.loads(wrapped_input)

    def test_encode_list_long_conversion(self):
        long_input = [
            9223372036854775807,
            9223372036854775807,
            9223372036854775807,
            9223372036854775807,
            9223372036854775807,
            9223372036854775807,
        ]
        output = ujson.ujson_dumps(long_input)

        assert long_input == json.loads(output)
        assert long_input == ujson.ujson_loads(output)

    @pytest.mark.parametrize("long_input", [9223372036854775807, 18446744073709551615])
    def test_encode_long_conversion(self, long_input):
        output = ujson.ujson_dumps(long_input)

        assert long_input == json.loads(output)
        assert output == json.dumps(long_input)
        assert long_input == ujson.ujson_loads(output)

    @pytest.mark.parametrize("bigNum", [2**64, -(2**63) - 1])
    def test_dumps_ints_larger_than_maxsize(self, bigNum):
        encoding = ujson.ujson_dumps(bigNum)
        assert str(bigNum) == encoding

        with pytest.raises(
            ValueError,
            match="|".join(["Value is too big", "Value is too small"]),
        ):
            assert ujson.ujson_loads(encoding) == bigNum

    @pytest.mark.parametrize(
        "int_exp", ["1337E40", "1.337E40", "1337E+9", "1.337e+40", "1.337E-4"]
    )
    def test_decode_numeric_int_exp(self, int_exp):
        assert ujson.ujson_loads(int_exp) == json.loads(int_exp)

    def test_loads_non_str_bytes_raises(self):
        msg = "a bytes-like object is required, not 'NoneType'"
        with pytest.raises(TypeError, match=msg):
            ujson.ujson_loads(None)

    @pytest.mark.parametrize("val", [3590016419, 2**31, 2**32, (2**32) - 1])
    def test_decode_number_with_32bit_sign_bit(self, val):
        # Test that numbers that fit within 32 bits but would have the
        # sign bit set (2**31 <= x < 2**32) are decoded properly.
        doc = f'{{"id": {val}}}'
        assert ujson.ujson_loads(doc)["id"] == val

    def test_encode_big_escape(self):
        # Make sure no Exception is raised.
        base = "\u00e5".encode()
        escape_input = base * 1024 * 1024 * 2
        ujson.ujson_dumps(escape_input)

    def test_decode_big_escape(self):
        # Make sure no Exception is raised.
        for _ in range(10):
            base = "\u00e5".encode()
            quote = b'"'

            escape_input = quote + (base * 1024 * 1024 * 2) + quote
            ujson.ujson_loads(escape_input)

    def test_to_dict(self):
        d = {"key": 31337}

        class DictTest:
            def toDict(self):
                return d

        o = DictTest()
        output = ujson.ujson_dumps(o)

        dec = ujson.ujson_loads(output)
        assert dec == d

    def test_default_handler(self):
        class _TestObject:
            def __init__(self, val) -> None:
                self.val = val

            @property
            def recursive_attr(self):
                return _TestObject("recursive_attr")

            def __str__(self) -> str:
                return str(self.val)

        msg = "Maximum recursion level reached"
        with pytest.raises(OverflowError, match=msg):
            ujson.ujson_dumps(_TestObject("foo"))
        assert '"foo"' == ujson.ujson_dumps(_TestObject("foo"), default_handler=str)

        def my_handler(_):
            return "foobar"

        assert '"foobar"' == ujson.ujson_dumps(
            _TestObject("foo"), default_handler=my_handler
        )

        def my_handler_raises(_):
            raise TypeError("I raise for anything")

        with pytest.raises(TypeError, match="I raise for anything"):
            ujson.ujson_dumps(_TestObject("foo"), default_handler=my_handler_raises)

        def my_int_handler(_):
            return 42

        assert (
            ujson.ujson_loads(
                ujson.ujson_dumps(_TestObject("foo"), default_handler=my_int_handler)
            )
            == 42
        )

        def my_obj_handler(_):
            return datetime.datetime(2013, 2, 3)

        assert ujson.ujson_loads(
            ujson.ujson_dumps(datetime.datetime(2013, 2, 3))
        ) == ujson.ujson_loads(
            ujson.ujson_dumps(_TestObject("foo"), default_handler=my_obj_handler)
        )

        obj_list = [_TestObject("foo"), _TestObject("bar")]
        assert json.loads(json.dumps(obj_list, default=str)) == ujson.ujson_loads(
            ujson.ujson_dumps(obj_list, default_handler=str)
        )

    def test_encode_object(self):
        class _TestObject:
            def __init__(self, a, b, _c, d) -> None:
                self.a = a
                self.b = b
                self._c = _c
                self.d = d

            def e(self):
                return 5

        # JSON keys should be all non-callable non-underscore attributes, see GH-42768
        test_object = _TestObject(a=1, b=2, _c=3, d=4)
        assert ujson.ujson_loads(ujson.ujson_dumps(test_object)) == {
            "a": 1,
            "b": 2,
            "d": 4,
        }

    def test_encode_invalid_decimal_subclass(self):
        class InvalidDecimal(decimal.Decimal):
            def __format__(self, *a, **kw):
                return "\ud800"

        with pytest.raises(UnicodeEncodeError, match="surrogates not allowed"):
            ujson.ujson_dumps(InvalidDecimal(1))

    def test_encode_large_int_object(self):
        cur_limit = sys.get_int_max_str_digits()
        sys.set_int_max_str_digits(1000)
        try:
            with pytest.raises(ValueError, match="Exceeds the limit"):
                ujson.ujson_dumps(1 << 1_000_000)
        finally:
            sys.set_int_max_str_digits(cur_limit)

    def test_encode_invalid_int_subclass(self):
        class InvalidInt(int):
            def __str__(self):
                return "\ud800"

        with pytest.raises(UnicodeEncodeError, match="surrogates not allowed"):
            ujson.ujson_dumps(InvalidInt(1 << 1_000_000))

    def test_ujson__name__(self):
        # GH 52898
        assert ujson.__name__ == "pandas._libs._ujson"


class TestNumpyJSONTests:
    @pytest.mark.parametrize("bool_input", [True, False])
    def test_bool(self, bool_input):
        b = bool(bool_input)
        assert ujson.ujson_loads(ujson.ujson_dumps(b)) == b

    def test_bool_array(self):
        bool_array = np.array(
            [True, False, True, True, False, True, False, False], dtype=bool
        )
        output = np.array(ujson.ujson_loads(ujson.ujson_dumps(bool_array)), dtype=bool)
        tm.assert_numpy_array_equal(bool_array, output)

    def test_int(self, any_int_numpy_dtype):
        klass = np.dtype(any_int_numpy_dtype).type
        num = klass(1)

        assert klass(ujson.ujson_loads(ujson.ujson_dumps(num))) == num

    def test_int_array(self, any_int_numpy_dtype):
        arr = np.arange(100, dtype=int)
        arr_input = arr.astype(any_int_numpy_dtype)

        arr_output = np.array(
            ujson.ujson_loads(ujson.ujson_dumps(arr_input)), dtype=any_int_numpy_dtype
        )
        tm.assert_numpy_array_equal(arr_input, arr_output)

    def test_int_max(self, any_int_numpy_dtype):
        if any_int_numpy_dtype in ("int64", "uint64") and not IS64:
            pytest.skip("Cannot test 64-bit integer on 32-bit platform")

        klass = np.dtype(any_int_numpy_dtype).type
        num = klass(np.iinfo(any_int_numpy_dtype).max)

        assert klass(ujson.ujson_loads(ujson.ujson_dumps(num))) == num

    def test_float(self, float_numpy_dtype):
        klass = np.dtype(float_numpy_dtype).type
        num = klass(256.2013)

        assert klass(ujson.ujson_loads(ujson.ujson_dumps(num))) == num

    def test_float_array(self, float_numpy_dtype):
        arr = np.arange(12.5, 185.72, 1.7322, dtype=float)
        float_input = arr.astype(float_numpy_dtype)

        float_output = np.array(
            ujson.ujson_loads(ujson.ujson_dumps(float_input, double_precision=15)),
            dtype=float_numpy_dtype,
        )
        tm.assert_almost_equal(float_input, float_output)

    def test_float_max(self, float_numpy_dtype):
        klass = np.dtype(float_numpy_dtype).type
        num = klass(np.finfo(float_numpy_dtype).max / 10)

        tm.assert_almost_equal(
            klass(ujson.ujson_loads(ujson.ujson_dumps(num, double_precision=15))), num
        )

    def test_array_basic(self):
        arr = np.arange(96)
        arr = arr.reshape((2, 2, 2, 2, 3, 2))

        tm.assert_numpy_array_equal(
            np.array(ujson.ujson_loads(ujson.ujson_dumps(arr))), arr
        )

    @pytest.mark.parametrize("shape", [(10, 10), (5, 5, 4), (100, 1)])
    def test_array_reshaped(self, shape):
        arr = np.arange(100)
        arr = arr.reshape(shape)

        tm.assert_numpy_array_equal(
            np.array(ujson.ujson_loads(ujson.ujson_dumps(arr))), arr
        )

    def test_array_list(self):
        arr_list = [
            "a",
            [],
            {},
            {},
            [],
            42,
            97.8,
            ["a", "b"],
            {"key": "val"},
        ]
        arr = np.array(arr_list, dtype=object)
        result = np.array(ujson.ujson_loads(ujson.ujson_dumps(arr)), dtype=object)
        tm.assert_numpy_array_equal(result, arr)

    def test_array_float(self):
        dtype = np.float32

        arr = np.arange(100.202, 200.202, 1, dtype=dtype)
        arr = arr.reshape((5, 5, 4))

        arr_out = np.array(ujson.ujson_loads(ujson.ujson_dumps(arr)), dtype=dtype)
        tm.assert_almost_equal(arr, arr_out)

    def test_0d_array(self):
        # gh-18878
        msg = re.escape(
            "array(1) (numpy-scalar) is not JSON serializable at the moment"
        )
        with pytest.raises(TypeError, match=msg):
            ujson.ujson_dumps(np.array(1))

    def test_array_long_double(self):
        msg = re.compile(
            "1234.5.* \\(numpy-scalar\\) is not JSON serializable at the moment"
        )
        with pytest.raises(TypeError, match=msg):
            ujson.ujson_dumps(np.longdouble(1234.5))


class TestPandasJSONTests:
    def test_dataframe(self, orient):
        dtype = np.int64

        df = pd.DataFrame(
            [[1, 2, 3], [4, 5, 6]],
            index=["a", "b"],
            columns=["x", "y", "z"],
            dtype=dtype,
        )
        encode_kwargs = {} if orient is None else {"orient": orient}
        assert (df.dtypes == dtype).all()

        output = ujson.ujson_loads(ujson.ujson_dumps(df, **encode_kwargs))
        assert (df.dtypes == dtype).all()

        # Ensure proper DataFrame initialization.
        if orient == "split":
            dec = _clean_dict(output)
            output = pd.DataFrame(**dec)
        else:
            output = pd.DataFrame(output)

        # Corrections to enable DataFrame comparison.
        if orient == "values":
            df.columns = [0, 1, 2]
            df.index = [0, 1]
        elif orient == "records":
            df.index = [0, 1]
        elif orient == "index":
            df = df.transpose()

        assert (df.dtypes == dtype).all()
        tm.assert_frame_equal(output, df)

    def test_dataframe_nested(self, orient):
        df = pd.DataFrame(
            [[1, 2, 3], [4, 5, 6]], index=["a", "b"], columns=["x", "y", "z"]
        )

        nested = {"df1": df, "df2": df.copy()}
        kwargs = {} if orient is None else {"orient": orient}

        exp = {
            "df1": ujson.ujson_loads(ujson.ujson_dumps(df, **kwargs)),
            "df2": ujson.ujson_loads(ujson.ujson_dumps(df, **kwargs)),
        }
        assert ujson.ujson_loads(ujson.ujson_dumps(nested, **kwargs)) == exp

    def test_series(self, orient):
        dtype = np.int64
        s = pd.Series(
            [10, 20, 30, 40, 50, 60],
            name="series",
            index=[6, 7, 8, 9, 10, 15],
            dtype=dtype,
        ).sort_values()
        assert s.dtype == dtype

        encode_kwargs = {} if orient is None else {"orient": orient}

        output = ujson.ujson_loads(ujson.ujson_dumps(s, **encode_kwargs))
        assert s.dtype == dtype

        if orient == "split":
            dec = _clean_dict(output)
            output = pd.Series(**dec)
        else:
            output = pd.Series(output)

        if orient in (None, "index"):
            s.name = None
            output = output.sort_values()
            s.index = ["6", "7", "8", "9", "10", "15"]
        elif orient in ("records", "values"):
            s.name = None
            s.index = [0, 1, 2, 3, 4, 5]

        assert s.dtype == dtype
        tm.assert_series_equal(output, s)

    def test_series_nested(self, orient):
        s = pd.Series(
            [10, 20, 30, 40, 50, 60], name="series", index=[6, 7, 8, 9, 10, 15]
        ).sort_values()
        nested = {"s1": s, "s2": s.copy()}
        kwargs = {} if orient is None else {"orient": orient}

        exp = {
            "s1": ujson.ujson_loads(ujson.ujson_dumps(s, **kwargs)),
            "s2": ujson.ujson_loads(ujson.ujson_dumps(s, **kwargs)),
        }
        assert ujson.ujson_loads(ujson.ujson_dumps(nested, **kwargs)) == exp

    def test_index(self):
        i = pd.Index([23, 45, 18, 98, 43, 11], name="index")

        # Column indexed.
        output = pd.Index(ujson.ujson_loads(ujson.ujson_dumps(i)), name="index")
        tm.assert_index_equal(i, output)

        dec = _clean_dict(ujson.ujson_loads(ujson.ujson_dumps(i, orient="split")))
        output = pd.Index(**dec)

        tm.assert_index_equal(i, output)
        assert i.name == output.name

        tm.assert_index_equal(i, output)
        assert i.name == output.name

        output = pd.Index(
            ujson.ujson_loads(ujson.ujson_dumps(i, orient="values")), name="index"
        )
        tm.assert_index_equal(i, output)

        output = pd.Index(
            ujson.ujson_loads(ujson.ujson_dumps(i, orient="records")), name="index"
        )
        tm.assert_index_equal(i, output)

        output = pd.Index(
            ujson.ujson_loads(ujson.ujson_dumps(i, orient="index")), name="index"
        )
        tm.assert_index_equal(i, output)

    def test_datetime_index(self):
        date_unit = "ns"

        # freq doesn't round-trip
        rng = pd.DatetimeIndex(
            list(pd.date_range("1/1/2000", periods=20, unit="ns")), freq=None
        )
        encoded = ujson.ujson_dumps(rng, date_unit=date_unit)

        decoded = pd.DatetimeIndex(np.array(ujson.ujson_loads(encoded)))
        tm.assert_index_equal(rng, decoded)

        ts = pd.Series(np.random.default_rng(2).standard_normal(len(rng)), index=rng)
        decoded = pd.Series(
            ujson.ujson_loads(ujson.ujson_dumps(ts, date_unit=date_unit))
        )

        idx_values = decoded.index.values.astype(np.int64)
        decoded.index = pd.DatetimeIndex(idx_values)
        tm.assert_series_equal(ts, decoded)

    @pytest.mark.parametrize(
        "invalid_arr",
        [
            "[31337,]",  # Trailing comma.
            "[,31337]",  # Leading comma.
            "[]]",  # Unmatched bracket.
            "[,]",  # Only comma.
        ],
    )
    def test_decode_invalid_array(self, invalid_arr):
        msg = "|".join(
            [
                "Expected object or value",
                "Trailing data",
                "Unexpected character found when decoding array value",
            ]
        )
        with pytest.raises(ValueError, match=msg):
            ujson.ujson_loads(invalid_arr)

    @pytest.mark.parametrize("arr", [[], [31337]])
    def test_decode_array(self, arr):
        assert arr == ujson.ujson_loads(str(arr))

    @pytest.mark.parametrize("extreme_num", [9223372036854775807, -9223372036854775808])
    def test_decode_extreme_numbers(self, extreme_num):
        assert extreme_num == ujson.ujson_loads(str(extreme_num))

    @pytest.mark.parametrize("too_extreme_num", [f"{2**64}", f"{-(2**63) - 1}"])
    def test_decode_too_extreme_numbers(self, too_extreme_num):
        with pytest.raises(
            ValueError,
            match="|".join(["Value is too big", "Value is too small"]),
        ):
            ujson.ujson_loads(too_extreme_num)

    def test_decode_with_trailing_whitespaces(self):
        assert {} == ujson.ujson_loads("{}\n\t ")

    def test_decode_with_trailing_non_whitespaces(self):
        with pytest.raises(ValueError, match="Trailing data"):
            ujson.ujson_loads("{}\n\t a")

    @pytest.mark.parametrize("value", [f"{2**64}", f"{-(2**63) - 1}"])
    def test_decode_array_with_big_int(self, value):
        with pytest.raises(
            ValueError,
            match="|".join(["Value is too big", "Value is too small"]),
        ):
            ujson.ujson_loads(value)

    @pytest.mark.parametrize(
        "float_number",
        [
            1.1234567893,
            1.234567893,
            1.34567893,
            1.4567893,
            1.567893,
            1.67893,
            1.7893,
            1.893,
            1.3,
        ],
    )
    @pytest.mark.parametrize("sign", [-1, 1])
    def test_decode_floating_point(self, sign, float_number):
        float_number *= sign
        tm.assert_almost_equal(
            float_number, ujson.ujson_loads(str(float_number)), rtol=1e-15
        )

    def test_encode_big_set(self):
        s = set(range(100000))
        # Make sure no Exception is raised.
        ujson.ujson_dumps(s)

    def test_encode_empty_set(self):
        assert "[]" == ujson.ujson_dumps(set())

    def test_encode_set(self):
        s = {1, 2, 3, 4, 5, 6, 7, 8, 9}
        enc = ujson.ujson_dumps(s)
        dec = ujson.ujson_loads(enc)

        for v in dec:
            assert v in s

    @pytest.mark.parametrize(
        "td",
        [
            pd.Timedelta(days=366),
            pd.Timedelta(days=-1),
            pd.Timedelta(hours=13, minutes=5, seconds=5),
            pd.Timedelta(hours=13, minutes=20, seconds=30),
            pd.Timedelta(days=-1, nanoseconds=5),
            pd.Timedelta(nanoseconds=1),
            pd.Timedelta(microseconds=1, nanoseconds=1),
            pd.Timedelta(milliseconds=1, microseconds=1, nanoseconds=1),
            pd.Timedelta(milliseconds=999, microseconds=999, nanoseconds=999),
        ],
    )
    def test_encode_timedelta_iso(self, td):
        # GH 28256
        result = ujson.ujson_dumps(td, iso_dates=True)
        expected = f'"{td.isoformat()}"'

        assert result == expected

    def test_encode_periodindex(self):
        # GH 46683
        p = pd.PeriodIndex(["2022-04-06", "2022-04-07"], freq="D")
        df = pd.DataFrame(index=p)
        assert df.to_json() == "{}"


LONE_SURROGATE = "\ud800"


class _LoneSurrogateStr:
    def __str__(self) -> str:
        return LONE_SURROGATE

    def __hash__(self) -> int:
        return 1


def test_encode_dict_key_lone_surrogate():
    # GH#66356
    with pytest.raises(UnicodeEncodeError, match="surrogates not allowed"):
        ujson.ujson_dumps({LONE_SURROGATE: "value"})


def test_encode_dict_key_str_lone_surrogate():
    # GH#66356
    with pytest.raises(UnicodeEncodeError, match="surrogates not allowed"):
        ujson.ujson_dumps({_LoneSurrogateStr(): "value"})


def test_encode_dict_key_large_int():
    # GH#66356
    # the key has more digits than sys.get_int_max_str_digits() allows, so
    # str() on it raises. Set the limit explicitly so the test does not depend
    # on the interpreter default (PYTHONINTMAXSTRDIGITS can disable it).
    cur_limit = sys.get_int_max_str_digits()
    sys.set_int_max_str_digits(1000)
    try:
        with pytest.raises(ValueError, match="Exceeds the limit"):
            ujson.ujson_dumps({1 << 1_000_000: "value"})
    finally:
        sys.set_int_max_str_digits(cur_limit)


def test_encode_object_dir_raises():
    # GH#66356
    class _TestObj:
        def __dir__(self):
            raise TypeError("I raise you one exception")

    with pytest.raises(TypeError, match="I raise you one exception"):
        ujson.ujson_dumps(_TestObj())


def test_encode_object_dir_lone_surrogate():
    # GH#66356
    class _TestObj:
        def __dir__(self):
            return [LONE_SURROGATE]

    with pytest.raises(UnicodeEncodeError, match="surrogates not allowed"):
        ujson.ujson_dumps(_TestObj())


def test_encode_object_dir_non_str():
    # GH#66356
    class _TestObj:
        def __dir__(self):
            return [42]

    with pytest.raises(TypeError, match="must return str entries, not int"):
        ujson.ujson_dumps(_TestObj())


def test_encode_set_iter_raises():
    # GH#66356
    class _TestSet(set):
        def __iter__(self):
            raise TypeError("I raise you one exception")

    with pytest.raises(TypeError, match="I raise you one exception"):
        ujson.ujson_dumps(_TestSet([1, 2, 3]))


def test_encode_labels_lone_surrogate():
    # GH#66356
    df = pd.DataFrame({_LoneSurrogateStr(): [1, 2, 3]})
    with pytest.raises(UnicodeEncodeError, match="surrogates not allowed"):
        ujson.ujson_dumps(df)


def test_encode_set_iter_raises_midway():
    # GH#66489
    class _TestSet(set):
        def __iter__(self):
            yield 1
            raise TypeError("I raise you one exception")

    with pytest.raises(TypeError, match="I raise you one exception"):
        ujson.ujson_dumps([_TestSet([1, 2]), _TestSet([1, 2])])


class _Plain:
    attr = 1


class _OrderedSet(set):
    # a plain one-element set would be exhausted before the guard in
    # Set_iterNext is reached, so drive the iteration order explicitly
    def __iter__(self):
        return iter([np.complex128(1 + 2j), _Plain()])


@pytest.mark.parametrize(
    "container",
    [
        [np.complex128(1 + 2j), _Plain()],
        (np.complex128(1 + 2j), _Plain()),
        {"first": np.complex128(1 + 2j), "second": _Plain()},
        _OrderedSet([1, 2]),
    ],
)
def test_encode_error_not_masked_by_sibling(container):
    # GH#66356
    # the plain object encoded after the unserializable one clears the pending
    # exception while looking for a to_dict/__json__ attribute, so iteration
    # has to stop instead; otherwise ujson_dumps returned invalid JSON such as
    # '[,{"attr":1}]' without raising
    with pytest.raises(TypeError, match="is not JSON serializable"):
        ujson.ujson_dumps(container)


def test_to_json_error_not_masked_by_sibling():
    # GH#66356
    ser = pd.Series([[np.complex128(1 + 2j), _Plain()]])
    with pytest.raises(TypeError, match="is not JSON serializable"):
        ser.to_json()


def _deeply_nested():
    outer = current = []
    for _ in range(2000):
        nested = []
        current.append(nested)
        current = nested
    return outer


@pytest.mark.parametrize(
    "bad, error",
    [
        # raises a Python exception
        (np.complex128(1j), TypeError),
        # sets the encoder's own errorMsg instead
        (_deeply_nested(), OverflowError),
    ],
)
def test_encode_failure_frees_grown_buffer(bad, error):
    # GH#66356
    # the two failure modes leave the encoder by different exits, and both
    # leaked the output buffer once it had outgrown the one the encoder starts
    # with. The buffer is allocated with PyObject_Malloc, so tracemalloc sees
    # it; a leak here would be ~50 * 256 KiB.
    payload = ["x" * 1000] * 200 + [bad]
    tracemalloc.start()
    before = tracemalloc.take_snapshot()
    for _ in range(50):
        with pytest.raises(error):
            ujson.ujson_dumps(payload)
    after = tracemalloc.take_snapshot()
    tracemalloc.stop()

    grew = sum(stat.size_diff for stat in after.compare_to(before, "filename"))
    assert grew < 100_000


def test_to_json_bad_label_frees_values():
    # GH#66356
    # the labels failed to encode, so Object_beginTypeContext bails out via
    # JT_INVALID; that exit used to drop the reference it had taken on the
    # values array
    index = pd.Index([_LoneSurrogateStr()], dtype=object)
    ser = pd.Series(np.arange(1), index=index)
    values = ser._mgr.blocks[0].values
    # the encoder reaches the values through _values_for_json; if that ever
    # starts handing back a fresh object the check below would pass vacuously
    assert ser.array._values_for_json() is values

    ref = weakref.ref(values)
    with pytest.raises(UnicodeEncodeError):
        ser.to_json()

    del ser, values
    # a reference held by the failed encode would keep this alive
    assert ref() is None


@pytest.mark.parametrize("indent", [1, 2, 4, 8])
@pytest.mark.parametrize(
    "wrap", [lambda value: [value], lambda value: {"k": value}], ids=["list", "dict"]
)
def test_indent_deeply_nested_does_not_overflow_buffer(indent, wrap):
    # GH#67929
    # encode() reserves a fixed 256 bytes per frame, but the writes a frame
    # makes after its recursive encode() calls -- the indent, the separator and
    # the closing bracket -- are not covered by that reservation, so deep
    # nesting wrote past the output buffer. Arrays and objects have their own
    # copies of those writes. Which depths overflow depends on where the writes
    # land relative to the end of the buffer, so sweep.
    for depth in range(2, 400):
        nested = [1]
        for _ in range(depth):
            nested = wrap(nested)

        result = ujson.ujson_dumps(nested, indent=indent)

        assert json.loads(result) == nested, depth


@pytest.mark.parametrize("indent", [0, 1, 2])
def test_indent_past_recursion_max_raises(indent):
    # GH#67929
    # nesting past recursionMax unwinds ~1024 frames with the error already
    # set. Each of those frames still wrote its indent -- half a megabyte of
    # spaces in all -- so the OverflowError came back as a crash instead.
    # indent=0 is the control: one bracket per frame stayed in bounds.
    nested = [1]
    for _ in range(1100):
        nested = [nested]

    with pytest.raises(OverflowError, match="Maximum recursion level reached"):
        ujson.ujson_dumps(nested, indent=indent)


def test_dumps_string_filling_buffer_does_not_overflow():
    # GH#67929
    # a string whose escaped form exactly fills the reservation it asked for
    # leaves the enclosing array with no room for the separator and closing
    # bracket that follow it. "\x01" escapes to six bytes, so "\x01" * 100 is an
    # exact fit; the leading elements put it near the end of the output buffer,
    # and the two sweeps walk it across the boundary a byte at a time.
    for nfill in range(1505, 1515):
        for pad in range(46):
            doc = ["a" * 40] * nfill + ["a" * pad] + ["\x01" * 100] + ["z"] * 2000

            result = ujson.ujson_dumps(doc)

            assert json.loads(result) == doc, (nfill, pad)


def test_to_json_indent_deeply_nested_does_not_overflow_buffer():
    # GH#67929
    # the same overflow, reachable from the public API through an object-dtype
    # column holding a deeply nested value
    nested = [1]
    for _ in range(300):
        nested = [nested]
    frame = pd.DataFrame({"c": [nested]})

    result = frame.to_json(indent=2)

    assert json.loads(result)["c"]["0"] == nested


def test_to_json_datetime64_scalar_frees_dtype():
    # GH#67930
    # Object_beginTypeContext took a reference on the scalar's dtype via
    # PyArray_DescrFromScalar and never gave it back. The builtin descrs are
    # immortal so most scalar types hid this, but a datetime64 descr carries
    # unit metadata and so is a freshly allocated, mortal object every call.
    # The encoder is called directly so that a leak of ~160 bytes per call
    # stands out against the allocations the loop itself makes.
    payload = [np.datetime64("2020-01-01T00:00:00", "s")]

    tracemalloc.start()
    before = tracemalloc.take_snapshot()
    for _ in range(20_000):
        ujson.ujson_dumps(payload, iso_dates=True)
    after = tracemalloc.take_snapshot()
    tracemalloc.stop()

    grew = sum(stat.size_diff for stat in after.compare_to(before, "filename"))
    # a leak here would be ~3.2 MiB over this loop
    assert grew < 1_000_000
