"""
Test lldb-dap runInTerminal reverse request
"""

from contextlib import contextmanager
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import line_number
import lldbdap_testcase
import os
import subprocess
import json


@contextmanager
def fifo(*args, **kwargs):
    if sys.platform == "win32":
        import ctypes

        comm_file = r"\\.\pipe\lldb-dap-run-in-terminal-comm"
        PIPE_ACCESS_DUPLEX = 0x00000003
        PIPE_TYPE_MESSAGE = 0x00000004
        PIPE_READMODE_MESSAGE = 0x00000002
        PIPE_WAIT = 0x00000000
        PIPE_UNLIMITED_INSTANCES = 255
        kernel32 = ctypes.windll.kernel32

        pipe = kernel32.CreateNamedPipeW(
            comm_file,
            PIPE_ACCESS_DUPLEX,
            PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
            PIPE_UNLIMITED_INSTANCES,
            4096,
            4096,
            0,
            None,
        )
    else:
        comm_file = os.path.join(kwargs["directory"], "comm-file")
        pipe = None
        os.mkfifo(comm_file)

    try:
        yield comm_file, pipe
    finally:
        if pipe is not None:
            kernel32.DisconnectNamedPipe(pipe)
            kernel32.CloseHandle(pipe)


def read_pipe_message(pipe):
    import ctypes

    ERROR_MORE_DATA = 234
    kernel32 = ctypes.windll.kernel32
    buffer = b""
    while True:
        chunk = ctypes.create_string_buffer(4096)
        bytes_read = ctypes.wintypes.DWORD()
        success = kernel32.ReadFile(pipe, chunk, 4096, ctypes.byref(bytes_read), None)
        buffer += chunk.raw[: bytes_read.value]
        if success:
            break
        if ctypes.GetLastError() != ERROR_MORE_DATA:
            break
    return buffer.decode()


@skipIfBuildType(["debug"])
class TestDAP_runInTerminal(lldbdap_testcase.DAPTestCaseBase):
    SHARED_BUILD_TESTCASE = False

    def read_pid_message(self, fifo_file, pipe):
        if sys.platform == "win32":
            import ctypes

            ctypes.windll.kernel32.ConnectNamedPipe(pipe, None)
            self.assertIn("pid", read_pipe_message(pipe))
        else:
            with open(fifo_file, "r") as file:
                self.assertIn("pid", file.readline())

    @staticmethod
    def send_did_attach_message(fifo_file, pipe=None):
        message = json.dumps({"kind": "didAttach"}) + "\n"
        if sys.platform == "win32":
            import ctypes

            kernel32 = ctypes.windll.kernel32
            bytes_written = ctypes.wintypes.DWORD()
            kernel32.ConnectNamedPipe(pipe, None)
            kernel32.WriteFile(
                pipe, message.encode(), len(message), ctypes.byref(bytes_written), None
            )
        else:
            with open(fifo_file, "w") as file:
                file.write(message)

    @staticmethod
    def read_error_message(fifo_file, pipe=None):
        if sys.platform == "win32":
            import ctypes

            ctypes.windll.kernel32.ConnectNamedPipe(pipe, None)
            return read_pipe_message(pipe)
        else:
            with open(fifo_file, "r") as file:
                return file.readline()

    @skipIfAsan
    def test_runInTerminal(self):
        """
        Tests the "runInTerminal" reverse request. It makes sure that the IDE can
        launch the inferior with the correct environment variables and arguments.
        """
        program = self.getBuildArtifact("a.out")
        source = "main.c"
        self.build_and_launch(
            program, console="integratedTerminal", args=["foobar"], env=["FOO=bar"]
        )
        self.dap_server.wait_for_initialized()

        self.assertEqual(
            len(self.dap_server.reverse_requests),
            1,
            "make sure we got a reverse request",
        )

        request = self.dap_server.reverse_requests[0]
        self.assertIn(self.lldbDAPExec, request["arguments"]["args"])
        self.assertIn(program, request["arguments"]["args"])
        self.assertIn("foobar", request["arguments"]["args"])
        self.assertIn("FOO", request["arguments"]["env"])

        breakpoint_line = line_number(source, "// breakpoint")

        self.set_source_breakpoints(source, [breakpoint_line])
        self.continue_to_next_stop()

        # We verify we actually stopped inside the loop
        counter = int(self.dap_server.get_local_variable_value("counter"))
        self.assertEqual(counter, 1)

        # We verify we were able to set the launch arguments
        argc = int(self.dap_server.get_local_variable_value("argc"))
        self.assertEqual(argc, 2)

        argv1 = self.dap_server.request_evaluate("argv[1]")["body"]["result"]
        self.assertIn("foobar", argv1)

        # We verify we were able to set the environment
        env = self.dap_server.request_evaluate("foo")["body"]["result"]
        self.assertIn("bar", env)

        self.continue_to_exit()

    @skipIfAsan
    def test_runInTerminalWithObjectEnv(self):
        """
        Tests the "runInTerminal" reverse request. It makes sure that the IDE can
        launch the inferior with the correct environment variables using an object.
        """
        program = self.getBuildArtifact("a.out")
        self.build_and_launch(program, console="integratedTerminal", env={"FOO": "BAR"})
        self.dap_server.wait_for_initialized()

        self.assertEqual(
            len(self.dap_server.reverse_requests),
            1,
            "make sure we got a reverse request",
        )

        request = self.dap_server.reverse_requests[0]
        request_envs = request["arguments"]["env"]

        self.assertIn("FOO", request_envs)
        self.assertEqual("BAR", request_envs["FOO"])

        self.continue_to_exit()

    def test_runInTerminalInvalidTarget(self):
        self.build_and_create_debug_adapter()
        response = self.launch_and_configurationDone(
            "INVALIDPROGRAM",
            console="integratedTerminal",
            args=["foobar"],
            env=["FOO=bar"],
        )
        self.assertFalse(response["success"])
        self.assertIn(
            "'INVALIDPROGRAM' does not exist",
            response["body"]["error"]["format"],
        )

    def test_missingArgInRunInTerminalLauncher(self):
        proc = subprocess.run(
            [self.lldbDAPExec, "--launch-target", "INVALIDPROGRAM"],
            capture_output=True,
            universal_newlines=True,
        )
        self.assertNotEqual(proc.returncode, 0)
        self.assertIn(
            '"--launch-target" requires "--comm-file" to be specified', proc.stderr
        )

    def test_FakeAttachedRunInTerminalLauncherWithInvalidProgram(self):
        with fifo(directory=self.getBuildDir()) as (comm_file, pipe):
            proc = subprocess.Popen(
                [
                    self.lldbDAPExec,
                    "--comm-file",
                    comm_file,
                    "--launch-target",
                    "INVALIDPROGRAM",
                ],
                universal_newlines=True,
                stderr=subprocess.PIPE,
            )
            if sys.platform == "win32":
                _, stderr = proc.communicate()
                self.assertIn("Failed to launch target process", stderr)
            else:
                self.read_pid_message(comm_file, pipe)
                self.send_did_attach_message(comm_file, pipe)
                self.assertIn(
                    "No such file or directory",
                    self.read_error_message(comm_file, pipe),
                )

                _, stderr = proc.communicate()
                self.assertIn("No such file or directory", stderr)

    def test_FakeAttachedRunInTerminalLauncherWithValidProgram(self):
        with fifo(directory=self.getBuildDir()) as (comm_file, pipe):
            proc = subprocess.Popen(
                [
                    self.lldbDAPExec,
                    "--comm-file",
                    comm_file,
                    "--launch-target",
                    "echo",
                    "foo",
                ],
                universal_newlines=True,
                stdout=subprocess.PIPE,
            )

            self.read_pid_message(comm_file, pipe)
            self.send_did_attach_message(comm_file, pipe)

            stdout, _ = proc.communicate()

        self.assertIn("foo", stdout)

    def test_FakeAttachedRunInTerminalLauncherAndCheckEnvironment(self):
        with fifo(directory=self.getBuildDir()) as (comm_file, pipe):
            proc = subprocess.Popen(
                [self.lldbDAPExec, "--comm-file", comm_file, "--launch-target", "env"],
                universal_newlines=True,
                stdout=subprocess.PIPE,
                env={**os.environ, "FOO": "BAR"},
            )

            self.read_pid_message(comm_file, pipe)
            self.send_did_attach_message(comm_file, pipe)

            stdout, _ = proc.communicate()

        self.assertIn("FOO=BAR", stdout)

    def test_NonAttachedRunInTerminalLauncher(self):
        with fifo(directory=self.getBuildDir()) as (comm_file, pipe):
            proc = subprocess.Popen(
                [
                    self.lldbDAPExec,
                    "--comm-file",
                    comm_file,
                    "--launch-target",
                    "echo",
                    "foo",
                ],
                universal_newlines=True,
                stderr=subprocess.PIPE,
                env={**os.environ, "LLDB_DAP_RIT_TIMEOUT_IN_MS": "1000"},
            )

            self.read_pid_message(comm_file, pipe)

            _, stderr = proc.communicate()

        self.assertIn("Timed out trying to get messages from the debug adapter", stderr)

    def test_client_missing_runInTerminal_feature(self):
        program = self.getBuildArtifact("a.out")
        self.build_and_create_debug_adapter()
        response = self.launch_and_configurationDone(
            program,
            console="integratedTerminal",
            client_features={"supportsRunInTerminalRequest": False},
        )
        self.assertFalse(response["success"], f"Expected failure got {response!r}")
        error_message = response["body"]["error"]["format"]
        self.assertIn("Client does not support RunInTerminal.", error_message)
