# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: BSD-2-Clause

from numba import cuda
from numba.core.errors import TypingError
from numba.cuda.testing import CUDATestCase, skip_on_cudasim
import numpy as np
import unittest


class TestSelfRecursion(CUDATestCase):
    def setUp(self):
        # Avoid importing this module at the top level, as it triggers
        # compilation and can therefore fail
        from numba.cuda.tests.cudapy import recursion_usecases

        self.mod = recursion_usecases
        super().setUp()

    def check_fib(self, cfunc):
        @cuda.jit
        def kernel(r, x):
            r[0] = cfunc(x[0])

        x = np.asarray([10], dtype=np.int64)
        r = np.zeros_like(x)
        kernel[1, 1](r, x)

        actual = r[0]
        expected = 55
        self.assertPreciseEqual(actual, expected)

    def test_global_explicit_sig(self):
        self.check_fib(self.mod.fib1)

    def test_inner_explicit_sig(self):
        self.check_fib(self.mod.fib2)

    def test_global_implicit_sig(self):
        self.check_fib(self.mod.fib3)

    @skip_on_cudasim("Simulator does not compile")
    def test_runaway(self):
        with self.assertRaises(TypingError) as raises:
            cfunc = self.mod.runaway_self

            @cuda.jit("void()")
            def kernel():
                cfunc(1)

        self.assertIn(
            "cannot type infer runaway recursion", str(raises.exception)
        )

    @unittest.skip("Needs insert_unresolved_ref support in target")
    def test_type_change(self):
        pfunc = self.mod.type_change_self.py_func
        cfunc = self.mod.type_change_self

        @cuda.jit
        def kernel(r, x, y):
            r[0] = cfunc(x[0], y[0])

        args = 13, 0.125
        x = np.asarray([args[0]], dtype=np.int64)
        y = np.asarray([args[1]], dtype=np.float64)
        r = np.zeros_like(x)

        kernel[1, 1](r, x, y)

        expected = pfunc(*args)
        actual = r[0]

        self.assertPreciseEqual(actual, expected)

    @unittest.expectedFailure
    def test_raise(self):
        # This is an expected failure because reporting of exceptions raised in
        # device functions does not work correctly - see Issue #8036:
        # https://github.com/numba/numba/issues/8036
        with self.assertRaises(ValueError) as raises:
            self.mod.raise_self_kernel[1, 1](3)

        self.assertEqual(str(raises.exception), "raise_self")

    @unittest.skip("Needs insert_unresolved_ref support in target")
    def test_optional_return(self):
        pfunc = self.mod.make_optional_return_case()
        cfunc = self.mod.make_optional_return_case(cuda.jit)

        @cuda.jit
        def kernel(r, x):
            res = cfunc(x[0])
            if res is None:
                res = 999
            r[0] = res

        def cpu_kernel(x):
            res = pfunc(x)
            if res is None:
                res = 999
            return res

        for arg in (0, 5, 10, 15):
            expected = cpu_kernel(arg)
            x = np.asarray([arg], dtype=np.int64)
            r = np.zeros_like(x)
            kernel[1, 1](r, x)
            actual = r[0]

            self.assertEqual(expected, actual)

    @skip_on_cudasim("Recursion handled because simulator does not compile")
    def test_growing_return_tuple(self):
        cfunc = self.mod.make_growing_tuple_case(cuda.jit)

        with self.assertRaises(TypingError) as raises:

            @cuda.jit("void()")
            def kernel():
                cfunc(100)

        self.assertIn(
            "Return type of recursive function does not converge",
            str(raises.exception),
        )


if __name__ == "__main__":
    unittest.main()
