"""
Test stop hook functionality
"""


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


class TestStopHooks(TestBase):
    # If your test case doesn't stress debug info, then
    # set this to true.  That way it won't be run once for
    # each debug info format.
    NO_DEBUG_INFO_TESTCASE = True

    def setUp(self):
        TestBase.setUp(self)
        self.build()
        self.main_source_file = lldb.SBFileSpec("main.c")
        full_path = os.path.join(self.getSourceDir(), "main.c")
        self.main_start_line = line_number(full_path, "main()")

    def test_bad_handler(self):
        """Test that we give a good error message when the handler is bad"""
        self.script_setup()
        result = lldb.SBCommandReturnObject()

        # First try the wrong number of args handler:
        command = "target stop-hook add -P stop_hook.bad_handle_stop"
        self.interp.HandleCommand(command, result)
        self.assertFalse(result.Succeeded(), "Set the target stop hook")
        self.assertIn(
            "has unexpected argument count",
            result.GetError(),
            "Got the wrong number of args error",
        )

        # Next the no handler at all handler:
        command = "target stop-hook add -P stop_hook.no_handle_stop"

        self.interp.HandleCommand(command, result)
        self.assertFalse(result.Succeeded(), "Set the target stop hook")
        self.assertIn(
            "Abstract method no_handle_stop.handle_stop not implemented",
            result.GetError(),
            "Got the right error",
        )

    def test_self_deleting(self):
        """Test that we can handle a stop hook that deletes itself"""
        self.script_setup()
        # Run to the first breakpoint before setting the stop hook
        # so we don't have to figure out where it showed up in the new
        # target.
        (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
            self, "Stop here first", self.main_source_file
        )

        # Now add our stop hook and register it:
        result = lldb.SBCommandReturnObject()
        command = "target stop-hook add -P stop_hook.self_deleting_stop"
        self.interp.HandleCommand(command, result)
        self.assertCommandReturn(result, f"Added my stop hook: {result.GetError()}")

        result_str = result.GetOutput()
        p = re.compile("Stop hook #([0-9]+) added.")
        m = p.match(result_str)
        current_stop_hook_id = m.group(1)
        command = "command script add -o -f stop_hook.handle_stop_hook_id handle_id"
        self.interp.HandleCommand(command, result)
        self.assertCommandReturn(result, "Added my command")

        command = f"handle_id {current_stop_hook_id}"
        self.interp.HandleCommand(command, result)
        self.assertCommandReturn(result, "Registered my stop ID")

        # Now step the process and make sure the stop hook was deleted.
        thread.StepOver()
        self.interp.HandleCommand("target stop-hook list", result)
        self.assertEqual(result.GetOutput().rstrip(), "No stop hooks.", "Deleted hook")

    def test_stop_hooks_scripted(self):
        """Test that a scripted stop hook works with no specifiers"""
        self.stop_hooks_scripted(5, "-I false")

    def test_stop_hooks_scripted_no_entry(self):
        """Test that a scripted stop hook works with no specifiers"""
        self.stop_hooks_scripted(10)

    def test_stop_hooks_scripted_right_func(self):
        """Test that a scripted stop hook fires when there is a function match"""
        self.stop_hooks_scripted(5, "-I 0 -n step_out_of_me")

    def test_stop_hooks_scripted_wrong_func(self):
        """Test that a scripted stop hook doesn't fire when the function does not match"""
        self.stop_hooks_scripted(0, "-I 0 -n main")

    def test_stop_hooks_scripted_right_lines(self):
        """Test that a scripted stop hook fires when there is a function match"""
        self.stop_hooks_scripted(
            5, "-I 0 -f main.c -l 1 -e %d" % (self.main_start_line)
        )

    def test_stop_hooks_scripted_wrong_lines(self):
        """Test that a scripted stop hook doesn't fire when the function does not match"""
        self.stop_hooks_scripted(
            0, "-I 0 -f main.c -l %d -e 100" % (self.main_start_line)
        )

    def test_stop_hooks_scripted_auto_continue(self):
        """Test that the --auto-continue flag works"""
        self.do_test_auto_continue(False)

    def test_stop_hooks_scripted_return_false(self):
        """Test that the returning False from a stop hook works"""
        self.do_test_auto_continue(True)

    def do_test_auto_continue(self, return_true):
        """Test that auto-continue works."""
        # We set auto-continue to 1 but the stop hook only applies to step_out_of_me,
        # so we should end up stopped in main, having run the expression only once.
        self.script_setup()

        result = lldb.SBCommandReturnObject()

        if return_true:
            command = "target stop-hook add -I 0 -P stop_hook.stop_handler -k increment -v 5 -k return_false -v 1 -n step_out_of_me"
        else:
            command = "target stop-hook add -I 0 -G 1 -P stop_hook.stop_handler -k increment -v 5 -n step_out_of_me"

        self.interp.HandleCommand(command, result)
        self.assertTrue(result.Succeeded(), "Set the target stop hook")

        # First run to main.  If we go straight to the first stop hook hit,
        # run_to_source_breakpoint will fail because we aren't at original breakpoint

        (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
            self, "Stop here first", self.main_source_file
        )

        # Now set the breakpoint on step_out_of_me, and make sure we run the
        # expression, then continue back to main.
        bkpt = target.BreakpointCreateBySourceRegex(
            "Set a breakpoint here and step out", self.main_source_file
        )
        self.assertNotEqual(
            bkpt.GetNumLocations(), 0, "Got breakpoints in step_out_of_me"
        )
        process.Continue()

        var = target.FindFirstGlobalVariable("g_var")
        self.assertTrue(var.IsValid())
        self.assertEqual(var.GetValueAsUnsigned(), 6, "Updated g_var")

        func_name = process.GetSelectedThread().frames[0].GetFunctionName()
        self.assertEqual("main", func_name, "Didn't stop at the expected function.")

    def script_setup(self):
        self.interp = self.dbg.GetCommandInterpreter()
        result = lldb.SBCommandReturnObject()

        # Bring in our script file:
        script_name = os.path.join(self.getSourceDir(), "stop_hook.py")
        command = "command script import " + script_name
        self.interp.HandleCommand(command, result)
        self.assertTrue(
            result.Succeeded(), "com scr imp failed: %s" % (result.GetError())
        )

        # set a breakpoint at the end of main to catch our auto-continue tests.
        # Do it in the dummy target so it will get copied to our target even when
        # we don't have a chance to stop.
        dummy_target = self.dbg.GetDummyTarget()
        dummy_target.BreakpointCreateBySourceRegex(
            "return result", self.main_source_file
        )

    def stop_hooks_scripted(self, g_var_value, specifier=None):
        self.script_setup()

        result = lldb.SBCommandReturnObject()

        command = "target stop-hook add -P stop_hook.stop_handler -k increment -v 5 "
        if specifier:
            command += specifier

        self.interp.HandleCommand(command, result)
        self.assertTrue(result.Succeeded(), "Set the target stop hook")
        (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
            self, "Set a breakpoint here", self.main_source_file
        )
        # At this point we've hit our stop hook so we should have run our expression,
        # which increments g_var by the amount specified by the increment key's value.
        while process.GetState() == lldb.eStateRunning:
            continue

        var = target.FindFirstGlobalVariable("g_var")
        self.assertTrue(var.IsValid())
        self.assertEqual(var.GetValueAsUnsigned(), g_var_value, "Updated g_var")
