"""
Test lldb-dap stack trace when some of the source paths are missing
"""

from lldbsuite.test.lldbtest import line_number
import lldbdap_testcase
from contextlib import contextmanager
import os


OTHER_C_SOURCE_CODE = """
int no_source_func(int n) {
    return n + 1; // Break here
}
"""


@contextmanager
def delete_file_on_exit(path):
    try:
        yield path
    finally:
        if os.path.exists(path):
            os.remove(path)


class TestDAP_stackTraceMissingSourcePath(lldbdap_testcase.DAPTestCaseBase):
    def build_and_run_until_breakpoint(self):
        """
        Build the program and run until the breakpoint is hit, and return the stack frames.
        """
        other_source_file = "other.c"
        with delete_file_on_exit(other_source_file):
            with open(other_source_file, "w") as f:
                f.write(OTHER_C_SOURCE_CODE)

            breakpoint_line = line_number(other_source_file, "// Break here")

            program = self.getBuildArtifact("a.out")
            self.build_and_launch(program, commandEscapePrefix="")

            breakpoint_ids = self.set_source_breakpoints(
                other_source_file, [breakpoint_line]
            )
            self.assertEqual(
                len(breakpoint_ids), 1, "expect correct number of breakpoints"
            )

            self.continue_to_breakpoints(breakpoint_ids)

        frames = self.get_stackFrames()
        self.assertLessEqual(2, len(frames), "expect at least 2 frames")

        self.assertIn(
            "path",
            frames[0]["source"],
            "Expect source path to always be in frame (other.c)",
        )
        self.assertIn(
            "path",
            frames[1]["source"],
            "Expect source path in always be in frame (main.c)",
        )

        return frames

    def verify_frames_source(
        self, frames, main_frame_assembly: bool, other_frame_assembly: bool
    ):
        self.assertLessEqual(2, len(frames), "expect at least 2 frames")
        source_0 = frames[0].get("source")
        source_1 = frames[1].get("source")
        self.assertIsNotNone(source_0, "Expects a source object in frame 0")
        self.assertIsNotNone(source_1, "Expects a source object in frame 1")

        # it does not always have a path.
        source_0_path: str = source_0.get("path", "")
        source_1_path: str = source_1.get("path", "")

        if other_frame_assembly:
            self.assertFalse(
                source_0_path.endswith("other.c"),
                "Expect original source path to not be in unavailable source frame (other.c)",
            )
            self.assertIn(
                "sourceReference",
                source_0,
                "Expect sourceReference to be in unavailable source frame (other.c)",
            )
        else:
            self.assertTrue(
                source_0_path.endswith("other.c"),
                "Expect original source path to be in normal source frame (other.c)",
            )
            self.assertNotIn(
                "sourceReference",
                source_0,
                "Expect sourceReference to not be in normal source frame (other.c)",
            )

        if main_frame_assembly:
            self.assertFalse(
                source_1_path.endswith("main.c"),
                "Expect original source path to not be in unavailable source frame (main.c)",
            )
            self.assertIn(
                "sourceReference",
                source_1,
                "Expect sourceReference to be in unavailable source frame (main.c)",
            )
        else:
            self.assertTrue(
                source_1_path.endswith("main.c"),
                "Expect original source path to be in normal source frame (main.c)",
            )
            self.assertNotIn(
                "sourceReference",
                source_1,
                "Expect sourceReference to not be in normal source code frame (main.c)",
            )

    def test_stopDisassemblyDisplay(self):
        """
        Test that with with all stop-disassembly-display values you get correct assembly / no assembly source code.
        """
        self.build_and_run_until_breakpoint()
        frames = self.get_stackFrames()
        self.assertLessEqual(2, len(frames), "expect at least 2 frames")

        self.assertIn(
            "path",
            frames[0]["source"],
            "Expect source path to always be in frame (other.c)",
        )
        self.assertIn(
            "path",
            frames[1]["source"],
            "Expect source path in always be in frame (main.c)",
        )

        self.dap_server.request_evaluate(
            "settings set stop-disassembly-display never", context="repl"
        )
        frames = self.get_stackFrames()
        self.verify_frames_source(
            frames, main_frame_assembly=False, other_frame_assembly=False
        )

        self.dap_server.request_evaluate(
            "settings set stop-disassembly-display always", context="repl"
        )
        frames = self.get_stackFrames()
        self.verify_frames_source(
            frames, main_frame_assembly=True, other_frame_assembly=True
        )

        self.dap_server.request_evaluate(
            "settings set stop-disassembly-display no-source", context="repl"
        )
        frames = self.get_stackFrames()
        self.verify_frames_source(
            frames, main_frame_assembly=False, other_frame_assembly=True
        )

        self.dap_server.request_evaluate(
            "settings set stop-disassembly-display no-debuginfo", context="repl"
        )
        frames = self.get_stackFrames()
        self.verify_frames_source(
            frames, main_frame_assembly=False, other_frame_assembly=False
        )
