"""
Test the diagnostics emitted by our embeded Clang instance that parses expressions.
"""

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


class ExprDiagnosticsTestCase(TestBase):
    def setUp(self):
        # Call super's setUp().
        TestBase.setUp(self)

        self.main_source = "main.cpp"
        self.main_source_spec = lldb.SBFileSpec(self.main_source)

    def test_source_and_caret_printing(self):
        """Test that the source and caret positions LLDB prints are correct"""
        self.build()

        (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
            self, "// Break here", self.main_source_spec
        )
        frame = thread.GetFrameAtIndex(0)

        # Test that source/caret are at the right position.
        value = frame.EvaluateExpression("unknown_identifier")
        self.assertFalse(value.GetError().Success())
        # We should get a nice diagnostic with a caret pointing at the start of
        # the identifier.
        self.assertIn(
            """
    1 | unknown_identifier
      | ^~~~~~~~~~~~~~~~~~
""",
            value.GetError().GetCString(),
        )
        self.assertIn("<user expression 0>:1:1", value.GetError().GetCString())

        # Same as above but with the identifier in the middle.
        value = frame.EvaluateExpression("1 + unknown_identifier")
        self.assertFalse(value.GetError().Success())
        self.assertIn(
            """
    1 | 1 + unknown_identifier
      |     ^~~~~~~~~~~~~~~~~~
""",
            value.GetError().GetCString(),
        )

        # Multiline expressions.
        value = frame.EvaluateExpression("int a = 0;\nfoobar +=1;\na")
        self.assertFalse(value.GetError().Success())
        # We should still get the right line information and caret position.
        self.assertIn(
            """
    2 | foobar +=1;
      | ^~~~~~
""",
            value.GetError().GetCString(),
        )

        # It's the second line of the user expression.
        self.assertIn("<user expression 2>:2:1", value.GetError().GetCString())

        # Top-level expressions.
        top_level_opts = lldb.SBExpressionOptions()
        top_level_opts.SetTopLevel(True)

        value = frame.EvaluateExpression("void foo(unknown_type x) {}", top_level_opts)
        self.assertFalse(value.GetError().Success())
        self.assertIn(
            """
    1 | void foo(unknown_type x) {}
      |          ^
""",
            value.GetError().GetCString(),
        )

        # Top-level expressions might use a different wrapper code, but the file name should still
        # be the same.
        self.assertIn("<user expression 3>:1:10", value.GetError().GetCString())

        # Multiline top-level expressions.
        value = frame.EvaluateExpression("void x() {}\nvoid foo;", top_level_opts)
        self.assertFalse(value.GetError().Success())
        self.assertIn(
            """
    2 | void foo;
      |      ^
""",
            value.GetError().GetCString(),
        )

        self.assertIn("<user expression 4>:2:6", value.GetError().GetCString())

        # Test that we render Clang's 'notes' correctly.
        value = frame.EvaluateExpression(
            "struct SFoo{}; struct SFoo { int x; };", top_level_opts
        )
        self.assertFalse(value.GetError().Success())
        self.assertIn(
            "<user expression 5>:1:8: previous definition is here\n",
            value.GetError().GetCString(),
        )
        self.assertIn(
            """
    1 | struct SFoo{}; struct SFoo { int x; };
      |        ^
""",
            value.GetError().GetCString(),
        )

        # Declarations from the debug information currently have no debug information. It's not clear what
        # we should do in this case, but we should at least not print anything that's wrong.
        # In the future our declarations should have valid source locations.
        value = frame.EvaluateExpression("struct FooBar { double x };", top_level_opts)
        self.assertFalse(value.GetError().Success())
        self.assertIn(
            "error: <user expression 6>:1:8: redefinition of 'FooBar'\n",
            value.GetError().GetCString(),
        )
        self.assertIn(
            """
    1 | struct FooBar { double x };
      |        ^
""",
            value.GetError().GetCString(),
        )

        value = frame.EvaluateExpression("foo(1, 2)")
        self.assertFalse(value.GetError().Success())
        self.assertIn(
            "error: <user expression 7>:1:1: no matching function for call to 'foo'\n",
            value.GetError().GetCString(),
        )
        self.assertIn(
            """
    1 | foo(1, 2)
      | ^~~
note: candidate function not viable: requires single argument 'x', but 2 arguments were provided
""",
            value.GetError().GetCString(),
        )

        # Redefine something that we defined in a user-expression. We should use the previous expression file name
        # for the original decl.
        value = frame.EvaluateExpression("struct Redef { double x; };", top_level_opts)
        value = frame.EvaluateExpression("struct Redef { float y; };", top_level_opts)
        self.assertFalse(value.GetError().Success())
        self.assertIn(
            """error: <user expression 9>:1:8: redefinition of 'Redef'
    1 | struct Redef { float y; };
      |        ^
<user expression 8>:1:8: previous definition is here
    1 | struct Redef { double x; };
      |        ^
""",
            value.GetError().GetCString(),
        )

    @add_test_categories(["objc"])
    def test_source_locations_from_objc_modules(self):
        self.build()

        (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
            self, "// Break here", self.main_source_spec
        )
        frame = thread.GetFrameAtIndex(0)

        # Import foundation so that the Obj-C module is loaded (which contains source locations
        # that can be used by LLDB).
        self.runCmd("expr --language objective-c++ -- @import Foundation")
        value = frame.EvaluateExpression("NSLog(1);")
        self.assertFalse(value.GetError().Success())
        # LLDB should print the source line that defines NSLog. To not rely on any
        # header paths/line numbers or the actual formatting of the Foundation headers, only look
        # for a few tokens in the output.
        # File path should come from Foundation framework.
        self.assertIn("/Foundation.framework/", value.GetError().GetCString())
        # The NSLog definition source line should be printed. Return value and
        # the first argument are probably stable enough that this test can check for them.
        self.assertIn("void NSLog(NSString *format", value.GetError().GetCString())

    def test_error_type(self):
        """Test the error reporting in the API"""
        self.build()

        (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
            self, "// Break here", self.main_source_spec
        )
        frame = thread.GetFrameAtIndex(0)
        value = frame.EvaluateExpression('#error("I am error.")')
        error = value.GetError()
        self.assertEqual(error.GetType(), lldb.eErrorTypeExpression)
        value = frame.FindVariable("f")
        self.assertTrue(value.IsValid())
        desc = value.GetObjectDescription()
        self.assertEqual(desc, None)

    def test_command_expr_sbdata(self):
        """Test the structured diagnostics data"""
        self.build()

        (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
            self, "// Break here", self.main_source_spec
        )

        def check_error(diags):
            # Version.
            version = diags.GetValueForKey("version")
            self.assertEqual(version.GetIntegerValue(), 1)

            details = diags.GetValueForKey("details")

            # Detail 1/3: note: requested expression language
            diag = details.GetItemAtIndex(0)
            self.assertEqual(str(diag.GetValueForKey("severity")), "note")
            self.assertIn("Ran expression as 'C++", str(diag.GetValueForKey("message")))
            self.assertIn(
                "Ran expression as 'C++", str(diag.GetValueForKey("rendered"))
            )
            self.assertEqual(str(diag.GetValueForKey("source_location")), "")
            self.assertEqual(str(diag.GetValueForKey("file")), "")
            self.assertFalse(diag.GetValueForKey("hidden").GetBooleanValue())
            self.assertFalse(diag.GetValueForKey("in_user_input").GetBooleanValue())

            # Detail 2/3: undeclared 'a'
            diag = details.GetItemAtIndex(1)

            severity = diag.GetValueForKey("severity")
            message = diag.GetValueForKey("message")
            rendered = diag.GetValueForKey("rendered")
            sloc = diag.GetValueForKey("source_location")
            filename = sloc.GetValueForKey("file")
            hidden = sloc.GetValueForKey("hidden")
            in_user_input = sloc.GetValueForKey("in_user_input")

            self.assertEqual(str(severity), "error")
            self.assertIn("undeclared identifier 'a'", str(message))
            # The rendered string should contain the source file.
            self.assertIn("user expression", str(rendered))
            self.assertIn("user expression", str(filename))
            self.assertFalse(hidden.GetBooleanValue())
            self.assertTrue(in_user_input.GetBooleanValue())

            # Detail 3/3: undeclared 'b'
            diag = details.GetItemAtIndex(2)
            message = diag.GetValueForKey("message")
            self.assertIn("undeclared identifier 'b'", str(message))

        # Test diagnostics in CommandReturnObject
        interp = self.dbg.GetCommandInterpreter()
        cro = lldb.SBCommandReturnObject()
        interp.HandleCommand("expression -- a+b", cro)

        diags = cro.GetErrorData()
        check_error(diags)

        # Test diagnostics in SBError
        frame = thread.GetSelectedFrame()
        value = frame.EvaluateExpression("a+b")
        error = value.GetError()
        self.assertTrue(error.Fail())
        self.assertEqual(error.GetType(), lldb.eErrorTypeExpression)
        data = error.GetErrorData()
        version = data.GetValueForKey("version")
        self.assertEqual(version.GetIntegerValue(), 1)
        err_ty = data.GetValueForKey("type")
        self.assertEqual(err_ty.GetIntegerValue(), lldb.eErrorTypeExpression)
        diags = data.GetValueForKey("errors").GetItemAtIndex(0)
        check_error(diags)

    def test_no_location(self):
        """Test the error reporting for missing locations"""
        self.build()

        (target, process, thread, bkpt) = lldbutil.run_to_source_breakpoint(
            self, "// Break here", self.main_source_spec
        )
        self.ci.HandleCommand(
            "settings set testing.inject-variable-location-error true", self.res
        )
        if not self.res.Succeeded():
            # This test needs assertions.
            return
        frame = thread.GetFrameAtIndex(0)
        value = frame.EvaluateExpression("f")
        error = value.GetError()
        self.assertIn("variable not available", str(error))
