"""
Test lldb data formatter subsystem.
"""

import lldb
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
from lldbsuite.test import lldbutil


class TestCoroutineHandle(TestBase):
    SHARED_BUILD_TESTCASE = False

    def do_test(self):
        """Test std::coroutine_handle is displayed correctly."""
        is_clang = self.expectedCompiler(["clang"])

        # Clang <= 20 used to also name the resume/destroy functions
        # as `my_generator_func`.
        # Never versions of clang name the clones as `.resume`/`.destroy`.
        test_generator_func_ptr_re = re.compile(
            r"^\(a.out`my_generator_func\(\)( \(\..*\))? at main.cpp:[0-9]*\)$"
        )

        # Run until the initial suspension point
        lldbutil.run_to_source_breakpoint(
            self, "// Break at initial_suspend", lldb.SBFileSpec("main.cpp", False)
        )

        if self.frame().FindVariable("is_supported").GetValueAsUnsigned(1) == 0:
            self.skipTest("c++ library not supported")

        # Check that we show the correct function pointers and the `promise`.
        self.expect_expr(
            "gen.hdl",
            result_summary=re.compile("^coro frame = 0x[0-9a-f]*$"),
            result_children=[
                ValueCheck(name="resume", summary=test_generator_func_ptr_re),
                ValueCheck(name="destroy", summary=test_generator_func_ptr_re),
                ValueCheck(
                    name="promise",
                    children=[
                        ValueCheck(name="current_value", value="-1"),
                    ],
                ),
                # We don not check any members inside the `coro_frame`,
                # as its contents are highly compiler-specific.
                ValueCheck(name="coro_frame"),
            ],
        )

        # For a type-erased `coroutine_handle<>`, we can still devirtualize
        # the promise call and display the correctly typed promise. This
        # currently only works in clang, because gcc is not adding the
        # artificial `__promise` variable to the destroy function.
        if is_clang:
            self.expect_expr(
                "type_erased_hdl",
                result_summary=re.compile("^coro frame = 0x[0-9a-f]*$"),
                result_children=[
                    ValueCheck(name="resume", summary=test_generator_func_ptr_re),
                    ValueCheck(name="destroy", summary=test_generator_func_ptr_re),
                    ValueCheck(
                        name="promise",
                        children=[
                            ValueCheck(name="current_value", value="-1"),
                        ],
                    ),
                    ValueCheck(name="coro_frame"),
                ],
            )

        # For an incorrectly typed `coroutine_handle`, we use the user-supplied
        # incorrect type instead of inferring the correct type. Strictly speaking,
        # incorrectly typed coroutine handles are undefined behavior. However,
        # it provides probably a better debugging experience if we display the
        # promise as seen by the program instead of fixing this bug based on
        # the available debug info.
        self.expect_expr(
            "incorrectly_typed_hdl",
            result_summary=re.compile("^coro frame = 0x[0-9a-f]*$"),
            result_children=[
                ValueCheck(name="resume", summary=test_generator_func_ptr_re),
                ValueCheck(name="destroy", summary=test_generator_func_ptr_re),
                ValueCheck(name="promise", dereference=ValueCheck(value="-1")),
                ValueCheck(name="coro_frame"),
            ],
        )

        process = self.process()

        # Break at a coroutine body
        lldbutil.continue_to_source_breakpoint(
            self,
            process,
            "int_generator my_generator_func",
            lldb.SBFileSpec("main.cpp", False),
        )

        # Expect artificial variables to be displayed
        self.expect("frame variable", substrs=["__promise", "__coro_frame"])

        # Run until after the `co_yield`
        lldbutil.continue_to_source_breakpoint(
            self, process, "// Break after co_yield", lldb.SBFileSpec("main.cpp", False)
        )
        # We correctly show the updated value inside `promise.current_value`.
        self.expect_expr(
            "gen.hdl",
            result_children=[
                ValueCheck(name="resume", summary=test_generator_func_ptr_re),
                ValueCheck(name="destroy", summary=test_generator_func_ptr_re),
                ValueCheck(
                    name="promise",
                    children=[
                        ValueCheck(name="current_value", value="42"),
                    ],
                ),
                ValueCheck(name="coro_frame"),
            ],
        )

        # Run until the `final_suspend`
        lldbutil.continue_to_source_breakpoint(
            self,
            process,
            "// Break at final_suspend",
            lldb.SBFileSpec("main.cpp", False),
        )
        # At the final suspension point, `resume` is set to a nullptr.
        # Check that we still show the remaining data correctly.
        self.expect_expr(
            "gen.hdl",
            result_children=[
                ValueCheck(name="resume", value=re.compile("^0x0+$")),
                ValueCheck(name="destroy", summary=test_generator_func_ptr_re),
                ValueCheck(
                    name="promise",
                    children=[
                        ValueCheck(name="current_value", value="42"),
                    ],
                ),
                ValueCheck(name="coro_frame"),
            ],
        )
        if is_clang:
            # Devirtualization still works, also at the final suspension point, despite
            # the `resume` pointer being reset to a nullptr
            self.expect_expr(
                "type_erased_hdl",
                result_summary=re.compile("^coro frame = 0x[0-9a-f]*$"),
                result_children=[
                    ValueCheck(name="resume", value=re.compile("^0x0+$")),
                    ValueCheck(name="destroy", summary=test_generator_func_ptr_re),
                    ValueCheck(
                        name="promise",
                        children=[
                            ValueCheck(name="current_value", value="42"),
                        ],
                    ),
                    ValueCheck(name="coro_frame"),
                ],
            )

    @add_test_categories(["libstdcxx"])
    def test_libstdcpp(self):
        self.build(dictionary={"USE_LIBSTDCPP": 1})
        self.do_test()

    @add_test_categories(["libc++"])
    @skipIf(compiler="clang", compiler_version=["<", "15.0"])
    def test_libcpp(self):
        self.build(dictionary={"USE_LIBCPP": 1})
        self.do_test()
