"""
Tests for the language server symbol-related functionality.

These tests focus on the following methods:
- request_containing_symbol
- request_referencing_symbols
"""

import os

import pytest

from serena.symbol import LanguageServerSymbol
from serena.util.text_utils import find_text_coordinates
from solidlsp import SolidLanguageServer
from solidlsp.ls_types import SymbolKind
from test.solidlsp.conftest import PYTHON_BACKEND_LANGUAGES

pytestmark = pytest.mark.python


class TestLanguageServerSymbols:
    """Test the language server's symbol-related functionality."""

    @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
    def test_request_containing_symbol_function(self, language_server: SolidLanguageServer) -> None:
        """Test request_containing_symbol for a function."""
        # Test for a position inside the create_user method
        file_path = os.path.join("test_repo", "services.py")
        # Line 17 is inside the create_user method body
        containing_symbol = language_server.request_containing_symbol(file_path, 17, 20, include_body=True)

        # Verify that we found the containing symbol
        assert containing_symbol is not None
        assert containing_symbol["name"] == "create_user"
        assert containing_symbol["kind"] == SymbolKind.Method
        if "body" in containing_symbol:
            assert containing_symbol["body"].get_text().strip().startswith("def create_user(self")

    @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
    def test_references_to_variables(self, language_server: SolidLanguageServer) -> None:
        """Test request_referencing_symbols for a variable."""
        file_path = os.path.join("test_repo", "variables.py")

        # find references to field `status`
        with language_server.open_file(file_path, open_in_ls=False) as f:
            file_content = f.contents
        coords = find_text_coordinates(file_content, r"(status): str")
        ref_symbols = [ref.symbol for ref in language_server.request_referencing_symbols(file_path, coords.line, coords.col)]

        assert len(ref_symbols) > 0
        ref_names = [ref["name"] for ref in ref_symbols]
        assert "dataclass_instance" in ref_names
        assert "second_dataclass" in ref_names

    @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
    def test_request_containing_symbol_class(self, language_server: SolidLanguageServer) -> None:
        """Test request_containing_symbol for a class."""
        # Test for a position inside the UserService class but outside any method
        file_path = os.path.join("test_repo", "services.py")
        # Line 9 is the class definition line for UserService
        containing_symbol = language_server.request_containing_symbol(file_path, 9, 7)

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

    @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
    def test_request_containing_symbol_nested(self, language_server: SolidLanguageServer) -> None:
        """Test request_containing_symbol with nested scopes."""
        # Test for a position inside a method which is inside a class
        file_path = os.path.join("test_repo", "services.py")
        # Line 18 is inside the create_user method inside UserService class
        containing_symbol = language_server.request_containing_symbol(file_path, 18, 25)

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

        # Get the parent containing symbol
        if "location" in containing_symbol and "range" in containing_symbol["location"]:
            parent_symbol = language_server.request_containing_symbol(
                file_path,
                containing_symbol["location"]["range"]["start"]["line"],
                containing_symbol["location"]["range"]["start"]["character"] - 1,
            )

            # Verify that the parent is the class
            assert parent_symbol is not None
            assert parent_symbol["name"] == "UserService"
            assert parent_symbol["kind"] == SymbolKind.Class

    @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
    def test_request_containing_symbol_none(self, language_server: SolidLanguageServer) -> None:
        """Test request_containing_symbol for a position with no containing symbol."""
        # Test for a position outside any function/class (e.g., in imports)
        file_path = os.path.join("test_repo", "services.py")
        # Line 1 is in imports, not inside any function or class
        containing_symbol = language_server.request_containing_symbol(file_path, 1, 10)

        # Should return None or an empty dictionary
        assert containing_symbol is None or containing_symbol == {}

    @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
    def test_request_referencing_symbols_function(self, language_server: SolidLanguageServer) -> None:
        """Test request_referencing_symbols for a function."""
        # Test referencing symbols for create_user function
        file_path = os.path.join("test_repo", "services.py")
        # Line 15 contains the create_user function definition
        symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
        create_user_symbol = next((s for s in symbols[0] if s.get("name") == "create_user"), None)
        if not create_user_symbol or "selectionRange" not in create_user_symbol:
            raise AssertionError("create_user symbol or its selectionRange not found")
        sel_start = create_user_symbol["selectionRange"]["start"]
        ref_symbols = [
            ref.symbol for ref in language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"])
        ]
        assert len(ref_symbols) > 0, "No referencing symbols found for create_user (selectionRange)"

        # Verify the structure of referencing symbols
        for symbol in ref_symbols:
            assert "name" in symbol
            assert "kind" in symbol
            if "location" in symbol and "range" in symbol["location"]:
                assert "start" in symbol["location"]["range"]
                assert "end" in symbol["location"]["range"]

    @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
    def test_request_referencing_symbols_class(self, language_server: SolidLanguageServer) -> None:
        """Test request_referencing_symbols for a class."""
        # Test referencing symbols for User class
        file_path = os.path.join("test_repo", "models.py")
        # Line 31 contains the User class definition
        symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
        user_symbol = next((s for s in symbols[0] if s.get("name") == "User"), None)
        if not user_symbol or "selectionRange" not in user_symbol:
            raise AssertionError("User symbol or its selectionRange not found")
        sel_start = user_symbol["selectionRange"]["start"]
        ref_symbols = [
            ref.symbol for ref in language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"])
        ]
        services_references = [
            symbol
            for symbol in ref_symbols
            if "location" in symbol and "uri" in symbol["location"] and "services.py" in symbol["location"]["uri"]
        ]
        assert len(services_references) > 0, "No referencing symbols from services.py for User (selectionRange)"

    @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
    def test_request_referencing_symbols_parameter(self, language_server: SolidLanguageServer) -> None:
        """Test request_referencing_symbols for a function parameter."""
        # Test referencing symbols for id parameter in get_user
        file_path = os.path.join("test_repo", "services.py")
        # Line 24 contains the get_user method with id parameter
        symbols = language_server.request_document_symbols(file_path).get_all_symbols_and_roots()
        get_user_symbol = next((s for s in symbols[0] if s.get("name") == "get_user"), None)
        if not get_user_symbol or "selectionRange" not in get_user_symbol:
            raise AssertionError("get_user symbol or its selectionRange not found")
        sel_start = get_user_symbol["selectionRange"]["start"]
        ref_symbols = [
            ref.symbol for ref in language_server.request_referencing_symbols(file_path, sel_start["line"], sel_start["character"])
        ]
        method_refs = [
            symbol
            for symbol in ref_symbols
            if "location" in symbol and "range" in symbol["location"] and symbol["location"]["range"]["start"]["line"] > sel_start["line"]
        ]
        assert len(method_refs) > 0, "No referencing symbols within method body for get_user (selectionRange)"

    @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
    def test_request_referencing_symbols_none(self, language_server: SolidLanguageServer) -> None:
        """Test request_referencing_symbols for a position with no symbol."""
        # For positions with no symbol, the method might throw an error or return None/empty list
        # We'll modify our test to handle this by using a try-except block

        file_path = os.path.join("test_repo", "services.py")
        # Line 3 is a blank line or comment
        try:
            ref_symbols = [ref.symbol for ref in language_server.request_referencing_symbols(file_path, 3, 0)]
            # If we get here, make sure we got an empty result
            assert ref_symbols == [] or ref_symbols is None
        except Exception:
            # The method might raise an exception for invalid positions
            # which is acceptable behavior
            pass

    # Tests for request_defining_symbol
    @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
    def test_request_defining_symbol_variable(self, language_server: SolidLanguageServer) -> None:
        """Test request_defining_symbol for a variable usage."""
        # Test finding the definition of a symbol in the create_user method
        file_path = os.path.join("test_repo", "services.py")
        # Line 21 contains self.users[id] = user
        defining_symbol = language_server.request_defining_symbol(file_path, 21, 10)

        # Verify that we found the defining symbol
        # The defining symbol method returns a dictionary with information about the defining symbol
        assert defining_symbol is not None
        assert defining_symbol.get("name") == "create_user"

        # Verify the location and kind of the symbol
        # SymbolKind.Method = 6 for a method
        assert defining_symbol.get("kind") == SymbolKind.Method.value
        if "location" in defining_symbol and "uri" in defining_symbol["location"]:
            assert "services.py" in defining_symbol["location"]["uri"]

    @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
    def test_request_defining_symbol_imported_class(self, language_server: SolidLanguageServer) -> None:
        """Test request_defining_symbol for an imported class."""
        # Test finding the definition of the 'User' class used in the UserService.create_user method
        file_path = os.path.join("test_repo", "services.py")
        # Line 20 references 'User' which was imported from models
        defining_symbol = language_server.request_defining_symbol(file_path, 20, 15)

        # Verify that we found the defining symbol - this should be the User class from models
        assert defining_symbol is not None
        assert defining_symbol.get("name") == "User"

    @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
    def test_request_defining_symbol_method_call(self, language_server: SolidLanguageServer) -> None:
        """Test request_defining_symbol for a method call."""
        # Create an example file path for a file that calls UserService.create_user
        examples_file_path = os.path.join("examples", "user_management.py")

        # Find the line number where create_user is called
        # This could vary, so we'll use a relative position that makes sense
        defining_symbol = language_server.request_defining_symbol(examples_file_path, 46, 29)
        assert defining_symbol is not None
        assert defining_symbol.get("name") == "create_user"
        # The defining symbol should be in the services.py file
        assert "services.py" in defining_symbol["location"]["uri"]

    @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
    def test_request_defining_symbol_none(self, language_server: SolidLanguageServer) -> None:
        """Test request_defining_symbol for a position with no symbol."""
        # Test for a position with no symbol (e.g., whitespace or comment)
        file_path = os.path.join("test_repo", "services.py")
        # Line 3 is a blank line
        defining_symbol = language_server.request_defining_symbol(file_path, 3, 0)

        # Should return None for positions with no symbol
        assert defining_symbol is None

    @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
    def test_request_containing_symbol_variable(self, language_server: SolidLanguageServer) -> None:
        """Test request_containing_symbol where the symbol is a variable."""
        # Test for a position inside a variable definition
        file_path = os.path.join("test_repo", "services.py")
        # Line 74 defines the 'user' variable
        containing_symbol = language_server.request_containing_symbol(file_path, 73, 1)

        # Verify that we found the containing symbol
        assert containing_symbol is not None
        assert containing_symbol["name"] == "user_var_str"
        assert containing_symbol["kind"] == SymbolKind.Variable

    @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
    def test_request_defining_symbol_nested_function(self, language_server: SolidLanguageServer) -> None:
        """Test request_defining_symbol for a nested function or closure."""
        # Use the existing nested.py file which contains nested classes and methods
        file_path = os.path.join("test_repo", "nested.py")

        # Test 1: Find definition of nested method - line with 'b = OuterClass().NestedClass().find_me()'
        defining_symbol = language_server.request_defining_symbol(file_path, 15, 35)  # Position of find_me() call

        # This should resolve to the find_me method in the NestedClass
        assert defining_symbol is not None
        assert defining_symbol.get("name") == "find_me"
        assert defining_symbol.get("kind") == SymbolKind.Method.value

        # Test 2: Find definition of the nested class
        defining_symbol = language_server.request_defining_symbol(file_path, 15, 18)  # Position of NestedClass

        # This should resolve to the NestedClass
        assert defining_symbol is not None
        assert defining_symbol.get("name") == "NestedClass"
        assert defining_symbol.get("kind") == SymbolKind.Class.value

        # Test 3: Find definition of a method-local function
        defining_symbol = language_server.request_defining_symbol(file_path, 9, 15)  # Position inside func_within_func

        assert defining_symbol is not None
        assert defining_symbol.get("name") == "func_within_func"

        # Test 2: Find definition of the nested class
        defining_symbol = language_server.request_defining_symbol(file_path, 15, 18)  # Position of NestedClass

        # This should resolve to the NestedClass
        assert defining_symbol is not None
        assert defining_symbol.get("name") == "NestedClass"
        assert defining_symbol.get("kind") == SymbolKind.Class.value

        # Test 3: Find definition of a method-local function
        defining_symbol = language_server.request_defining_symbol(file_path, 9, 15)  # Position inside func_within_func

        # This is challenging for many language servers and may fail
        assert defining_symbol is not None
        assert defining_symbol.get("name") == "func_within_func"
        assert defining_symbol.get("kind") in [SymbolKind.Function.value, SymbolKind.Method.value]

    @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
    def test_symbol_methods_integration(self, language_server: SolidLanguageServer) -> None:
        """Test the integration between different symbol-related methods."""
        # This test demonstrates using the various symbol methods together
        # by finding a symbol and then checking its definition

        file_path = os.path.join("test_repo", "services.py")

        # First approach: Use a method from the UserService class
        # Step 1: Find a method we know exists
        containing_symbol = language_server.request_containing_symbol(file_path, 15, 8)  # create_user method
        assert containing_symbol is not None
        assert containing_symbol["name"] == "create_user"

        # Step 2: Get the defining symbol for the same position
        # This should be the same method
        defining_symbol = language_server.request_defining_symbol(file_path, 15, 8)
        assert defining_symbol is not None
        assert defining_symbol["name"] == "create_user"

        # Step 3: Verify that they refer to the same symbol
        assert defining_symbol["kind"] == containing_symbol["kind"]
        assert defining_symbol["location"]["uri"] == containing_symbol["location"]["uri"]

    @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
    def test_symbol_tree_structure(self, language_server: SolidLanguageServer) -> None:
        """Test that the symbol tree structure is correctly built."""
        # Get all symbols in the test file
        repo_structure = language_server.request_full_symbol_tree()
        assert len(repo_structure) == 1
        # Assert that the root symbol is the test_repo directory
        assert repo_structure[0]["name"] == "test_repo"
        assert repo_structure[0]["kind"] == SymbolKind.Package
        assert "children" in repo_structure[0]
        # Assert that the children are the top-level packages
        child_names = {child["name"] for child in repo_structure[0]["children"]}
        child_kinds = {child["kind"] for child in repo_structure[0]["children"]}
        assert child_names == {"test_repo", "custom_test", "examples", "scripts"}
        assert child_kinds == {SymbolKind.Package}
        examples_package = next(child for child in repo_structure[0]["children"] if child["name"] == "examples")
        # assert that children are __init__ and user_management
        assert {child["name"] for child in examples_package["children"]} == {"__init__", "user_management"}
        assert {child["kind"] for child in examples_package["children"]} == {SymbolKind.File}

        # assert that tree of user_management node is same as retrieved directly
        user_management_node = next(child for child in examples_package["children"] if child["name"] == "user_management")
        if "location" in user_management_node and "relativePath" in user_management_node["location"]:
            user_management_rel_path = user_management_node["location"]["relativePath"]
            assert user_management_rel_path == os.path.join("examples", "user_management.py")
            _, user_management_roots = language_server.request_document_symbols(
                os.path.join("examples", "user_management.py")
            ).get_all_symbols_and_roots()
            assert user_management_roots == user_management_node["children"]

    @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
    def test_symbol_tree_structure_subdir(self, language_server: SolidLanguageServer) -> None:
        """Test that the symbol tree structure is correctly built."""
        # Get all symbols in the test file
        examples_package_roots = language_server.request_full_symbol_tree(within_relative_path="examples")
        assert len(examples_package_roots) == 1
        examples_package = examples_package_roots[0]
        assert examples_package["name"] == "examples"
        assert examples_package["kind"] == SymbolKind.Package
        # assert that children are __init__ and user_management
        assert {child["name"] for child in examples_package["children"]} == {"__init__", "user_management"}
        assert {child["kind"] for child in examples_package["children"]} == {SymbolKind.File}

        # assert that tree of user_management node is same as retrieved directly
        user_management_node = next(child for child in examples_package["children"] if child["name"] == "user_management")
        if "location" in user_management_node and "relativePath" in user_management_node["location"]:
            user_management_rel_path = user_management_node["location"]["relativePath"]
            assert user_management_rel_path == os.path.join("examples", "user_management.py")
            _, user_management_roots = language_server.request_document_symbols(
                os.path.join("examples", "user_management.py")
            ).get_all_symbols_and_roots()
            assert user_management_roots == user_management_node["children"]

    @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
    def test_request_dir_overview(self, language_server: SolidLanguageServer) -> None:
        """Test that request_dir_overview returns correct symbol information for files in a directory."""
        # Get overview of the examples directory
        overview = language_server.request_dir_overview("test_repo")

        # Verify that we have entries for both files
        assert os.path.join("test_repo", "nested.py") in overview

        # Get the symbols for user_management.py
        services_symbols = overview[os.path.join("test_repo", "services.py")]
        assert len(services_symbols) > 0

        # Check for specific symbols from services.py
        expected_symbols = {
            "UserService",
            "ItemService",
            "create_service_container",
            "user_var_str",
            "user_service",
        }
        retrieved_symbols = {symbol["name"] for symbol in services_symbols if "name" in symbol}
        assert expected_symbols.issubset(retrieved_symbols)

    @pytest.mark.parametrize("language_server", PYTHON_BACKEND_LANGUAGES, indirect=True)
    def test_request_document_overview(self, language_server: SolidLanguageServer) -> None:
        """Test that request_document_overview returns correct symbol information for a file."""
        # Get overview of the user_management.py file
        overview = language_server.request_document_overview(os.path.join("examples", "user_management.py"))

        # Verify that we have entries for both files
        symbol_names = {LanguageServerSymbol(s_info).name for s_info in overview}
        assert {"UserStats", "UserManager", "process_user_data", "main"}.issubset(symbol_names)
