Replace isort, flake8, pycln and pyupgrade by ruff. [Ruff](https://github.com/charliermarsh/ruff) is getting popular lately, and I really like the idea of having a single tool for Python lining instead of N which currently exist. Also written in Rust which is a good use case for this.

 It's supposed to have equivalent functionality, to the ones we are using. The fact that major projects are adopting it is a good sign (see list in their readme).  There are also a few extra options in their readme that look interesting and that we could enable (without having to install yet another plugin)

We won't gain much speed-wise: previously isort, flake8 (which itself is an abstraction over pycodestyle and pyflakes), pycln and pyupgrade  on all files was around 1s, and now it's 20ms. But with mypy taking 10s, it makes mostly no difference.
Still it makes the overall linting configuration easier to maintain.


If we go forward with this, I imagine a changelog is not needed? 

# See also test_pyproxy, test_jsproxy, and test_python.
from typing import Any

import pytest
from hypothesis import example, given, settings
from hypothesis import strategies as st
from hypothesis.strategies import text
from pytest_pyodide import run_in_pyodide
from pytest_pyodide.fixture import selenium_context_manager
from pytest_pyodide.hypothesis import (
    any_equal_to_self_strategy,
    any_strategy,
    std_hypothesis_settings,
)


@given(s=text())
@settings(deadline=10000)
@example("\ufeff")
def test_string_conversion(selenium_module_scope, s):
    @run_in_pyodide
    def main(selenium, sbytes):
        from pyodide.code import run_js

        run_js("self.encoder = new TextEncoder()")
        run_js("self.decoder = new TextDecoder('utf8', {ignoreBOM: true})")

        spy = bytes(sbytes).decode()
        sjs = run_js(
            """
            (sbytes) => {
                self.sjs = self.decoder.decode(new Uint8Array(sbytes));
                return sjs;
            }
            """
        )(sbytes)
        assert sjs == spy
        assert run_js("(spy) => spy === self.sjs")(spy)

    with selenium_context_manager(selenium_module_scope) as selenium:
        sbytes = list(s.encode())
        main(selenium, sbytes)


@given(s=text())
@std_hypothesis_settings
@example("\ufeff")
@run_in_pyodide
def test_string_conversion2(selenium, s):
    from pyodide.code import run_js

    run_js("self.encoder = new TextEncoder()")
    run_js("self.decoder = new TextDecoder('utf8', {ignoreBOM: true})")

    s_encoded = s.encode()
    sjs = run_js(
        """
        (s_encoded) => {
            let buf = s_encoded.getBuffer();
            self.sjs = self.decoder.decode(buf.data);
            buf.release();
            return sjs
        }
        """
    )(s_encoded)
    assert sjs == s
    assert run_js("""(spy) => spy === self.sjs""")(s)


def blns():
    import base64
    import json

    with open("./src/tests/blns.base64.json") as f:
        BLNS = json.load(f)
    for s in BLNS:
        yield base64.b64decode(s).decode(errors="ignore")


@pytest.mark.driver_timeout(60)
def test_string_conversion_blns(selenium):
    @run_in_pyodide
    def _string_conversion_blns_internal(selenium, s):
        from pyodide.code import run_js

        run_js("self.encoder = new TextEncoder()")
        run_js("self.decoder = new TextDecoder('utf8', {ignoreBOM: true})")

        s_encoded = s.encode()
        sjs = run_js(
            """
            (s_encoded) => {
                let buf = s_encoded.getBuffer();
                self.sjs = self.decoder.decode(buf.data);
                buf.release();
                return sjs
            }
            """
        )(s_encoded)
        assert sjs == s
        assert run_js("""(spy) => spy === self.sjs""")(s)

    strings = blns()
    for s in strings:
        _string_conversion_blns_internal(selenium, s)


@run_in_pyodide
def test_large_string_conversion(selenium):
    from pyodide.code import run_js

    longstr = run_js('"ab".repeat(200_000)')
    res = longstr.count("ab")
    assert res == 200_000
    run_js(
        """
        (s) => {
            assert(() => s.length === 40_000);
            for(let n = 0; n < 20_000; n++){
                assert(() => s.slice(2*n, 2*n+2) === "ab");
            }
        }
        """
    )("ab" * 20_000)


@given(
    n=st.one_of(
        st.integers(),
        st.floats(allow_nan=False),
    )
)
@std_hypothesis_settings
@example(2**53)
@example(2**53 - 1)
@example(2**53 + 1)
@example(-(2**53))
@example(-(2**53) - 1)
@example(-(2**53) + 1)
@run_in_pyodide
def test_number_conversions(selenium_module_scope, n):
    import json

    from pyodide.code import run_js

    x_js = run_js("(s) => self.x_js = eval(s)")(json.dumps(n))
    run_js("(x_py) => Number(x_py) === x_js")(n)

    if type(x_js) is float:
        assert x_js == float(n)
    else:
        assert x_js == n


@given(n=st.floats())
@std_hypothesis_settings
@run_in_pyodide
def test_number_conversions_2(selenium_module_scope, n):
    from pyodide.code import run_js

    assert run_js("(n) => typeof n")(n) == "number"
    from math import isinf, isnan

    if isnan(n):
        return
    import json

    n_js = run_js("(s) => eval(s)")(json.dumps(n))
    if not isinf(n) and float(int(n)) == n and -(2**53) < n < 2**53:
        assert isinstance(n_js, int)
    else:
        assert isinstance(n_js, float)


@given(n=st.integers())
@std_hypothesis_settings
@example(2**53)
@example(2**53 - 1)
@example(2**53 + 1)
@example(-(2**53))
@example(-(2**53) - 1)
@example(-(2**53) + 1)
@run_in_pyodide
def test_number_conversions_3(selenium_module_scope, n):
    from pyodide.code import run_js

    jsty = run_js("(n) => typeof n")(n)
    if -(2**53) + 1 < n < 2**53 - 1:
        assert jsty == "number"
    else:
        assert jsty == "bigint"
    import json

    n_js = run_js("(s) => eval(s)")(json.dumps(n))
    if -(2**53) < n < 2**53:
        assert isinstance(n_js, int)
    else:
        assert isinstance(n_js, float)


@run_in_pyodide
def test_nan_conversions(selenium):
    from pyodide.code import run_js

    jsnan = run_js("NaN")
    from math import isnan

    assert isnan(jsnan)
    assert run_js(
        """
        let mathmod = pyodide.pyimport("math");
        const res = Number.isNaN(mathmod.nan);
        mathmod.destroy();
        res
        """
    )


@given(n=st.integers())
@std_hypothesis_settings
def test_bigint_conversions(selenium_module_scope, n):
    with selenium_context_manager(selenium_module_scope) as selenium:
        h = hex(n)
        selenium.run_js(f"self.h = {h!r};")
        selenium.run_js(
            """
            let negative = false;
            let h2 = h;
            if(h2.startsWith('-')){
                h2 = h2.slice(1);
                negative = true;
            }
            self.n = BigInt(h2);
            if(negative){
                self.n = -n;
            }
            pyodide.runPython(`
                from js import n, h
                n2 = int(h, 16)
                assert n == n2
            `);
            let n2 = pyodide.globals.get("n2");
            let n3 = Number(n2);
            if(Number.isSafeInteger(n3)){
                assert(() => typeof n2 === "number");
                assert(() => n2 === Number(n));
            } else {
                assert(() => typeof n2 === "bigint");
                assert(() => n2 === n);
            }
            """
        )


@given(
    n=st.one_of(
        st.integers(min_value=2**53 + 1),
        st.integers(max_value=-(2**53) - 1),
    )
)
@std_hypothesis_settings
def test_big_int_conversions2(selenium_module_scope, n):
    @run_in_pyodide
    def main(selenium, s):
        import json

        from pyodide.code import run_js

        x_py = json.loads(s)
        x_js, check = run_js(
            """
            (s, x_py) => {
                let x_js = eval(s + 'n');

                return [x_js, x_py === x_js];
            }
            """
        )(s, x_py)
        assert check
        assert x_js == x_py

    with selenium_context_manager(selenium_module_scope) as selenium:
        import json

        s = json.dumps(n)
        main(selenium, s)


@given(
    n=st.integers(),
    exp=st.integers(min_value=1, max_value=10),
)
@std_hypothesis_settings
def test_big_int_conversions3(selenium_module_scope, n, exp):
    @run_in_pyodide
    def main(selenium, s):
        import json

        from pyodide.code import run_js

        x_py = json.loads(s)
        x_js = run_js(
            f"""
            self.x_js = eval('{s}n'); // JSON.parse apparently doesn't work
            """
        )
        [x1, x2] = run_js(
            """
            (x_py) => [x_py.toString(), x_js.toString()]
            """
        )(x_py)
        assert x1 == x2

        check = run_js(
            """
            (x) => {
                const [a, b] = x.toJs();
                return a === b;
            }
            """
        )([str(x_js), str(x_py)])
        assert check

    with selenium_context_manager(selenium_module_scope) as selenium:
        val = 2 ** (32 * exp) - n
        import json

        s = json.dumps(val)
        main(selenium, s)


@given(obj=any_equal_to_self_strategy)
@std_hypothesis_settings
@run_in_pyodide
def test_hyp_py2js2py(selenium, obj):
    import __main__

    from pyodide.code import run_js

    __main__.obj = obj

    try:
        run_js('self.obj2 = pyodide.globals.get("obj"); 0;')
        from js import obj2  # type:ignore[attr-defined]

        assert obj2 == obj
        run_js(
            """
            if(self.obj2 && self.obj2.destroy){
                self.obj2.destroy();
            }
            delete self.obj2
            """
        )
    finally:
        del __main__.obj


@given(obj=any_equal_to_self_strategy)
@std_hypothesis_settings
@run_in_pyodide
def test_hyp_py2js2py_2(selenium, obj):
    import __main__

    from pyodide.code import run_js

    __main__.o = obj
    try:
        assert obj == run_js("pyodide.globals.get('o')")
    finally:
        del __main__.o


@pytest.mark.parametrize("a", [9992361673228537, -9992361673228537])
@run_in_pyodide
def test_big_integer_py2js2py(selenium, a):
    import __main__

    from pyodide.code import run_js

    __main__.a = a
    try:
        b = run_js("pyodide.globals.get('a')")
        assert a == b
    finally:
        del __main__.a


# Generate an object of any type
@pytest.mark.skip_refcount_check
@pytest.mark.skip_pyproxy_check
@given(obj=any_strategy)
@std_hypothesis_settings
@run_in_pyodide
def test_hyp_tojs_no_crash(selenium, obj):
    import __main__

    from pyodide.code import run_js

    __main__.x = obj
    try:
        run_js(
            """
            let x = pyodide.globals.get("x");
            if(x && x.toJs){
                x.toJs();
            }
            """
        )
    finally:
        del __main__.x


@pytest.mark.parametrize(
    "py,js",
    [
        (None, "undefined"),
        (True, "true"),
        (False, "false"),
        (42, "42"),
        (3.14, "3.14"),
        # Need to test all three internal string representations in Python:
        # UCS1, UCS2 and UCS4
        ("ascii", "'ascii'"),
        ("ιωδιούχο", "'ιωδιούχο'"),
        ("碘化物", "'碘化物'"),
        ("🐍", "'🐍'"),
    ],
)
@run_in_pyodide
def test_python2js1(selenium, py, js):
    from pyodide.code import run_js

    assert run_js(
        f"""
        (py) => py === {js}
        """
    )(py)


@run_in_pyodide
def test_python2js2(selenium):
    from pyodide.code import run_js

    assert (
        list(
            run_js(
                """
                (x) => {
                    x = x.toJs();
                    return [x.constructor.name, x.length, x[0]];
                }
                """
            )(b"bytes")
        )
        == ["Uint8Array", 5, 98]
    )


@run_in_pyodide
def test_python2js3(selenium):
    from pyodide.code import run_js

    l = [7, 9, 13]
    result = run_js(
        """
        (proxy) => {
            x = proxy.toJs();
            return [proxy.type, x.constructor.name, x.length, x[0], x[1], x[2]]
        }
        """
    )(l)
    assert list(result) == ["list", "Array", 3, *l]


@run_in_pyodide
def test_python2js4(selenium):
    from pyodide.code import run_js

    assert (
        list(
            run_js(
                """
                (proxy) => {
                    let typename = proxy.type;
                    let x = proxy.toJs();
                    return [proxy.type, x.constructor.name, x.get(42)];
                }
                """
            )({42: 64})
        )
        == ["dict", "Map", 64]
    )


@run_in_pyodide
def test_python2js5(selenium):
    from pyodide.code import run_js

    assert run_js("(x) => x.tell()")(open("/foo.txt", "wb")) == 0

    from tempfile import TemporaryFile

    with TemporaryFile(mode="w+") as f:
        contents = ["a\n", "b\n", "hello there!\n"]
        f.writelines(contents)
        assert run_js("(f) => f.tell()")(f) == 17

        assert (
            run_js(
                """
                (f) => {
                    f.seek(0);
                    return [f.readline(), f.readline(), f.readline()];
                }
                """
            )(f).to_py()
            == contents
        )


def test_python2js_track_proxies(selenium):
    selenium.run_js(
        """
        let x = pyodide.runPython(`
            class T:
                pass
            [[T()],[T()], [[[T()],[T()]],[T(), [], [[T()]], T()], T(), T()], T()]
        `);
        let proxies = [];
        let result = x.toJs({ pyproxies : proxies });
        assert(() => proxies.length === 10);
        for(let x of proxies){
            x.destroy();
        }
        function check(l){
            for(let x of l){
                if(x instanceof pyodide.ffi.PyProxy){
                    assert(() => x.$$.ptr === 0);
                } else {
                    check(x);
                }
            }
        }
        check(result);
        assertThrows(() => x.toJs({create_pyproxies : false}), "PythonError", "pyodide.ffi.ConversionError");
        x.destroy();
        """
    )


@run_in_pyodide
def test_wrong_way_track_proxies(selenium):
    from pyodide.code import run_js

    checkDestroyed = run_js(
        """
        function checkDestroyed(l){
            for(let e of l){
                if(e instanceof pyodide.ffi.PyProxy){
                    console.log("\\n\\n", "!!!!!!!!!", e.$$.ptr);
                    assert(() => e.$$.ptr === 0);
                } else {
                    checkDestroyed(e);
                }
            }
        };
        checkDestroyed
        """
    )
    from unittest import TestCase

    from js import Array, Object
    from pyodide.ffi import ConversionError, destroy_proxies, to_js

    raises = TestCase().assertRaises

    class T:
        pass

    x = [[T()], [T()], [[[T()], [T()]], [T(), [], [[T()]], T()], T(), T()], T()]
    proxylist = Array.new()
    r = to_js(x, pyproxies=proxylist)
    assert len(proxylist) == 10
    destroy_proxies(proxylist)
    checkDestroyed(r)
    with raises(TypeError):
        to_js(x, pyproxies=[])  # type:ignore[call-overload]
    with raises(TypeError):
        to_js(x, pyproxies=Object.new())
    with raises(ConversionError):
        to_js(x, create_pyproxies=False)


def test_wrong_way_conversions1(selenium):
    selenium.run_js(
        """
        assert(() => pyodide.toPy(5) === 5);
        assert(() => pyodide.toPy(5n) === 5n);
        assert(() => pyodide.toPy("abc") === "abc");
        class Test {};
        let t = new Test();
        assert(() => pyodide.toPy(t) === t);

        self.a1 = [1,2,3];
        self.b1 = pyodide.toPy(a1);
        self.a2 = { a : 1, b : 2, c : 3};
        self.b2 = pyodide.toPy(a2);
        pyodide.runPython(`
            from js import a1, b1, a2, b2
            assert a1.to_py() == b1
            assert a2.to_py() == b2
        `);
        self.b1.destroy();
        self.b2.destroy();
        """
    )


@run_in_pyodide
def test_wrong_way_conversions2(selenium):
    from pyodide.code import run_js
    from pyodide.ffi import to_js

    [astr, bstr] = run_js(
        """
        (a) => {
            b = [1,2,3];
            return [JSON.stringify(a), JSON.stringify(b)]
        }
        """
    )(to_js([1, 2, 3]))
    assert astr == bstr


@run_in_pyodide
def test_wrong_way_conversions3(selenium):
    from pyodide.code import run_js
    from pyodide.ffi import to_js

    class Test:
        pass

    t1 = Test()
    t2 = to_js(t1)
    t3 = run_js("(t2) => t2.copy()")(t2)

    assert t1 is t3
    t2.destroy()


@run_in_pyodide
def test_wrong_way_conversions4(selenium):
    from pyodide.ffi import to_js

    s = "avafhjpa"
    t = 55
    assert to_js(s) is s
    assert to_js(t) is t


@run_in_pyodide
def test_dict_converter1(selenium):
    import json

    from pyodide.code import run_js
    from pyodide.ffi import to_js

    arrayFrom = run_js("Array.from")
    d = {x: x + 2 for x in range(5)}
    res = to_js(d, dict_converter=arrayFrom)
    constructor, serialized = run_js(
        """
        (res) => {
            return [res.constructor.name, JSON.stringify(res)];
        }
        """
    )(res)

    assert constructor == "Array"
    assert json.loads(serialized) == [list(x) for x in d.items()]


@run_in_pyodide
def test_dict_converter2(selenium):
    import json

    from pyodide.code import run_js

    d = {x: x + 2 for x in range(5)}
    constructor, serialized = run_js(
        """
        (d) => {
            const res = d.toJs({dict_converter : Array.from});
            return [res.constructor.name, JSON.stringify(res)];
        }
        """
    )(d)

    assert constructor == "Array"
    assert json.loads(serialized) == [list(x) for x in d.items()]


@run_in_pyodide
def test_dict_converter3(selenium):
    import json

    from js import Object
    from pyodide.code import run_js
    from pyodide.ffi import to_js

    d = {x: x + 2 for x in range(5)}

    res = to_js(d, dict_converter=Object.fromEntries)
    constructor, serialized = run_js(
        """
        (res) => [res.constructor.name, JSON.stringify(res)]
        """
    )(res)

    assert constructor == "Object"
    assert json.loads(serialized) == {str(k): v for k, v in d.items()}


@run_in_pyodide
def test_dict_converter4(selenium):
    import json

    from pyodide.code import run_js

    d = {x: x + 2 for x in range(5)}

    constructor, serialized = run_js(
        """
        (px) => {
            let res = px.toJs({dict_converter : Object.fromEntries});
            return [res.constructor.name, JSON.stringify(res)];
        }
        """
    )(d)

    assert constructor == "Object"
    assert json.loads(serialized) == {str(k): v for k, v in d.items()}


@pytest.mark.parametrize(
    "formula",
    ["2**30", "2**31", "2**30 - 1 + 2**30", "2**32 / 2**4", "-2**30", "-2**31"],
)
def test_python2js_long_ints(selenium, formula):
    assert selenium.run(formula) == eval(formula)


@run_in_pyodide
def test_python2js_long_ints2(selenium):
    from pyodide.code import run_js

    assert run_js(
        """
        (x) => x === 2n**64n;
        """
    )(2**64)

    assert run_js(
        """
        (x) => x === -(2n**64n);
        """
    )(-(2**64))


def test_pythonexc2js(selenium):
    msg = "ZeroDivisionError"
    with pytest.raises(selenium.JavascriptException, match=msg):
        selenium.run_js('return pyodide.runPython("5 / 0")')


def test_js2python(selenium):
    selenium.run_js(
        """
        self.test_objects = {
            jsstring_ucs1 : "pyodidé",
            jsstring_ucs2 : "碘化物",
            jsstring_ucs4 : "🐍",
            jsnumber0 : 42,
            jsnumber1 : 42.5,
            jsundefined : undefined,
            jsnull : null,
            jstrue : true,
            jsfalse : false,
            jsarray0 : [],
            jsarray1 : [1, 2, 3],
            jspython : pyodide.globals.get("open"),
            jsbytes : new Uint8Array([1, 2, 3]),
            jsfloats : new Float32Array([1, 2, 3]),
            jsobject : new TextDecoder(),
        };
        """
    )
    selenium.run("from js import test_objects as t")
    assert selenium.run('t.jsstring_ucs1 == "pyodidé"')
    assert selenium.run('t.jsstring_ucs2 == "碘化物"')
    assert selenium.run('t.jsstring_ucs4 == "🐍"')
    assert selenium.run("t.jsnumber0 == 42 and isinstance(t.jsnumber0, int)")
    assert selenium.run("t.jsnumber1 == 42.5 and isinstance(t.jsnumber1, float)")
    assert selenium.run("t.jsundefined is None")
    assert selenium.run("t.jsnull is None")
    assert selenium.run("t.jstrue is True")
    assert selenium.run("t.jsfalse is False")
    assert selenium.run("t.jspython is open")
    assert selenium.run(
        """
        jsbytes = t.jsbytes.to_py()
        ((jsbytes.tolist() == [1, 2, 3])
         and (jsbytes.tobytes() == b"\x01\x02\x03"))
        """
    )
    assert selenium.run(
        """
        jsfloats = t.jsfloats.to_py()
        import struct
        expected = struct.pack("fff", 1, 2, 3)
        (jsfloats.tolist() == [1, 2, 3]) and (jsfloats.tobytes() == expected)
        """
    )
    assert selenium.run('str(t.jsobject) == "[object TextDecoder]"')
    assert selenium.run("bool(t.jsobject) == True")
    assert selenium.run("bool(t.jsarray0) == False")
    assert selenium.run("bool(t.jsarray1) == True")
    selenium.run_js("test_objects.jspython.destroy()")


@pytest.mark.parametrize(
    "jsval, is_truthy",
    [
        ("()=>{}", True),
        ("new Map()", False),
        ("new Map([[0, 1]])", True),
        ("new Set()", False),
        ("new Set([0])", True),
    ],
)
@run_in_pyodide
def test_js2python_bool(selenium, jsval, is_truthy):
    from pyodide.code import run_js

    assert bool(run_js(jsval)) is is_truthy


@pytest.mark.parametrize(
    "jstype, pytype",
    (
        ("Int8Array", "b"),
        ("Uint8Array", "B"),
        ("Uint8ClampedArray", "B"),
        ("Int16Array", "h"),
        ("Uint16Array", "H"),
        ("Int32Array", "i"),
        ("Uint32Array", "I"),
        ("Float32Array", "f"),
        ("Float64Array", "d"),
    ),
)
@run_in_pyodide
def test_typed_arrays(selenium, jstype, pytype):
    from pyodide.code import run_js

    array = run_js(f"new {jstype}([1, 2, 3, 4]);").to_py()
    print(array.format, array.tolist(), array.tobytes())
    assert array.format == pytype
    assert array.tolist() == [1, 2, 3, 4]
    import struct

    assert array.tobytes() == struct.pack(pytype * 4, 1, 2, 3, 4)


@run_in_pyodide
def test_array_buffer(selenium):
    from pyodide.code import run_js

    array = run_js("new ArrayBuffer(100);").to_py()
    assert len(array.tobytes()) == 100


def assert_js_to_py_to_js(selenium, name):
    selenium.run_js(f"self.obj = {name};")
    selenium.run("from js import obj")
    assert selenium.run_js(
        """
        let pyobj = pyodide.globals.get("obj");
        return pyobj === obj;
        """
    )


def assert_py_to_js_to_py(selenium, name):
    selenium.run_js(
        f"""
        self.obj = pyodide.runPython('{name}');
        pyodide.runPython(`
            from js import obj
            assert obj is {name}
        `);
        obj.destroy();
        """
    )


@run_in_pyodide
def test_recursive_list_to_js(selenium):
    x: Any = []
    x.append(x)
    from pyodide.ffi import to_js

    to_js(x)


@run_in_pyodide
def test_recursive_dict_to_js(selenium):
    x: Any = {}
    x[0] = x
    from pyodide.ffi import to_js

    to_js(x)


def test_list_js2py2js(selenium):
    selenium.run_js("self.x = [1,2,3];")
    assert_js_to_py_to_js(selenium, "x")


def test_dict_js2py2js(selenium):
    selenium.run_js("self.x = { a : 1, b : 2, 0 : 3 };")
    assert_js_to_py_to_js(selenium, "x")


def test_error_js2py2js(selenium):
    selenium.run_js("self.err = new Error('hello there?');")
    assert_js_to_py_to_js(selenium, "err")


def test_error_py2js2py(selenium):
    selenium.run("err = Exception('hello there?');")
    assert_py_to_js_to_py(selenium, "err")


def test_list_py2js2py(selenium):
    selenium.run("x = ['a', 'b']")
    assert_py_to_js_to_py(selenium, "x")


def test_dict_py2js2py(selenium):
    selenium.run("x = {'a' : 5, 'b' : 1}")
    assert_py_to_js_to_py(selenium, "x")


@run_in_pyodide
def test_jsproxy_attribute_error(selenium):
    import pytest

    from pyodide.code import run_js

    point = run_js(
        """
        class Point {
            constructor(x, y) {
                this.x = x;
                this.y = y;
            }
        }
        new Point(42, 43);
        """
    )
    assert point.y == 43

    with pytest.raises(AttributeError, match="z"):
        point.z

    del point.y
    with pytest.raises(AttributeError, match="y"):
        point.y

    assert run_js("(point) => point.y;")(point) is None


def test_javascript_error(selenium):
    msg = "JsException: Error: This is a js error"
    with pytest.raises(selenium.JavascriptException, match=msg):
        selenium.run(
            """
            from js import Error
            err = Error.new("This is a js error")
            err2 = Error.new("This is another js error")
            raise err
            """
        )


@run_in_pyodide
def test_javascript_error_back_to_js(selenium):
    from pyodide.code import run_js

    err = run_js('self.err = new Error("This is a js error"); err')
    assert type(err).__name__ == "JsException"
    assert run_js(
        """
        (py_err) => py_err === err;
        """
    )(err)


def test_memoryview_conversion(selenium):
    selenium.run(
        """
        import array
        a = array.array("Q", [1,2,3])
        b = array.array("u", "123")
        """
    )
    selenium.run_js(
        """
        pyodide.runPython("a").destroy()
        // Implicit assertion: this doesn't leave python error indicator set
        // (automatically checked in conftest.py)
        """
    )

    selenium.run_js(
        """
        pyodide.runPython("b").destroy()
        // Implicit assertion: this doesn't leave python error indicator set
        // (automatically checked in conftest.py)
        """
    )


def test_python2js_with_depth(selenium):
    selenium.run_js(
        """
        let x = pyodide.runPython(`
            class Test: pass
            [Test(), [Test(), [Test(), [Test()]]]]
        `);
        let Module = pyodide._module;
        let proxies = [];
        let proxies_id = Module.hiwire.new_value(proxies);
        let result = Module.hiwire.pop_value(Module._python2js_with_depth(x.$$.ptr, -1, proxies_id));
        Module.hiwire.decref(proxies_id);

        assert(() => proxies.length === 4);

        let result_proxies = [result[0], result[1][0], result[1][1][0], result[1][1][1][0]];
        proxies.sort((x, y) => x.$$.ptr < y.$$.ptr);
        result_proxies.sort((x, y) => x.$$.ptr < y.$$.ptr);
        for(let i = 0; i < 4; i++){
            assert(() => proxies[i] == result_proxies[i]);
        }
        x.destroy();
        for(let px of proxies){
            px.destroy();
        }
        """
    )


@pytest.mark.parametrize("ty", [list, tuple])
@run_in_pyodide
def test_tojs1(selenium, ty):
    import json

    from pyodide.code import run_js

    l = [1, 2, 3]
    x = ty(l)
    assert run_js("x => Array.isArray(x.toJs())")(x)
    serialized = run_js("x => JSON.stringify(x.toJs())")(x)
    assert l == json.loads(serialized)


@run_in_pyodide
def test_tojs2(selenium):
    import json

    from pyodide.code import run_js

    o = [(1, 2), (3, 4), [5, 6], {2: 3, 4: 9}]

    assert run_js("(o) => Array.isArray(o.toJs())")(o)
    serialized = run_js("(o) => JSON.stringify(o.toJs())")(o)
    assert json.loads(serialized) == [[1, 2], [3, 4], [5, 6], {}]
    serialized = run_js("(o) => JSON.stringify(Array.from(o.toJs()[3].entries()))")(o)
    assert json.loads(serialized) == [[2, 3], [4, 9]]


def test_tojs4(selenium):
    selenium.run_js(
        """
        let a = pyodide.runPython("[1,[2,[3,[4,[5,[6,[7]]]]]]]")
        for(let i=0; i < 7; i++){
            let x = a.toJs({depth : i});
            for(let j=0; j < i; j++){
                assert(() => Array.isArray(x), `i: ${i}, j: ${j}`);
                x = x[1];
            }
            assert(() => x instanceof pyodide.ffi.PyProxy, `i: ${i}, j: ${i}`);
            x.destroy();
        }
        a.destroy()
        """
    )


def test_tojs5(selenium):
    selenium.run_js(
        """
        let a = pyodide.runPython("[1, (2, (3, [4, (5, (6, [7]))]))]")
        for(let i=0; i < 7; i++){
            let x = a.toJs({depth : i});
            for(let j=0; j < i; j++){
                assert(() => Array.isArray(x), `i: ${i}, j: ${j}`);
                x = x[1];
            }
            assert(() => x instanceof pyodide.ffi.PyProxy, `i: ${i}, j: ${i}`);
            x.destroy();
        }
        a.destroy()
        """
    )


def test_tojs6(selenium):
    selenium.run_js(
        """
        let respy = pyodide.runPython(`
            a = [1, 2, 3, 4, 5]
            b = [a, a, a, a, a]
            [b, b, b, b, b]
        `);
        let total_refs = pyodide._module.hiwire.num_keys();
        let res = respy.toJs();
        let new_total_refs = pyodide._module.hiwire.num_keys();
        respy.destroy();
        assert(() => total_refs === new_total_refs);
        assert(() => res[0] === res[1]);
        assert(() => res[0][0] === res[1][1]);
        assert(() => res[4][0] === res[1][4]);
        """
    )


def test_tojs7(selenium):
    selenium.run_js(
        """
        let respy = pyodide.runPython(`
            a = [["b"]]
            b = [1,2,3, a[0]]
            a[0].append(b)
            a.append(b)
            a
        `);
        let total_refs = pyodide._module.hiwire.num_keys();
        let res = respy.toJs();
        let new_total_refs = pyodide._module.hiwire.num_keys();
        respy.destroy();
        assert(() => total_refs === new_total_refs);
        assert(() => res[0][0] === "b");
        assert(() => res[1][2] === 3);
        assert(() => res[1][3] === res[0]);
        assert(() => res[0][1] === res[1]);
        """
    )


@pytest.mark.skip_pyproxy_check
@run_in_pyodide
def test_tojs8(selenium):
    import pytest

    from pyodide.ffi import ConversionError, to_js

    msg = r"Cannot use \(2, 2\) as a key for a Javascript"
    with pytest.raises(ConversionError, match=msg):
        to_js({(2, 2): 0})

    with pytest.raises(ConversionError, match=msg):
        to_js({(2, 2)})


def test_tojs9(selenium):
    assert (
        set(
            selenium.run_js(
                """
                return Array.from(pyodide.runPython(`
                    from pyodide.ffi import to_js
                    to_js({ 1, "1" })
                `).values())
                """
            )
        )
        == {1, "1"}
    )

    assert (
        dict(
            selenium.run_js(
                """
                return Array.from(pyodide.runPython(`
                    from pyodide.ffi import to_js
                    to_js({ 1 : 7, "1" : 9 })
                `).entries())
                """
            )
        )
        == {1: 7, "1": 9}
    )


@run_in_pyodide
def test_to_py1(selenium):
    from pyodide.code import run_js

    a = run_js(
        """
        let a = new Map([[1, [1,2,new Set([1,2,3])]], [2, new Map([[1,2],[2,7]])]]);
        a.get(2).set("a", a);
        a;
        """
    )
    result = [repr(a.to_py(depth=i)) for i in range(4)]
    assert result == [
        "[object Map]",
        "{1: 1,2,[object Set], 2: [object Map]}",
        "{1: [1, 2, [object Set]], 2: {1: 2, 2: 7, 'a': [object Map]}}",
        "{1: [1, 2, {1, 2, 3}], 2: {1: 2, 2: 7, 'a': {...}}}",
    ]


@run_in_pyodide
def test_to_py2(selenium):
    from pyodide.code import run_js

    a = run_js(
        """
        let a = { "x" : 2, "y" : 7, "z" : [1,2] };
        a.z.push(a);
        a
        """
    )
    result = [repr(a.to_py(depth=i)) for i in range(4)]
    assert result == [
        "[object Object]",
        "{'x': 2, 'y': 7, 'z': 1,2,[object Object]}",
        "{'x': 2, 'y': 7, 'z': [1, 2, [object Object]]}",
        "{'x': 2, 'y': 7, 'z': [1, 2, {...}]}",
    ]


@run_in_pyodide
def test_to_py3(selenium):
    from pyodide.code import run_js

    a = run_js(
        """
        class Temp {
            constructor(){
                this.x = 2;
                this.y = 7;
            }
        }
        new Temp();
        """
    )
    assert repr(type(a.to_py())) == "<class 'pyodide.ffi.JsProxy'>"


@pytest.mark.parametrize(
    "obj, msg",
    [
        ("Map([[[1,1], 2]])", "Cannot use key of type Array as a key to a Python dict"),
        ("Set([[1,1]])", "Cannot use key of type Array as a key to a Python set"),
        ("Map([[0, 2], [false, 3]])", "contains both 0 and false"),
        ("Set([0, false])", "contains both 0 and false"),
        ("Map([[1, 2], [true, 3]])", "contains both 1 and true"),
        ("Set([1, true])", "contains both 1 and true"),
    ],
)
@run_in_pyodide
def test_to_py4(selenium, obj, msg):
    import pytest

    from pyodide.code import run_js
    from pyodide.ffi import ConversionError, JsException

    a = run_js(f"new {obj}")

    with pytest.raises((ConversionError, JsException), match=msg):
        a.to_py()


@run_in_pyodide
def test_to_py_default_converter(selenium):
    from pyodide.code import run_js

    [r1, r2] = run_js(
        """
        class Pair {
            constructor(first, second){
                this.first = first;
                this.second = second;
            }
        }
        let l = [1,2,3];
        const r1 = new Pair(l, [l]);
        const r2 = new Pair(l, [l]);
        r2.first = r2;
        const opts = {defaultConverter(value, converter, cache){
            if(value.constructor.name !== "Pair"){
                return value;
            }
            let list = pyodide.globals.get("list");
            l = list();
            list.destroy();
            cache(value, l);
            const first = converter(value.first);
            const second = converter(value.second);
            l.append(first);
            l.append(second);
            first.destroy();
            second.destroy();
            return l;
        }};
        pyodide.toPy([r1, r2], opts);
        """
    )

    assert isinstance(r1, list)
    assert r1[0] is r1[1][0]
    assert r1[0] == [1, 2, 3]
    assert r2[0] is r2


@run_in_pyodide
def test_to_py_default_converter2(selenium):
    from typing import Any

    from pyodide.code import run_js

    [p1, p2] = run_js(
        """
        class Pair {
            constructor(first, second){
                this.first = first;
                this.second = second;
            }
        }
        const l = [1,2,3];
        const r1 = new Pair(l, [l]);
        const r2 = new Pair(l, [l]);
        r2.first = r2;
        [r1, r2]
        """
    )

    def default_converter(value, converter, cache):
        if value.constructor.name != "Pair":
            return value
        l: list[Any] = []
        cache(value, l)
        l.append(converter(value.first))
        l.append(converter(value.second))
        return l

    r1 = p1.to_py(default_converter=default_converter)
    assert isinstance(r1, list)
    assert r1[0] is r1[1][0]
    assert r1[0] == [1, 2, 3]

    r2 = p2.to_py(default_converter=default_converter)
    assert r2[0] is r2


def test_to_js_default_converter(selenium):
    selenium.run_js(
        """
        p = pyodide.runPython(`
        class Pair:
            def __init__(self, first, second):
                self.first = first
                self.second = second
        p = Pair(1,2)
        p
        `);
        let res = p.toJs({ default_converter(x, convert, cacheConversion){
            let result = [];
            cacheConversion(x, result);
            result.push(convert(x.first));
            result.push(convert(x.second));
            return result;
        }});
        assert(() => res[0] === 1);
        assert(() => res[1] === 2);
        p.first = p;
        let res2 = p.toJs({ default_converter(x, convert, cacheConversion){
            let result = [];
            cacheConversion(x, result);
            result.push(convert(x.first));
            result.push(convert(x.second));
            return result;
        }});
        assert(() => res2[0] === res2);
        assert(() => res2[1] === 2);
        p.destroy();
        """
    )


@run_in_pyodide
def test_to_js_default_converter2(selenium):
    import json

    import pytest

    from js import JSON, Array
    from pyodide.code import run_js
    from pyodide.ffi import JsException, to_js

    class Pair:
        __slots__ = ("first", "second")

        def __init__(self, first, second):
            self.first = first
            self.second = second

    p1 = Pair(1, 2)
    p2 = Pair(1, 2)
    p2.first = p2

    def default_converter(value, convert, cacheConversion):
        result = Array.new()
        cacheConversion(value, result)
        result.push(convert(value.first))
        result.push(convert(value.second))
        return result

    p1js = to_js(p1, default_converter=default_converter)
    p2js = to_js(p2, default_converter=default_converter)

    assert json.loads(JSON.stringify(p1js)) == [1, 2]

    with pytest.raises(JsException, match="TypeError"):
        JSON.stringify(p2js)

    assert run_js("(x) => x[0] === x")(p2js)
    assert run_js("(x) => x[1] === 2")(p2js)


def test_buffer_format_string(selenium):
    errors = [
        ["aaa", "Expected format string to have length <= 2, got 'aaa'"],
        ["II", "Unrecognized alignment character I."],
        ["x", "Unrecognized format character 'x'."],
        ["x", "Unrecognized format character 'x'."],
        ["e", "Javascript has no Float16 support."],
    ]
    for fmt, msg in errors:
        with pytest.raises(selenium.JavascriptException, match=msg):
            selenium.run_js(
                f"""
                pyodide._module.processBufferFormatString({fmt!r});
                """
            )

    format_tests = [
        ("c", "Uint8"),
        ("b", "Int8"),
        ("B", "Uint8"),
        ("?", "Uint8"),
        ("h", "Int16"),
        ("H", "Uint16"),
        ("i", "Int32"),
        ("I", "Uint32"),
        ("l", "Int32"),
        ("L", "Uint32"),
        ("n", "Int32"),
        ("N", "Uint32"),
        ("q", "BigInt64"),
        ("Q", "BigUint64"),
        ("f", "Float32"),
        ("d", "Float64"),
        ("s", "Uint8"),
        ("p", "Uint8"),
        ("P", "Uint32"),
    ]

    def process_fmt_string(fmt):
        return selenium.run_js(
            f"""
            let [array, is_big_endian] = pyodide._module.processBufferFormatString({fmt!r});
            if(!array || typeof array.name !== "string" || !array.name.endsWith("Array")){{
                throw new Error("Unexpected output on input {fmt}: " + array);
            }}
            let arrayName = array.name.slice(0, -"Array".length);
            return [arrayName, is_big_endian];
            """
        )

    for fmt, expected_array_name in format_tests:
        [array_name, is_big_endian] = process_fmt_string(fmt)
        assert not is_big_endian
        assert array_name == expected_array_name

    endian_tests = [
        ("@h", "Int16", False),
        ("=H", "Uint16", False),
        ("<i", "Int32", False),
        (">I", "Uint32", True),
        ("!l", "Int32", True),
    ]

    for fmt, expected_array_name, expected_is_big_endian in endian_tests:
        [array_name, is_big_endian] = process_fmt_string(fmt)
        assert is_big_endian == expected_is_big_endian
        assert array_name == expected_array_name


def test_dict_converter_cache(selenium):
    selenium.run_js(
        """
        let d1 = pyodide.runPython('d={0: {1: 2}}; d[1]=d[0]; d');
        let d = d1.toJs({dict_converter: Object.fromEntries});
        d1.destroy();
        assert(() => d[0] === d[1]);
        """
    )


@pytest.mark.parametrize("n", [1 << 31, 1 << 32, 1 << 33, 1 << 63, 1 << 64, 1 << 65])
@run_in_pyodide
def test_very_large_length(selenium, n):
    from unittest import TestCase

    from pyodide.code import run_js

    raises = TestCase().assertRaises(
        OverflowError, msg=f"length {n} of object is larger than INT_MAX (2147483647)"
    )

    o = run_js(f"({{length : {n}}})")
    with raises:
        len(o)

    # 1. Set toStringTag to NodeList to force JsProxy to feature detect this object
    # as an array
    # 2. Return a very large length
    # 3. JsProxy_subscript_array should successfully handle this and propagate the error.
    a = run_js(f"({{[Symbol.toStringTag] : 'NodeList', length: {n}}})")
    with raises:
        a[-1]


@pytest.mark.parametrize(
    "n", [-1, -2, -3, -100, -1 << 31, -1 << 32, -1 << 33, -1 << 63, -1 << 64, -1 << 65]
)
@run_in_pyodide
def test_negative_length(selenium, n):
    from unittest import TestCase

    from pyodide.code import run_js

    raises = TestCase().assertRaises(
        ValueError, msg=f"length {n} of object is negative"
    )

    o = run_js(f"({{length : {n}}})")
    with raises:
        len(o)

    # 1. Set toStringTag to NodeList to force JsProxy to feature detect this object
    # as an array
    # 2. Return a negative length
    # 3. JsProxy_subscript_array should successfully handle this and propagate the error.
    a = run_js(f"({{[Symbol.toStringTag] : 'NodeList', length: {n}}})")
    with raises:
        a[-1]


@std_hypothesis_settings
@given(l=st.lists(st.integers()), slice=st.slices(50))
@example(l=[0, 1], slice=slice(None, None, -1))
@example(l=list(range(4)), slice=slice(None, None, -2))
@example(l=list(range(10)), slice=slice(-1, 12))
@example(l=list(range(10)), slice=slice(12, -1))
@example(l=list(range(10)), slice=slice(12, -1, -1))
@example(l=list(range(10)), slice=slice(-1, 12, 2))
@example(l=list(range(10)), slice=slice(12, -1, -1))
@example(l=list(range(10)), slice=slice(12, -1, -2))
@run_in_pyodide
def test_array_slices(selenium, l, slice):
    expected = l[slice]
    from pyodide.ffi import JsArray, to_js

    jsl = to_js(l)
    assert isinstance(jsl, JsArray)
    result = jsl[slice]
    assert result.to_py() == expected


@std_hypothesis_settings
@given(l=st.lists(st.integers()), slice=st.slices(50))
@example(l=[0, 1], slice=slice(None, None, -1))
@example(l=list(range(4)), slice=slice(None, None, -2))
@example(l=list(range(10)), slice=slice(-1, 12))
@example(l=list(range(10)), slice=slice(12, -1))
@example(l=list(range(10)), slice=slice(12, -1, -1))
@example(l=list(range(10)), slice=slice(-1, 12, 2))
@example(l=list(range(10)), slice=slice(12, -1, -1))
@example(l=list(range(10)), slice=slice(12, -1, -2))
@run_in_pyodide
def test_array_slice_del(selenium, l, slice):
    from pyodide.ffi import JsArray, to_js

    jsl = to_js(l)
    assert isinstance(jsl, JsArray)
    del l[slice]
    del jsl[slice]
    assert jsl.to_py() == l


@st.composite
def list_slice_and_value(draw):
    l = draw(st.lists(st.integers()))
    step_one = draw(st.booleans())
    if step_one:
        start = draw(st.integers(0, max(len(l) - 1, 0)) | st.none())
        stop = draw(st.integers(start, len(l)) | st.none())
        if draw(st.booleans()) and start is not None:
            start -= len(l)
        if draw(st.booleans()) and stop is not None:
            stop -= len(l)
        s = slice(start, stop)
        vals = draw(st.lists(st.integers()))
    else:
        s = draw(st.slices(50))
        vals_len = len(l[s])
        vals = draw(st.lists(st.integers(), min_size=vals_len, max_size=vals_len))
    return (l, s, vals)


@std_hypothesis_settings
@given(lsv=list_slice_and_value())
@example(lsv=(list(range(5)), slice(5, 2), []))
@example(lsv=(list(range(5)), slice(2, 5, -1), []))
@example(lsv=(list(range(5)), slice(5, 2), [-1, -2, -3]))
@run_in_pyodide
def test_array_slice_assign_1(selenium, lsv):
    from pyodide.ffi import JsArray, to_js

    [l, s, v] = lsv
    jsl = to_js(l)
    assert isinstance(jsl, JsArray)
    l[s] = v
    jsl[s] = v
    assert jsl.to_py() == l


@run_in_pyodide
def test_array_slice_assign_2(selenium):
    import pytest

    from pyodide.ffi import JsArray, to_js

    l = list(range(10))
    with pytest.raises(ValueError) as exc_info_1a:
        l[0:4:2] = [1, 2, 3, 4]

    jsl = to_js(l)
    assert isinstance(jsl, JsArray)
    with pytest.raises(ValueError) as exc_info_1b:
        jsl[0:4:2] = [1, 2, 3, 4]

    l = list(range(10))
    with pytest.raises(ValueError) as exc_info_2a:
        l[0:4:2] = []

    with pytest.raises(ValueError) as exc_info_2b:
        jsl[0:4:2] = []

    with pytest.raises(TypeError) as exc_info_3a:
        l[:] = 1  # type: ignore[call-overload]

    with pytest.raises(TypeError) as exc_info_3b:
        jsl[:] = 1

    assert exc_info_1a.value.args == exc_info_1b.value.args
    assert exc_info_2a.value.args == exc_info_2b.value.args
    assert exc_info_3a.value.args == exc_info_3b.value.args


@std_hypothesis_settings
@given(l1=st.lists(st.integers()), l2=st.lists(st.integers()))
@example(l1=[], l2=[])
@example(l1=[], l2=[1])
@run_in_pyodide
def test_array_extend(selenium_module_scope, l1, l2):
    from pyodide.ffi import to_js

    l1js1 = to_js(l1)
    l1js1.extend(l2)

    l1js2 = to_js(l1)
    l1js2 += l2

    l1.extend(l2)

    assert l1 == l1js1.to_py()
    assert l1 == l1js2.to_py()


@run_in_pyodide
def test_typed_array(selenium):
    from pyodide.code import run_js

    a = run_js("self.a = new Uint8Array([1,2,3,4]); a")
    assert a[0] == 1
    assert a[-1] == 4
    a[-2] = 7
    assert run_js("self.a[2]") == 7

    import pytest

    with pytest.raises(ValueError, match="cannot delete array elements"):
        del a[0]

    msg = "Slice subscripting isn't implemented for typed arrays"
    with pytest.raises(NotImplementedError, match=msg):
        a[:]

    msg = "Slice assignment isn't implemented for typed arrays"
    with pytest.raises(NotImplementedError, match=msg):
        a[:] = [-1, -2, -3, -4]

    assert not hasattr(a, "extend")
    with pytest.raises(TypeError):
        a += [1, 2, 3]


@pytest.mark.xfail_browsers(node="No document in node")
@run_in_pyodide
def test_html_array(selenium):
    from pyodide.code import run_js

    x = run_js("document.querySelectorAll('*')")
    assert run_js("(a, b) => a === b[0]")(x[0], x)
    assert run_js("(a, b) => a === Array.from(b).pop()")(x[-1], x)

    import pytest

    with pytest.raises(TypeError, match="does ?n[o']t support item assignment"):
        x[0] = 0

    with pytest.raises(TypeError, match="does ?n[o']t support item deletion"):
        del x[0]

# See also test_pyproxy, test_jsproxy, and test_python.
from typing import Any

import pytest
from hypothesis import example, given, settings
from hypothesis import strategies as st
from hypothesis.strategies import text
from pytest_pyodide import run_in_pyodide
from pytest_pyodide.fixture import selenium_context_manager
from pytest_pyodide.hypothesis import (
    any_equal_to_self_strategy,
    any_strategy,
    std_hypothesis_settings,
)


@given(s=text())
@settings(deadline=10000)
@example("\ufeff")
def test_string_conversion(selenium_module_scope, s):
    @run_in_pyodide
    def main(selenium, sbytes):
        from pyodide.code import run_js

        run_js("self.encoder = new TextEncoder()")
        run_js("self.decoder = new TextDecoder('utf8', {ignoreBOM: true})")

        spy = bytes(sbytes).decode()
        sjs = run_js(
            """
            (sbytes) => {
                self.sjs = self.decoder.decode(new Uint8Array(sbytes));
                return sjs;
            }
            """
        )(sbytes)
        assert sjs == spy
        assert run_js("(spy) => spy === self.sjs")(spy)

    with selenium_context_manager(selenium_module_scope) as selenium:
        sbytes = list(s.encode())
        main(selenium, sbytes)


@given(s=text())
@std_hypothesis_settings
@example("\ufeff")
@run_in_pyodide
def test_string_conversion2(selenium, s):
    from pyodide.code import run_js

    run_js("self.encoder = new TextEncoder()")
    run_js("self.decoder = new TextDecoder('utf8', {ignoreBOM: true})")

    s_encoded = s.encode()
    sjs = run_js(
        """
        (s_encoded) => {
            let buf = s_encoded.getBuffer();
            self.sjs = self.decoder.decode(buf.data);
            buf.release();
            return sjs
        }
        """
    )(s_encoded)
    assert sjs == s
    assert run_js("""(spy) => spy === self.sjs""")(s)


def blns():
    import base64
    import json

    with open("./src/tests/blns.base64.json") as f:
        BLNS = json.load(f)
    for s in BLNS:
        yield base64.b64decode(s).decode(errors="ignore")


@pytest.mark.driver_timeout(60)
def test_string_conversion_blns(selenium):
    @run_in_pyodide
    def _string_conversion_blns_internal(selenium, s):
        from pyodide.code import run_js

        run_js("self.encoder = new TextEncoder()")
        run_js("self.decoder = new TextDecoder('utf8', {ignoreBOM: true})")

        s_encoded = s.encode()
        sjs = run_js(
            """
            (s_encoded) => {
                let buf = s_encoded.getBuffer();
                self.sjs = self.decoder.decode(buf.data);
                buf.release();
                return sjs
            }
            """
        )(s_encoded)
        assert sjs == s
        assert run_js("""(spy) => spy === self.sjs""")(s)

    strings = blns()
    for s in strings:
        _string_conversion_blns_internal(selenium, s)


@run_in_pyodide
def test_large_string_conversion(selenium):
    from pyodide.code import run_js

    longstr = run_js('"ab".repeat(200_000)')
    res = longstr.count("ab")
    assert res == 200_000
    run_js(
        """
        (s) => {
            assert(() => s.length === 40_000);
            for(let n = 0; n < 20_000; n++){
                assert(() => s.slice(2*n, 2*n+2) === "ab");
            }
        }
        """
    )("ab" * 20_000)


@given(
    n=st.one_of(
        st.integers(),
        st.floats(allow_nan=False),
    )
)
@std_hypothesis_settings
@example(2**53)
@example(2**53 - 1)
@example(2**53 + 1)
@example(-(2**53))
@example(-(2**53) - 1)
@example(-(2**53) + 1)
@run_in_pyodide
def test_number_conversions(selenium_module_scope, n):
    import json

    from pyodide.code import run_js

    x_js = run_js("(s) => self.x_js = eval(s)")(json.dumps(n))
    run_js("(x_py) => Number(x_py) === x_js")(n)

    if type(x_js) is float:
        assert x_js == float(n)
    else:
        assert x_js == n


@given(n=st.floats())
@std_hypothesis_settings
@run_in_pyodide
def test_number_conversions_2(selenium_module_scope, n):
    from pyodide.code import run_js

    assert run_js("(n) => typeof n")(n) == "number"
    from math import isinf, isnan

    if isnan(n):
        return
    import json

    n_js = run_js("(s) => eval(s)")(json.dumps(n))
    if not isinf(n) and float(int(n)) == n and -(2**53) < n < 2**53:
        assert isinstance(n_js, int)
    else:
        assert isinstance(n_js, float)


@given(n=st.integers())
@std_hypothesis_settings
@example(2**53)
@example(2**53 - 1)
@example(2**53 + 1)
@example(-(2**53))
@example(-(2**53) - 1)
@example(-(2**53) + 1)
@run_in_pyodide
def test_number_conversions_3(selenium_module_scope, n):
    from pyodide.code import run_js

    jsty = run_js("(n) => typeof n")(n)
    if -(2**53) + 1 < n < 2**53 - 1:
        assert jsty == "number"
    else:
        assert jsty == "bigint"
    import json

    n_js = run_js("(s) => eval(s)")(json.dumps(n))
    if -(2**53) < n < 2**53:
        assert isinstance(n_js, int)
    else:
        assert isinstance(n_js, float)


@run_in_pyodide
def test_nan_conversions(selenium):
    from pyodide.code import run_js

    jsnan = run_js("NaN")
    from math import isnan

    assert isnan(jsnan)
    assert run_js(
        """
        let mathmod = pyodide.pyimport("math");
        const res = Number.isNaN(mathmod.nan);
        mathmod.destroy();
        res
        """
    )


@given(n=st.integers())
@std_hypothesis_settings
def test_bigint_conversions(selenium_module_scope, n):
    with selenium_context_manager(selenium_module_scope) as selenium:
        h = hex(n)
        selenium.run_js(f"self.h = {h!r};")
        selenium.run_js(
            """
            let negative = false;
            let h2 = h;
            if(h2.startsWith('-')){
                h2 = h2.slice(1);
                negative = true;
            }
            self.n = BigInt(h2);
            if(negative){
                self.n = -n;
            }
            pyodide.runPython(`
                from js import n, h
                n2 = int(h, 16)
                assert n == n2
            `);
            let n2 = pyodide.globals.get("n2");
            let n3 = Number(n2);
            if(Number.isSafeInteger(n3)){
                assert(() => typeof n2 === "number");
                assert(() => n2 === Number(n));
            } else {
                assert(() => typeof n2 === "bigint");
                assert(() => n2 === n);
            }
            """
        )


@given(
    n=st.one_of(
        st.integers(min_value=2**53 + 1),
        st.integers(max_value=-(2**53) - 1),
    )
)
@std_hypothesis_settings
def test_big_int_conversions2(selenium_module_scope, n):
    @run_in_pyodide
    def main(selenium, s):
        import json

        from pyodide.code import run_js

        x_py = json.loads(s)
        x_js, check = run_js(
            """
            (s, x_py) => {
                let x_js = eval(s + 'n');

                return [x_js, x_py === x_js];
            }
            """
        )(s, x_py)
        assert check
        assert x_js == x_py

    with selenium_context_manager(selenium_module_scope) as selenium:
        import json

        s = json.dumps(n)
        main(selenium, s)


@given(
    n=st.integers(),
    exp=st.integers(min_value=1, max_value=10),
)
@std_hypothesis_settings
def test_big_int_conversions3(selenium_module_scope, n, exp):
    @run_in_pyodide
    def main(selenium, s):
        import json

        from pyodide.code import run_js

        x_py = json.loads(s)
        x_js = run_js(
            f"""
            self.x_js = eval('{s}n'); // JSON.parse apparently doesn't work
            """
        )
        [x1, x2] = run_js(
            """
            (x_py) => [x_py.toString(), x_js.toString()]
            """
        )(x_py)
        assert x1 == x2

        check = run_js(
            """
            (x) => {
                const [a, b] = x.toJs();
                return a === b;
            }
            """
        )([str(x_js), str(x_py)])
        assert check

    with selenium_context_manager(selenium_module_scope) as selenium:
        val = 2 ** (32 * exp) - n
        import json

        s = json.dumps(val)
        main(selenium, s)


@given(obj=any_equal_to_self_strategy)
@std_hypothesis_settings
@run_in_pyodide
def test_hyp_py2js2py(selenium, obj):
    import __main__

    from pyodide.code import run_js

    __main__.obj = obj

    try:
        run_js('self.obj2 = pyodide.globals.get("obj"); 0;')
        from js import obj2  # type:ignore[attr-defined]

        assert obj2 == obj
        run_js(
            """
            if(self.obj2 && self.obj2.destroy){
                self.obj2.destroy();
            }
            delete self.obj2
            """
        )
    finally:
        del __main__.obj


@given(obj=any_equal_to_self_strategy)
@std_hypothesis_settings
@run_in_pyodide
def test_hyp_py2js2py_2(selenium, obj):
    import __main__

    from pyodide.code import run_js

    __main__.o = obj
    try:
        assert obj == run_js("pyodide.globals.get('o')")
    finally:
        del __main__.o


@pytest.mark.parametrize("a", [9992361673228537, -9992361673228537])
@run_in_pyodide
def test_big_integer_py2js2py(selenium, a):
    import __main__

    from pyodide.code import run_js

    __main__.a = a
    try:
        b = run_js("pyodide.globals.get('a')")
        assert a == b
    finally:
        del __main__.a


# Generate an object of any type
@pytest.mark.skip_refcount_check
@pytest.mark.skip_pyproxy_check
@given(obj=any_strategy)
@std_hypothesis_settings
@run_in_pyodide
def test_hyp_tojs_no_crash(selenium, obj):
    import __main__

    from pyodide.code import run_js

    __main__.x = obj
    try:
        run_js(
            """
            let x = pyodide.globals.get("x");
            if(x && x.toJs){
                x.toJs();
            }
            """
        )
    finally:
        del __main__.x


@pytest.mark.parametrize(
    "py,js",
    [
        (None, "undefined"),
        (True, "true"),
        (False, "false"),
        (42, "42"),
        (3.14, "3.14"),
        # Need to test all three internal string representations in Python:
        # UCS1, UCS2 and UCS4
        ("ascii", "'ascii'"),
        ("ιωδιούχο", "'ιωδιούχο'"),
        ("碘化物", "'碘化物'"),
        ("🐍", "'🐍'"),
    ],
)
@run_in_pyodide
def test_python2js1(selenium, py, js):
    from pyodide.code import run_js

    assert run_js(
        f"""
        (py) => py === {js}
        """
    )(py)


@run_in_pyodide
def test_python2js2(selenium):
    from pyodide.code import run_js

    assert (
        list(
            run_js(
                """
                (x) => {
                    x = x.toJs();
                    return [x.constructor.name, x.length, x[0]];
                }
                """
            )(b"bytes")
        )
        == ["Uint8Array", 5, 98]
    )


@run_in_pyodide
def test_python2js3(selenium):
    from pyodide.code import run_js

    l = [7, 9, 13]
    result = run_js(
        """
        (proxy) => {
            x = proxy.toJs();
            return [proxy.type, x.constructor.name, x.length, x[0], x[1], x[2]]
        }
        """
    )(l)
    assert list(result) == ["list", "Array", 3, *l]


@run_in_pyodide
def test_python2js4(selenium):
    from pyodide.code import run_js

    assert (
        list(
            run_js(
                """
                (proxy) => {
                    let typename = proxy.type;
                    let x = proxy.toJs();
                    return [proxy.type, x.constructor.name, x.get(42)];
                }
                """
            )({42: 64})
        )
        == ["dict", "Map", 64]
    )


@run_in_pyodide
def test_python2js5(selenium):
    from pyodide.code import run_js

    assert run_js("(x) => x.tell()")(open("/foo.txt", "wb")) == 0

    from tempfile import TemporaryFile

    with TemporaryFile(mode="w+") as f:
        contents = ["a\n", "b\n", "hello there!\n"]
        f.writelines(contents)
        assert run_js("(f) => f.tell()")(f) == 17

        assert (
            run_js(
                """
                (f) => {
                    f.seek(0);
                    return [f.readline(), f.readline(), f.readline()];
                }
                """
            )(f).to_py()
            == contents
        )


def test_python2js_track_proxies(selenium):
    selenium.run_js(
        """
        let x = pyodide.runPython(`
            class T:
                pass
            [[T()],[T()], [[[T()],[T()]],[T(), [], [[T()]], T()], T(), T()], T()]
        `);
        let proxies = [];
        let result = x.toJs({ pyproxies : proxies });
        assert(() => proxies.length === 10);
        for(let x of proxies){
            x.destroy();
        }
        function check(l){
            for(let x of l){
                if(x instanceof pyodide.ffi.PyProxy){
                    assert(() => x.$$.ptr === 0);
                } else {
                    check(x);
                }
            }
        }
        check(result);
        assertThrows(() => x.toJs({create_pyproxies : false}), "PythonError", "pyodide.ffi.ConversionError");
        x.destroy();
        """
    )


@run_in_pyodide
def test_wrong_way_track_proxies(selenium):
    from pyodide.code import run_js

    checkDestroyed = run_js(
        """
        function checkDestroyed(l){
            for(let e of l){
                if(e instanceof pyodide.ffi.PyProxy){
                    console.log("\\n\\n", "!!!!!!!!!", e.$$.ptr);
                    assert(() => e.$$.ptr === 0);
                } else {
                    checkDestroyed(e);
                }
            }
        };
        checkDestroyed
        """
    )
    from unittest import TestCase

    from js import Array, Object
    from pyodide.ffi import ConversionError, destroy_proxies, to_js

    raises = TestCase().assertRaises

    class T:
        pass

    x = [[T()], [T()], [[[T()], [T()]], [T(), [], [[T()]], T()], T(), T()], T()]
    proxylist = Array.new()
    r = to_js(x, pyproxies=proxylist)
    assert len(proxylist) == 10
    destroy_proxies(proxylist)
    checkDestroyed(r)
    with raises(TypeError):
        to_js(x, pyproxies=[])  # type:ignore[call-overload]
    with raises(TypeError):
        to_js(x, pyproxies=Object.new())
    with raises(ConversionError):
        to_js(x, create_pyproxies=False)


def test_wrong_way_conversions1(selenium):
    selenium.run_js(
        """
        assert(() => pyodide.toPy(5) === 5);
        assert(() => pyodide.toPy(5n) === 5n);
        assert(() => pyodide.toPy("abc") === "abc");
        class Test {};
        let t = new Test();
        assert(() => pyodide.toPy(t) === t);

        self.a1 = [1,2,3];
        self.b1 = pyodide.toPy(a1);
        self.a2 = { a : 1, b : 2, c : 3};
        self.b2 = pyodide.toPy(a2);
        pyodide.runPython(`
            from js import a1, b1, a2, b2
            assert a1.to_py() == b1
            assert a2.to_py() == b2
        `);
        self.b1.destroy();
        self.b2.destroy();
        """
    )


@run_in_pyodide
def test_wrong_way_conversions2(selenium):
    from pyodide.code import run_js
    from pyodide.ffi import to_js

    [astr, bstr] = run_js(
        """
        (a) => {
            b = [1,2,3];
            return [JSON.stringify(a), JSON.stringify(b)]
        }
        """
    )(to_js([1, 2, 3]))
    assert astr == bstr


@run_in_pyodide
def test_wrong_way_conversions3(selenium):
    from pyodide.code import run_js
    from pyodide.ffi import to_js

    class Test:
        pass

    t1 = Test()
    t2 = to_js(t1)
    t3 = run_js("(t2) => t2.copy()")(t2)

    assert t1 is t3
    t2.destroy()


@run_in_pyodide
def test_wrong_way_conversions4(selenium):
    from pyodide.ffi import to_js

    s = "avafhjpa"
    t = 55
    assert to_js(s) is s
    assert to_js(t) is t


@run_in_pyodide
def test_dict_converter1(selenium):
    import json

    from pyodide.code import run_js
    from pyodide.ffi import to_js

    arrayFrom = run_js("Array.from")
    d = {x: x + 2 for x in range(5)}
    res = to_js(d, dict_converter=arrayFrom)
    constructor, serialized = run_js(
        """
        (res) => {
            return [res.constructor.name, JSON.stringify(res)];
        }
        """
    )(res)

    assert constructor == "Array"
    assert json.loads(serialized) == [list(x) for x in d.items()]


@run_in_pyodide
def test_dict_converter2(selenium):
    import json

    from pyodide.code import run_js

    d = {x: x + 2 for x in range(5)}
    constructor, serialized = run_js(
        """
        (d) => {
            const res = d.toJs({dict_converter : Array.from});
            return [res.constructor.name, JSON.stringify(res)];
        }
        """
    )(d)

    assert constructor == "Array"
    assert json.loads(serialized) == [list(x) for x in d.items()]


@run_in_pyodide
def test_dict_converter3(selenium):
    import json

    from js import Object
    from pyodide.code import run_js
    from pyodide.ffi import to_js

    d = {x: x + 2 for x in range(5)}

    res = to_js(d, dict_converter=Object.fromEntries)
    constructor, serialized = run_js(
        """
        (res) => [res.constructor.name, JSON.stringify(res)]
        """
    )(res)

    assert constructor == "Object"
    assert json.loads(serialized) == {str(k): v for k, v in d.items()}


@run_in_pyodide
def test_dict_converter4(selenium):
    import json

    from pyodide.code import run_js

    d = {x: x + 2 for x in range(5)}

    constructor, serialized = run_js(
        """
        (px) => {
            let res = px.toJs({dict_converter : Object.fromEntries});
            return [res.constructor.name, JSON.stringify(res)];
        }
        """
    )(d)

    assert constructor == "Object"
    assert json.loads(serialized) == {str(k): v for k, v in d.items()}


@pytest.mark.parametrize(
    "formula",
    ["2**30", "2**31", "2**30 - 1 + 2**30", "2**32 / 2**4", "-2**30", "-2**31"],
)
def test_python2js_long_ints(selenium, formula):
    assert selenium.run(formula) == eval(formula)


@run_in_pyodide
def test_python2js_long_ints2(selenium):
    from pyodide.code import run_js

    assert run_js(
        """
        (x) => x === 2n**64n;
        """
    )(2**64)

    assert run_js(
        """
        (x) => x === -(2n**64n);
        """
    )(-(2**64))


def test_pythonexc2js(selenium):
    msg = "ZeroDivisionError"
    with pytest.raises(selenium.JavascriptException, match=msg):
        selenium.run_js('return pyodide.runPython("5 / 0")')


def test_js2python(selenium):
    selenium.run_js(
        """
        self.test_objects = {
            jsstring_ucs1 : "pyodidé",
            jsstring_ucs2 : "碘化物",
            jsstring_ucs4 : "🐍",
            jsnumber0 : 42,
            jsnumber1 : 42.5,
            jsundefined : undefined,
            jsnull : null,
            jstrue : true,
            jsfalse : false,
            jsarray0 : [],
            jsarray1 : [1, 2, 3],
            jspython : pyodide.globals.get("open"),
            jsbytes : new Uint8Array([1, 2, 3]),
            jsfloats : new Float32Array([1, 2, 3]),
            jsobject : new TextDecoder(),
        };
        """
    )
    selenium.run("from js import test_objects as t")
    assert selenium.run('t.jsstring_ucs1 == "pyodidé"')
    assert selenium.run('t.jsstring_ucs2 == "碘化物"')
    assert selenium.run('t.jsstring_ucs4 == "🐍"')
    assert selenium.run("t.jsnumber0 == 42 and isinstance(t.jsnumber0, int)")
    assert selenium.run("t.jsnumber1 == 42.5 and isinstance(t.jsnumber1, float)")
    assert selenium.run("t.jsundefined is None")
    assert selenium.run("t.jsnull is None")
    assert selenium.run("t.jstrue is True")
    assert selenium.run("t.jsfalse is False")
    assert selenium.run("t.jspython is open")
    assert selenium.run(
        """
        jsbytes = t.jsbytes.to_py()
        ((jsbytes.tolist() == [1, 2, 3])
         and (jsbytes.tobytes() == b"\x01\x02\x03"))
        """
    )
    assert selenium.run(
        """
        jsfloats = t.jsfloats.to_py()
        import struct
        expected = struct.pack("fff", 1, 2, 3)
        (jsfloats.tolist() == [1, 2, 3]) and (jsfloats.tobytes() == expected)
        """
    )
    assert selenium.run('str(t.jsobject) == "[object TextDecoder]"')
    assert selenium.run("bool(t.jsobject) == True")
    assert selenium.run("bool(t.jsarray0) == False")
    assert selenium.run("bool(t.jsarray1) == True")
    selenium.run_js("test_objects.jspython.destroy()")


@pytest.mark.parametrize(
    "jsval, is_truthy",
    [
        ("()=>{}", True),
        ("new Map()", False),
        ("new Map([[0, 1]])", True),
        ("new Set()", False),
        ("new Set([0])", True),
    ],
)
@run_in_pyodide
def test_js2python_bool(selenium, jsval, is_truthy):
    from pyodide.code import run_js

    assert bool(run_js(jsval)) is is_truthy


@pytest.mark.parametrize(
    "jstype, pytype",
    (
        ("Int8Array", "b"),
        ("Uint8Array", "B"),
        ("Uint8ClampedArray", "B"),
        ("Int16Array", "h"),
        ("Uint16Array", "H"),
        ("Int32Array", "i"),
        ("Uint32Array", "I"),
        ("Float32Array", "f"),
        ("Float64Array", "d"),
    ),
)
@run_in_pyodide
def test_typed_arrays(selenium, jstype, pytype):
    from pyodide.code import run_js

    array = run_js(f"new {jstype}([1, 2, 3, 4]);").to_py()
    print(array.format, array.tolist(), array.tobytes())
    assert array.format == pytype
    assert array.tolist() == [1, 2, 3, 4]
    import struct

    assert array.tobytes() == struct.pack(pytype * 4, 1, 2, 3, 4)


@run_in_pyodide
def test_array_buffer(selenium):
    from pyodide.code import run_js

    array = run_js("new ArrayBuffer(100);").to_py()
    assert len(array.tobytes()) == 100


def assert_js_to_py_to_js(selenium, name):
    selenium.run_js(f"self.obj = {name};")
    selenium.run("from js import obj")
    assert selenium.run_js(
        """
        let pyobj = pyodide.globals.get("obj");
        return pyobj === obj;
        """
    )


def assert_py_to_js_to_py(selenium, name):
    selenium.run_js(
        f"""
        self.obj = pyodide.runPython('{name}');
        pyodide.runPython(`
            from js import obj
            assert obj is {name}
        `);
        obj.destroy();
        """
    )


@run_in_pyodide
def test_recursive_list_to_js(selenium):
    x: Any = []
    x.append(x)
    from pyodide.ffi import to_js

    to_js(x)


@run_in_pyodide
def test_recursive_dict_to_js(selenium):
    x: Any = {}
    x[0] = x
    from pyodide.ffi import to_js

    to_js(x)


def test_list_js2py2js(selenium):
    selenium.run_js("self.x = [1,2,3];")
    assert_js_to_py_to_js(selenium, "x")


def test_dict_js2py2js(selenium):
    selenium.run_js("self.x = { a : 1, b : 2, 0 : 3 };")
    assert_js_to_py_to_js(selenium, "x")


def test_error_js2py2js(selenium):
    selenium.run_js("self.err = new Error('hello there?');")
    assert_js_to_py_to_js(selenium, "err")


def test_error_py2js2py(selenium):
    selenium.run("err = Exception('hello there?');")
    assert_py_to_js_to_py(selenium, "err")


def test_list_py2js2py(selenium):
    selenium.run("x = ['a', 'b']")
    assert_py_to_js_to_py(selenium, "x")


def test_dict_py2js2py(selenium):
    selenium.run("x = {'a' : 5, 'b' : 1}")
    assert_py_to_js_to_py(selenium, "x")


@run_in_pyodide
def test_jsproxy_attribute_error(selenium):
    import pytest

    from pyodide.code import run_js

    point = run_js(
        """
        class Point {
            constructor(x, y) {
                this.x = x;
                this.y = y;
            }
        }
        new Point(42, 43);
        """
    )
    assert point.y == 43

    with pytest.raises(AttributeError, match="z"):
        point.z

    del point.y
    with pytest.raises(AttributeError, match="y"):
        point.y

    assert run_js("(point) => point.y;")(point) is None


def test_javascript_error(selenium):
    msg = "JsException: Error: This is a js error"
    with pytest.raises(selenium.JavascriptException, match=msg):
        selenium.run(
            """
            from js import Error
            err = Error.new("This is a js error")
            err2 = Error.new("This is another js error")
            raise err
            """
        )


@run_in_pyodide
def test_javascript_error_back_to_js(selenium):
    from pyodide.code import run_js

    err = run_js('self.err = new Error("This is a js error"); err')
    assert type(err).__name__ == "JsException"
    assert run_js(
        """
        (py_err) => py_err === err;
        """
    )(err)


def test_memoryview_conversion(selenium):
    selenium.run(
        """
        import array
        a = array.array("Q", [1,2,3])
        b = array.array("u", "123")
        """
    )
    selenium.run_js(
        """
        pyodide.runPython("a").destroy()
        // Implicit assertion: this doesn't leave python error indicator set
        // (automatically checked in conftest.py)
        """
    )

    selenium.run_js(
        """
        pyodide.runPython("b").destroy()
        // Implicit assertion: this doesn't leave python error indicator set
        // (automatically checked in conftest.py)
        """
    )


def test_python2js_with_depth(selenium):
    selenium.run_js(
        """
        let x = pyodide.runPython(`
            class Test: pass
            [Test(), [Test(), [Test(), [Test()]]]]
        `);
        let Module = pyodide._module;
        let proxies = [];
        let proxies_id = Module.hiwire.new_value(proxies);
        let result = Module.hiwire.pop_value(Module._python2js_with_depth(x.$$.ptr, -1, proxies_id));
        Module.hiwire.decref(proxies_id);

        assert(() => proxies.length === 4);

        let result_proxies = [result[0], result[1][0], result[1][1][0], result[1][1][1][0]];
        proxies.sort((x, y) => x.$$.ptr < y.$$.ptr);
        result_proxies.sort((x, y) => x.$$.ptr < y.$$.ptr);
        for(let i = 0; i < 4; i++){
            assert(() => proxies[i] == result_proxies[i]);
        }
        x.destroy();
        for(let px of proxies){
            px.destroy();
        }
        """
    )


@pytest.mark.parametrize("ty", [list, tuple])
@run_in_pyodide
def test_tojs1(selenium, ty):
    import json

    from pyodide.code import run_js

    l = [1, 2, 3]
    x = ty(l)
    assert run_js("x => Array.isArray(x.toJs())")(x)
    serialized = run_js("x => JSON.stringify(x.toJs())")(x)
    assert l == json.loads(serialized)


@run_in_pyodide
def test_tojs2(selenium):
    import json

    from pyodide.code import run_js

    o = [(1, 2), (3, 4), [5, 6], {2: 3, 4: 9}]

    assert run_js("(o) => Array.isArray(o.toJs())")(o)
    serialized = run_js("(o) => JSON.stringify(o.toJs())")(o)
    assert json.loads(serialized) == [[1, 2], [3, 4], [5, 6], {}]
    serialized = run_js("(o) => JSON.stringify(Array.from(o.toJs()[3].entries()))")(o)
    assert json.loads(serialized) == [[2, 3], [4, 9]]


def test_tojs4(selenium):
    selenium.run_js(
        """
        let a = pyodide.runPython("[1,[2,[3,[4,[5,[6,[7]]]]]]]")
        for(let i=0; i < 7; i++){
            let x = a.toJs({depth : i});
            for(let j=0; j < i; j++){
                assert(() => Array.isArray(x), `i: ${i}, j: ${j}`);
                x = x[1];
            }
            assert(() => x instanceof pyodide.ffi.PyProxy, `i: ${i}, j: ${i}`);
            x.destroy();
        }
        a.destroy()
        """
    )


def test_tojs5(selenium):
    selenium.run_js(
        """
        let a = pyodide.runPython("[1, (2, (3, [4, (5, (6, [7]))]))]")
        for(let i=0; i < 7; i++){
            let x = a.toJs({depth : i});
            for(let j=0; j < i; j++){
                assert(() => Array.isArray(x), `i: ${i}, j: ${j}`);
                x = x[1];
            }
            assert(() => x instanceof pyodide.ffi.PyProxy, `i: ${i}, j: ${i}`);
            x.destroy();
        }
        a.destroy()
        """
    )


def test_tojs6(selenium):
    selenium.run_js(
        """
        let respy = pyodide.runPython(`
            a = [1, 2, 3, 4, 5]
            b = [a, a, a, a, a]
            [b, b, b, b, b]
        `);
        let total_refs = pyodide._module.hiwire.num_keys();
        let res = respy.toJs();
        let new_total_refs = pyodide._module.hiwire.num_keys();
        respy.destroy();
        assert(() => total_refs === new_total_refs);
        assert(() => res[0] === res[1]);
        assert(() => res[0][0] === res[1][1]);
        assert(() => res[4][0] === res[1][4]);
        """
    )


def test_tojs7(selenium):
    selenium.run_js(
        """
        let respy = pyodide.runPython(`
            a = [["b"]]
            b = [1,2,3, a[0]]
            a[0].append(b)
            a.append(b)
            a
        `);
        let total_refs = pyodide._module.hiwire.num_keys();
        let res = respy.toJs();
        let new_total_refs = pyodide._module.hiwire.num_keys();
        respy.destroy();
        assert(() => total_refs === new_total_refs);
        assert(() => res[0][0] === "b");
        assert(() => res[1][2] === 3);
        assert(() => res[1][3] === res[0]);
        assert(() => res[0][1] === res[1]);
        """
    )


@pytest.mark.skip_pyproxy_check
@run_in_pyodide
def test_tojs8(selenium):
    import pytest

    from pyodide.ffi import ConversionError, to_js

    msg = r"Cannot use \(2, 2\) as a key for a Javascript"
    with pytest.raises(ConversionError, match=msg):
        to_js({(2, 2): 0})

    with pytest.raises(ConversionError, match=msg):
        to_js({(2, 2)})


def test_tojs9(selenium):
    assert (
        set(
            selenium.run_js(
                """
                return Array.from(pyodide.runPython(`
                    from pyodide.ffi import to_js
                    to_js({ 1, "1" })
                `).values())
                """
            )
        )
        == {1, "1"}
    )

    assert (
        dict(
            selenium.run_js(
                """
                return Array.from(pyodide.runPython(`
                    from pyodide.ffi import to_js
                    to_js({ 1 : 7, "1" : 9 })
                `).entries())
                """
            )
        )
        == {1: 7, "1": 9}
    )


@run_in_pyodide
def test_to_py1(selenium):
    from pyodide.code import run_js

    a = run_js(
        """
        let a = new Map([[1, [1,2,new Set([1,2,3])]], [2, new Map([[1,2],[2,7]])]]);
        a.get(2).set("a", a);
        a;
        """
    )
    result = [repr(a.to_py(depth=i)) for i in range(4)]
    assert result == [
        "[object Map]",
        "{1: 1,2,[object Set], 2: [object Map]}",
        "{1: [1, 2, [object Set]], 2: {1: 2, 2: 7, 'a': [object Map]}}",
        "{1: [1, 2, {1, 2, 3}], 2: {1: 2, 2: 7, 'a': {...}}}",
    ]


@run_in_pyodide
def test_to_py2(selenium):
    from pyodide.code import run_js

    a = run_js(
        """
        let a = { "x" : 2, "y" : 7, "z" : [1,2] };
        a.z.push(a);
        a
        """
    )
    result = [repr(a.to_py(depth=i)) for i in range(4)]
    assert result == [
        "[object Object]",
        "{'x': 2, 'y': 7, 'z': 1,2,[object Object]}",
        "{'x': 2, 'y': 7, 'z': [1, 2, [object Object]]}",
        "{'x': 2, 'y': 7, 'z': [1, 2, {...}]}",
    ]


@run_in_pyodide
def test_to_py3(selenium):
    from pyodide.code import run_js

    a = run_js(
        """
        class Temp {
            constructor(){
                this.x = 2;
                this.y = 7;
            }
        }
        new Temp();
        """
    )
    assert repr(type(a.to_py())) == "<class 'pyodide.ffi.JsProxy'>"


@pytest.mark.parametrize(
    "obj, msg",
    [
        ("Map([[[1,1], 2]])", "Cannot use key of type Array as a key to a Python dict"),
        ("Set([[1,1]])", "Cannot use key of type Array as a key to a Python set"),
        ("Map([[0, 2], [false, 3]])", "contains both 0 and false"),
        ("Set([0, false])", "contains both 0 and false"),
        ("Map([[1, 2], [true, 3]])", "contains both 1 and true"),
        ("Set([1, true])", "contains both 1 and true"),
    ],
)
@run_in_pyodide
def test_to_py4(selenium, obj, msg):
    import pytest

    from pyodide.code import run_js
    from pyodide.ffi import ConversionError, JsException

    a = run_js(f"new {obj}")

    with pytest.raises((ConversionError, JsException), match=msg):
        a.to_py()


@run_in_pyodide
def test_to_py_default_converter(selenium):
    from pyodide.code import run_js

    [r1, r2] = run_js(
        """
        class Pair {
            constructor(first, second){
                this.first = first;
                this.second = second;
            }
        }
        let l = [1,2,3];
        const r1 = new Pair(l, [l]);
        const r2 = new Pair(l, [l]);
        r2.first = r2;
        const opts = {defaultConverter(value, converter, cache){
            if(value.constructor.name !== "Pair"){
                return value;
            }
            let list = pyodide.globals.get("list");
            l = list();
            list.destroy();
            cache(value, l);
            const first = converter(value.first);
            const second = converter(value.second);
            l.append(first);
            l.append(second);
            first.destroy();
            second.destroy();
            return l;
        }};
        pyodide.toPy([r1, r2], opts);
        """
    )

    assert isinstance(r1, list)
    assert r1[0] is r1[1][0]
    assert r1[0] == [1, 2, 3]
    assert r2[0] is r2


@run_in_pyodide
def test_to_py_default_converter2(selenium):
    from typing import Any

    from pyodide.code import run_js

    [p1, p2] = run_js(
        """
        class Pair {
            constructor(first, second){
                this.first = first;
                this.second = second;
            }
        }
        const l = [1,2,3];
        const r1 = new Pair(l, [l]);
        const r2 = new Pair(l, [l]);
        r2.first = r2;
        [r1, r2]
        """
    )

    def default_converter(value, converter, cache):
        if value.constructor.name != "Pair":
            return value
        l: list[Any] = []
        cache(value, l)
        l.append(converter(value.first))
        l.append(converter(value.second))
        return l

    r1 = p1.to_py(default_converter=default_converter)
    assert isinstance(r1, list)
    assert r1[0] is r1[1][0]
    assert r1[0] == [1, 2, 3]

    r2 = p2.to_py(default_converter=default_converter)
    assert r2[0] is r2


def test_to_js_default_converter(selenium):
    selenium.run_js(
        """
        p = pyodide.runPython(`
        class Pair:
            def __init__(self, first, second):
                self.first = first
                self.second = second
        p = Pair(1,2)
        p
        `);
        let res = p.toJs({ default_converter(x, convert, cacheConversion){
            let result = [];
            cacheConversion(x, result);
            result.push(convert(x.first));
            result.push(convert(x.second));
            return result;
        }});
        assert(() => res[0] === 1);
        assert(() => res[1] === 2);
        p.first = p;
        let res2 = p.toJs({ default_converter(x, convert, cacheConversion){
            let result = [];
            cacheConversion(x, result);
            result.push(convert(x.first));
            result.push(convert(x.second));
            return result;
        }});
        assert(() => res2[0] === res2);
        assert(() => res2[1] === 2);
        p.destroy();
        """
    )


@run_in_pyodide
def test_to_js_default_converter2(selenium):
    import json

    import pytest

    from js import JSON, Array
    from pyodide.code import run_js
    from pyodide.ffi import JsException, to_js

    class Pair:
        __slots__ = ("first", "second")

        def __init__(self, first, second):
            self.first = first
            self.second = second

    p1 = Pair(1, 2)
    p2 = Pair(1, 2)
    p2.first = p2

    def default_converter(value, convert, cacheConversion):
        result = Array.new()
        cacheConversion(value, result)
        result.push(convert(value.first))
        result.push(convert(value.second))
        return result

    p1js = to_js(p1, default_converter=default_converter)
    p2js = to_js(p2, default_converter=default_converter)

    assert json.loads(JSON.stringify(p1js)) == [1, 2]

    with pytest.raises(JsException, match="TypeError"):
        JSON.stringify(p2js)

    assert run_js("(x) => x[0] === x")(p2js)
    assert run_js("(x) => x[1] === 2")(p2js)


def test_buffer_format_string(selenium):
    errors = [
        ["aaa", "Expected format string to have length <= 2, got 'aaa'"],
        ["II", "Unrecognized alignment character I."],
        ["x", "Unrecognized format character 'x'."],
        ["x", "Unrecognized format character 'x'."],
        ["e", "Javascript has no Float16 support."],
    ]
    for fmt, msg in errors:
        with pytest.raises(selenium.JavascriptException, match=msg):
            selenium.run_js(
                f"""
                pyodide._module.processBufferFormatString({fmt!r});
                """
            )

    format_tests = [
        ("c", "Uint8"),
        ("b", "Int8"),
        ("B", "Uint8"),
        ("?", "Uint8"),
        ("h", "Int16"),
        ("H", "Uint16"),
        ("i", "Int32"),
        ("I", "Uint32"),
        ("l", "Int32"),
        ("L", "Uint32"),
        ("n", "Int32"),
        ("N", "Uint32"),
        ("q", "BigInt64"),
        ("Q", "BigUint64"),
        ("f", "Float32"),
        ("d", "Float64"),
        ("s", "Uint8"),
        ("p", "Uint8"),
        ("P", "Uint32"),
    ]

    def process_fmt_string(fmt):
        return selenium.run_js(
            f"""
            let [array, is_big_endian] = pyodide._module.processBufferFormatString({fmt!r});
            if(!array || typeof array.name !== "string" || !array.name.endsWith("Array")){{
                throw new Error("Unexpected output on input {fmt}: " + array);
            }}
            let arrayName = array.name.slice(0, -"Array".length);
            return [arrayName, is_big_endian];
            """
        )

    for fmt, expected_array_name in format_tests:
        [array_name, is_big_endian] = process_fmt_string(fmt)
        assert not is_big_endian
        assert array_name == expected_array_name

    endian_tests = [
        ("@h", "Int16", False),
        ("=H", "Uint16", False),
        ("<i", "Int32", False),
        (">I", "Uint32", True),
        ("!l", "Int32", True),
    ]

    for fmt, expected_array_name, expected_is_big_endian in endian_tests:
        [array_name, is_big_endian] = process_fmt_string(fmt)
        assert is_big_endian == expected_is_big_endian
        assert array_name == expected_array_name


def test_dict_converter_cache(selenium):
    selenium.run_js(
        """
        let d1 = pyodide.runPython('d={0: {1: 2}}; d[1]=d[0]; d');
        let d = d1.toJs({dict_converter: Object.fromEntries});
        d1.destroy();
        assert(() => d[0] === d[1]);
        """
    )


@pytest.mark.parametrize("n", [1 << 31, 1 << 32, 1 << 33, 1 << 63, 1 << 64, 1 << 65])
@run_in_pyodide
def test_very_large_length(selenium, n):
    from unittest import TestCase

    from pyodide.code import run_js

    raises = TestCase().assertRaises(
        OverflowError, msg=f"length {n} of object is larger than INT_MAX (2147483647)"
    )

    o = run_js(f"({{length : {n}}})")
    with raises:
        len(o)

    # 1. Set toStringTag to NodeList to force JsProxy to feature detect this object
    # as an array
    # 2. Return a very large length
    # 3. JsProxy_subscript_array should successfully handle this and propagate the error.
    a = run_js(f"({{[Symbol.toStringTag] : 'NodeList', length: {n}}})")
    with raises:
        a[-1]


@pytest.mark.parametrize(
    "n", [-1, -2, -3, -100, -1 << 31, -1 << 32, -1 << 33, -1 << 63, -1 << 64, -1 << 65]
)
@run_in_pyodide
def test_negative_length(selenium, n):
    from unittest import TestCase

    from pyodide.code import run_js

    raises = TestCase().assertRaises(
        ValueError, msg=f"length {n} of object is negative"
    )

    o = run_js(f"({{length : {n}}})")
    with raises:
        len(o)

    # 1. Set toStringTag to NodeList to force JsProxy to feature detect this object
    # as an array
    # 2. Return a negative length
    # 3. JsProxy_subscript_array should successfully handle this and propagate the error.
    a = run_js(f"({{[Symbol.toStringTag] : 'NodeList', length: {n}}})")
    with raises:
        a[-1]


@std_hypothesis_settings
@given(l=st.lists(st.integers()), slice=st.slices(50))
@example(l=[0, 1], slice=slice(None, None, -1))
@example(l=list(range(4)), slice=slice(None, None, -2))
@example(l=list(range(10)), slice=slice(-1, 12))
@example(l=list(range(10)), slice=slice(12, -1))
@example(l=list(range(10)), slice=slice(12, -1, -1))
@example(l=list(range(10)), slice=slice(-1, 12, 2))
@example(l=list(range(10)), slice=slice(12, -1, -1))
@example(l=list(range(10)), slice=slice(12, -1, -2))
@run_in_pyodide
def test_array_slices(selenium, l, slice):
    expected = l[slice]
    from pyodide.ffi import JsArray, to_js

    jsl = to_js(l)
    assert isinstance(jsl, JsArray)
    result = jsl[slice]
    assert result.to_py() == expected


@std_hypothesis_settings
@given(l=st.lists(st.integers()), slice=st.slices(50))
@example(l=[0, 1], slice=slice(None, None, -1))
@example(l=list(range(4)), slice=slice(None, None, -2))
@example(l=list(range(10)), slice=slice(-1, 12))
@example(l=list(range(10)), slice=slice(12, -1))
@example(l=list(range(10)), slice=slice(12, -1, -1))
@example(l=list(range(10)), slice=slice(-1, 12, 2))
@example(l=list(range(10)), slice=slice(12, -1, -1))
@example(l=list(range(10)), slice=slice(12, -1, -2))
@run_in_pyodide
def test_array_slice_del(selenium, l, slice):
    from pyodide.ffi import JsArray, to_js

    jsl = to_js(l)
    assert isinstance(jsl, JsArray)
    del l[slice]
    del jsl[slice]
    assert jsl.to_py() == l


@st.composite
def list_slice_and_value(draw):
    l = draw(st.lists(st.integers()))
    step_one = draw(st.booleans())
    if step_one:
        start = draw(st.integers(0, max(len(l) - 1, 0)) | st.none())
        stop = draw(st.integers(start, len(l)) | st.none())
        if draw(st.booleans()) and start is not None:
            start -= len(l)
        if draw(st.booleans()) and stop is not None:
            stop -= len(l)
        s = slice(start, stop)
        vals = draw(st.lists(st.integers()))
    else:
        s = draw(st.slices(50))
        vals_len = len(l[s])
        vals = draw(st.lists(st.integers(), min_size=vals_len, max_size=vals_len))
    return (l, s, vals)


@std_hypothesis_settings
@given(lsv=list_slice_and_value())
@example(lsv=(list(range(5)), slice(5, 2), []))
@example(lsv=(list(range(5)), slice(2, 5, -1), []))
@example(lsv=(list(range(5)), slice(5, 2), [-1, -2, -3]))
@run_in_pyodide
def test_array_slice_assign_1(selenium, lsv):
    from pyodide.ffi import JsArray, to_js

    [l, s, v] = lsv
    jsl = to_js(l)
    assert isinstance(jsl, JsArray)
    l[s] = v
    jsl[s] = v
    assert jsl.to_py() == l


@run_in_pyodide
def test_array_slice_assign_2(selenium):
    import pytest

    from pyodide.ffi import JsArray, to_js

    l = list(range(10))
    with pytest.raises(ValueError) as exc_info_1a:
        l[0:4:2] = [1, 2, 3, 4]

    jsl = to_js(l)
    assert isinstance(jsl, JsArray)
    with pytest.raises(ValueError) as exc_info_1b:
        jsl[0:4:2] = [1, 2, 3, 4]

    l = list(range(10))
    with pytest.raises(ValueError) as exc_info_2a:
        l[0:4:2] = []

    with pytest.raises(ValueError) as exc_info_2b:
        jsl[0:4:2] = []

    with pytest.raises(TypeError) as exc_info_3a:
        l[:] = 1  # type: ignore[call-overload]

    with pytest.raises(TypeError) as exc_info_3b:
        jsl[:] = 1

    assert exc_info_1a.value.args == exc_info_1b.value.args
    assert exc_info_2a.value.args == exc_info_2b.value.args
    assert exc_info_3a.value.args == exc_info_3b.value.args


@std_hypothesis_settings
@given(l1=st.lists(st.integers()), l2=st.lists(st.integers()))
@example(l1=[], l2=[])
@example(l1=[], l2=[1])
@run_in_pyodide
def test_array_extend(selenium_module_scope, l1, l2):
    from pyodide.ffi import to_js

    l1js1 = to_js(l1)
    l1js1.extend(l2)

    l1js2 = to_js(l1)
    l1js2 += l2

    l1.extend(l2)

    assert l1 == l1js1.to_py()
    assert l1 == l1js2.to_py()


@run_in_pyodide
def test_typed_array(selenium):
    from pyodide.code import run_js

    a = run_js("self.a = new Uint8Array([1,2,3,4]); a")
    assert a[0] == 1
    assert a[-1] == 4
    a[-2] = 7
    assert run_js("self.a[2]") == 7

    import pytest

    with pytest.raises(ValueError, match="cannot delete array elements"):
        del a[0]

    msg = "Slice subscripting isn't implemented for typed arrays"
    with pytest.raises(NotImplementedError, match=msg):
        a[:]

    msg = "Slice assignment isn't implemented for typed arrays"
    with pytest.raises(NotImplementedError, match=msg):
        a[:] = [-1, -2, -3, -4]

    assert not hasattr(a, "extend")
    with pytest.raises(TypeError):
        a += [1, 2, 3]


@pytest.mark.xfail_browsers(node="No document in node")
@run_in_pyodide
def test_html_array(selenium):
    from pyodide.code import run_js

    x = run_js("document.querySelectorAll('*')")
    assert run_js("(a, b) => a === b[0]")(x[0], x)
    assert run_js("(a, b) => a === Array.from(b).pop()")(x[-1], x)

    import pytest

    with pytest.raises(TypeError, match="does ?n[o']t support item assignment"):
        x[0] = 0

    with pytest.raises(TypeError, match="does ?n[o']t support item deletion"):
        del x[0]
