"""
Test breakpoint names.
"""

import os
import lldb
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import TestBase, VALID_BREAKPOINT, VALID_TARGET
from lldbsuite.test import lldbutil


class BreakpointNames(TestBase):
    NO_DEBUG_INFO_TESTCASE = True

    @add_test_categories(["pyapi"])
    def test_setting_names(self):
        """Use Python APIs to test that we can set breakpoint names."""
        self.build()
        self.setup_target()
        self.do_check_names()

    def test_illegal_names(self):
        """Use Python APIs to test that we don't allow illegal names."""
        self.build()
        self.setup_target()
        self.do_check_illegal_names()

    def test_using_names(self):
        """Use Python APIs to test that operations on names works correctly."""
        self.build()
        self.setup_target()
        self.do_check_using_names()

    def test_configuring_names(self):
        """Use Python APIs to test that configuring options on breakpoint names works correctly."""
        self.build()
        self.make_a_dummy_name()
        self.setup_target()
        self.do_check_configuring_names()

    def test_configuring_permissions_sb(self):
        """Use Python APIs to test that configuring permissions on names works correctly."""
        self.build()
        self.setup_target()
        self.do_check_configuring_permissions_sb()

    def test_configuring_permissions_cli(self):
        """Use Python APIs to test that configuring permissions on names works correctly."""
        self.build()
        self.setup_target()
        self.do_check_configuring_permissions_cli()

    def test_breakpointname_enabled(self):
        """Use Commands and Python APIs to test that enabling and disabling a
        breakpoint name is propagated to all breakpoints with that name.
        """
        self.build()
        self.setup_target()
        self.do_check_breakpointname_enabled()

    def setup_target(self):
        exe = self.getBuildArtifact("a.out")

        # Create a targets we are making breakpoint in and copying to:
        self.target = self.dbg.CreateTarget(exe)
        self.assertTrue(self.target, VALID_TARGET)
        self.main_file_spec = lldb.SBFileSpec(
            os.path.join(self.getSourceDir(), "main.c")
        )

    def check_name_in_target(self, bkpt_name):
        name_list = lldb.SBStringList()
        self.target.GetBreakpointNames(name_list)
        found_it = False
        for name in name_list:
            if name == bkpt_name:
                found_it = True
                break
        self.assertTrue(
            found_it, "Didn't find the name %s in the target's name list:" % (bkpt_name)
        )

    def setUp(self):
        # Call super's setUp().
        TestBase.setUp(self)

        # These are the settings we're going to be putting into names & breakpoints:
        self.bp_name_string = "ABreakpoint"
        self.is_one_shot = True
        self.ignore_count = 1000
        self.condition = "1 == 2"
        self.auto_continue = True
        self.tid = 0xAAAA
        self.tidx = 10
        self.thread_name = "Fooey"
        self.queue_name = "Blooey"
        self.cmd_list = lldb.SBStringList()
        self.cmd_list.AppendString("frame var")
        self.cmd_list.AppendString("bt")
        self.help_string = "I do something interesting"

    def do_check_names(self):
        """Use Python APIs to check that we can set & retrieve breakpoint names"""
        bkpt = self.target.BreakpointCreateByLocation(self.main_file_spec, 10)
        bkpt_name = "ABreakpoint"
        other_bkpt_name = "_AnotherBreakpoint"

        # Add a name and make sure we match it:
        success = bkpt.AddNameWithErrorHandling(bkpt_name)
        self.assertSuccess(success, "We couldn't add a legal name to a breakpoint.")

        matches = bkpt.MatchesName(bkpt_name)
        self.assertTrue(matches, "We didn't match the name we just set")

        # Make sure we don't match irrelevant names:
        matches = bkpt.MatchesName("NotABreakpoint")
        self.assertTrue(not matches, "We matched a name we didn't set.")

        # Make sure the name is also in the target:
        self.check_name_in_target(bkpt_name)

        # Add another name, make sure that works too:
        bkpt.AddNameWithErrorHandling(other_bkpt_name)

        matches = bkpt.MatchesName(bkpt_name)
        self.assertTrue(
            matches, "Adding a name means we didn't match the name we just set"
        )
        self.check_name_in_target(other_bkpt_name)

        # Remove the name and make sure we no longer match it:
        bkpt.RemoveName(bkpt_name)
        matches = bkpt.MatchesName(bkpt_name)
        self.assertTrue(not matches, "We still match a name after removing it.")

        # Make sure the name list has the remaining name:
        name_list = lldb.SBStringList()
        bkpt.GetNames(name_list)
        num_names = name_list.GetSize()
        self.assertEqual(
            num_names, 1, "Name list has %d items, expected 1." % (num_names)
        )

        name = name_list.GetStringAtIndex(0)
        self.assertEqual(
            name,
            other_bkpt_name,
            "Remaining name was: %s expected %s." % (name, other_bkpt_name),
        )

    def do_check_illegal_names(self):
        """Use Python APIs to check that we reject illegal names."""
        bkpt = self.target.BreakpointCreateByLocation(self.main_file_spec, 10)
        bad_names = [
            "-CantStartWithADash",
            "1CantStartWithANumber",
            "^CantStartWithNonAlpha",
            "CantHave-ADash",
            "Cant Have Spaces",
        ]
        for bad_name in bad_names:
            success = bkpt.AddNameWithErrorHandling(bad_name)
            self.assertTrue(
                success.Fail(), "We allowed an illegal name: %s" % (bad_name)
            )
            bp_name = lldb.SBBreakpointName(self.target, bad_name)
            self.assertFalse(
                bp_name.IsValid(),
                "We made a breakpoint name with an illegal name: %s" % (bad_name),
            )

            retval = lldb.SBCommandReturnObject()
            self.dbg.GetCommandInterpreter().HandleCommand(
                "break set -n whatever -N '%s'" % (bad_name), retval
            )
            self.assertTrue(
                not retval.Succeeded(),
                "break set succeeded with: illegal name: %s" % (bad_name),
            )

    def do_check_using_names(self):
        """Use Python APIs to check names work in place of breakpoint ID's."""

        # Create a dummy breakpoint to use up ID 1
        _ = self.target.BreakpointCreateByLocation(self.main_file_spec, 30)

        # Create a breakpoint to test with
        bkpt = self.target.BreakpointCreateByLocation(self.main_file_spec, 10)
        bkpt_name = "ABreakpoint"
        bkpt_id = bkpt.GetID()
        other_bkpt_name = "_AnotherBreakpoint"

        # Add a name and make sure we match it:
        success = bkpt.AddNameWithErrorHandling(bkpt_name)
        self.assertSuccess(success, "We couldn't add a legal name to a breakpoint.")

        bkpts = lldb.SBBreakpointList(self.target)
        self.target.FindBreakpointsByName(bkpt_name, bkpts)

        self.assertEqual(bkpts.GetSize(), 1, "One breakpoint matched.")
        found_bkpt = bkpts.GetBreakpointAtIndex(0)
        self.assertEqual(bkpt.GetID(), found_bkpt.GetID(), "The right breakpoint.")
        self.assertEqual(bkpt.GetID(), bkpt_id, "With the same ID as before.")
        self.assertEqual(bkpts[0].GetID(), bkpt.GetID(), "subscript [0] matches")
        self.assertEqual(bkpts[-1].GetID(), bkpt.GetID(), "subscript [-1] matches")

        retval = lldb.SBCommandReturnObject()
        self.dbg.GetCommandInterpreter().HandleCommand(
            "break disable %s" % (bkpt_name), retval
        )
        self.assertTrue(
            retval.Succeeded(), "break disable failed with: %s." % (retval.GetError())
        )
        self.assertTrue(not bkpt.IsEnabled(), "We didn't disable the breakpoint.")

        # Also make sure we don't apply commands to non-matching names:
        self.dbg.GetCommandInterpreter().HandleCommand(
            "break modify --one-shot 1 %s" % (other_bkpt_name), retval
        )
        self.assertTrue(
            retval.Succeeded(), "break modify failed with: %s." % (retval.GetError())
        )
        self.assertTrue(
            not bkpt.IsOneShot(), "We applied one-shot to the wrong breakpoint."
        )

    def check_option_values(self, bp_object):
        self.assertEqual(bp_object.one_shot, self.is_one_shot, "IsOneShot")
        self.assertEqual(bp_object.ignore_count, self.ignore_count, "IgnoreCount")
        self.assertEqual(bp_object.condition, self.condition, "Condition")
        self.assertEqual(bp_object.auto_continue, self.auto_continue, "AutoContinue")
        self.assertEqual(bp_object.thread_id, self.tid, "Thread ID")
        self.assertEqual(bp_object.thread_index, self.tidx, "Thread Index")
        self.assertEqual(bp_object.thread_name, self.thread_name, "Thread Name")
        self.assertEqual(bp_object.queue_name, self.queue_name, "Queue Name")
        set_cmds = lldb.SBStringList()
        bp_object.GetCommandLineCommands(set_cmds)
        self.assertEqual(
            set_cmds.GetSize(), self.cmd_list.GetSize(), "Size of command line commands"
        )
        for idx in range(0, set_cmds.GetSize()):
            self.assertEqual(
                self.cmd_list.GetStringAtIndex(idx),
                set_cmds.GetStringAtIndex(idx),
                "Command %d" % (idx),
            )

    def make_a_dummy_name(self):
        "This makes a breakpoint name in the dummy target to make sure it gets copied over"

        dummy_target = self.dbg.GetDummyTarget()
        self.assertTrue(dummy_target.IsValid(), "Dummy target was not valid.")

        def cleanup():
            self.dbg.GetDummyTarget().DeleteBreakpointName(self.bp_name_string)

        # Execute the cleanup function during test case tear down.
        self.addTearDownHook(cleanup)

        # Now find it in the dummy target, and make sure these settings took:
        bp_name = lldb.SBBreakpointName(dummy_target, self.bp_name_string)
        # Make sure the name is right:
        self.assertEqual(
            bp_name.GetName(),
            self.bp_name_string,
            "Wrong bp_name: %s" % (bp_name.GetName()),
        )
        bp_name.SetOneShot(self.is_one_shot)
        bp_name.SetIgnoreCount(self.ignore_count)
        bp_name.SetCondition(self.condition)
        bp_name.SetAutoContinue(self.auto_continue)
        bp_name.SetThreadID(self.tid)
        bp_name.SetThreadIndex(self.tidx)
        bp_name.SetThreadName(self.thread_name)
        bp_name.SetQueueName(self.queue_name)
        bp_name.SetCommandLineCommands(self.cmd_list)

        # Now look it up again, and make sure it got set correctly.
        bp_name = lldb.SBBreakpointName(dummy_target, self.bp_name_string)
        self.assertTrue(bp_name.IsValid(), "Failed to make breakpoint name.")
        self.check_option_values(bp_name)

    def do_check_configuring_names(self):
        """Use Python APIs to check that configuring breakpoint names works correctly."""
        other_bp_name_string = "AnotherBreakpointName"
        cl_bp_name_string = "CLBreakpointName"

        # Now find the version copied in from the dummy target, and make sure these settings took:
        bp_name = lldb.SBBreakpointName(self.target, self.bp_name_string)
        self.assertTrue(bp_name.IsValid(), "Failed to make breakpoint name.")
        self.check_option_values(bp_name)

        # Now add this name to a breakpoint, and make sure it gets configured properly
        bkpt = self.target.BreakpointCreateByLocation(self.main_file_spec, 10)
        success = bkpt.AddNameWithErrorHandling(self.bp_name_string)
        self.assertSuccess(success, "Couldn't add this name to the breakpoint")
        self.check_option_values(bkpt)

        # Now make a name from this breakpoint, and make sure the new name is properly configured:
        new_name = lldb.SBBreakpointName(bkpt, other_bp_name_string)
        self.assertTrue(
            new_name.IsValid(), "Couldn't make a valid bp_name from a breakpoint."
        )
        self.check_option_values(bkpt)

        # Now change the name's option and make sure it gets propagated to
        # the breakpoint:
        new_auto_continue = not self.auto_continue
        bp_name.SetAutoContinue(new_auto_continue)
        self.assertEqual(
            bp_name.GetAutoContinue(),
            new_auto_continue,
            "Couldn't change auto-continue on the name",
        )
        self.assertEqual(
            bkpt.GetAutoContinue(),
            new_auto_continue,
            "Option didn't propagate to the breakpoint.",
        )

        # Now make this same breakpoint name - but from the command line
        cmd_str = (
            "breakpoint name configure %s -o %d -i %d -c '%s' -G %d -t %d -x %d -T '%s' -q '%s' -H '%s'"
            % (
                cl_bp_name_string,
                self.is_one_shot,
                self.ignore_count,
                self.condition,
                self.auto_continue,
                self.tid,
                self.tidx,
                self.thread_name,
                self.queue_name,
                self.help_string,
            )
        )
        for cmd in self.cmd_list:
            cmd_str += " -C '%s'" % (cmd)

        self.runCmd(cmd_str, check=True)
        # Now look up this name again and check its options:
        cl_name = lldb.SBBreakpointName(self.target, cl_bp_name_string)
        self.check_option_values(cl_name)
        # Also check the help string:
        self.assertEqual(
            self.help_string, cl_name.GetHelpString(), "Help string didn't match"
        )
        # Change the name and make sure that works:
        new_help = "I do something even more interesting"
        cl_name.SetHelpString(new_help)
        self.assertEqual(new_help, cl_name.GetHelpString(), "SetHelpString didn't")

        # We should have three names now, make sure the target can list them:
        name_list = lldb.SBStringList()
        self.target.GetBreakpointNames(name_list)
        for name_string in [
            self.bp_name_string,
            other_bp_name_string,
            cl_bp_name_string,
        ]:
            self.assertIn(
                name_string, name_list, "Didn't find %s in names" % (name_string)
            )

        # Delete the name from the current target.  Make sure that works and deletes the
        # name from the breakpoint as well:
        self.target.DeleteBreakpointName(self.bp_name_string)
        name_list.Clear()
        self.target.GetBreakpointNames(name_list)
        self.assertNotIn(
            self.bp_name_string,
            name_list,
            "Didn't delete %s from a real target" % (self.bp_name_string),
        )
        # Also make sure the name got removed from breakpoints holding it:
        self.assertFalse(
            bkpt.MatchesName(self.bp_name_string),
            "Didn't remove the name from the breakpoint.",
        )

        # Test that deleting the name we injected into the dummy target works (there's also a
        # cleanup that will do this, but that won't test the result...
        dummy_target = self.dbg.GetDummyTarget()
        dummy_target.DeleteBreakpointName(self.bp_name_string)
        name_list.Clear()
        dummy_target.GetBreakpointNames(name_list)
        self.assertNotIn(
            self.bp_name_string,
            name_list,
            "Didn't delete %s from the dummy target" % (self.bp_name_string),
        )
        # Also make sure the name got removed from breakpoints holding it:
        self.assertFalse(
            bkpt.MatchesName(self.bp_name_string),
            "Didn't remove the name from the breakpoint.",
        )

    def check_permission_results(self, bp_name):
        self.assertFalse(bp_name.GetAllowDelete(), "Didn't set allow delete.")
        protected_bkpt = self.target.BreakpointCreateByLocation(self.main_file_spec, 10)
        protected_id = protected_bkpt.GetID()

        unprotected_bkpt = self.target.BreakpointCreateByLocation(
            self.main_file_spec, 10
        )
        unprotected_id = unprotected_bkpt.GetID()

        success = protected_bkpt.AddNameWithErrorHandling(self.bp_name_string)
        self.assertSuccess(success, "Couldn't add this name to the breakpoint")

        self.target.DisableAllBreakpoints()
        self.assertTrue(
            protected_bkpt.IsEnabled(), "Didnt' keep breakpoint from being disabled"
        )
        self.assertFalse(
            unprotected_bkpt.IsEnabled(),
            "Protected too many breakpoints from disabling.",
        )

        # Try from the command line too:
        unprotected_bkpt.SetEnabled(True)
        result = lldb.SBCommandReturnObject()
        self.dbg.GetCommandInterpreter().HandleCommand("break disable", result)
        self.assertTrue(result.Succeeded())
        self.assertTrue(
            protected_bkpt.IsEnabled(), "Didnt' keep breakpoint from being disabled"
        )
        self.assertFalse(
            unprotected_bkpt.IsEnabled(),
            "Protected too many breakpoints from disabling.",
        )

        self.target.DeleteAllBreakpoints()
        bkpt = self.target.FindBreakpointByID(protected_id)
        self.assertTrue(
            bkpt.IsValid(), "Didn't keep the breakpoint from being deleted."
        )
        bkpt = self.target.FindBreakpointByID(unprotected_id)
        self.assertFalse(
            bkpt.IsValid(), "Protected too many breakpoints from deletion."
        )

        # Remake the unprotected breakpoint and try again from the command line:
        unprotected_bkpt = self.target.BreakpointCreateByLocation(
            self.main_file_spec, 10
        )
        unprotected_id = unprotected_bkpt.GetID()

        self.dbg.GetCommandInterpreter().HandleCommand("break delete -f", result)
        self.assertTrue(result.Succeeded())
        bkpt = self.target.FindBreakpointByID(protected_id)
        self.assertTrue(
            bkpt.IsValid(), "Didn't keep the breakpoint from being deleted."
        )
        bkpt = self.target.FindBreakpointByID(unprotected_id)
        self.assertFalse(
            bkpt.IsValid(), "Protected too many breakpoints from deletion."
        )

    def do_check_configuring_permissions_sb(self):
        bp_name = lldb.SBBreakpointName(self.target, self.bp_name_string)

        # Make a breakpoint name with delete disallowed:
        bp_name = lldb.SBBreakpointName(self.target, self.bp_name_string)
        self.assertTrue(
            bp_name.IsValid(), "Failed to make breakpoint name for valid name."
        )

        bp_name.SetAllowDelete(False)
        bp_name.SetAllowDisable(False)
        bp_name.SetAllowList(False)
        self.check_permission_results(bp_name)

    def do_check_configuring_permissions_cli(self):
        # Make the name with the right options using the command line:
        self.runCmd(
            "breakpoint name configure -L 0 -D 0 -A 0 %s" % (self.bp_name_string),
            check=True,
        )
        # Now look up the breakpoint we made, and check that it works.
        bp_name = lldb.SBBreakpointName(self.target, self.bp_name_string)
        self.assertTrue(
            bp_name.IsValid(), "Didn't make a breakpoint name we could find."
        )
        self.check_permission_results(bp_name)

    def do_check_breakpointname_enabled(self):
        target: lldb.SBTarget = self.target
        self.assertTrue(target.IsValid(), "Target name must be valid.")
        bp_name: lldb.SBBreakpointName = lldb.SBBreakpointName(
            target, self.bp_name_string
        )
        self.assertTrue(bp_name.IsValid(), "Breakpoint name must be valid.")

        # Create two function breakpoints a and b.
        a_breakpoint: lldb.SBBreakpoint = target.BreakpointCreateByName("a")
        self.assertTrue(a_breakpoint and a_breakpoint.IsValid(), VALID_BREAKPOINT)
        self.assertEqual(a_breakpoint.GetNumLocations(), 1)
        self.assertTrue(a_breakpoint.IsEnabled())
        self.assertTrue(a_breakpoint.AddNameWithErrorHandling(self.bp_name_string))

        b_breakpoint = target.BreakpointCreateByName("b")
        self.assertTrue(b_breakpoint and b_breakpoint.IsValid(), VALID_BREAKPOINT)
        self.assertEqual(b_breakpoint.GetNumLocations(), 1)
        self.assertTrue(b_breakpoint.IsEnabled())
        self.assertTrue(b_breakpoint.AddNameWithErrorHandling(self.bp_name_string))

        # enabled and disable the function breakpoints with the breakpoint name.
        # With API.
        bp_name.SetEnabled(False)
        self.assertFalse(bp_name.IsEnabled())
        self.assertFalse(a_breakpoint.IsEnabled())
        self.assertFalse(b_breakpoint.IsEnabled())
        bp_name.SetEnabled(True)
        self.assertTrue(a_breakpoint.IsEnabled())
        self.assertTrue(b_breakpoint.IsEnabled())
        self.assertTrue(bp_name.IsEnabled())

        # With cli.
        self.runCmd(f"breakpoint name configure {self.bp_name_string} --disable")
        self.assertFalse(bp_name.IsEnabled())
        self.assertFalse(a_breakpoint.IsEnabled())
        self.assertFalse(b_breakpoint.IsEnabled())
        self.runCmd(f"breakpoint name configure {self.bp_name_string} --enable")
        self.assertTrue(a_breakpoint.IsEnabled())
        self.assertTrue(b_breakpoint.IsEnabled())
        self.assertTrue(bp_name.IsEnabled())

        # Disabling all the Breakpoints in a BreakpointName
        # does not disable the BreakpointName.
        a_breakpoint.SetEnabled(False)
        b_breakpoint.SetEnabled(False)
        self.assertFalse(a_breakpoint.IsEnabled())
        self.assertFalse(b_breakpoint.IsEnabled())
        self.assertTrue(bp_name.IsEnabled())

        # BreakpointName should enable all disabled breakpoints with the name.
        b_breakpoint.SetEnabled(True)
        bp_name.SetEnabled(True)
        self.assertTrue(a_breakpoint.IsEnabled())
        self.assertTrue(b_breakpoint.IsEnabled())
        self.assertTrue(bp_name.IsEnabled())
