"""
Test the use of the global module cache in lldb
"""

import lldb

from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
from lldbsuite.test import lldbutil
import os
import shutil
from pathlib import Path
import time


class GlobalModuleCacheTestCase(TestBase):
    # NO_DEBUG_INFO_TESTCASE = True

    def check_counter_var(self, thread, value):
        frame = thread.frames[0]
        var = frame.FindVariable("counter")
        self.assertTrue(var.GetError().Success(), "Got counter variable")
        self.assertEqual(var.GetValueAsUnsigned(), value, "This was one-print")

    def copy_to_main(self, src, dst):
        # We are relying on the source file being newer than the .o file from
        # a previous build, so sleep a bit here to ensure that the touch is later.
        time.sleep(2)
        try:
            # Make sure dst is writeable before trying to write to it.
            subprocess.run(
                ["chmod", "777", dst],
                stdin=None,
                capture_output=False,
                encoding="utf-8",
            )
            shutil.copy(src, dst)
        except:
            self.fail(f"Could not copy {src} to {dst}")
        Path(dst).touch()

    # The rerun tests indicate rerunning on Windows doesn't really work, so
    # this one won't either.
    @skipIfWindows
    # On Arm and AArch64 Linux, this test attempts to pop a thread plan when
    # we only have the base plan remaining. Skip it until we can figure out
    # the bug this is exposing (https://github.com/llvm/llvm-project/issues/76057).
    @skipIf(oslist=["linux"], archs=["arm$", "aarch64"])
    def test_OneTargetOneDebugger(self):
        self.do_test(True, True)

    # This behaves as implemented but that behavior is not desirable.
    # This test tests for the desired behavior as an expected fail.
    @skipIfWindows
    @expectedFailureAll
    @skipIf(oslist=["linux"], archs=["arm$", "aarch64"])
    def test_TwoTargetsOneDebugger(self):
        self.do_test(False, True)

    @skipIfWindows
    @expectedFailureAll
    @skipIf(oslist=["linux"], archs=["arm$", "aarch64"])
    def test_OneTargetTwoDebuggers(self):
        self.do_test(True, False)

    def do_test(self, one_target, one_debugger):
        # Make sure that if we have one target, and we run, then
        # change the binary and rerun, the binary (and any .o files
        # if using dwarf in .o file debugging) get removed from the
        # shared module cache.  They are no longer reachable.
        debug_style = self.getDebugInfo()

        # Before we do anything, clear the global module cache so we don't
        # see objects from other runs:
        lldb.SBDebugger.MemoryPressureDetected()

        # Set up the paths for our two versions of main.c:
        main_c_path = os.path.join(self.getBuildDir(), "main.c")
        one_print_path = os.path.join(self.getSourceDir(), "one-print.c")
        two_print_path = os.path.join(self.getSourceDir(), "two-print.c")
        main_filespec = lldb.SBFileSpec(main_c_path)

        # First copy the one-print.c to main.c in the build folder and
        # build our a.out from there:
        self.copy_to_main(one_print_path, main_c_path)
        self.build(dictionary={"C_SOURCES": main_c_path, "EXE": "a.out"})

        (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
            self, "return counter;", main_filespec
        )

        # Make sure we ran the version we intended here:
        self.check_counter_var(thread, 1)
        process.Kill()

        # Now copy two-print.c over main.c, rebuild, and rerun:
        # os.unlink(target.GetExecutable().fullpath)
        self.copy_to_main(two_print_path, main_c_path)

        self.build(dictionary={"C_SOURCES": main_c_path, "EXE": "a.out"})
        error = lldb.SBError()
        if one_debugger:
            if one_target:
                (_, process, thread, _) = lldbutil.run_to_breakpoint_do_run(
                    self, target, bkpt
                )
            else:
                (target2, process2, thread, bkpt) = lldbutil.run_to_source_breakpoint(
                    self, "return counter;", main_filespec
                )
        else:
            if one_target:
                new_debugger = lldb.SBDebugger().Create()
                new_debugger.SetSelectedPlatform(lldb.selected_platform)
                new_debugger.SetAsync(False)
                self.old_debugger = self.dbg
                self.dbg = new_debugger

                def cleanupDebugger(self):
                    lldb.SBDebugger.Destroy(self.dbg)
                    self.dbg = self.old_debugger
                    self.old_debugger = None

                self.addTearDownHook(cleanupDebugger)
                (target2, process2, thread, bkpt) = lldbutil.run_to_source_breakpoint(
                    self, "return counter;", main_filespec
                )

        # In two-print.c counter will be 2:
        self.check_counter_var(thread, 2)

        # If we made two targets, destroy the first one, that should free up the
        # unreachable Modules:
        if not one_target:
            target.Clear()

        num_a_dot_out_entries = 1
        # For dSYM's there will be two lines of output, one for the a.out and one
        # for the dSYM.
        if debug_style == "dsym":
            num_a_dot_out_entries += 1

        error = self.check_image_list_result(num_a_dot_out_entries, 1)
        # Even if this fails, MemoryPressureDetected should fix this.
        lldb.SBDebugger.MemoryPressureDetected()
        error_after_mpd = self.check_image_list_result(num_a_dot_out_entries, 1)
        fail_msg = ""
        if error != "":
            fail_msg = "Error before MPD: " + error

        if error_after_mpd != "":
            fail_msg = fail_msg + "\nError after MPD: " + error_after_mpd
        if fail_msg != "":
            self.fail(fail_msg)

    def check_image_list_result(self, num_a_dot_out, num_main_dot_o):
        # Check the global module list, there should only be one a.out, and if we are
        # doing dwarf in .o file, there should only be one .o file.  This returns
        # an error string on error - rather than asserting, so you can stage this
        # failing.
        image_cmd_result = lldb.SBCommandReturnObject()
        interp = self.dbg.GetCommandInterpreter()
        interp.HandleCommand("image list -g", image_cmd_result)
        if self.TraceOn():
            print(f"Expected: a.out: {num_a_dot_out} main.o: {num_main_dot_o}")
            print(image_cmd_result)

        image_list_str = image_cmd_result.GetOutput()
        image_list = image_list_str.splitlines()
        found_a_dot_out = 0
        found_main_dot_o = 0

        for line in image_list:
            # FIXME: force this to be at the end of the string:
            if "a.out" in line:
                found_a_dot_out += 1
            if "main.o" in line:
                found_main_dot_o += 1

        if num_a_dot_out != found_a_dot_out:
            return f"Got {found_a_dot_out} number of a.out's, expected {num_a_dot_out}"

        if found_main_dot_o > 0 and num_main_dot_o != found_main_dot_o:
            return (
                f"Got {found_main_dot_o} number of main.o's, expected {num_main_dot_o}"
            )

        return ""
