"""
Test breakpoint serialization.
"""

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


class BreakpointSerialization(TestBase):
    NO_DEBUG_INFO_TESTCASE = True

    @add_test_categories(["pyapi"])
    def test_resolvers(self):
        """Use Python APIs to test that we serialize resolvers."""
        self.build()
        self.setup_targets_and_cleanup()
        self.do_check_resolvers()

    def test_filters(self):
        """Use Python APIs to test that we serialize search filters correctly."""
        self.build()
        self.setup_targets_and_cleanup()
        self.do_check_filters()

    def test_options(self):
        """Use Python APIs to test that we serialize breakpoint options correctly."""
        self.build()
        self.setup_targets_and_cleanup()
        self.do_check_options()

    def test_appending(self):
        """Use Python APIs to test that we serialize breakpoint options correctly."""
        self.build()
        self.setup_targets_and_cleanup()
        self.do_check_appending()

    def test_name_filters(self):
        """Use python APIs to test that reading in by name works correctly."""
        self.build()
        self.setup_targets_and_cleanup()
        self.do_check_names()

    def test_scripted_extra_args(self):
        self.build()
        self.setup_targets_and_cleanup()
        self.do_check_extra_args()

    def test_resolver_serialization(self):
        """Test that breakpoint resolvers contain the expected information"""
        self.build()
        self.setup_targets_and_cleanup()

        exe_path = self.getBuildArtifact("a.out")
        exe_module = self.orig_target.module["a.out"]
        self.assertTrue(
            exe_module.IsValid(), "Failed to find the executable module in target"
        )
        sym_ctx_list = exe_module.FindFunctions("main")
        self.assertEqual(sym_ctx_list.GetSize(), 1, "Unable to find function 'main'")
        sym_ctx = sym_ctx_list.GetContextAtIndex(0)
        self.assertTrue(
            sym_ctx.IsValid(), "SBSymbolContext representing function 'main' is invalid"
        )
        main_func = sym_ctx.GetFunction()
        self.assertTrue(
            main_func.IsValid(), "SBFunction representing 'main' is invalid"
        )
        main_addr = main_func.GetStartAddress()

        bkpt = self.orig_target.BreakpointCreateBySBAddress(main_addr)
        self.assertTrue(
            bkpt.IsValid(), "Could not place breakpoint on 'main' by address"
        )
        stream = lldb.SBStream()
        sd = bkpt.SerializeToStructuredData()
        sd.GetAsJSON(stream)
        serialized_data = json.loads(stream.GetData())

        self.assertIn(
            exe_path,
            serialized_data["Breakpoint"]["BKPTResolver"]["Options"]["ModuleName"],
        )

    def test_structured_data_serialization(self):
        target = self.dbg.GetDummyTarget()
        self.assertTrue(target.IsValid(), VALID_TARGET)

        interpreter = self.dbg.GetCommandInterpreter()
        result = lldb.SBCommandReturnObject()
        interpreter.HandleCommand("br set -f foo -l 42", result)
        result = lldb.SBCommandReturnObject()
        interpreter.HandleCommand("br set -c 'argc == 1' -n main", result)

        bkp1 = target.GetBreakpointAtIndex(0)
        self.assertTrue(bkp1.IsValid(), VALID_BREAKPOINT)
        stream = lldb.SBStream()
        sd = bkp1.SerializeToStructuredData()
        sd.GetAsJSON(stream)
        serialized_data = json.loads(stream.GetData())
        self.assertEqual(
            serialized_data["Breakpoint"]["BKPTResolver"]["Options"]["FileName"], "foo"
        )
        self.assertEqual(
            serialized_data["Breakpoint"]["BKPTResolver"]["Options"]["LineNumber"], 42
        )

        bkp2 = target.GetBreakpointAtIndex(1)
        self.assertTrue(bkp2.IsValid(), VALID_BREAKPOINT)
        stream = lldb.SBStream()
        sd = bkp2.SerializeToStructuredData()
        sd.GetAsJSON(stream)
        serialized_data = json.loads(stream.GetData())
        self.assertIn(
            "main",
            serialized_data["Breakpoint"]["BKPTResolver"]["Options"]["SymbolNames"],
        )
        self.assertEqual(
            serialized_data["Breakpoint"]["BKPTOptions"]["ConditionText"], "argc == 1"
        )

        invalid_bkp = lldb.SBBreakpoint()
        self.assertFalse(invalid_bkp.IsValid(), "Breakpoint should not be valid.")
        stream = lldb.SBStream()
        sd = invalid_bkp.SerializeToStructuredData()
        sd.GetAsJSON(stream)
        self.assertFalse(
            stream.GetData(), "Invalid breakpoint should have an empty structured data"
        )

    def setup_targets_and_cleanup(self):
        def cleanup():
            self.RemoveTempFile(self.bkpts_file_path)

            if self.orig_target.IsValid():
                self.dbg.DeleteTarget(self.orig_target)
                self.dbg.DeleteTarget(self.copy_target)

        self.addTearDownHook(cleanup)
        self.RemoveTempFile(self.bkpts_file_path)

        exe = self.getBuildArtifact("a.out")

        # Create the targets we are making breakpoints in and copying them to:
        self.orig_target = self.dbg.CreateTarget(exe)
        self.assertTrue(self.orig_target, VALID_TARGET)

        self.copy_target = self.dbg.CreateTarget(exe)
        self.assertTrue(self.copy_target, VALID_TARGET)

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

        self.bkpts_file_path = self.getBuildArtifact("breakpoints.json")
        self.bkpts_file_spec = lldb.SBFileSpec(self.bkpts_file_path)

    def check_equivalence(self, source_bps, do_write=True):
        error = lldb.SBError()

        if do_write:
            error = self.orig_target.BreakpointsWriteToFile(
                self.bkpts_file_spec, source_bps
            )
            self.assertSuccess(error, "Failed writing breakpoints to file")

        copy_bps = lldb.SBBreakpointList(self.copy_target)
        error = self.copy_target.BreakpointsCreateFromFile(
            self.bkpts_file_spec, copy_bps
        )
        self.assertSuccess(error, "Failed reading breakpoints from file")

        num_source_bps = source_bps.GetSize()
        num_copy_bps = copy_bps.GetSize()
        self.assertEqual(
            num_source_bps,
            num_copy_bps,
            "Didn't get same number of input and output breakpoints - orig: %d copy: %d"
            % (num_source_bps, num_copy_bps),
        )

        for i in range(0, num_source_bps):
            source_bp = source_bps.GetBreakpointAtIndex(i)
            source_desc = lldb.SBStream()
            source_bp.GetDescription(source_desc, False)
            source_text = source_desc.GetData()

            # I am assuming here that the breakpoints will get written out in breakpoint ID order, and
            # read back in ditto.  That is true right now, and I can't see any reason to do it differently
            # but if we do we can go to writing the breakpoints one by one, or sniffing the descriptions to
            # see which one is which.
            copy_id = source_bp.GetID()
            copy_bp = copy_bps.FindBreakpointByID(copy_id)
            self.assertTrue(
                copy_bp.IsValid(), "Could not find copy breakpoint %d." % (copy_id)
            )

            copy_desc = lldb.SBStream()
            copy_bp.GetDescription(copy_desc, False)
            copy_text = copy_desc.GetData()

            # These two should be identical.
            self.trace("Source text for %d is %s." % (i, source_text))
            self.assertEqual(
                source_text,
                copy_text,
                "Source and dest breakpoints are not identical: \nsource: %s\ndest: %s"
                % (source_text, copy_text),
            )
            self.assertEqual(
                source_bp.GetNumLocations(),
                copy_bp.GetNumLocations(),
                "Source and dest num locations are not the same",
            )

    def do_check_resolvers(self):
        """Use Python APIs to check serialization of breakpoint resolvers"""

        empty_module_list = lldb.SBFileSpecList()
        empty_cu_list = lldb.SBFileSpecList()
        blubby_file_spec = lldb.SBFileSpec(
            os.path.join(self.getSourceDir(), "blubby.c")
        )

        # It isn't actually important for these purposes that these breakpoint
        # actually have locations.
        source_bps = lldb.SBBreakpointList(self.orig_target)
        source_bps.Append(self.orig_target.BreakpointCreateByLocation("blubby.c", 666))
        # Make sure we do one breakpoint right:
        self.check_equivalence(source_bps)
        source_bps.Clear()

        source_bps.Append(
            self.orig_target.BreakpointCreateByName(
                "blubby", lldb.eFunctionNameTypeAuto, empty_module_list, empty_cu_list
            )
        )
        source_bps.Append(
            self.orig_target.BreakpointCreateByName(
                "blubby", lldb.eFunctionNameTypeFull, empty_module_list, empty_cu_list
            )
        )
        source_bps.Append(
            self.orig_target.BreakpointCreateBySourceRegex(
                "dont really care", blubby_file_spec
            )
        )

        # And some number greater than one:
        self.check_equivalence(source_bps)

    def do_check_filters(self):
        """Use Python APIs to check serialization of breakpoint filters."""
        module_list = lldb.SBFileSpecList()
        module_list.Append(lldb.SBFileSpec("SomeBinary"))
        module_list.Append(lldb.SBFileSpec("SomeOtherBinary"))

        cu_list = lldb.SBFileSpecList()
        cu_list.Append(lldb.SBFileSpec("SomeCU.c"))
        cu_list.Append(lldb.SBFileSpec("AnotherCU.c"))
        cu_list.Append(lldb.SBFileSpec("ThirdCU.c"))

        blubby_file_spec = lldb.SBFileSpec(
            os.path.join(self.getSourceDir(), "blubby.c")
        )

        # It isn't actually important for these purposes that these breakpoint
        # actually have locations.
        source_bps = lldb.SBBreakpointList(self.orig_target)
        bkpt = self.orig_target.BreakpointCreateByLocation(
            blubby_file_spec, 666, 0, module_list
        )
        source_bps.Append(bkpt)

        # Make sure we do one right:
        self.check_equivalence(source_bps)
        source_bps.Clear()

        bkpt = self.orig_target.BreakpointCreateByName(
            "blubby", lldb.eFunctionNameTypeAuto, module_list, cu_list
        )
        source_bps.Append(bkpt)
        bkpt = self.orig_target.BreakpointCreateByName(
            "blubby", lldb.eFunctionNameTypeFull, module_list, cu_list
        )
        source_bps.Append(bkpt)
        bkpt = self.orig_target.BreakpointCreateBySourceRegex(
            "dont really care", blubby_file_spec
        )
        source_bps.Append(bkpt)

        # And some number greater than one:
        self.check_equivalence(source_bps)

    def do_check_options(self):
        """Use Python APIs to check serialization of breakpoint options."""

        empty_module_list = lldb.SBFileSpecList()
        empty_cu_list = lldb.SBFileSpecList()
        blubby_file_spec = lldb.SBFileSpec(
            os.path.join(self.getSourceDir(), "blubby.c")
        )

        # It isn't actually important for these purposes that these breakpoint
        # actually have locations.
        source_bps = lldb.SBBreakpointList(self.orig_target)

        bkpt = self.orig_target.BreakpointCreateByLocation(
            lldb.SBFileSpec("blubby.c"), 666, 333, 0, lldb.SBFileSpecList()
        )
        bkpt.SetEnabled(False)
        bkpt.SetOneShot(True)
        bkpt.SetThreadID(10)
        source_bps.Append(bkpt)

        # Make sure we get one right:
        self.check_equivalence(source_bps)
        source_bps.Clear()

        bkpt = self.orig_target.BreakpointCreateByName(
            "blubby", lldb.eFunctionNameTypeAuto, empty_module_list, empty_cu_list
        )
        bkpt.SetIgnoreCount(10)
        bkpt.SetThreadName("grubby")
        source_bps.Append(bkpt)

        bkpt = self.orig_target.BreakpointCreateByName(
            "blubby", lldb.eFunctionNameTypeAuto, empty_module_list, empty_cu_list
        )
        bkpt.SetCondition("gonna remove this")
        bkpt.SetCondition("")
        source_bps.Append(bkpt)

        bkpt = self.orig_target.BreakpointCreateByName(
            "blubby", lldb.eFunctionNameTypeFull, empty_module_list, empty_cu_list
        )
        bkpt.SetCondition("something != something_else")
        bkpt.SetQueueName("grubby")
        bkpt.AddName("FirstName")
        bkpt.AddName("SecondName")
        bkpt.SetScriptCallbackBody(
            '\tprint("I am a function that prints.")\n\tprint("I don\'t do anything else")\n'
        )
        source_bps.Append(bkpt)

        bkpt = self.orig_target.BreakpointCreateBySourceRegex(
            "dont really care", blubby_file_spec
        )
        cmd_list = lldb.SBStringList()
        cmd_list.AppendString("frame var")
        cmd_list.AppendString("thread backtrace")

        bkpt.SetCommandLineCommands(cmd_list)
        source_bps.Append(bkpt)

        self.check_equivalence(source_bps)

    def do_check_appending(self):
        """Use Python APIs to check appending to already serialized options."""

        empty_module_list = lldb.SBFileSpecList()
        empty_cu_list = lldb.SBFileSpecList()
        blubby_file_spec = lldb.SBFileSpec(
            os.path.join(self.getSourceDir(), "blubby.c")
        )

        # It isn't actually important for these purposes that these breakpoint
        # actually have locations.

        all_bps = lldb.SBBreakpointList(self.orig_target)
        source_bps = lldb.SBBreakpointList(self.orig_target)

        bkpt = self.orig_target.BreakpointCreateByLocation(
            lldb.SBFileSpec("blubby.c"), 666, 333, 0, lldb.SBFileSpecList()
        )
        bkpt.SetEnabled(False)
        bkpt.SetOneShot(True)
        bkpt.SetThreadID(10)
        source_bps.Append(bkpt)
        all_bps.Append(bkpt)

        error = lldb.SBError()
        error = self.orig_target.BreakpointsWriteToFile(
            self.bkpts_file_spec, source_bps
        )
        self.assertSuccess(error, "Failed writing breakpoints to file")

        source_bps.Clear()

        bkpt = self.orig_target.BreakpointCreateByName(
            "main", lldb.eFunctionNameTypeAuto, empty_module_list, empty_cu_list
        )
        bkpt.SetIgnoreCount(10)
        bkpt.SetThreadName("grubby")
        source_bps.Append(bkpt)
        all_bps.Append(bkpt)

        bkpt = self.orig_target.BreakpointCreateByName(
            "main", lldb.eFunctionNameTypeFull, empty_module_list, empty_cu_list
        )
        bkpt.SetCondition("something != something_else")
        bkpt.SetQueueName("grubby")
        bkpt.AddName("FirstName")
        bkpt.AddName("SecondName")

        source_bps.Append(bkpt)
        all_bps.Append(bkpt)

        error = self.orig_target.BreakpointsWriteToFile(
            self.bkpts_file_spec, source_bps, True
        )
        self.assertSuccess(error, "Failed appending breakpoints to file")

        self.check_equivalence(all_bps)

    def do_check_names(self):
        bkpt = self.orig_target.BreakpointCreateByLocation(
            lldb.SBFileSpec("blubby.c"), 666, 333, 0, lldb.SBFileSpecList()
        )
        good_bkpt_name = "GoodBreakpoint"
        write_bps = lldb.SBBreakpointList(self.orig_target)
        bkpt.AddName(good_bkpt_name)
        write_bps.Append(bkpt)

        error = lldb.SBError()
        error = self.orig_target.BreakpointsWriteToFile(self.bkpts_file_spec, write_bps)
        self.assertSuccess(error, "Failed writing breakpoints to file")

        copy_bps = lldb.SBBreakpointList(self.copy_target)
        names_list = lldb.SBStringList()
        names_list.AppendString("NoSuchName")

        error = self.copy_target.BreakpointsCreateFromFile(
            self.bkpts_file_spec, names_list, copy_bps
        )
        self.assertSuccess(error, "Failed reading breakpoints from file")
        self.assertEqual(
            copy_bps.GetSize(), 0, "Found breakpoints with a nonexistent name."
        )

        names_list.AppendString(good_bkpt_name)
        error = self.copy_target.BreakpointsCreateFromFile(
            self.bkpts_file_spec, names_list, copy_bps
        )
        self.assertSuccess(error, "Failed reading breakpoints from file")
        self.assertEqual(copy_bps.GetSize(), 1, "Found the matching breakpoint.")

    def do_check_extra_args(self):
        import side_effect

        interp = self.dbg.GetCommandInterpreter()
        error = lldb.SBError()

        script_name = os.path.join(self.getSourceDir(), "resolver.py")

        command = "command script import " + script_name
        result = lldb.SBCommandReturnObject()
        interp.HandleCommand(command, result)
        self.assertTrue(
            result.Succeeded(), "com scr imp failed: %s" % (result.GetError())
        )

        # First make sure a scripted breakpoint with no args works:
        bkpt = self.orig_target.BreakpointCreateFromScript(
            "resolver.Resolver",
            lldb.SBStructuredData(),
            lldb.SBFileSpecList(),
            lldb.SBFileSpecList(),
        )
        self.assertTrue(bkpt.IsValid(), "Bkpt is valid")
        write_bps = lldb.SBBreakpointList(self.orig_target)

        error = self.orig_target.BreakpointsWriteToFile(self.bkpts_file_spec, write_bps)
        self.assertSuccess(error, "Failed writing breakpoints")

        side_effect.g_extra_args = None
        copy_bps = lldb.SBBreakpointList(self.copy_target)
        error = self.copy_target.BreakpointsCreateFromFile(
            self.bkpts_file_spec, copy_bps
        )
        self.assertSuccess(error, "Failed reading breakpoints")

        self.assertEqual(copy_bps.GetSize(), 1, "Got one breakpoint from file.")
        no_keys = lldb.SBStringList()
        side_effect.g_extra_args.GetKeys(no_keys)
        self.assertEqual(no_keys.GetSize(), 0, "Should have no keys")

        self.orig_target.DeleteAllBreakpoints()
        self.copy_target.DeleteAllBreakpoints()

        # Now try one with extra args:

        extra_args = lldb.SBStructuredData()
        stream = lldb.SBStream()
        stream.Print('{"first_arg" : "first_value", "second_arg" : "second_value"}')
        extra_args.SetFromJSON(stream)
        self.assertTrue(extra_args.IsValid(), "SBStructuredData is valid.")

        bkpt = self.orig_target.BreakpointCreateFromScript(
            "resolver.Resolver",
            extra_args,
            lldb.SBFileSpecList(),
            lldb.SBFileSpecList(),
        )
        self.assertTrue(bkpt.IsValid(), "Bkpt is valid")
        write_bps = lldb.SBBreakpointList(self.orig_target)

        error = self.orig_target.BreakpointsWriteToFile(self.bkpts_file_spec, write_bps)
        self.assertSuccess(error, "Failed writing breakpoints")

        orig_extra_args = side_effect.g_extra_args
        self.assertTrue(orig_extra_args.IsValid(), "Extra args originally valid")

        orig_keys = lldb.SBStringList()
        orig_extra_args.GetKeys(orig_keys)
        self.assertEqual(2, orig_keys.GetSize(), "Should have two keys")

        side_effect.g_extra_args = None

        copy_bps = lldb.SBBreakpointList(self.copy_target)
        error = self.copy_target.BreakpointsCreateFromFile(
            self.bkpts_file_spec, copy_bps
        )
        self.assertSuccess(error, "Failed reading breakpoints")

        self.assertEqual(copy_bps.GetSize(), 1, "Got one breakpoint from file.")

        copy_extra_args = side_effect.g_extra_args
        copy_keys = lldb.SBStringList()
        copy_extra_args.GetKeys(copy_keys)
        self.assertEqual(2, copy_keys.GetSize(), "Copy should have two keys")

        for idx in range(0, orig_keys.GetSize()):
            key = orig_keys.GetStringAtIndex(idx)
            copy_value = copy_extra_args.GetValueForKey(key).GetStringValue(100)

            if key == "first_arg":
                self.assertEqual(copy_value, "first_value")
            elif key == "second_arg":
                self.assertEqual(copy_value, "second_value")
            else:
                self.Fail("Unknown key: %s" % (key))
