import os
from pathlib import Path

import pytest

from serena.util.text_utils import find_text_coordinates
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_types import SymbolKind
from solidlsp.ls_utils import SymbolUtils
from test.solidlsp.conftest import format_symbol_for_assert, has_malformed_name, request_all_symbols


@pytest.mark.dart
class TestDartLanguageServer:
    @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
    @pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
    def test_ls_is_running(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
        """Test that the language server starts and stops successfully."""
        # The fixture already handles start and stop
        assert language_server.is_running()
        assert Path(language_server.language_server.repository_root_path).resolve() == repo_path.resolve()

    @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
    @pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
    def test_find_definition_within_file(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
        """Test finding definition of a method within the same file."""
        # In lib/main.dart:
        # Line 105: final result1 = calc.add(5, 3); // Reference to add method
        # Line 12: int add(int a, int b) {        // Definition of add method
        # Find definition of 'add' method from its usage
        main_dart_path = str(repo_path / "lib" / "main.dart")

        # Position: calc.add(5, 3) - cursor on 'add'
        # Line 105 (1-indexed) = line 104 (0-indexed), char position around 22
        definition_location_list = language_server.request_definition(main_dart_path, 104, 22)

        assert definition_location_list, f"Expected non-empty definition_location_list but got {definition_location_list=}"
        assert len(definition_location_list) >= 1
        definition_location = definition_location_list[0]
        assert definition_location["uri"].endswith("main.dart")
        # Definition of add method should be around line 11 (0-indexed)
        # But language server may return different positions
        assert definition_location["range"]["start"]["line"] >= 0

    @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
    @pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
    def test_find_definition_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
        """Test finding definition across different files."""
        # Test finding definition of MathHelper class which is in helper.dart
        # In lib/main.dart line 50: MathHelper.power(step1, 2)
        main_dart_path = str(repo_path / "lib" / "main.dart")

        # Position: MathHelper.power(step1, 2) - cursor on 'MathHelper'
        # Line 50 (1-indexed) = line 49 (0-indexed), char position around 18
        definition_location_list = language_server.request_definition(main_dart_path, 49, 18)

        # Skip the test if language server doesn't find cross-file references
        # This is acceptable for a basic test - the important thing is that LS is working
        if not definition_location_list:
            pytest.skip("Language server doesn't support cross-file definition lookup for this case")

        assert len(definition_location_list) >= 1
        definition_location = definition_location_list[0]
        assert definition_location["uri"].endswith("helper.dart")
        assert definition_location["range"]["start"]["line"] >= 0

    @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
    @pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
    def test_find_definition_class_method(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
        """Test finding definition of a class method."""
        # In lib/main.dart:
        # Line 50: final step2 = MathHelper.power(step1, 2); // Reference to MathHelper.power method
        # In lib/helper.dart:
        # Line 14: static double power(double base, int exponent) { // Definition of power method
        main_dart_path = str(repo_path / "lib" / "main.dart")

        # Position: MathHelper.power(step1, 2) - cursor on 'power'
        # Line 50 (1-indexed) = line 49 (0-indexed), char position around 30
        definition_location_list = language_server.request_definition(main_dart_path, 49, 30)

        assert definition_location_list, f"Expected non-empty definition_location_list but got {definition_location_list=}"
        assert len(definition_location_list) >= 1
        definition_location = definition_location_list[0]
        assert definition_location["uri"].endswith("helper.dart")
        # Definition of power method should be around line 13 (0-indexed)
        assert 12 <= definition_location["range"]["start"]["line"] <= 16

    @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
    @pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
    def test_find_references_within_file(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
        """Test finding references to a method within the same file."""
        main_dart_path = str(repo_path / "lib" / "main.dart")

        # Find references to the 'add' method from its definition
        # Line 12: int add(int a, int b) { // Definition of add method
        # Line 105: final result1 = calc.add(5, 3); // Usage of add method
        references = language_server.request_references(main_dart_path, 11, 6)  # cursor on 'add' in definition

        assert references, f"Expected non-empty references but got {references=}"
        # Should find at least the usage of add method
        assert len(references) >= 1

        # Check that we have a reference in main.dart
        main_dart_references = [ref for ref in references if ref["uri"].endswith("main.dart")]
        assert len(main_dart_references) >= 1

    @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
    @pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
    def test_find_references_across_files(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
        """Test finding references across different files."""
        helper_dart_path = str(repo_path / "lib" / "helper.dart")

        # Find references to the 'subtract' function from its definition in helper.dart
        # Definition is in helper.dart, usage is in main.dart
        references = language_server.request_references(helper_dart_path, 4, 4)  # cursor on 'subtract' in definition

        assert references, f"Expected non-empty references for subtract function but got {references=}"

        # Should find references in main.dart
        main_dart_references = [ref for ref in references if ref["uri"].endswith("main.dart")]
        assert len(main_dart_references) >= 1

    @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
    @pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
    def test_find_definition_constructor(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
        """Test finding definition of a constructor call."""
        main_dart_path = str(repo_path / "lib" / "main.dart")

        # In lib/main.dart:
        # Line 104: final calc = Calculator(); // Reference to Calculator constructor
        # Line 4: class Calculator {          // Definition of Calculator class
        definition_location_list = language_server.request_definition(main_dart_path, 103, 18)  # cursor on 'Calculator'

        assert definition_location_list, f"Expected non-empty definition_location_list but got {definition_location_list=}"
        assert len(definition_location_list) >= 1
        definition_location = definition_location_list[0]
        assert definition_location["uri"].endswith("main.dart")
        # Definition of Calculator class should be around line 3 (0-indexed)
        assert 3 <= definition_location["range"]["start"]["line"] <= 7

    @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
    @pytest.mark.parametrize("repo_path", [Language.DART], indirect=True)
    def test_find_definition_import(self, language_server: SolidLanguageServer, repo_path: Path) -> None:
        """Test finding definition through imports."""
        models_dart_path = str(repo_path / "lib" / "models.dart")

        # Test finding definition of User class name where it's used
        # In lib/models.dart line 27 (constructor): User(this.id, this.name, this.email, this._age);
        definition_location_list = language_server.request_definition(models_dart_path, 26, 2)  # cursor on 'User' in constructor

        # Skip if language server doesn't find definition in this case
        if not definition_location_list:
            pytest.skip("Language server doesn't support definition lookup for this case")

        assert len(definition_location_list) >= 1
        definition_location = definition_location_list[0]
        # Language server might return SDK files instead of local files
        # This is acceptable behavior - the important thing is that it found a definition
        assert "dart" in definition_location["uri"].lower()

    @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
    def test_find_symbol(self, language_server: SolidLanguageServer) -> None:
        """Test finding symbols in the full symbol tree."""
        symbols = language_server.request_full_symbol_tree()
        assert SymbolUtils.symbol_tree_contains_name(symbols, "Calculator"), "Calculator class not found in symbol tree"
        assert SymbolUtils.symbol_tree_contains_name(symbols, "add"), "add method not found in symbol tree"
        assert SymbolUtils.symbol_tree_contains_name(symbols, "subtract"), "subtract function not found in symbol tree"
        assert SymbolUtils.symbol_tree_contains_name(symbols, "MathHelper"), "MathHelper class not found in symbol tree"
        assert SymbolUtils.symbol_tree_contains_name(symbols, "User"), "User class not found in symbol tree"

    @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
    def test_find_referencing_symbols(self, language_server: SolidLanguageServer) -> None:
        """Test finding references using symbol selection range."""
        file_path = os.path.join("lib", "main.dart")
        symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()

        # Handle nested symbol structure - symbols can be nested in lists
        symbol_list = symbols[0] if symbols and isinstance(symbols[0], list) else symbols

        # Find the 'add' method symbol in Calculator class
        add_symbol = None
        for sym in symbol_list:
            if sym.get("name") == "add":
                add_symbol = sym
                break
            # Check for nested symbols (methods inside classes)
            if "children" in sym and sym.get("name") == "Calculator":
                for child in sym["children"]:
                    if child.get("name") == "add":
                        add_symbol = child
                        break
                if add_symbol:
                    break

        assert add_symbol is not None, "Could not find 'add' method symbol in main.dart"
        sel_start = add_symbol["selectionRange"]["start"]
        refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"])

        # Check that we found references - at least one should be in main.dart
        assert any("main.dart" in ref.get("relativePath", "") or "main.dart" in ref.get("uri", "") for ref in refs), (
            "main.dart should reference add method (tried all positions in selectionRange)"
        )

    @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
    def test_request_containing_symbol_method(self, language_server: SolidLanguageServer) -> None:
        """Test request_containing_symbol for a method."""
        file_path = os.path.join("lib", "main.dart")
        # Line 14 is inside the add method body (around 'final result = a + b;')
        containing_symbol = language_server.request_containing_symbol(file_path, 13, 10, include_body=True)

        # Verify that we found the containing symbol
        if containing_symbol is not None:
            assert containing_symbol["name"] == "add"
            assert containing_symbol["kind"] == SymbolKind.Method
            if "body" in containing_symbol:
                body = containing_symbol["body"].get_text()
                assert "add" in body or "final result" in body

    @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
    def test_request_containing_symbol_class(self, language_server: SolidLanguageServer) -> None:
        """Test request_containing_symbol for a class."""
        file_path = os.path.join("lib", "main.dart")
        # Line 4 is the Calculator class definition line
        containing_symbol = language_server.request_containing_symbol(file_path, 4, 6)

        # Verify that we found the containing symbol
        if containing_symbol is not None:
            assert containing_symbol["name"] == "Calculator"
            assert containing_symbol["kind"] == SymbolKind.Class

    @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
    def test_request_containing_symbol_nested(self, language_server: SolidLanguageServer) -> None:
        """Test request_containing_symbol with nested scopes."""
        file_path = os.path.join("lib", "main.dart")
        # Line 14 is inside the add method inside Calculator class
        containing_symbol = language_server.request_containing_symbol(file_path, 13, 20)

        # Verify that we found the innermost containing symbol (the method)
        if containing_symbol is not None:
            assert containing_symbol["name"] == "add"
            assert containing_symbol["kind"] == SymbolKind.Method

    @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
    def test_request_defining_symbol_variable(self, language_server: SolidLanguageServer) -> None:
        """Test request_defining_symbol for a variable usage."""
        file_path = os.path.join("lib", "main.dart")

        # Find coordinates of 'final result = a + b;' - test position on 'result'
        with language_server.open_file(file_path, open_in_ls=False) as f:
            pos = find_text_coordinates(f.contents, r"final (result) = a \+ b;")

        defining_symbol = language_server.request_defining_symbol(file_path, pos.line, pos.col)

        # The defining symbol might be the variable itself or the containing method
        # This is acceptable behavior - different language servers handle this differently
        if defining_symbol is not None:
            assert defining_symbol.get("name") in ["result", "add"]
            if defining_symbol.get("name") == "add":
                assert defining_symbol.get("kind") == SymbolKind.Method.value

    @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
    def test_request_defining_symbol_imported_class(self, language_server: SolidLanguageServer) -> None:
        """Test request_defining_symbol for an imported class/function."""
        file_path = os.path.join("lib", "main.dart")
        # Line 20 references 'subtract' which was imported from helper.dart
        defining_symbol = language_server.request_defining_symbol(file_path, 19, 18)

        # Verify that we found the defining symbol - this should be the subtract function from helper.dart
        if defining_symbol is not None:
            assert defining_symbol.get("name") == "subtract"
            # Could be Function or Method depending on language server interpretation
            assert defining_symbol.get("kind") in [SymbolKind.Function.value, SymbolKind.Method.value]

    @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
    def test_request_defining_symbol_class_method(self, language_server: SolidLanguageServer) -> None:
        """Test request_defining_symbol for a static class method."""
        file_path = os.path.join("lib", "main.dart")
        # Line 50 references MathHelper.power - test position on 'power'
        defining_symbol = language_server.request_defining_symbol(file_path, 49, 30)

        # Verify that we found the defining symbol - should be the power method
        if defining_symbol is not None:
            assert defining_symbol.get("name") == "power"
            assert defining_symbol.get("kind") == SymbolKind.Method.value

    @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
    def test_request_document_symbols(self, language_server: SolidLanguageServer) -> None:
        """Test getting document symbols from a Dart file."""
        file_path = os.path.join("lib", "main.dart")
        symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()

        # Check that we have symbols
        assert len(symbols) > 0

        # Flatten the symbols if they're nested
        symbol_list = symbols[0] if symbols and isinstance(symbols[0], list) else symbols

        # Look for expected classes and methods
        symbol_names = [s.get("name") for s in symbol_list]
        assert "Calculator" in symbol_names

        # Check for nested symbols (methods inside classes) - optional
        calculator_symbol = next((s for s in symbol_list if s.get("name") == "Calculator"), None)
        if calculator_symbol and "children" in calculator_symbol and calculator_symbol["children"]:
            method_names = [child.get("name") for child in calculator_symbol["children"]]
            # If children are populated, we should find the add method
            assert "add" in method_names
        else:
            # Some language servers may not populate children in document symbols
            # This is acceptable behavior - the important thing is we found the class
            pass

    @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
    def test_request_referencing_symbols_comprehensive(self, language_server: SolidLanguageServer) -> None:
        """Test comprehensive referencing symbols functionality."""
        file_path = os.path.join("lib", "main.dart")
        symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()

        # Handle nested symbol structure
        symbol_list = symbols[0] if symbols and isinstance(symbols[0], list) else symbols

        # Find Calculator class and test its references
        calculator_symbol = None
        for sym in symbol_list:
            if sym.get("name") == "Calculator":
                calculator_symbol = sym
                break

        if calculator_symbol and "selectionRange" in calculator_symbol:
            sel_start = calculator_symbol["selectionRange"]["start"]
            refs = language_server.request_references(file_path, sel_start["line"], sel_start["character"])

            # Should find references to Calculator (constructor calls, etc.)
            if refs:
                # Verify the structure of referencing symbols
                for ref in refs:
                    assert "uri" in ref or "relativePath" in ref
                    if "range" in ref:
                        assert "start" in ref["range"]
                        assert "end" in ref["range"]

    @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
    def test_cross_file_symbol_resolution(self, language_server: SolidLanguageServer) -> None:
        """Test symbol resolution across multiple files."""
        helper_file_path = os.path.join("lib", "helper.dart")

        # Test finding references to subtract function from helper.dart in main.dart
        helper_symbols = language_server.request_document_symbols(helper_file_path).get_all_symbols_and_roots()
        symbol_list = helper_symbols[0] if helper_symbols and isinstance(helper_symbols[0], list) else helper_symbols

        subtract_symbol = next((s for s in symbol_list if s.get("name") == "subtract"), None)

        if subtract_symbol and "selectionRange" in subtract_symbol:
            sel_start = subtract_symbol["selectionRange"]["start"]
            refs = language_server.request_references(helper_file_path, sel_start["line"], sel_start["character"])

            # Should find references in main.dart
            main_dart_refs = [ref for ref in refs if "main.dart" in ref.get("uri", "") or "main.dart" in ref.get("relativePath", "")]
            # Note: This may not always work depending on language server capabilities
            # So we don't assert - just verify the structure if we get results
            if main_dart_refs:
                for ref in main_dart_refs:
                    assert "range" in ref or "location" in ref

    @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
    def test_bare_symbol_names(self, language_server) -> None:
        all_symbols = request_all_symbols(language_server)
        malformed_symbols = []
        for s in all_symbols:
            if has_malformed_name(s):
                malformed_symbols.append(s)
        if malformed_symbols:
            pytest.fail(
                f"Found malformed symbols: {[format_symbol_for_assert(sym) for sym in malformed_symbols]}",
                pytrace=False,
            )

    @pytest.mark.parametrize("language_server", [Language.DART], indirect=True)
    def test_symbol_body_contains_full_method(self, language_server: SolidLanguageServer) -> None:
        """Test that document symbols return the full method body range, not just the identifier.

        Regression test: when hierarchicalDocumentSymbolSupport was not declared in client
        capabilities, the Dart LSP returned SymbolInformation[] (flat format) where
        location.range only covered the identifier name (single line), causing find_symbol
        with include_body=True to return just the symbol name instead of its implementation.
        """
        file_path = os.path.join("lib", "main.dart")
        symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
        symbol_list = symbols[0] if symbols and isinstance(symbols[0], list) else symbols

        # Find the 'add' method — defined across multiple lines in main.dart:
        #   int add(int a, int b) {
        #     final result = a + b;
        #     _history.add('$a + $b = $result');
        #     return result;
        #   }
        add_symbol = None
        for sym in symbol_list:
            if sym.get("name") == "add":
                add_symbol = sym
                break
            if sym.get("name") == "Calculator" and "children" in sym:
                for child in sym["children"]:
                    if child.get("name") == "add":
                        add_symbol = child
                        break
                if add_symbol:
                    break

        assert add_symbol is not None, "Could not find 'add' method symbol in main.dart"

        # The body range must span multiple lines (not just the identifier line).
        # With hierarchicalDocumentSymbolSupport declared, the Dart LSP returns
        # DocumentSymbol[] where range covers the full method body.
        body_start = add_symbol["location"]["range"]["start"]["line"]
        body_end = add_symbol["location"]["range"]["end"]["line"]
        assert body_end > body_start, (
            f"Expected multi-line body range for 'add' method, got start={body_start}, end={body_end}. "
            f"This likely means hierarchicalDocumentSymbolSupport is not declared in client capabilities."
        )

        # The body text must contain the method implementation, not just the name.
        if add_symbol.get("body"):
            body_text = add_symbol["body"].get_text()
            assert "return result" in body_text, f"Expected method body to contain implementation, got: {body_text!r}"
