import concurrent.futures
import json
import unittest

import quickjs


class LoadModule(unittest.TestCase):
    def test_42(self):
        self.assertEqual(quickjs.test(), 42)


class Context(unittest.TestCase):
    def setUp(self):
        self.context = quickjs.Context()

    def test_eval_int(self):
        self.assertEqual(self.context.eval("40 + 2"), 42)

    def test_eval_float(self):
        self.assertEqual(self.context.eval("40.0 + 2.0"), 42.0)

    def test_eval_str(self):
        self.assertEqual(self.context.eval("'4' + '2'"), "42")

    def test_eval_bool(self):
        self.assertEqual(self.context.eval("true || false"), True)
        self.assertEqual(self.context.eval("true && false"), False)

    def test_eval_null(self):
        self.assertIsNone(self.context.eval("null"))

    def test_eval_undefined(self):
        self.assertIsNone(self.context.eval("undefined"))

    def test_wrong_type(self):
        with self.assertRaises(TypeError):
            self.assertEqual(self.context.eval(1), 42)

    def test_context_between_calls(self):
        self.context.eval("x = 40; y = 2;")
        self.assertEqual(self.context.eval("x + y"), 42)

    def test_function(self):
        self.context.eval("""
            function special(x) {
                return 40 + x;
            }
            """)
        self.assertEqual(self.context.eval("special(2)"), 42)

    def test_get(self):
        self.context.eval("x = 42; y = 'foo';")
        self.assertEqual(self.context.get("x"), 42)
        self.assertEqual(self.context.get("y"), "foo")
        self.assertEqual(self.context.get("z"), None)

    def test_set(self):
        self.context.eval("x = 'overriden'")
        self.context.set("x", 42)
        self.context.set("y", "foo")
        self.assertTrue(self.context.eval("x == 42"))
        self.assertTrue(self.context.eval("y == 'foo'"))

    def test_module(self):
        self.context.module("""
            export function test() {
                return 42;
            }
        """)

    def test_error(self):
        with self.assertRaisesRegex(quickjs.JSException, "ReferenceError: 'missing' is not defined"):
            self.context.eval("missing + missing")

    def test_lifetime(self):
        def get_f():
            context = quickjs.Context()
            f = context.eval("""
            a = function(x) {
                return 40 + x;
            }
            """)
            return f

        f = get_f()
        self.assertTrue(f)
        # The context has left the scope after f. f needs to keep the context alive for the
        # its lifetime. Otherwise, we will get problems.

    def test_backtrace(self):
        try:
            self.context.eval("""
                function funcA(x) {
                    x.a.b = 1;
                }
                function funcB(x) {
                    funcA(x);
                }
                funcB({});
            """)
        except Exception as e:
            msg = str(e)
        else:
            self.fail("Expected exception.")

        self.assertIn("at funcA (<input>:3)\n", msg)
        self.assertIn("at funcB (<input>:6)\n", msg)

    def test_memory_limit(self):
        code = """
            (function() {
                let arr = [];
                for (let i = 0; i < 1000; ++i) {
                    arr.push(i);
                }
            })();
        """
        self.context.eval(code)
        self.context.set_memory_limit(1000)
        with self.assertRaisesRegex(quickjs.JSException, "null"):
            self.context.eval(code)
        self.context.set_memory_limit(1000000)
        self.context.eval(code)

    def test_time_limit(self):
        code = """
            (function() {
                let arr = [];
                for (let i = 0; i < 100000; ++i) {
                    arr.push(i);
                }
                return arr;
            })();
        """
        self.context.eval(code)
        self.context.set_time_limit(0)
        with self.assertRaisesRegex(quickjs.JSException, "InternalError: interrupted"):
            self.context.eval(code)
        self.context.set_time_limit(-1)
        self.context.eval(code)

    def test_memory_usage(self):
        self.assertIn("memory_used_size", self.context.memory().keys())

    def test_json_simple(self):
        self.assertEqual(self.context.parse_json("42"), 42)

    def test_json_error(self):
        with self.assertRaisesRegex(quickjs.JSException, "unexpected token"):
            self.context.parse_json("a b c")

    def test_execute_pending_job(self):
        self.context.eval("obj = {}")
        self.assertEqual(self.context.execute_pending_job(), False)
        self.context.eval("Promise.resolve().then(() => {obj.x = 1;})")
        self.assertEqual(self.context.execute_pending_job(), True)
        self.assertEqual(self.context.eval("obj.x"), 1)
        self.assertEqual(self.context.execute_pending_job(), False)


class CallIntoPython(unittest.TestCase):
    def setUp(self):
        self.context = quickjs.Context()

    def test_make_function(self):
        self.context.add_callable("f", lambda x: x + 2)
        self.assertEqual(self.context.eval("f(40)"), 42)

    def test_make_two_functions(self):
        for i in range(10):
            self.context.add_callable("f", lambda x: i + x + 2)
            self.context.add_callable("g", lambda x: i + x + 40)
            f = self.context.get("f")
            g = self.context.get("g")
            self.assertEqual(f(40) - i, 42)
            self.assertEqual(g(2) - i, 42)
            self.assertEqual(self.context.eval("((f, a) => f(a))")(f, 40) - i, 42)

    def test_make_function_call_from_js(self):
        self.context.add_callable("f", lambda x: x + 2)
        g = self.context.eval("""(
            function() {
                return f(20) + 20;
            }
        )""")
        self.assertEqual(g(), 42)

    def test_python_function_raises(self):
        def error(a):
            raise ValueError("A")

        self.context.add_callable("error", error)
        with self.assertRaisesRegex(quickjs.JSException, "Python call failed"):
            self.context.eval("error(0)")

    def test_make_function_two_args(self):
        def concat(a, b):
            return a + b

        self.context.add_callable("concat", concat)
        result = self.context.eval("concat(40, 2)")
        self.assertEqual(result, 42)

        concat = self.context.get("concat")
        result = self.context.eval("((f, a, b) => 22 + f(a, b))")(concat, 10, 10)
        self.assertEqual(result, 42)

    def test_make_function_two_string_args(self):
        """Without the JS_DupValue in js_c_function, this test crashes."""
        def concat(a, b):
            return a + "-" + b

        self.context.add_callable("concat", concat)
        concat = self.context.get("concat")
        result = concat("aaa", "bbb")
        self.assertEqual(result, "aaa-bbb")

    def test_can_eval_in_same_context(self):
        self.context.add_callable("f", lambda: 40 + self.context.eval("1 + 1"))
        self.assertEqual(self.context.eval("f()"), 42)

    def test_can_call_in_same_context(self):
        inner = self.context.eval("(function() { return 42; })")
        self.context.add_callable("f", lambda: inner())
        self.assertEqual(self.context.eval("f()"), 42)

    def test_invalid_argument(self):
        self.context.add_callable("p", lambda: 42)
        self.assertEqual(self.context.eval("p()"), 42)
        with self.assertRaisesRegex(quickjs.JSException, "Python call failed"):
            self.context.eval("p(1)")
        with self.assertRaisesRegex(quickjs.JSException, "Python call failed"):
            self.context.eval("p({})")

    def test_time_limit_disallowed(self):
        self.context.add_callable("f", lambda x: x + 2)
        self.context.set_time_limit(1000)
        with self.assertRaises(quickjs.JSException):
            self.context.eval("f(40)")

    def test_conversion_failure_does_not_raise_system_error(self):
        # https://github.com/PetterS/quickjs/issues/38

        def test_list():
            return [1, 2, 3]

        self.context.add_callable("test_list", test_list)
        with self.assertRaises(quickjs.JSException):
            # With incorrect error handling, this (safely) made Python raise a SystemError
            # instead of a JS exception.
            self.context.eval("test_list()")


class Object(unittest.TestCase):
    def setUp(self):
        self.context = quickjs.Context()

    def test_function_is_object(self):
        f = self.context.eval("""
            a = function(x) {
                return 40 + x;
            }
            """)
        self.assertIsInstance(f, quickjs.Object)

    def test_function_call_int(self):
        f = self.context.eval("""
            f = function(x) {
                return 40 + x;
            }
            """)
        self.assertEqual(f(2), 42)

    def test_function_call_int_two_args(self):
        f = self.context.eval("""
            f = function(x, y) {
                return 40 + x + y;
            }
            """)
        self.assertEqual(f(3, -1), 42)

    def test_function_call_many_times(self):
        n = 1000
        f = self.context.eval("""
            f = function(x, y) {
                return x + y;
            }
            """)
        s = 0
        for i in range(n):
            s += f(1, 1)
        self.assertEqual(s, 2 * n)

    def test_function_call_str(self):
        f = self.context.eval("""
            f = function(a) {
                return a + " hej";
            }
            """)
        self.assertEqual(f("1"), "1 hej")

    def test_function_call_str_three_args(self):
        f = self.context.eval("""
            f = function(a, b, c) {
                return a + " hej " + b + " ho " + c;
            }
            """)
        self.assertEqual(f("1", "2", "3"), "1 hej 2 ho 3")

    def test_function_call_object(self):
        d = self.context.eval("d = {data: 42};")
        f = self.context.eval("""
            f = function(d) {
                return d.data;
            }
            """)
        self.assertEqual(f(d), 42)
        # Try again to make sure refcounting works.
        self.assertEqual(f(d), 42)
        self.assertEqual(f(d), 42)

    def test_function_call_unsupported_arg(self):
        f = self.context.eval("""
            f = function(x) {
                return 40 + x;
            }
            """)
        with self.assertRaisesRegex(TypeError, "Unsupported type"):
            self.assertEqual(f({}), 42)

    def test_json(self):
        d = self.context.eval("d = {data: 42};")
        self.assertEqual(json.loads(d.json()), {"data": 42})

    def test_call_nonfunction(self):
        d = self.context.eval("({data: 42})")
        with self.assertRaisesRegex(quickjs.JSException, "TypeError: not a function"):
            d(1)

    def test_wrong_context(self):
        context1 = quickjs.Context()
        context2 = quickjs.Context()
        f = context1.eval("(function(x) { return x.a; })")
        d = context2.eval("({a: 1})")
        with self.assertRaisesRegex(ValueError, "Can not mix JS objects from different contexts."):
            f(d)

    def test_get(self):
        self.context.eval("a = {x: 42, y: 'foo'};")
        a = self.context.get_global().get("a")
        self.assertEqual(a.get("x"), 42)
        self.assertEqual(a.get("y"), "foo")
        self.assertEqual(a.get("z"), None)

    def test_set(self):
        self.context.eval("a = {x: 'overridden'}")
        a = self.context.get_global().get("a")
        a.set("x", 42)
        a.set("y", "foo")
        self.assertTrue(self.context.eval("a.x == 42"))
        self.assertTrue(self.context.eval("a.y == 'foo'"))

    def test_make_function(self):
        print(11)
        self.context.get_global().set("f", lambda x: x + 2)
        self.assertEqual(self.context.eval("f(40)"), 42)


class FunctionTest(unittest.TestCase):
    def test_adder(self):
        f = quickjs.Function(
            "adder", """
            function adder(x, y) {
                return x + y;
            }
            """)
        self.assertEqual(f(1, 1), 2)
        self.assertEqual(f(100, 200), 300)
        self.assertEqual(f("a", "b"), "ab")

    def test_identity(self):
        identity = quickjs.Function(
            "identity", """
            function identity(x) {
                return x;
            }
            """)
        for x in [True, [1], {"a": 2}, 1, 1.5, "hej", None]:
            self.assertEqual(identity(x), x)

    def test_bool(self):
        f = quickjs.Function(
            "f", """
            function f(x) {
                return [typeof x ,!x];
            }
            """)
        self.assertEqual(f(False), ["boolean", True])
        self.assertEqual(f(True), ["boolean", False])

    def test_empty(self):
        f = quickjs.Function("f", "function f() { }")
        self.assertEqual(f(), None)

    def test_lists(self):
        f = quickjs.Function(
            "f", """
            function f(arr) {
                const result = [];
                arr.forEach(function(elem) {
                    result.push(elem + 42);
                });
                return result;
            }""")
        self.assertEqual(f([0, 1, 2]), [42, 43, 44])

    def test_dict(self):
        f = quickjs.Function(
            "f", """
            function f(obj) {
                return obj.data;
            }""")
        self.assertEqual(f({"data": {"value": 42}}), {"value": 42})

    def test_time_limit(self):
        f = quickjs.Function(
            "f", """
            function f() {
                let arr = [];
                for (let i = 0; i < 100000; ++i) {
                    arr.push(i);
                }
                return arr;
            }
        """)
        f()
        f.set_time_limit(0)
        with self.assertRaisesRegex(quickjs.JSException, "InternalError: interrupted"):
            f()
        f.set_time_limit(-1)
        f()

    def test_garbage_collection(self):
        f = quickjs.Function(
            "f", """
            function f() {
                let a = {};
                let b = {};
                a.b = b;
                b.a = a;
                a.i = 42;
                return a.i;
            }
        """)
        initial_count = f.memory()["obj_count"]
        for i in range(10):
            prev_count = f.memory()["obj_count"]
            self.assertEqual(f(run_gc=False), 42)
            current_count = f.memory()["obj_count"]
            self.assertGreater(current_count, prev_count)

        f.gc()
        self.assertLessEqual(f.memory()["obj_count"], initial_count)

    def test_deep_recursion(self):
        f = quickjs.Function(
            "f", """
            function f(v) {
                if (v <= 0) {
                    return 0;
                } else {
                    return 1 + f(v - 1);
                }
            }
        """)

        self.assertEqual(f(100), 100)
        limit = 500
        with self.assertRaises(quickjs.StackOverflow):
            f(limit)
        f.set_max_stack_size(2000 * limit)
        self.assertEqual(f(limit), limit)

    def test_add_callable(self):
        f = quickjs.Function(
            "f", """
            function f() {
                return pfunc();
            }
        """)
        f.add_callable("pfunc", lambda: 42)

        self.assertEqual(f(), 42)

    def test_execute_pending_job(self):
        f = quickjs.Function(
            "f", """
            obj = {x: 0, y: 0};
            async function a() {
                obj.x = await 1;
            }
            a();
            Promise.resolve().then(() => {obj.y = 1});
            function f() {
                return obj.x + obj.y;
            }
        """)
        self.assertEqual(f(), 0)
        self.assertEqual(f.execute_pending_job(), True)
        self.assertEqual(f(), 1)
        self.assertEqual(f.execute_pending_job(), True)
        self.assertEqual(f(), 2)
        self.assertEqual(f.execute_pending_job(), False)


class JavascriptFeatures(unittest.TestCase):
    def test_unicode_strings(self):
        identity = quickjs.Function(
            "identity", """
            function identity(x) {
                return x;
            }
            """)
        context = quickjs.Context()
        for x in ["äpple", "≤≥", "☺"]:
            self.assertEqual(identity(x), x)
            self.assertEqual(context.eval('(function(){ return "' + x + '";})()'), x)

    def test_es2020_optional_chaining(self):
        f = quickjs.Function(
            "f", """
            function f(x) {
                return x?.one?.two;
            }
        """)
        self.assertIsNone(f({}))
        self.assertIsNone(f({"one": 12}))
        self.assertEqual(f({"one": {"two": 42}}), 42)

    def test_es2020_null_coalescing(self):
        f = quickjs.Function(
            "f", """
            function f(x) {
                return x ?? 42;
            }
        """)
        self.assertEqual(f(""), "")
        self.assertEqual(f(0), 0)
        self.assertEqual(f(11), 11)
        self.assertEqual(f(None), 42)

    def test_symbol_conversion(self):
        context = quickjs.Context()
        context.eval("a = Symbol();")
        context.set("b", context.eval("a"))
        self.assertTrue(context.eval("a === b"))

    def test_large_python_integers_to_quickjs(self):
        context = quickjs.Context()
        # Without a careful implementation, this made Python raise a SystemError/OverflowError.
        context.set("v", 10**25)
        # There is precision loss occurring in JS due to
        # the floating point implementation of numbers.
        self.assertTrue(context.eval("v == 1e25"))

    def test_bigint(self):
        context = quickjs.Context()
        self.assertEqual(context.eval(f"BigInt('{10**100}')"), 10**100)
        self.assertEqual(context.eval(f"BigInt('{-10**100}')"), -10**100)

class Threads(unittest.TestCase):
    def setUp(self):
        self.context = quickjs.Context()
        self.executor = concurrent.futures.ThreadPoolExecutor()

    def tearDown(self):
        self.executor.shutdown()

    def test_concurrent(self):
        """Demonstrates that the execution will crash unless the function executes on the same
           thread every time.

           If the executor in Function is not present, this test will fail.
        """
        data = list(range(1000))
        jssum = quickjs.Function(
            "sum", """
                function sum(data) {
                    return data.reduce((a, b) => a + b, 0)
                }
            """)

        futures = [self.executor.submit(jssum, data) for _ in range(10)]
        expected = sum(data)
        for future in concurrent.futures.as_completed(futures):
            self.assertEqual(future.result(), expected)

    def test_concurrent_own_executor(self):
        data = list(range(1000))
        jssum1 = quickjs.Function("sum",
                                  """
                                    function sum(data) {
                                        return data.reduce((a, b) => a + b, 0)
                                    }
                                  """,
                                  own_executor=True)
        jssum2 = quickjs.Function("sum",
                                  """
                                    function sum(data) {
                                        return data.reduce((a, b) => a + b, 0)
                                    }
                                  """,
                                  own_executor=True)

        futures = [self.executor.submit(f, data) for _ in range(10) for f in (jssum1, jssum2)]
        expected = sum(data)
        for future in concurrent.futures.as_completed(futures):
            self.assertEqual(future.result(), expected)


class QJS(object):
    def __init__(self):
        self.interp = quickjs.Context()
        self.interp.eval('var foo = "bar";')


class QuickJSContextInClass(unittest.TestCase):
    def test_github_issue_7(self):
        # This used to give stack overflow internal error, due to how QuickJS calculates stack
        # frames. Passes with the 2021-03-27 release.
        # 
        # TODO: Use the new JS_UpdateStackTop function in order to better handle stacks.
        qjs = QJS()
        self.assertEqual(qjs.interp.eval('2+2'), 4)
