"""
Test lldb-dap memory support
"""

from base64 import b64decode
import dap_server
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
from lldbsuite.test import lldbutil
import lldbdap_testcase
import os


class TestDAP_memory(lldbdap_testcase.DAPTestCaseBase):
    @skipIfWindows
    def test_memory_refs_variables(self):
        """
        Tests memory references for evaluate
        """
        program = self.getBuildArtifact("a.out")
        self.build_and_launch(program)
        source = "main.cpp"
        self.source_path = os.path.join(os.getcwd(), source)
        self.set_source_breakpoints(
            source,
            [line_number(source, "// Breakpoint")],
        )
        self.continue_to_next_stop()

        locals = {l["name"]: l for l in self.dap_server.get_local_variables()}

        # Pointers should have memory-references
        self.assertIn("memoryReference", locals["rawptr"].keys())
        # Non-pointers should also have memory-references
        self.assertIn("memoryReference", locals["not_a_ptr"].keys())

    @skipIfWindows
    def test_memory_refs_evaluate(self):
        """
        Tests memory references for evaluate
        """
        program = self.getBuildArtifact("a.out")
        self.build_and_launch(program)
        source = "main.cpp"
        self.source_path = os.path.join(os.getcwd(), source)
        self.set_source_breakpoints(
            source,
            [line_number(source, "// Breakpoint")],
        )
        self.continue_to_next_stop()

        self.assertIn(
            "memoryReference",
            self.dap_server.request_evaluate("rawptr")["body"].keys(),
        )

    @skipIfWindows
    def test_memory_refs_set_variable(self):
        """
        Tests memory references for `setVariable`
        """
        program = self.getBuildArtifact("a.out")
        self.build_and_launch(program)
        source = "main.cpp"
        self.source_path = os.path.join(os.getcwd(), source)
        self.set_source_breakpoints(
            source,
            [line_number(source, "// Breakpoint")],
        )
        self.continue_to_next_stop()

        ptr_value = self.get_local_as_int("rawptr")
        self.assertIn(
            "memoryReference",
            self.dap_server.request_setVariable(1, "rawptr", ptr_value + 2)[
                "body"
            ].keys(),
        )

    @skipIfWindows
    def test_readMemory(self):
        """
        Tests the 'readMemory' request
        """
        program = self.getBuildArtifact("a.out")
        self.build_and_launch(program)
        source = "main.cpp"
        self.source_path = os.path.join(os.getcwd(), source)
        self.set_source_breakpoints(
            source,
            [line_number(source, "// Breakpoint")],
        )
        self.continue_to_next_stop()

        ptr_deref = self.dap_server.request_evaluate("*rawptr")["body"]
        memref = ptr_deref["memoryReference"]

        # We can read the complete string
        mem = self.dap_server.request_readMemory(memref, 0, 5)["body"]
        self.assertEqual(b64decode(mem["data"]), b"dead\0")

        # We can read large chunks, potentially returning partial results
        mem = self.dap_server.request_readMemory(memref, 0, 4096)["body"]
        self.assertEqual(b64decode(mem["data"])[0:5], b"dead\0")

        # Use an offset
        mem = self.dap_server.request_readMemory(memref, 2, 3)["body"]
        self.assertEqual(b64decode(mem["data"]), b"ad\0")

        # Reads of size 0 are successful
        # VS Code sends those in order to check if a `memoryReference` can actually be dereferenced.
        mem = self.dap_server.request_readMemory(memref, 0, 0)
        self.assertEqual(mem["success"], True)
        self.assertNotIn(
            "data", mem["body"], f"expects no data key in response: {mem!r}"
        )

        # Reads at offset 0x0 return unreadable bytes
        bytes_to_read = 6
        mem = self.dap_server.request_readMemory("0x0", 0, bytes_to_read)
        self.assertEqual(mem["body"]["unreadableBytes"], bytes_to_read)

        # Reads with invalid address fails.
        mem = self.dap_server.request_readMemory("-3204", 0, 10)
        self.assertFalse(mem["success"], "expect fail on reading memory.")

        self.continue_to_exit()

    def test_writeMemory(self):
        """
        Tests the 'writeMemory' request
        """
        program = self.getBuildArtifact("a.out")
        self.build_and_launch(program)
        source = "main.cpp"
        self.source_path = os.path.join(os.getcwd(), source)
        self.set_source_breakpoints(
            source,
            [line_number(source, "// Breakpoint")],
        )
        self.continue_to_next_stop()

        # Get the 'not_a_ptr' writable variable reference address.
        ptr_deref = self.dap_server.request_evaluate("not_a_ptr")["body"]
        memref = ptr_deref["memoryReference"]

        # Write the decimal value 50 (0x32 in hexadecimal) to memory.
        # This corresponds to the ASCII character '2' and encodes to Base64 as "Mg==".
        mem_response = self.writeMemory(memref, 50, 0, True)
        self.assertEqual(mem_response["success"], True)
        self.assertEqual(mem_response["body"]["bytesWritten"], 1)

        # Read back the modified memory and verify that the written data matches
        # the expected result.
        mem_response = self.dap_server.request_readMemory(memref, 0, 1)
        self.assertEqual(mem_response["success"], True)
        self.assertEqual(mem_response["body"]["data"], "Mg==")

        # Write the decimal value 100 (0x64 in hexadecimal) to memory.
        # This corresponds to the ASCII character 'd' and encodes to Base64 as "ZA==".
        # allowPartial=False
        mem_response = self.writeMemory(memref, 100, 0, False)
        self.assertEqual(mem_response["success"], True)
        self.assertEqual(mem_response["body"]["bytesWritten"], 1)

        # Read back the modified memory and verify that the written data matches
        # the expected result.
        mem_response = self.dap_server.request_readMemory(memref, 0, 1)
        self.assertEqual(mem_response["success"], True)
        self.assertEqual(mem_response["body"]["data"], "ZA==")

        # Memory write failed for 0x0.
        mem_response = self.writeMemory("0x0", 50, 0, True)
        self.assertEqual(mem_response["success"], False)

        # Malformed memory reference.
        mem_response = self.writeMemory("12345", 50, 0, True)
        self.assertEqual(mem_response["success"], False)

        ptr_deref = self.dap_server.request_evaluate("nonWritable")["body"]
        memref = ptr_deref["memoryReference"]

        # Writing to non-writable region should return an appropriate error.
        mem_response = self.writeMemory(memref, 50, 0, False)
        self.assertEqual(mem_response["success"], False)
        self.assertRegex(
            mem_response["body"]["error"]["format"],
            r"Memory " + memref + " region is not writable",
        )

        # Trying to write empty value; data=""
        mem_response = self.writeMemory(memref)
        self.assertEqual(mem_response["success"], False)
        self.assertRegex(
            mem_response["body"]["error"]["format"],
            r"Data cannot be empty value. Provide valid data",
        )

        # Verify that large memory writes fail if the range spans non-writable
        # or non -contiguous regions.
        data = bytes([0xFF] * 8192)
        mem_response = self.writeMemory(
            memref, int.from_bytes(data, byteorder="little"), 0, False
        )
        self.assertEqual(mem_response["success"], False)
        self.assertRegex(
            mem_response["body"]["error"]["format"],
            r"Memory " + memref + " region is not writable",
        )
