"""Test stepping over watchpoints and instruction stepping past watchpoints."""


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


class TestStepOverWatchpoint(TestBase):
    NO_DEBUG_INFO_TESTCASE = True

    def get_to_start(self, bkpt_text):
        """Test stepping over watchpoints and instruction stepping past watchpoints.."""
        self.build()
        target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
            self, bkpt_text, lldb.SBFileSpec("main.c")
        )
        return (target, process, thread, frame, read_watchpoint)

    @add_test_categories(["basic_process"])
    # kernel disables wp's over instruction step, fixed in macOS 14.4.
    @skipIf(macos_version=["<", "14.4"])
    def test_step_over_read_watchpoint(self):
        self.build()
        target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
            self, "break here for read watchpoints", lldb.SBFileSpec("main.c")
        )

        frame = thread.GetFrameAtIndex(0)
        self.assertTrue(frame.IsValid(), "Failed to get frame.")

        read_value = frame.FindValue("g_watch_me_read", lldb.eValueTypeVariableGlobal)
        self.assertTrue(read_value.IsValid(), "Failed to find read value.")

        error = lldb.SBError()

        # resolve_location=True, read=True, write=False
        read_watchpoint = read_value.Watch(True, True, False, error)
        self.assertSuccess(error, "Error while setting watchpoint")
        self.assertTrue(read_watchpoint, "Failed to set read watchpoint.")

        # Disable the breakpoint we hit so we don't muddy the waters with
        # stepping off from the breakpoint:
        bkpt.SetEnabled(False)

        thread.StepOver()
        self.assertStopReason(
            thread.GetStopReason(),
            lldb.eStopReasonWatchpoint,
            STOPPED_DUE_TO_WATCHPOINT,
        )
        self.assertEqual(thread.stop_description, "watchpoint 1")

        process.Continue()
        self.assertState(process.GetState(), lldb.eStateStopped, PROCESS_STOPPED)
        self.assertEqual(thread.stop_description, "step over")

        self.step_inst_for_watchpoint(1)

    @add_test_categories(["basic_process"])
    # kernel disables wp's over instruction step, fixed in macOS 14.4.
    @skipIf(macos_version=["<", "14.4"])
    def test_step_over_write_watchpoint(self):
        self.build()
        target, process, thread, bkpt = lldbutil.run_to_source_breakpoint(
            self, "break here for modify watchpoints", lldb.SBFileSpec("main.c")
        )

        # Disable the breakpoint we hit so we don't muddy the waters with
        # stepping off from the breakpoint:
        bkpt.SetEnabled(False)

        frame = thread.GetFrameAtIndex(0)
        self.assertTrue(frame.IsValid(), "Failed to get frame.")

        write_value = frame.FindValue("g_watch_me_write", lldb.eValueTypeVariableGlobal)
        self.assertTrue(write_value, "Failed to find write value.")

        error = lldb.SBError()
        # resolve_location=True, read=False, modify=True
        write_watchpoint = write_value.Watch(True, False, True, error)
        self.assertTrue(write_watchpoint, "Failed to set write watchpoint.")
        self.assertSuccess(error, "Error while setting watchpoint")

        thread.StepOver()
        self.assertStopReason(
            thread.GetStopReason(),
            lldb.eStopReasonWatchpoint,
            STOPPED_DUE_TO_WATCHPOINT,
        )
        self.assertEqual(thread.stop_description, "watchpoint 1")

        process.Continue()
        self.assertState(process.GetState(), lldb.eStateStopped, PROCESS_STOPPED)
        self.assertEqual(thread.stop_description, "step over")

        self.step_inst_for_watchpoint(1)

    def step_inst_for_watchpoint(self, wp_id):
        watchpoint_hit = False
        current_line = self.frame().GetLineEntry().GetLine()
        while self.frame().GetLineEntry().GetLine() == current_line:
            self.thread().StepInstruction(False)  # step_over=False
            stop_reason = self.thread().GetStopReason()
            if stop_reason == lldb.eStopReasonWatchpoint:
                self.assertFalse(watchpoint_hit, "Watchpoint already hit.")
                expected_stop_desc = "watchpoint %d" % wp_id
                actual_stop_desc = self.thread().stop_description
                self.assertEqual(
                    actual_stop_desc, expected_stop_desc, "Watchpoint ID didn't match."
                )
                watchpoint_hit = True
            else:
                self.assertStopReason(
                    stop_reason, lldb.eStopReasonPlanComplete, STOPPED_DUE_TO_STEP_IN
                )
        self.assertTrue(watchpoint_hit, "Watchpoint never hit.")
