"""
Test that libunwind correctly injects 'ret' instructions to rebalance execution flow
when unwinding C++ exceptions. This is important for Apple Processor Trace analysis.
"""

import lldb
import os
import subprocess
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
from lldbsuite.test import lldbutil
from lldbsuite.test import configuration


class LibunwindRetInjectionTestCase(TestBase):
    def find_in_tree_libunwind(self):
        llvm_lib_dir = os.path.join(
            os.path.dirname(configuration.llvm_tools_dir), "lib"
        )

        # Find the libunwind library (platform-agnostic).
        for filename in os.listdir(llvm_lib_dir):
            if filename.startswith("libunwind.") or filename.startswith("unwind."):
                return os.path.join(llvm_lib_dir, filename)

        return None

    def libunwind_arch_no_match(self):
        test_arch = configuration.arch

        libunwind_path = self.find_in_tree_libunwind()
        if not libunwind_path:
            return "Libunwind not found"

        # Note: This is Darwin-specific. If this test is to be ported, we need
        # a platform-agnostic way to find the relevant architectures of the
        # libunwind library.
        libunwind_archs = (
            subprocess.check_output(["lipo", "-archs", libunwind_path])
            .decode("utf-8")
            .split()
        )

        for arch in libunwind_archs:
            if test_arch in arch:
                return None

        return "Libunwind not built with compatible architecture"

    @skipIf(archs=no_match(["arm64", "arm64e", "aarch64"]))
    @skipUnlessDarwin
    @skipIfOutOfTreeLibunwind
    @skipTestIfFn(libunwind_arch_no_match)
    def test_ret_injection_on_exception_unwind(self):
        """Test that __libunwind_Registers_arm64_jumpto receives correct walkedFrames count and injects the right number of ret instructions."""
        self.build()

        exe = self.getBuildArtifact("a.out")
        target = self.dbg.CreateTarget(exe)
        self.assertTrue(target, VALID_TARGET)

        # Find the just-built libunwind, not the system one.
        # llvm_tools_dir is typically <build>/bin, so lib is a sibling.
        self.assertIsNotNone(
            configuration.llvm_tools_dir,
            "llvm_tools_dir must be set to find in-tree libunwind",
        )

        # Find the libunwind library (platform-agnostic).
        libunwind_path = self.find_in_tree_libunwind()

        self.assertIsNotNone(libunwind_path, f"Failed to find libunwind in build tree")

        # Set breakpoint in __libunwind_Registers_arm64_jumpto.
        # This is the function that performs the actual jump and ret injection.
        bp = target.BreakpointCreateByName("__libunwind_Registers_arm64_jumpto")
        self.assertTrue(bp.IsValid())
        self.assertGreater(bp.GetNumLocations(), 0)

        # Set up DYLD_INSERT_LIBRARIES to use the just-built libunwind.
        launch_info = lldb.SBLaunchInfo(None)
        env = target.GetEnvironment()
        env.Set("DYLD_INSERT_LIBRARIES", libunwind_path, True)
        launch_info.SetEnvironment(env, False)

        # Launch the process with our custom libunwind.
        error = lldb.SBError()
        process = target.Launch(launch_info, error)
        self.assertSuccess(
            error, f"Failed to launch process with libunwind at {libunwind_path}"
        )
        self.assertTrue(process, PROCESS_IS_VALID)

        # We should hit the breakpoint in __libunwind_Registers_arm64_jumpto
        # during the exception unwinding phase 2.
        threads = lldbutil.get_threads_stopped_at_breakpoint(process, bp)
        self.assertEqual(len(threads), 1, "Should have stopped at breakpoint")

        thread = threads[0]
        frame = thread.GetFrameAtIndex(0)

        # Verify we're in __libunwind_Registers_arm64_jumpto.
        function_name = frame.GetFunctionName()
        self.assertTrue(
            "__libunwind_Registers_arm64_jumpto" in function_name,
            f"Expected to be in __libunwind_Registers_arm64_jumpto, got {function_name}",
        )

        # On ARM64, the walkedFrames parameter should be in register x1 (second parameter).
        # According to the ARM64 calling convention, integer arguments are passed in x0-x7.
        # x0 = Registers_arm64* pointer.
        # x1 = unsigned walkedFrames.
        error = lldb.SBError()
        x1_value = frame.register["x1"].GetValueAsUnsigned(error)
        self.assertSuccess(error, "Failed to read x1 register")

        # According to the code in UnwindCursor.hpp, the walkedFrames value represents:
        # 1. The number of frames walked in unwind_phase2 to reach the landing pad.
        # 2. Plus _EXTRA_LIBUNWIND_FRAMES_WALKED = 5 - 1 = 4 additional libunwind frames.
        #
        # From the comment in the code:
        #   frame #0: __libunwind_Registers_arm64_jumpto
        #   frame #1: Registers_arm64::returnto
        #   frame #2: UnwindCursor::jumpto
        #   frame #3: __unw_resume
        #   frame #4: __unw_resume_with_frames_walked
        #   frame #5: unwind_phase2
        #
        # Since __libunwind_Registers_arm64_jumpto returns to the landing pad,
        # we subtract 1, so _EXTRA_LIBUNWIND_FRAMES_WALKED = 4.
        #
        # For our test program:
        # - unwind_phase2 starts walking (frame 0 counted here).
        # - Walks through: func_d (throw site), func_c, func_b, func_a.
        # - Finds landing pad in main.
        # That's approximately 4-5 frames from the user code.
        # Plus the 4 extra libunwind frames.
        #
        # So we expect x1 to be roughly 8-10.
        expected_min_frames = 8
        expected_max_frames = 13  # Allow some variation for libc++abi frames.

        self.assertGreaterEqual(
            x1_value,
            expected_min_frames,
            f"walkedFrames (x1) should be >= {expected_min_frames}, got {x1_value}. "
            "This is the number of 'ret' instructions that will be executed.",
        )

        self.assertLessEqual(
            x1_value,
            expected_max_frames,
            f"walkedFrames (x1) should be <= {expected_max_frames}, got {x1_value}. "
            "Value seems too high.",
        )

        # Now step through the ret injection loop and count the actual number of 'ret' executions.
        # The loop injects exactly x1_value ret instructions before continuing with register restoration.
        # We step until we hit the first 'ldp' instruction (register restoration starts with 'ldp x2, x3, [x0, #0x010]').
        ret_executed_count = 0
        max_steps = 100  # Safety limit to prevent infinite loops.

        for step_count in range(max_steps):
            # Get current instruction.
            pc = frame.GetPC()
            inst = process.ReadMemory(pc, 4, lldb.SBError())

            # Disassemble current instruction.
            current_inst = target.GetInstructions(lldb.SBAddress(pc, target), inst)[0]
            mnemonic = current_inst.GetMnemonic(target)
            operands = current_inst.GetOperands(target)

            # Check if we've reached the register restoration part (first ldp after the loop).
            if mnemonic == "ldp":
                # We've exited the ret injection loop.
                break

            # Count 'ret' instructions that get executed.
            if mnemonic == "ret":
                self.assertEqual(operands, "x16")
                ret_executed_count += 1

            # Step one instruction.
            thread.StepInstruction(False)  # False = step over.

            # Update frame reference.
            frame = thread.GetFrameAtIndex(0)

        # Verify we didn't hit the safety limit.
        self.assertLess(
            step_count,
            max_steps - 1,
            f"Stepped {max_steps} times without reaching 'ldp' instruction. Something is wrong.",
        )

        # The number of executed 'ret' instructions should match x1_value.
        # According to the implementation, the loop executes exactly x1_value times.
        self.assertEqual(
            ret_executed_count,
            x1_value,
            f"Expected {x1_value} 'ret' instructions to be executed (matching x1 register), "
            f"but counted {ret_executed_count} executed 'ret' instructions.",
        )
