import lldb
import unittest
import os
import json
import stat
import sys
from textwrap import dedent
import lldbsuite.test.lldbutil
from lldbsuite.test.lldbtest import *
from lldbsuite.test.decorators import *
from lldbsuite.test.gdbclientutils import *


@skipIfRemote
@skipIfWindows
class TestQemuLaunch(TestBase):
    NO_DEBUG_INFO_TESTCASE = True

    def set_emulator_setting(self, name, value):
        self.runCmd("settings set -- platform.plugin.qemu-user.%s %s" %
                (name, value))

    def setUp(self):
        super().setUp()
        emulator = self.getBuildArtifact("qemu.py")
        with os.fdopen(os.open(emulator, os.O_WRONLY|os.O_CREAT, stat.S_IRWXU),
                "w") as e:

            e.write(dedent("""\
                    #! {exec!s}

                    import runpy
                    import sys

                    sys.path = {path!r}
                    runpy.run_path({source!r}, run_name='__main__')
                    """).format(exec=sys.executable, path=sys.path,
                        source=self.getSourcePath("qemu.py")))

        self.set_emulator_setting("architecture", self.getArchitecture())
        self.set_emulator_setting("emulator-path", emulator)

    def _create_target(self):
        self.build()
        exe = self.getBuildArtifact()

        # Create a target using our platform
        error = lldb.SBError()
        target = self.dbg.CreateTarget(exe, '', 'qemu-user', False, error)
        self.assertSuccess(error)
        self.assertEqual(target.GetPlatform().GetName(), "qemu-user")
        return target

    def _run_and_get_state(self, target=None, info=None):
        if target is None:
            target = self._create_target()

        if info is None:
            info = target.GetLaunchInfo()

        # "Launch" the process. Our fake qemu implementation will pretend it
        # immediately exited.
        info.SetArguments(["dump:" + self.getBuildArtifact("state.log")], True)
        error = lldb.SBError()
        process = target.Launch(info, error)
        self.assertSuccess(error)
        self.assertIsNotNone(process)
        self.assertState(process.GetState(), lldb.eStateExited)
        self.assertEqual(process.GetExitStatus(), 0x47)

        # Verify the qemu invocation parameters.
        with open(self.getBuildArtifact("state.log")) as s:
            return json.load(s)

    def test_basic_launch(self):
        state = self._run_and_get_state()

        self.assertEqual(state["program"], self.getBuildArtifact())
        self.assertEqual(state["args"],
                ["dump:" + self.getBuildArtifact("state.log")])

    def test_stdio_pty(self):
        target = self._create_target()

        info = target.GetLaunchInfo()
        info.SetArguments([
            "stdin:stdin",
            "stdout:STDOUT CONTENT\n",
            "stderr:STDERR CONTENT\n",
            "dump:" + self.getBuildArtifact("state.log"),
            ], False)

        listener = lldb.SBListener("test_stdio")
        info.SetListener(listener)

        self.dbg.SetAsync(True)
        error = lldb.SBError()
        process = target.Launch(info, error)
        self.assertSuccess(error)
        lldbutil.expect_state_changes(self, listener, process,
                [lldb.eStateRunning])

        process.PutSTDIN("STDIN CONTENT\n")

        lldbutil.expect_state_changes(self, listener, process,
                [lldb.eStateExited])

        # Echoed stdin, stdout and stderr. With a pty we cannot split standard
        # output and error.
        self.assertEqual(process.GetSTDOUT(1000),
                "STDIN CONTENT\r\nSTDOUT CONTENT\r\nSTDERR CONTENT\r\n")
        with open(self.getBuildArtifact("state.log")) as s:
            state = json.load(s)
        self.assertEqual(state["stdin"], "STDIN CONTENT\n")

    def test_stdio_redirect(self):
        self.build()
        exe = self.getBuildArtifact()

        # Create a target using our platform
        error = lldb.SBError()
        target = self.dbg.CreateTarget(exe, '', 'qemu-user', False, error)
        self.assertSuccess(error)

        info = lldb.SBLaunchInfo([
            "stdin:stdin",
            "stdout:STDOUT CONTENT",
            "stderr:STDERR CONTENT",
            "dump:" + self.getBuildArtifact("state.log"),
            ])

        info.AddOpenFileAction(0, self.getBuildArtifact("stdin.txt"),
                True, False)
        info.AddOpenFileAction(1, self.getBuildArtifact("stdout.txt"),
                False, True)
        info.AddOpenFileAction(2, self.getBuildArtifact("stderr.txt"),
                False, True)

        with open(self.getBuildArtifact("stdin.txt"), "w") as f:
            f.write("STDIN CONTENT")

        process = target.Launch(info, error)
        self.assertSuccess(error)
        self.assertState(process.GetState(), lldb.eStateExited)

        with open(self.getBuildArtifact("stdout.txt")) as f:
            self.assertEqual(f.read(), "STDOUT CONTENT")
        with open(self.getBuildArtifact("stderr.txt")) as f:
            self.assertEqual(f.read(), "STDERR CONTENT")
        with open(self.getBuildArtifact("state.log")) as s:
            state = json.load(s)
        self.assertEqual(state["stdin"], "STDIN CONTENT")

    def test_find_in_PATH(self):
        emulator = self.getBuildArtifact("qemu-" + self.getArchitecture())
        os.rename(self.getBuildArtifact("qemu.py"), emulator)
        self.set_emulator_setting("emulator-path", "''")

        original_path = os.environ["PATH"]
        os.environ["PATH"] = (self.getBuildDir() +
            self.platformContext.shlib_path_separator + original_path)
        def cleanup():
            os.environ["PATH"] = original_path

        self.addTearDownHook(cleanup)
        state = self._run_and_get_state()

        self.assertEqual(state["program"], self.getBuildArtifact())
        self.assertEqual(state["args"],
                ["dump:" + self.getBuildArtifact("state.log")])

    def test_bad_emulator_path(self):
        self.set_emulator_setting("emulator-path",
                self.getBuildArtifact("nonexistent.file"))

        target = self._create_target()
        info = lldb.SBLaunchInfo([])
        error = lldb.SBError()
        target.Launch(info, error)
        self.assertTrue(error.Fail())
        self.assertIn("doesn't exist", error.GetCString())

    def test_extra_args(self):
        self.set_emulator_setting("emulator-args", "-fake-arg fake-value")
        state = self._run_and_get_state()

        self.assertEqual(state["fake-arg"], "fake-value")

    def test_env_vars(self):
        # First clear any global environment to have a clean slate for this test
        self.runCmd("settings clear target.env-vars")
        self.runCmd("settings clear target.unset-env-vars")

        def var(i):
            return "LLDB_TEST_QEMU_VAR%d" % i

        # Set some variables in the host environment.
        for i in range(4):
            os.environ[var(i)]="from host"
        def cleanup():
            for i in range(4):
                del os.environ[var(i)]
        self.addTearDownHook(cleanup)

        # Set some emulator-only variables.
        self.set_emulator_setting("emulator-env-vars",
                "%s='emulator only'"%var(4))

        # And through the platform setting.
        self.set_emulator_setting("target-env-vars",
                "%s='from platform' %s='from platform'" % (var(1), var(2)))

        target = self._create_target()
        info = target.GetLaunchInfo()
        env = info.GetEnvironment()

        # Platform settings should trump host values. Emulator-only variables
        # should not be visible.
        self.assertEqual(env.Get(var(0)), "from host")
        self.assertEqual(env.Get(var(1)), "from platform")
        self.assertEqual(env.Get(var(2)), "from platform")
        self.assertEqual(env.Get(var(3)), "from host")
        self.assertIsNone(env.Get(var(4)))

        # Finally, make some launch_info specific changes.
        env.Set(var(2), "from target", True)
        env.Unset(var(3))
        info.SetEnvironment(env, False)

        # Now check everything. Launch info changes should trump everything, but
        # only for the target environment -- the emulator should still get the
        # host values.
        state = self._run_and_get_state(target, info)
        for i in range(4):
            self.assertEqual(state["environ"][var(i)], "from host")
        self.assertEqual(state["environ"][var(4)], "emulator only")
        self.assertEqual(state["environ"]["QEMU_SET_ENV"],
                "%s=from platform,%s=from target" % (var(1), var(2)))
        self.assertEqual(state["environ"]["QEMU_UNSET_ENV"],
                "%s,%s,QEMU_SET_ENV,QEMU_UNSET_ENV" % (var(3), var(4)))

    def test_arg0(self):
        target = self._create_target()
        self.runCmd("settings set target.arg0 ARG0")
        state = self._run_and_get_state(target)

        self.assertEqual(state["program"], self.getBuildArtifact())
        self.assertEqual(state["0"], "ARG0")

    def test_sysroot(self):
        sysroot = self.getBuildArtifact("sysroot")
        self.runCmd("platform select qemu-user --sysroot %s" % sysroot)
        state = self._run_and_get_state()
        self.assertEqual(state["environ"]["QEMU_LD_PREFIX"], sysroot)
        self.assertIn("QEMU_LD_PREFIX",
                state["environ"]["QEMU_UNSET_ENV"].split(","))
