"""
Test scripted frame provider functionality.
"""

import os

import lldb
import lldbsuite.test.lldbplatformutil as lldbplatformutil
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import TestBase
from lldbsuite.test import lldbutil

class ScriptedFrameProviderTestCase(TestBase):
    NO_DEBUG_INFO_TESTCASE = True

    def setUp(self):
        TestBase.setUp(self)
        self.source = "main.cpp"

    def test_replace_all_frames(self):
        """Test that we can replace the entire stack."""
        self.build()
        target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
            self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
        )

        # Import the test frame provider.
        script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
        self.runCmd("command script import " + script_path)

        # Attach the Replace provider.
        error = lldb.SBError()
        provider_id = target.RegisterScriptedFrameProvider(
            "test_frame_providers.ReplaceFrameProvider",
            lldb.SBStructuredData(),
            error,
        )
        self.assertTrue(error.Success(), f"Failed to register provider: {error}")
        self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")

        # Verify we have exactly 3 synthetic frames.
        self.assertEqual(thread.GetNumFrames(), 3, "Should have 3 synthetic frames")

        # Verify frame indices and PCs (dictionary-based frames don't have custom function names).
        frame0 = thread.GetFrameAtIndex(0)
        self.assertIsNotNone(frame0)
        self.assertEqual(frame0.GetPC(), 0x1000)

        frame1 = thread.GetFrameAtIndex(1)
        self.assertIsNotNone(frame1)
        self.assertIn("thread_func", frame1.GetFunctionName())

        frame2 = thread.GetFrameAtIndex(2)
        self.assertIsNotNone(frame2)
        self.assertEqual(frame2.GetPC(), 0x3000)

    def test_prepend_frames(self):
        """Test that we can add frames before real stack."""
        self.build()
        target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
            self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
        )

        # Get original frame count and PC.
        original_frame_count = thread.GetNumFrames()
        self.assertGreaterEqual(
            original_frame_count, 2, "Should have at least 2 real frames"
        )

        # Import and attach Prepend provider.
        script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
        self.runCmd("command script import " + script_path)

        error = lldb.SBError()
        provider_id = target.RegisterScriptedFrameProvider(
            "test_frame_providers.PrependFrameProvider",
            lldb.SBStructuredData(),
            error,
        )
        self.assertTrue(error.Success(), f"Failed to register provider: {error}")
        self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")

        # Verify we have 2 more frames.
        new_frame_count = thread.GetNumFrames()
        self.assertEqual(new_frame_count, original_frame_count + 2)

        # Verify first 2 frames are synthetic (check PCs, not function names).
        frame0 = thread.GetFrameAtIndex(0)
        self.assertEqual(frame0.GetPC(), 0x9000)

        frame1 = thread.GetFrameAtIndex(1)
        self.assertEqual(frame1.GetPC(), 0xA000)

        # Verify frame 2 is the original real frame 0.
        frame2 = thread.GetFrameAtIndex(2)
        self.assertIn("thread_func", frame2.GetFunctionName())

    def test_append_frames(self):
        """Test that we can add frames after real stack."""
        self.build()
        target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
            self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
        )

        # Get original frame count.
        original_frame_count = thread.GetNumFrames()

        # Import and attach Append provider.
        script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
        self.runCmd("command script import " + script_path)

        error = lldb.SBError()
        provider_id = target.RegisterScriptedFrameProvider(
            "test_frame_providers.AppendFrameProvider",
            lldb.SBStructuredData(),
            error,
        )
        self.assertTrue(error.Success(), f"Failed to register provider: {error}")
        self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")

        # Verify we have 1 more frame.
        new_frame_count = thread.GetNumFrames()
        self.assertEqual(new_frame_count, original_frame_count + 1)

        # Verify first frames are still real.
        frame0 = thread.GetFrameAtIndex(0)
        self.assertIn("thread_func", frame0.GetFunctionName())

        frame_n_plus_1 = thread.GetFrameAtIndex(new_frame_count - 1)
        self.assertEqual(frame_n_plus_1.GetPC(), 0x10)

    def test_scripted_frame_thread_member(self):
        """Test that ScriptedFrame.thread is correctly set via GetThreadByID.

        This is a regression test for a bug where ScriptedFrame.__init__ used
        GetThreadByIndexID(tid) instead of GetThreadByID(tid). Since thread ID
        and index ID differ, the wrong API would produce an invalid thread.
        """
        self.build()
        target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
            self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
        )

        script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
        self.runCmd("command script import " + script_path)

        error = lldb.SBError()
        provider_id = target.RegisterScriptedFrameProvider(
            "test_frame_providers.ThreadValidatingFrameProvider",
            lldb.SBStructuredData(),
            error,
        )
        self.assertTrue(error.Success(), f"Failed to register provider: {error}")
        self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")

        # The ThreadValidatingFrame encodes thread validity in its function name.
        frame0 = thread.GetFrameAtIndex(0)
        func_name = frame0.GetFunctionName()

        # If GetThreadByIndexID were used instead of GetThreadByID, the thread
        # would be invalid and the function name would be "thread_INVALID".
        self.assertNotEqual(
            func_name,
            "thread_INVALID",
            "ScriptedFrame.thread should be valid "
            "(GetThreadByID vs GetThreadByIndexID)",
        )
        self.assertIn("thread_valid_id_", func_name)

        # Verify the encoded thread ID matches the actual thread ID.
        expected_tid = thread.GetThreadID()
        self.assertIn(
            hex(expected_tid),
            func_name,
            f"ScriptedFrame.thread ID should match: expected {expected_tid:#x} "
            f"in '{func_name}'",
        )

    def test_scripted_frame_objects(self):
        """Test that provider can return ScriptedFrame objects."""
        self.build()
        target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
            self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
        )

        # Import the provider that returns ScriptedFrame objects.
        script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
        self.runCmd("command script import " + script_path)

        error = lldb.SBError()
        provider_id = target.RegisterScriptedFrameProvider(
            "test_frame_providers.ScriptedFrameObjectProvider",
            lldb.SBStructuredData(),
            error,
        )
        self.assertTrue(error.Success(), f"Failed to register provider: {error}")
        self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")

        # Verify we have 5 frames.
        self.assertEqual(
            thread.GetNumFrames(), 5, "Should have 5 custom scripted frames"
        )

        # Verify frame properties from CustomScriptedFrame.
        frame0 = thread.GetFrameAtIndex(0)
        self.assertIsNotNone(frame0)
        self.assertEqual(frame0.GetFunctionName(), "custom_scripted_frame_0")
        self.assertEqual(frame0.GetPC(), 0x5000)
        self.assertTrue(frame0.IsSynthetic(), "Frame should be marked as synthetic")

        frame1 = thread.GetFrameAtIndex(1)
        self.assertIsNotNone(frame1)
        self.assertEqual(frame1.GetPC(), 0x6000)

        frame2 = thread.GetFrameAtIndex(2)
        self.assertIsNotNone(frame2)
        self.assertEqual(frame2.GetFunctionName(), "custom_scripted_frame_2")
        self.assertEqual(frame2.GetPC(), 0x7000)
        self.assertTrue(frame2.IsSynthetic(), "Frame should be marked as synthetic")

    def test_applies_to_thread(self):
        """Test that applies_to_thread filters which threads get the provider."""
        self.build()
        target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
            self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
        )

        # We should have at least 2 threads (worker threads) at the breakpoint.
        num_threads = process.GetNumThreads()
        self.assertGreaterEqual(
            num_threads, 2, "Should have at least 2 threads at breakpoint"
        )

        # Import the test frame provider.
        script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
        self.runCmd("command script import " + script_path)

        # Collect original thread info before applying provider.
        thread_info = {}
        for i in range(num_threads):
            t = process.GetThreadAtIndex(i)
            thread_info[t.GetIndexID()] = {
                "frame_count": t.GetNumFrames(),
                "pc": t.GetFrameAtIndex(0).GetPC(),
            }

        # Register the ThreadFilterFrameProvider which only applies to thread ID 1.
        error = lldb.SBError()
        provider_id = target.RegisterScriptedFrameProvider(
            "test_frame_providers.ThreadFilterFrameProvider",
            lldb.SBStructuredData(),
            error,
        )
        self.assertTrue(error.Success(), f"Failed to register provider: {error}")
        self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")

        # Check each thread.
        thread_id_1_found = False
        # On ARM32, FixCodeAddress clears bit 0, so synthetic PCs get modified.
        is_arm_32bit = lldbplatformutil.getArchitecture() == "arm"
        expected_synthetic_pc = 0xFFFE if is_arm_32bit else 0xFFFF

        for i in range(num_threads):
            t = process.GetThreadAtIndex(i)
            thread_id = t.GetIndexID()

            if thread_id == 1:
                # Thread with ID 1 should have synthetic frame.
                thread_id_1_found = True
                self.assertEqual(
                    t.GetNumFrames(),
                    1,
                    f"Thread with ID 1 should have 1 synthetic frame",
                )
                self.assertEqual(
                    t.GetFrameAtIndex(0).GetPC(),
                    expected_synthetic_pc,
                    f"Thread with ID 1 should have synthetic PC {expected_synthetic_pc:#x}",
                )
            else:
                # Other threads should keep their original frames.
                self.assertEqual(
                    t.GetNumFrames(),
                    thread_info[thread_id]["frame_count"],
                    f"Thread with ID {thread_id} should not be affected by provider",
                )
                self.assertEqual(
                    t.GetFrameAtIndex(0).GetPC(),
                    thread_info[thread_id]["pc"],
                    f"Thread with ID {thread_id} should have its original PC",
                )

        # We should have found at least one thread with ID 1.
        self.assertTrue(
            thread_id_1_found,
            "Should have found a thread with ID 1 to test filtering",
        )

    def test_remove_frame_provider_by_id(self):
        """Test that RemoveScriptedFrameProvider removes a specific provider by ID."""
        self.build()
        target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
            self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
        )

        # Import the test frame providers.
        script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
        self.runCmd("command script import " + script_path)

        # Get original frame count.
        original_frame_count = thread.GetNumFrames()
        original_pc = thread.GetFrameAtIndex(0).GetPC()

        # Register the first provider and get its ID.
        error = lldb.SBError()
        provider_id_1 = target.RegisterScriptedFrameProvider(
            "test_frame_providers.ReplaceFrameProvider",
            lldb.SBStructuredData(),
            error,
        )
        self.assertTrue(error.Success(), f"Failed to register provider 1: {error}")

        # Verify first provider is active (3 synthetic frames).
        self.assertEqual(thread.GetNumFrames(), 3, "Should have 3 synthetic frames")
        self.assertEqual(
            thread.GetFrameAtIndex(0).GetPC(), 0x1000, "Should have first provider's PC"
        )

        # Register a second provider and get its ID.
        provider_id_2 = target.RegisterScriptedFrameProvider(
            "test_frame_providers.PrependFrameProvider",
            lldb.SBStructuredData(),
            error,
        )
        self.assertTrue(error.Success(), f"Failed to register provider 2: {error}")

        # Verify IDs are different
        self.assertNotEqual(
            provider_id_1, provider_id_2, "Provider IDs should be unique"
        )

        # Now remove the first provider by ID
        result = target.RemoveScriptedFrameProvider(provider_id_1)
        self.assertSuccess(
            result, f"Should successfully remove provider with ID {provider_id_1}"
        )

        # After removing the first provider, the second provider should still be
        # active. The PrependFrameProvider adds 2 frames before the real stack.
        # Since ReplaceFrameProvider had 3 frames, and we removed it, we should now
        # have the original frames (from real stack) with PrependFrameProvider applied.
        new_frame_count = thread.GetNumFrames()
        self.assertEqual(
            new_frame_count,
            original_frame_count + 2,
            "Should have original frames + 2 prepended frames",
        )

        # First two frames should be from PrependFrameProvider.
        self.assertEqual(
            thread.GetFrameAtIndex(0).GetPC(),
            0x9000,
            "First frame should be from PrependFrameProvider",
        )
        self.assertEqual(
            thread.GetFrameAtIndex(1).GetPC(),
            0xA000,
            "Second frame should be from PrependFrameProvider",
        )

        # Remove the second provider.
        result = target.RemoveScriptedFrameProvider(provider_id_2)
        self.assertSuccess(
            result, f"Should successfully remove provider with ID {provider_id_2}"
        )

        # After removing both providers, frames should be back to original.
        self.assertEqual(
            thread.GetNumFrames(),
            original_frame_count,
            "Should restore original frame count",
        )
        self.assertEqual(
            thread.GetFrameAtIndex(0).GetPC(),
            original_pc,
            "Should restore original PC",
        )

        # Try to remove a provider that doesn't exist.
        result = target.RemoveScriptedFrameProvider(999999)
        self.assertTrue(result.Fail(), "Should fail to remove non-existent provider")

    def test_circular_dependency_fix(self):
        """Test that accessing input_frames in __init__ doesn't cause circular dependency.

        This test verifies the fix for the circular dependency issue where:
        1. Thread::GetStackFrameList() creates the frame provider
        2. Provider's __init__ accesses input_frames and calls methods on frames
        3. SBFrame methods trigger ExecutionContextRef::GetFrameSP()
        4. Before the fix: GetFrameSP() would call Thread::GetStackFrameList() again -> circular dependency!
        5. After the fix: GetFrameSP() uses the remembered frame list -> no circular dependency

        The fix works by:
        - StackFrame stores m_frame_list_wp (weak pointer to originating list)
        - ExecutionContextRef stores m_frame_list_wp when created from a frame
        - ExecutionContextRef::GetFrameSP() tries the remembered list first before asking the thread
        """
        self.build()
        target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
            self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
        )

        # Get original frame count and PC.
        original_frame_count = thread.GetNumFrames()
        original_pc = thread.GetFrameAtIndex(0).GetPC()
        self.assertGreaterEqual(
            original_frame_count, 2, "Should have at least 2 real frames"
        )

        # Import the provider that accesses input frames in __init__.
        script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
        self.runCmd("command script import " + script_path)

        # Register the CircularDependencyTestProvider.
        # Before the fix, this would crash or hang due to circular dependency.
        error = lldb.SBError()
        provider_id = target.RegisterScriptedFrameProvider(
            "test_frame_providers.CircularDependencyTestProvider",
            lldb.SBStructuredData(),
            error,
        )

        # If we get here without crashing, the fix is working!
        self.assertTrue(error.Success(), f"Failed to register provider: {error}")
        self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")

        # Verify the provider worked correctly,
        # Should have 1 synthetic frame + all original frames.
        new_frame_count = thread.GetNumFrames()
        self.assertEqual(
            new_frame_count,
            original_frame_count + 1,
            "Should have original frames + 1 synthetic frame",
        )

        # On ARM32, FixCodeAddress clears bit 0, so synthetic PCs get modified.
        is_arm_32bit = lldbplatformutil.getArchitecture() == "arm"
        expected_synthetic_pc = 0xDEADBEEE if is_arm_32bit else 0xDEADBEEF

        # First frame should be synthetic.
        frame0 = thread.GetFrameAtIndex(0)
        self.assertIsNotNone(frame0)
        self.assertEqual(
            frame0.GetPC(),
            expected_synthetic_pc,
            f"First frame should be synthetic frame with PC {expected_synthetic_pc:#x}",
        )

        # Second frame should be the original first frame.
        frame1 = thread.GetFrameAtIndex(1)
        self.assertIsNotNone(frame1)
        self.assertEqual(
            frame1.GetPC(),
            original_pc,
            "Second frame should be original first frame",
        )

        # Verify we can still call methods on frames (no circular dependency!).
        for i in range(min(3, new_frame_count)):
            frame = thread.GetFrameAtIndex(i)
            self.assertIsNotNone(frame)
            # These calls should not trigger circular dependency.
            pc = frame.GetPC()
            self.assertNotEqual(pc, 0, f"Frame {i} should have valid PC")

    def test_python_source_frames(self):
        """Test that frames can point to Python source files and display properly."""
        self.build()
        target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
            self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
        )

        # Get original frame count.
        original_frame_count = thread.GetNumFrames()
        self.assertGreaterEqual(
            original_frame_count, 2, "Should have at least 2 real frames"
        )

        # Import the provider.
        script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
        self.runCmd("command script import " + script_path)

        # Register the PythonSourceFrameProvider.
        error = lldb.SBError()
        provider_id = target.RegisterScriptedFrameProvider(
            "test_frame_providers.PythonSourceFrameProvider",
            lldb.SBStructuredData(),
            error,
        )
        self.assertTrue(error.Success(), f"Failed to register provider: {error}")
        self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")

        # Verify we have 3 more frames (Python frames).
        new_frame_count = thread.GetNumFrames()
        self.assertEqual(
            new_frame_count,
            original_frame_count + 3,
            "Should have original frames + 3 Python frames",
        )

        # Verify first three frames are Python source frames.
        frame0 = thread.GetFrameAtIndex(0)
        self.assertIsNotNone(frame0)
        self.assertEqual(
            frame0.GetFunctionName(),
            "compute_fibonacci",
            "First frame should be compute_fibonacci",
        )
        self.assertTrue(frame0.IsSynthetic(), "Frame should be marked as synthetic")
        # PC-less frames should show invalid address and not crash.
        self.assertEqual(
            frame0.GetPC(),
            lldb.LLDB_INVALID_ADDRESS,
            "PC-less frame should have LLDB_INVALID_ADDRESS",
        )

        self.assertEqual(
            frame0.GetFP(),
            lldb.LLDB_INVALID_ADDRESS,
            "PC-less frame FP should return LLDB_INVALID_ADDRESS",
        )
        self.assertEqual(
            frame0.GetSP(),
            lldb.LLDB_INVALID_ADDRESS,
            "PC-less frame SP should return LLDB_INVALID_ADDRESS",
        )
        self.assertEqual(
            frame0.GetCFA(),
            0,
            "PC-less frame CFA should return 0",
        )

        frame1 = thread.GetFrameAtIndex(1)
        self.assertIsNotNone(frame1)
        self.assertEqual(
            frame1.GetFunctionName(),
            "process_data",
            "Second frame should be process_data",
        )
        self.assertTrue(frame1.IsSynthetic(), "Frame should be marked as synthetic")

        frame2 = thread.GetFrameAtIndex(2)
        self.assertIsNotNone(frame2)
        self.assertEqual(frame2.GetFunctionName(), "main", "Third frame should be main")
        self.assertTrue(frame2.IsSynthetic(), "Frame should be marked as synthetic")

        # Verify line entry information is present.
        line_entry0 = frame0.GetLineEntry()
        self.assertTrue(line_entry0.IsValid(), "Frame 0 should have a valid line entry")
        self.assertEqual(line_entry0.GetLine(), 7, "Frame 0 should point to line 7")
        file_spec0 = line_entry0.GetFileSpec()
        self.assertTrue(file_spec0.IsValid(), "Frame 0 should have valid file spec")
        self.assertEqual(
            file_spec0.GetFilename(),
            "python_helper.py",
            "Frame 0 should point to python_helper.py",
        )

        line_entry1 = frame1.GetLineEntry()
        self.assertTrue(line_entry1.IsValid(), "Frame 1 should have a valid line entry")
        self.assertEqual(line_entry1.GetLine(), 16, "Frame 1 should point to line 16")

        line_entry2 = frame2.GetLineEntry()
        self.assertTrue(line_entry2.IsValid(), "Frame 2 should have a valid line entry")
        self.assertEqual(line_entry2.GetLine(), 27, "Frame 2 should point to line 27")

        # Verify the frames display properly in backtrace.
        # This tests that PC-less frames don't show 0xffffffffffffffff.
        self.runCmd("bt")
        output = self.res.GetOutput()

        # Should show function names.
        self.assertIn("compute_fibonacci", output)
        self.assertIn("process_data", output)
        self.assertIn("main", output)

        # Should show Python file.
        self.assertIn("python_helper.py", output)

        # Should show line numbers.
        self.assertIn(":7", output)  # compute_fibonacci line.
        self.assertIn(":16", output)  # process_data line.
        self.assertIn(":27", output)  # main line.

        # Should NOT show invalid address (0xffffffffffffffff).
        self.assertNotIn("0xffffffffffffffff", output.lower())

        # Verify frame 3 is the original real frame 0.
        frame3 = thread.GetFrameAtIndex(3)
        self.assertIsNotNone(frame3)
        self.assertIn("thread_func", frame3.GetFunctionName())

    def test_valid_pc_no_module_frames(self):
        """Test that frames with valid PC but no module display correctly in backtrace."""
        self.build()
        target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
            self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
        )

        # Get original frame count.
        original_frame_count = thread.GetNumFrames()
        self.assertGreaterEqual(
            original_frame_count, 2, "Should have at least 2 real frames"
        )

        # Import the provider.
        script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
        self.runCmd("command script import " + script_path)

        # Register the ValidPCNoModuleFrameProvider.
        error = lldb.SBError()
        provider_id = target.RegisterScriptedFrameProvider(
            "test_frame_providers.ValidPCNoModuleFrameProvider",
            lldb.SBStructuredData(),
            error,
        )
        self.assertTrue(error.Success(), f"Failed to register provider: {error}")
        self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")

        # Verify we have 2 more frames (the synthetic frames).
        new_frame_count = thread.GetNumFrames()
        self.assertEqual(
            new_frame_count,
            original_frame_count + 2,
            "Should have original frames + 2 synthetic frames",
        )

        # Verify first two frames have valid PCs and function names.
        frame0 = thread.GetFrameAtIndex(0)
        self.assertIsNotNone(frame0)
        self.assertEqual(
            frame0.GetFunctionName(),
            "unknown_function_1",
            "First frame should be unknown_function_1",
        )
        self.assertTrue(frame0.IsSynthetic(), "Frame should be marked as synthetic")
        self.assertEqual(
            frame0.GetPC(), 0x1234000, "First frame should have PC 0x1234000"
        )

        frame1 = thread.GetFrameAtIndex(1)
        self.assertIsNotNone(frame1)
        self.assertEqual(
            frame1.GetFunctionName(),
            "unknown_function_2",
            "Second frame should be unknown_function_2",
        )
        self.assertTrue(frame1.IsSynthetic(), "Frame should be marked as synthetic")
        self.assertEqual(
            frame1.GetPC(), 0x5678000, "Second frame should have PC 0x5678000"
        )

        # Verify the frames display properly in backtrace.
        # The backtrace should show the PC values without crashing or displaying
        # invalid addresses like 0xffffffffffffffff.
        self.runCmd("bt")
        output = self.res.GetOutput()

        # Should show function names.
        self.assertIn("unknown_function_1", output)
        self.assertIn("unknown_function_2", output)

        # Should show PC addresses in hex format.
        self.assertIn("1234000", output)
        self.assertIn("5678000", output)

        # Verify PC and function name are properly separated by space.
        self.assertIn("1234000 unknown_function_1", output)
        self.assertIn("5678000 unknown_function_2", output)

        # Should NOT show invalid address.
        self.assertNotIn("ffffff", output.lower())

        # Verify frame 2 is the original real frame 0.
        frame2 = thread.GetFrameAtIndex(2)
        self.assertIsNotNone(frame2)
        self.assertIn("thread_func", frame2.GetFunctionName())

    def test_chained_frame_providers(self):
        """Test that multiple frame providers chain together."""
        self.build()
        target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
            self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
        )

        # Get original frame count.
        original_frame_count = thread.GetNumFrames()
        self.assertGreaterEqual(
            original_frame_count, 2, "Should have at least 2 real frames"
        )

        # Import the test frame providers.
        script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
        self.runCmd("command script import " + script_path)

        # Register 3 providers with different priorities.
        # Each provider adds 1 frame at the beginning.
        error = lldb.SBError()

        # Provider 1: Priority 10 - adds "foo" frame
        provider_id_1 = target.RegisterScriptedFrameProvider(
            "test_frame_providers.AddFooFrameProvider",
            lldb.SBStructuredData(),
            error,
        )
        self.assertTrue(error.Success(), f"Failed to register foo provider: {error}")

        # Provider 2: Priority 20 - adds "bar" frame
        provider_id_2 = target.RegisterScriptedFrameProvider(
            "test_frame_providers.AddBarFrameProvider",
            lldb.SBStructuredData(),
            error,
        )
        self.assertTrue(error.Success(), f"Failed to register bar provider: {error}")

        # Provider 3: Priority 30 - adds "baz" frame
        provider_id_3 = target.RegisterScriptedFrameProvider(
            "test_frame_providers.AddBazFrameProvider",
            lldb.SBStructuredData(),
            error,
        )
        self.assertTrue(error.Success(), f"Failed to register baz provider: {error}")

        # Verify we have 3 more frames (one from each provider).
        new_frame_count = thread.GetNumFrames()
        self.assertEqual(
            new_frame_count,
            original_frame_count + 3,
            "Should have original frames + 3 chained frames",
        )

        # Verify the chaining order: baz, bar, foo, then real frames.
        # Since priority is lower = higher, the order should be:
        # Provider 1 (priority 10) transforms real frames first -> adds "foo"
        # Provider 2 (priority 20) transforms Provider 1's output -> adds "bar"
        # Provider 3 (priority 30) transforms Provider 2's output -> adds "baz"
        # So final stack is: baz, bar, foo, real frames...

        frame0 = thread.GetFrameAtIndex(0)
        self.assertIsNotNone(frame0)
        self.assertEqual(
            frame0.GetFunctionName(),
            "baz",
            "Frame 0 should be 'baz' from last provider in chain",
        )
        self.assertEqual(frame0.GetPC(), 0xBAC)

        frame1 = thread.GetFrameAtIndex(1)
        self.assertIsNotNone(frame1)
        self.assertEqual(
            frame1.GetFunctionName(),
            "bar",
            "Frame 1 should be 'bar' from second provider in chain",
        )
        self.assertEqual(frame1.GetPC(), 0xBAA)

        frame2 = thread.GetFrameAtIndex(2)
        self.assertIsNotNone(frame2)
        self.assertEqual(
            frame2.GetFunctionName(),
            "foo",
            "Frame 2 should be 'foo' from first provider in chain",
        )
        self.assertEqual(frame2.GetPC(), 0xF00)

        # Frame 3 should be the original real frame 0.
        frame3 = thread.GetFrameAtIndex(3)
        self.assertIsNotNone(frame3)
        self.assertIn("thread_func", frame3.GetFunctionName())

    def test_get_values(self):
        """Test a frame that provides values."""
        self.build()
        # Set the breakpoint after the variable_in_main variable exists and can be queried.
        target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
            self,
            "Breakpoint for variable tests",
            lldb.SBFileSpec(self.source),
            only_one_thread=False,
        )

        # Get original frame count.
        original_frame_count = thread.GetNumFrames()
        self.assertGreaterEqual(
            original_frame_count, 2, "Should have at least 2 real frames"
        )

        # Import the test frame providers.
        script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
        self.runCmd("command script import " + script_path)

        # Register a provider that can provide variables.
        error = lldb.SBError()
        target.RegisterScriptedFrameProvider(
            "test_frame_providers.ValueProvidingFrameProvider",
            lldb.SBStructuredData(),
            error,
        )
        self.assertTrue(error.Success(), f"Failed to register provider: {error}")

        # Verify we have 1 more frame.
        new_frame_count = thread.GetNumFrames()
        self.assertEqual(
            new_frame_count,
            original_frame_count + 1,
            "Should have original frames + 1 extra frames",
        )

        # Check that we can get variables from this frame.
        frame0 = thread.GetFrameAtIndex(0)
        self.assertIsNotNone(frame0)
        # Get every variable visible at this point
        variables = frame0.GetVariables(True, True, True, False)
        self.assertTrue(variables.IsValid())
        self.assertEqual(variables.GetSize(), 1)

        # Check that we can get values from paths. `_handler_one` is a special
        # value we provide through only our expression handler in the frame
        # implementation.
        one = frame0.GetValueForVariablePath("_handler_one")
        self.assertEqual(one.unsigned, 1)
        var = frame0.GetValueForVariablePath("variable_in_main")
        # The names won't necessarily match, but the values should (the frame renames the SBValue)
        self.assertEqual(var.unsigned, variables.GetValueAtIndex(0).unsigned)
        varp1 = frame0.GetValueForVariablePath("variable_in_main + 1")
        self.assertEqual(varp1.unsigned, 124)

    def test_frame_validity_after_step(self):
        """Test that SBFrame references from ScriptedFrameProvider remain valid after stepping.

        This test verifies that ExecutionContextRef properly handles frame list identifiers
        when the underlying stack changes. After stepping, old frame references should become
        invalid gracefully without crashing.
        """
        self.build()
        target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
            self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
        )

        # Import the test frame provider.
        script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
        self.runCmd("command script import " + script_path)

        # Register a provider that prepends synthetic frames.
        error = lldb.SBError()
        provider_id = target.RegisterScriptedFrameProvider(
            "test_frame_providers.PrependFrameProvider",
            lldb.SBStructuredData(),
            error,
        )
        self.assertTrue(error.Success(), f"Failed to register provider: {error}")

        # Get frame references before stepping.
        frame0_before = thread.GetFrameAtIndex(0)
        frame1_before = thread.GetFrameAtIndex(1)
        frame2_before = thread.GetFrameAtIndex(2)

        self.assertIsNotNone(frame0_before)
        self.assertIsNotNone(frame1_before)
        self.assertIsNotNone(frame2_before)

        # Verify frames are valid and have expected PCs.
        self.assertTrue(frame0_before.IsValid(), "Frame 0 should be valid before step")
        self.assertTrue(frame1_before.IsValid(), "Frame 1 should be valid before step")
        self.assertTrue(frame2_before.IsValid(), "Frame 2 should be valid before step")

        pc0_before = frame0_before.GetPC()
        pc1_before = frame1_before.GetPC()
        pc2_before = frame2_before.GetPC()

        self.assertEqual(pc0_before, 0x9000, "Frame 0 should have synthetic PC 0x9000")
        self.assertEqual(pc1_before, 0xA000, "Frame 1 should have synthetic PC 0xA000")

        # Step the thread, which will invalidate the old frame list.
        thread.StepInstruction(False)

        # After stepping, the frame list has changed. Old frame references should
        # detect this and become invalid, but shouldn't crash.
        # The key here is that GetPC() and other operations should handle the
        # "frame provider no longer available" case gracefully.

        # Try to access the old frames - they should either:
        # 1. Return invalid/default values gracefully, or
        # 2. Still work if the frame provider is re-applied.

        # Get new frames after stepping.
        frame0_after = thread.GetFrameAtIndex(0)
        self.assertIsNotNone(frame0_after)
        self.assertTrue(
            frame0_after.IsValid(), "New frame 0 should be valid after step"
        )

        # The old frame references might or might not be valid depending on whether
        # the frame provider is still active. What's important is that accessing
        # them doesn't crash and handles the situation gracefully.
        # We'll just verify we can call methods on them without crashing.
        try:
            _ = frame0_before.GetPC()
            _ = frame0_before.IsValid()
            _ = frame0_before.GetFunctionName()
        except Exception as e:
            self.fail(f"Accessing old frame reference should not crash: {e}")

    def test_provider_lifecycle_with_frame_validity(self):
        """Test provider registration/removal at breakpoints and SBFrame validity across lifecycle.

        This test verifies:
        1. Registering a provider while stopped at a breakpoint.
        2. SBFrame references from synthetic frames persist across continues.
        3. SBFrame references can access variables in real frames while provider is active.
        4. Removing a provider while stopped at a breakpoint.
        5. SBFrame references from removed provider don't crash when accessed.
        """
        self.build()
        target = self.dbg.CreateTarget(self.getBuildArtifact("a.out"))
        self.assertTrue(target.IsValid(), "Target should be valid")

        # Import the test frame provider.
        script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
        self.runCmd("command script import " + script_path)

        # Set up breakpoints at the return statements in foo, bar, and baz.
        # This ensures local variables are initialized.
        bp_foo = target.BreakpointCreateBySourceRegex(
            "Break in foo", lldb.SBFileSpec(self.source)
        )
        bp_bar = target.BreakpointCreateBySourceRegex(
            "Break in bar", lldb.SBFileSpec(self.source)
        )
        bp_baz = target.BreakpointCreateBySourceRegex(
            "Break in baz", lldb.SBFileSpec(self.source)
        )

        self.assertTrue(bp_foo.IsValid(), "Breakpoint at foo should be valid")
        self.assertTrue(bp_bar.IsValid(), "Breakpoint at bar should be valid")
        self.assertTrue(bp_baz.IsValid(), "Breakpoint at baz should be valid")

        # Launch the process.
        process = target.LaunchSimple(None, None, self.get_process_working_directory())
        self.assertTrue(process.IsValid(), "Process should be valid")

        # We should hit the foo breakpoint first.
        self.assertEqual(
            process.GetState(), lldb.eStateStopped, "Process should be stopped at foo"
        )
        thread = process.GetSelectedThread()
        self.assertIsNotNone(thread, "Should have a selected thread")

        # Register the provider at foo breakpoint.
        error = lldb.SBError()
        provider_id = target.RegisterScriptedFrameProvider(
            "test_frame_providers.PrependFrameProvider",
            lldb.SBStructuredData(),
            error,
        )
        self.assertTrue(error.Success(), f"Failed to register provider: {error}")
        self.assertNotEqual(provider_id, 0, "Provider ID should be non-zero")

        # Get individual frames BEFORE getting the full backtrace.
        # This tests accessing frames before forcing evaluation of all frames.
        frame0 = thread.GetFrameAtIndex(0)
        frame1 = thread.GetFrameAtIndex(1)
        frame2 = thread.GetFrameAtIndex(2)

        self.assertIsNotNone(frame0, "Frame 0 should exist")
        self.assertIsNotNone(frame1, "Frame 1 should exist")
        self.assertIsNotNone(frame2, "Frame 2 should exist")

        # First two frames should be synthetic with expected PCs.
        pc0 = frame0.GetPC()
        pc1 = frame1.GetPC()

        self.assertEqual(pc0, 0x9000, "Frame 0 should have synthetic PC 0x9000")
        self.assertEqual(pc1, 0xA000, "Frame 1 should have synthetic PC 0xA000")

        # Frame 2 should be the real foo frame.
        self.assertIn("foo", frame2.GetFunctionName(), "Frame 2 should be in foo")

        # Save references to the synthetic frames.
        saved_frames = [frame0, frame1, frame2]

        # Test accessing saved frames at foo BEFORE getting full backtrace.
        try:
            _ = saved_frames[0].GetPC()
            _ = saved_frames[1].GetPC()
            _ = saved_frames[2].GetFunctionName()
        except Exception as e:
            self.fail(
                f"Accessing saved frames at foo before full backtrace should not crash: {e}"
            )

        # Now verify the provider is active by checking frame count.
        # PrependFrameProvider adds 2 synthetic frames.
        # This forces a full backtrace evaluation.
        original_frame_count = thread.GetNumFrames()
        self.assertGreater(
            original_frame_count,
            2,
            "Should have at least synthetic frames + real frames",
        )

        # Test accessing saved frames at foo AFTER getting full backtrace.
        try:
            _ = saved_frames[0].GetPC()
            _ = saved_frames[1].GetPC()
            _ = saved_frames[2].GetFunctionName()
        except Exception as e:
            self.fail(
                f"Accessing saved frames at foo after full backtrace should not crash: {e}"
            )

        # Verify we can access variables in frame2 (real frame).
        foo_local = frame2.FindVariable("foo_local")
        self.assertTrue(foo_local.IsValid(), "Should find foo_local variable")
        self.assertEqual(
            foo_local.GetValueAsUnsigned(), 20, "foo_local should be 20 (10 * 2)"
        )

        # Continue to bar breakpoint.
        threads = lldbutil.continue_to_breakpoint(process, bp_bar)
        self.assertIsNotNone(threads, "Should have stopped at bar breakpoint")
        self.assertEqual(len(threads), 1, "Should have one thread stopped at bar")
        thread = threads[0]

        # Verify the saved frames are still accessible without crashing at bar.
        # Do this BEFORE getting the full backtrace.
        # Note: They might not be "valid" in the traditional sense since we've moved
        # to a different execution context, but they shouldn't crash.
        try:
            _ = saved_frames[0].GetPC()
            _ = saved_frames[1].GetPC()
        except Exception as e:
            self.fail(
                f"Accessing saved frames at bar before full backtrace should not crash: {e}"
            )

        # Verify the provider is still active by getting frame count.
        # This forces full backtrace evaluation.
        current_frame_count = thread.GetNumFrames()
        self.assertGreater(
            current_frame_count, 2, "Should still have synthetic frames at bar"
        )

        # Access the saved frames again AFTER getting the full backtrace.
        # This ensures that forcing a full backtrace evaluation doesn't break
        # the saved frame references.
        try:
            _ = saved_frames[0].GetPC()
            _ = saved_frames[1].GetPC()
        except Exception as e:
            self.fail(
                f"Accessing saved frames at bar after full backtrace should not crash: {e}"
            )

        # Get current frames at bar.
        bar_frame0 = thread.GetFrameAtIndex(0)
        bar_frame1 = thread.GetFrameAtIndex(1)
        bar_frame2 = thread.GetFrameAtIndex(2)

        # Verify current frames have synthetic PCs.
        self.assertEqual(
            bar_frame0.GetPC(), 0x9000, "Frame 0 at bar should have synthetic PC"
        )
        self.assertEqual(
            bar_frame1.GetPC(), 0xA000, "Frame 1 at bar should have synthetic PC"
        )
        self.assertIn("bar", bar_frame2.GetFunctionName(), "Frame 2 should be in bar")

        # Verify we can access variables in the bar frame.
        bar_local = bar_frame2.FindVariable("bar_local")
        self.assertTrue(bar_local.IsValid(), "Should find bar_local variable")
        self.assertEqual(
            bar_local.GetValueAsUnsigned(), 25, "bar_local should be 25 (5 * 5)"
        )

        # Continue to baz breakpoint.
        threads = lldbutil.continue_to_breakpoint(process, bp_baz)
        self.assertIsNotNone(threads, "Should have stopped at baz breakpoint")
        self.assertEqual(len(threads), 1, "Should have one thread stopped at baz")
        thread = threads[0]

        # Verify the saved frames are still accessible without crashing at baz.
        # Do this BEFORE getting the full backtrace.
        try:
            _ = saved_frames[0].GetPC()
            _ = saved_frames[1].GetPC()
            _ = saved_frames[2].GetFunctionName()
        except Exception as e:
            self.fail(
                f"Accessing saved frames at baz before full backtrace should not crash: {e}"
            )

        # Get the frame count to force full backtrace evaluation at baz.
        baz_frame_count = thread.GetNumFrames()
        self.assertGreater(
            baz_frame_count, 2, "Should still have synthetic frames at baz"
        )

        # Verify the saved frames are still accessible AFTER getting full backtrace at baz.
        try:
            _ = saved_frames[0].GetPC()
            _ = saved_frames[1].GetPC()
            _ = saved_frames[2].GetFunctionName()
        except Exception as e:
            self.fail(
                f"Accessing saved frames at baz after full backtrace should not crash: {e}"
            )

        # Now manually remove the provider.
        result = target.RemoveScriptedFrameProvider(provider_id)
        self.assertSuccess(
            result, f"Should successfully remove provider with ID {provider_id}"
        )
        # Verify frames no longer have synthetic frames.
        final_frame_count = thread.GetNumFrames()

        # Without the provider, we should have fewer frames (no synthetic ones).
        self.assertLess(
            final_frame_count,
            original_frame_count,
            "Frame count should decrease after provider removal",
        )

        # First frame should now be the real baz frame (no synthetic frames).
        baz_frame0 = thread.GetFrameAtIndex(0)
        self.assertIn(
            "baz", baz_frame0.GetFunctionName(), "Frame 0 should now be real baz frame"
        )

        # The synthetic PC values should no longer appear.
        for i in range(final_frame_count):
            frame = thread.GetFrameAtIndex(i)
            pc = frame.GetPC()
            self.assertNotEqual(
                pc, 0x9000, f"Frame {i} should not have synthetic PC 0x9000"
            )
            self.assertNotEqual(
                pc, 0xA000, f"Frame {i} should not have synthetic PC 0xA000"
            )

        # Verify the originally saved frames are now truly invalid/stale.
        # They should still not crash when accessed.
        try:
            _ = saved_frames[0].GetPC()
            _ = saved_frames[0].IsValid()
            _ = saved_frames[1].GetPC()
            _ = saved_frames[1].IsValid()
        except Exception as e:
            self.fail(f"Accessing invalidated frames should not crash: {e}")

    def test_event_broadcasting(self):
        """Test that adding/removing frame providers broadcasts eBroadcastBitStackChanged."""
        self.build()

        listener = lldb.SBListener("stack_changed_listener")
        listener.StartListeningForEventClass(
            self.dbg,
            lldb.SBThread.GetBroadcasterClassName(),
            lldb.SBThread.eBroadcastBitStackChanged,
        )

        target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
            self, "Break here", lldb.SBFileSpec(self.source), only_one_thread=False
        )

        expected_thread_ids = {
            process.GetThreadAtIndex(i).GetIndexID()
            for i in range(process.GetNumThreads())
        }

        def collect_stack_changed_thread_ids(count):
            event = lldb.SBEvent()
            thread_ids = set()
            for _ in range(count):
                if not listener.WaitForEvent(5, event):
                    break
                self.assertEqual(
                    event.GetType(),
                    lldb.SBThread.eBroadcastBitStackChanged,
                    "Event should be stack changed",
                )
                thread_ids.add(lldb.SBThread.GetThreadFromEvent(event).GetIndexID())
            return thread_ids

        # Import the test frame provider.
        script_path = os.path.join(self.getSourceDir(), "test_frame_providers.py")
        self.runCmd("command script import " + script_path)

        # 1. Test registration.
        error = lldb.SBError()
        provider_id = target.RegisterScriptedFrameProvider(
            "test_frame_providers.ReplaceFrameProvider",
            lldb.SBStructuredData(),
            error,
        )
        self.assertSuccess(error, f"Failed to register provider: {error}")
        self.assertEqual(
            collect_stack_changed_thread_ids(len(expected_thread_ids)),
            expected_thread_ids,
            "All threads should broadcast eBroadcastBitStackChanged on registration",
        )

        # 2. Test removal.
        result = target.RemoveScriptedFrameProvider(provider_id)
        self.assertSuccess(result, f"Failed to remove provider: {result}")
        self.assertEqual(
            collect_stack_changed_thread_ids(len(expected_thread_ids)),
            expected_thread_ids,
            "All threads should broadcast eBroadcastBitStackChanged on removal",
        )

        # 3. Test clear.
        target.RegisterScriptedFrameProvider(
            "test_frame_providers.ReplaceFrameProvider",
            lldb.SBStructuredData(),
            error,
        )
        # Consume registration
        collect_stack_changed_thread_ids(len(expected_thread_ids))

        self.runCmd("target frame-provider clear")
        self.assertEqual(
            collect_stack_changed_thread_ids(len(expected_thread_ids)),
            expected_thread_ids,
            "All threads should broadcast eBroadcastBitStackChanged on clear",
        )