"""
Test lldb core component: SourceManager.

Test cases:

o test_display_source_python:
  Test display of source using the SBSourceManager API.
o test_modify_source_file_while_debugging:
  Test the caching mechanism of the source manager.
"""

import os
import io
import stat

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


def ansi_underline_surround_regex(inner_regex_text):
    # return re.compile(r"\[4m%s\[0m" % inner_regex_text)
    return "4.+\033\\[4m%s\033\\[0m" % inner_regex_text


def ansi_color_surround_regex(inner_regex_text):
    return "\033\\[3[0-7]m%s\033\\[0m" % inner_regex_text


class SourceManagerTestCase(TestBase):
    NO_DEBUG_INFO_TESTCASE = True
    SHARED_BUILD_TESTCASE = False

    def setUp(self):
        # Call super's setUp().
        TestBase.setUp(self)
        # Find the line number to break inside main().
        self.file = os.path.realpath(self.getBuildArtifact("main-copy.c"))
        self.line = line_number("main.c", "// Set break point at this line.")

    def modify_content(self):
        # Read the main.c file content.
        with io.open(self.file, "r", newline="\n") as f:
            original_content = f.read()
            if self.TraceOn():
                print("original content:", original_content)

        # Modify the in-memory copy of the original source code.
        new_content = original_content.replace("Hello world", "Hello lldb", 1)

        # Modify the source code file.
        # If the source was read only, the copy will also be read only.
        # Run "chmod u+w" on it first so we can modify it.
        statinfo = os.stat(self.file)
        os.chmod(self.file, statinfo.st_mode | stat.S_IWUSR)

        with io.open(self.file, "w", newline="\n") as f:
            time.sleep(1)
            f.write(new_content)
            if self.TraceOn():
                print("new content:", new_content)
                print(
                    "os.path.getmtime() after writing new content:",
                    os.path.getmtime(self.file),
                )

    def get_expected_stop_column_number(self):
        """Return the 1-based column number of the first non-whitespace
        character in the breakpoint source line."""
        stop_line = get_line(self.file, self.line)
        # The number of spaces that must be skipped to get to the first non-
        # whitespace character --- where we expect the debugger breakpoint
        # column to be --- is equal to the number of characters that get
        # stripped off the front when we lstrip it, plus one to specify
        # the character column after the initial whitespace.
        return len(stop_line) - len(stop_line.lstrip()) + 1

    def do_display_source_python_api(
        self, use_color, needle_regex, highlight_source=False
    ):
        self.build()
        exe = self.getBuildArtifact("a.out")
        self.runCmd("file " + exe, CURRENT_EXECUTABLE_SET)

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

        # Launch the process, and do not stop at the entry point.
        args = None
        envp = None
        process = target.LaunchSimple(args, envp, self.get_process_working_directory())
        self.assertIsNotNone(process)

        #
        # Exercise Python APIs to display source lines.
        #

        # Setup whether we should use ansi escape sequences, including color
        # and styles such as underline.
        self.dbg.SetUseColor(use_color)
        # Disable syntax highlighting if needed.

        self.runCmd("settings set highlight-source " + str(highlight_source).lower())

        filespec = lldb.SBFileSpec(self.file, False)
        source_mgr = self.dbg.GetSourceManager()
        # Use a string stream as the destination.
        stream = lldb.SBStream()
        column = self.get_expected_stop_column_number()
        context_before = 2
        context_after = 2
        current_line_prefix = "=>"
        source_mgr.DisplaySourceLinesWithLineNumbersAndColumn(
            filespec,
            self.line,
            column,
            context_before,
            context_after,
            current_line_prefix,
            stream,
        )

        #    2
        #    3    int main(int argc, char const *argv[]) {
        # => 4        printf("Hello world.\n"); // Set break point at this line.
        #    5        return 0;
        #    6    }
        self.expect(
            stream.GetData(),
            "Source code displayed correctly:\n" + stream.GetData(),
            exe=False,
            ordered=False,
            patterns=["=>", "%d.*Hello world" % self.line, needle_regex],
        )

        # Boundary condition testings for SBStream().  LLDB should not crash!
        stream.Print(None)
        stream.RedirectToFile(None, True)

    @add_test_categories(["pyapi"])
    def test_display_source_python_dumb_terminal(self):
        """Test display of source using the SBSourceManager API, using a
        dumb terminal and thus no color support (the default)."""
        use_color = False
        self.do_display_source_python_api(use_color, r"\s+\^")

    @add_test_categories(["pyapi"])
    def test_display_source_python_ansi_terminal(self):
        """Test display of source using the SBSourceManager API, using a
        dumb terminal and thus no color support (the default)."""
        use_color = True
        underline_regex = ansi_underline_surround_regex(r"printf")
        self.do_display_source_python_api(use_color, underline_regex)

    @add_test_categories(["pyapi"])
    def test_display_source_python_ansi_terminal_syntax_highlighting(self):
        """Test display of source using the SBSourceManager API and check for
        the syntax highlighted output"""
        use_color = True
        syntax_highlighting = True

        # Just pick 'int' as something that should be colored.
        color_regex = ansi_color_surround_regex("int")
        self.do_display_source_python_api(use_color, color_regex, syntax_highlighting)

        # Same for 'char'.
        color_regex = ansi_color_surround_regex("char")
        self.do_display_source_python_api(use_color, color_regex, syntax_highlighting)

        # Test that we didn't color unrelated identifiers.
        self.do_display_source_python_api(use_color, r" main\(", syntax_highlighting)
        self.do_display_source_python_api(use_color, r"\);", syntax_highlighting)

    def test_move_and_then_display_source(self):
        """Test that target.source-map settings work by moving main.c to hidden/main.c."""
        self.build()
        exe = self.getBuildArtifact("a.out")
        self.runCmd("file " + exe, CURRENT_EXECUTABLE_SET)

        # Move main.c to hidden/main.c.
        hidden = self.getBuildArtifact("hidden")
        lldbutil.mkdir_p(hidden)
        main_c_hidden = os.path.join(hidden, "main-copy.c")
        os.rename(self.file, main_c_hidden)

        # Set source remapping with invalid replace path and verify we get an
        # error
        self.expect(
            "settings set target.source-map /a/b/c/d/e /q/r/s/t/u",
            error=True,
            substrs=['''error: the replacement path doesn't exist: "/q/r/s/t/u"'''],
        )

        # 'make -C' has resolved current directory to its realpath form.
        builddir_real = os.path.realpath(self.getBuildDir())
        hidden_real = os.path.realpath(hidden)
        # Set target.source-map settings.
        self.runCmd(
            "settings set target.source-map %s %s" % (builddir_real, hidden_real)
        )
        # And verify that the settings work.
        self.expect(
            "settings show target.source-map", substrs=[builddir_real, hidden_real]
        )

        # Display main() and verify that the source mapping has been kicked in.
        self.expect(
            "source list -n main", SOURCE_DISPLAYED_CORRECTLY, substrs=["Hello world"]
        )

    @skipIf(oslist=["windows"], bugnumber="llvm.org/pr44431")
    def test_modify_source_file_while_debugging(self):
        """Modify a source file while debugging the executable."""
        self.build()
        exe = self.getBuildArtifact("a.out")
        self.runCmd("file " + exe, CURRENT_EXECUTABLE_SET)

        lldbutil.run_break_set_by_file_and_line(
            self, "main-copy.c", self.line, num_expected_locations=1, loc_exact=True
        )

        self.runCmd("run", RUN_SUCCEEDED)

        # The stop reason of the thread should be breakpoint.
        self.expect(
            "thread list",
            STOPPED_DUE_TO_BREAKPOINT,
            substrs=[
                "stopped",
                "main-copy.c:%d" % self.line,
                "stop reason = breakpoint",
            ],
        )

        # Display some source code.
        self.expect(
            "source list -f main-copy.c -l %d" % self.line,
            SOURCE_DISPLAYED_CORRECTLY,
            substrs=["Hello world"],
        )

        # Do the same thing with a file & line spec:
        self.expect(
            "source list -y main-copy.c:%d" % self.line,
            SOURCE_DISPLAYED_CORRECTLY,
            substrs=["Hello world"],
        )

        # The '-b' option shows the line table locations from the debug information
        # that indicates valid places to set source level breakpoints.

        # The file to display is implicit in this case.
        self.runCmd("source list -l %d -c 3 -b" % self.line)
        output = self.res.GetOutput().splitlines()[0]

        # If the breakpoint set command succeeded, we should expect a positive number
        # of breakpoints for the current line, i.e., self.line.
        import re

        m = re.search(r"^\[(\d+)\].*// Set break point at this line.", output)
        if not m:
            self.fail("Fail to display source level breakpoints")
        self.assertGreater(int(m.group(1)), 0)

        # Modify content
        self.modify_content()

        # Display the source code again. We should not see the updated line.
        self.expect(
            "source list -f main-copy.c -l %d" % self.line,
            SOURCE_DISPLAYED_CORRECTLY,
            substrs=["Hello world"],
        )

        # clear the source cache.
        self.runCmd("source cache clear")

        # Display the source code again. Now we should see the updated line.
        self.expect(
            "source list -f main-copy.c -l %d" % self.line,
            SOURCE_DISPLAYED_CORRECTLY,
            substrs=["Hello lldb"],
        )

    def test_set_breakpoint_with_absolute_path(self):
        self.build()
        hidden = self.getBuildArtifact("hidden")
        lldbutil.mkdir_p(hidden)
        # 'make -C' has resolved current directory to its realpath form.
        builddir_real = os.path.realpath(self.getBuildDir())
        hidden_real = os.path.realpath(hidden)
        self.runCmd(
            "settings set target.source-map %s %s" % (builddir_real, hidden_real)
        )

        exe = self.getBuildArtifact("a.out")
        main = os.path.join(builddir_real, "hidden", "main-copy.c")
        self.runCmd("file " + exe, CURRENT_EXECUTABLE_SET)

        lldbutil.run_break_set_by_file_and_line(
            self, main, self.line, num_expected_locations=1, loc_exact=False
        )

        self.runCmd("run", RUN_SUCCEEDED)

        # The stop reason of the thread should be breakpoint.
        self.expect(
            "thread list",
            STOPPED_DUE_TO_BREAKPOINT,
            substrs=[
                "stopped",
                "main-copy.c:%d" % self.line,
                "stop reason = breakpoint",
            ],
        )

    def test_artificial_source_location(self):
        src_file = "artificial_location.cpp"
        d = {"C_SOURCES": "", "CXX_SOURCES": src_file}
        self.build(dictionary=d)

        target = lldbutil.run_to_breakpoint_make_target(self)

        # Find the instruction with line=0 and put a breakpoint there.
        sc_list = target.FindFunctions("A::foo")
        self.assertEqual(len(sc_list), 1)
        insns = sc_list[0].function.GetInstructions(target)
        insn0 = next(filter(lambda insn: insn.addr.line_entry.line == 0, insns))
        bkpt = target.BreakpointCreateBySBAddress(insn0.addr)
        self.assertGreater(bkpt.GetNumLocations(), 0)

        lldbutil.run_to_breakpoint_do_run(self, target, bkpt)

        self.expect(
            "process status",
            substrs=[
                "stop reason = breakpoint",
                f"{src_file}:0",
                "static int foo();",
                "note: This address is not associated with a specific line "
                "of code. This may be due to compiler optimizations.",
            ],
        )

    def test_source_cache_dump_and_clear(self):
        self.build()
        exe = self.getBuildArtifact("a.out")
        self.runCmd("file " + exe, CURRENT_EXECUTABLE_SET)
        lldbutil.run_break_set_by_file_and_line(
            self, self.file, self.line, num_expected_locations=1, loc_exact=True
        )
        self.runCmd("run", RUN_SUCCEEDED)

        # Make sure the main source file is in the source cache.
        self.expect(
            "source cache dump",
            substrs=["Modification time", "Lines", "Path", " 7", self.file],
        )

        # Clear the cache.
        self.expect("source cache clear")

        # Make sure the main source file is no longer in the source cache.
        self.expect("source cache dump", matching=False, substrs=[self.file])

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

        # Create a first target.
        self.runCmd("file " + exe, CURRENT_EXECUTABLE_SET)
        lldbutil.run_break_set_by_symbol(self, "main", num_expected_locations=1)
        self.expect("run", RUN_SUCCEEDED, substrs=["Hello world"])

        # Create a second target.
        self.runCmd("file " + exe, CURRENT_EXECUTABLE_SET)
        lldbutil.run_break_set_by_symbol(self, "main", num_expected_locations=1)
        self.expect("run", RUN_SUCCEEDED, substrs=["Hello world"])

        # Modify the source file content.
        self.modify_content()

        # Clear the source cache. This will wipe the debugger and the process
        # cache for the second process.
        self.runCmd("source cache clear")

        # Make sure we're seeing the new content from the clean process cache.
        self.expect(
            "next",
            SOURCE_DISPLAYED_CORRECTLY,
            substrs=["Hello lldb"],
        )

        # Switch back to the first target.
        self.runCmd("target select 0")

        # Make sure we're seeing the old content from the first target's
        # process cache.
        self.expect(
            "next",
            SOURCE_DISPLAYED_CORRECTLY,
            substrs=["Hello world"],
        )
