############################################################################
# Copyright(c) Open Law Library. All rights reserved.                      #
# See ThirdPartyNotices.txt in the project root for additional notices.    #
#                                                                          #
# Licensed under the Apache License, Version 2.0 (the "License")           #
# you may not use this file except in compliance with the License.         #
# You may obtain a copy of the License at                                  #
#                                                                          #
#     http: // www.apache.org/licenses/LICENSE-2.0                         #
#                                                                          #
# Unless required by applicable law or agreed to in writing, software      #
# distributed under the License is distributed on an "AS IS" BASIS,        #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
# See the License for the specific language governing permissions and      #
# limitations under the License.                                           #
############################################################################
import asyncio
from typing import Any

import pytest
from pygls.capabilities import ServerCapabilitiesBuilder
from pygls.exceptions import (
    CommandAlreadyRegisteredError,
    FeatureAlreadyRegisteredError,
    ValidationError,
)
from pygls.feature_manager import (
    FeatureManager,
    has_ls_param_or_annotation,
    wrap_with_server,
)
from lsprotocol import types as lsp


class Temp:
    pass


def test_has_ls_param_or_annotation():
    def f1(ls, a, b, c):
        pass

    def f2(temp: Temp, a, b, c):
        pass

    def f3(temp: "Temp", a, b, c):
        pass

    assert has_ls_param_or_annotation(f1, None)
    assert has_ls_param_or_annotation(f2, Temp)
    assert has_ls_param_or_annotation(f3, Temp)


def test_register_command_validation_error(feature_manager):
    with pytest.raises(ValidationError):

        @feature_manager.command(" \n\t")
        def cmd1():  # pylint: disable=unused-variable
            pass


def test_register_commands(feature_manager):
    cmd1_name = "cmd1"
    cmd2_name = "cmd2"

    @feature_manager.command(cmd1_name)
    def cmd1():
        pass

    @feature_manager.command(cmd2_name)
    def cmd2():
        pass

    reg_commands = feature_manager.commands.keys()

    assert cmd1_name in reg_commands
    assert cmd2_name in reg_commands

    assert feature_manager.commands[cmd1_name] is cmd1
    assert feature_manager.commands[cmd2_name] is cmd2


def test_register_feature_with_valid_options(feature_manager):
    options = lsp.CompletionOptions(trigger_characters=["!"])

    @feature_manager.feature(lsp.TEXT_DOCUMENT_COMPLETION, options)
    def completions():
        pass

    reg_features = feature_manager.features.keys()
    reg_feature_options = feature_manager.feature_options.keys()

    assert lsp.TEXT_DOCUMENT_COMPLETION in reg_features
    assert lsp.TEXT_DOCUMENT_COMPLETION in reg_feature_options

    assert feature_manager.features[lsp.TEXT_DOCUMENT_COMPLETION] is completions
    assert feature_manager.feature_options[lsp.TEXT_DOCUMENT_COMPLETION] is options


def test_register_feature_with_wrong_options(feature_manager):
    class Options:
        pass

    with pytest.raises(
        AttributeError,
        match=("'Options' object has no attribute 'trigger_characters'"),  # noqa
    ):

        @feature_manager.feature(lsp.TEXT_DOCUMENT_COMPLETION, Options())
        def completions():
            pass


def test_register_features(feature_manager):
    @feature_manager.feature(lsp.TEXT_DOCUMENT_COMPLETION)
    def completions():
        pass

    @feature_manager.feature(lsp.TEXT_DOCUMENT_CODE_LENS)
    def code_lens():
        pass

    reg_features = feature_manager.features.keys()

    assert lsp.TEXT_DOCUMENT_COMPLETION in reg_features
    assert lsp.TEXT_DOCUMENT_CODE_LENS in reg_features

    assert feature_manager.features[lsp.TEXT_DOCUMENT_COMPLETION] is completions
    assert feature_manager.features[lsp.TEXT_DOCUMENT_CODE_LENS] is code_lens


def test_register_same_command_twice_error(feature_manager):
    with pytest.raises(CommandAlreadyRegisteredError):

        @feature_manager.command("cmd1")
        def cmd1():  # pylint: disable=unused-variable
            pass

        @feature_manager.command("cmd1")
        def cmd2():  # pylint: disable=unused-variable
            pass


def test_register_same_feature_twice_error(feature_manager):
    with pytest.raises(FeatureAlreadyRegisteredError):

        @feature_manager.feature(lsp.TEXT_DOCUMENT_CODE_ACTION)
        def code_action1():  # pylint: disable=unused-variable
            pass

        @feature_manager.feature(lsp.TEXT_DOCUMENT_CODE_ACTION)
        def code_action2():  # pylint: disable=unused-variable
            pass


def test_wrap_with_server_async():
    class Server:
        pass

    async def f(ls):
        assert isinstance(ls, Server)

    wrapped = wrap_with_server(f, Server())
    assert asyncio.iscoroutinefunction(wrapped)


def test_wrap_with_server_sync():
    class Server:
        pass

    def f(ls):
        assert isinstance(ls, Server)

    wrapped = wrap_with_server(f, Server())
    wrapped()


def test_wrap_with_server_thread():
    class Server:
        pass

    def f(ls):
        assert isinstance(ls, Server)

    f.execute_in_thread = True

    wrapped = wrap_with_server(f, Server())
    assert wrapped.execute_in_thread is True


def server_capabilities(**kwargs):
    """Helper to reduce the amount of boilerplate required to specify the expected
    server capabilities by filling in some fields - unless they are explicitly
    overriden."""

    if "text_document_sync" not in kwargs:
        kwargs["text_document_sync"] = lsp.TextDocumentSyncOptions(
            open_close=False,
            save=False,
        )

    if "execute_command_provider" not in kwargs:
        kwargs["execute_command_provider"] = lsp.ExecuteCommandOptions(commands=[])

    if "workspace" not in kwargs:
        kwargs["workspace"] = lsp.ServerCapabilitiesWorkspaceType(
            workspace_folders=lsp.WorkspaceFoldersServerCapabilities(
                supported=True, change_notifications=True
            ),
            file_operations=lsp.FileOperationOptions(),
        )

    if "position_encoding" not in kwargs:
        kwargs["position_encoding"] = lsp.PositionEncodingKind.Utf16

    return lsp.ServerCapabilities(**kwargs)


@pytest.mark.parametrize(
    "method, options, capabilities, expected",
    [
        (
            lsp.INITIALIZE,
            None,
            lsp.ClientCapabilities(
                general=lsp.GeneralClientCapabilities(
                    position_encodings=[lsp.PositionEncodingKind.Utf8]
                )
            ),
            server_capabilities(position_encoding=lsp.PositionEncodingKind.Utf8),
        ),
        (
            lsp.INITIALIZE,
            None,
            lsp.ClientCapabilities(
                general=lsp.GeneralClientCapabilities(
                    position_encodings=[
                        lsp.PositionEncodingKind.Utf8,
                        lsp.PositionEncodingKind.Utf32,
                    ]
                )
            ),
            server_capabilities(position_encoding=lsp.PositionEncodingKind.Utf32),
        ),
        (
            lsp.TEXT_DOCUMENT_DID_SAVE,
            lsp.SaveOptions(include_text=True),
            lsp.ClientCapabilities(),
            server_capabilities(
                text_document_sync=lsp.TextDocumentSyncOptions(
                    open_close=False, save=lsp.SaveOptions(include_text=True)
                )
            ),
        ),
        (
            lsp.TEXT_DOCUMENT_DID_SAVE,
            None,
            lsp.ClientCapabilities(),
            server_capabilities(
                text_document_sync=lsp.TextDocumentSyncOptions(
                    open_close=False, save=True
                )
            ),
        ),
        (
            lsp.TEXT_DOCUMENT_WILL_SAVE,
            None,
            lsp.ClientCapabilities(),
            server_capabilities(
                text_document_sync=lsp.TextDocumentSyncOptions(
                    open_close=False, save=False
                )
            ),
        ),
        (
            lsp.TEXT_DOCUMENT_WILL_SAVE,
            None,
            lsp.ClientCapabilities(
                text_document=lsp.TextDocumentClientCapabilities(
                    synchronization=lsp.TextDocumentSyncClientCapabilities(
                        will_save=True
                    )
                )
            ),
            server_capabilities(
                text_document_sync=lsp.TextDocumentSyncOptions(
                    open_close=False, save=False, will_save=True
                )
            ),
        ),
        (
            lsp.TEXT_DOCUMENT_WILL_SAVE_WAIT_UNTIL,
            None,
            lsp.ClientCapabilities(
                text_document=lsp.TextDocumentClientCapabilities(
                    synchronization=lsp.TextDocumentSyncClientCapabilities(
                        will_save_wait_until=True
                    )
                )
            ),
            server_capabilities(
                text_document_sync=lsp.TextDocumentSyncOptions(
                    open_close=False, save=False, will_save_wait_until=True
                )
            ),
        ),
        (
            lsp.TEXT_DOCUMENT_DID_OPEN,
            None,
            lsp.ClientCapabilities(),
            server_capabilities(
                text_document_sync=lsp.TextDocumentSyncOptions(
                    open_close=True, save=False
                )
            ),
        ),
        (
            lsp.TEXT_DOCUMENT_DID_CLOSE,
            None,
            lsp.ClientCapabilities(),
            server_capabilities(
                text_document_sync=lsp.TextDocumentSyncOptions(
                    open_close=True, save=False
                )
            ),
        ),
        (
            lsp.TEXT_DOCUMENT_INLAY_HINT,
            None,
            lsp.ClientCapabilities(),
            server_capabilities(
                inlay_hint_provider=lsp.InlayHintOptions(resolve_provider=False),
            ),
        ),
        (
            lsp.WORKSPACE_WILL_CREATE_FILES,
            lsp.FileOperationRegistrationOptions(
                filters=[
                    lsp.FileOperationFilter(
                        pattern=lsp.FileOperationPattern(glob="**/*.py")
                    )
                ]
            ),
            lsp.ClientCapabilities(),
            server_capabilities(),
        ),
        (
            lsp.WORKSPACE_WILL_CREATE_FILES,
            lsp.FileOperationRegistrationOptions(
                filters=[
                    lsp.FileOperationFilter(
                        pattern=lsp.FileOperationPattern(glob="**/*.py")
                    )
                ]
            ),
            lsp.ClientCapabilities(
                workspace=lsp.WorkspaceClientCapabilities(
                    file_operations=lsp.FileOperationClientCapabilities(
                        will_create=True
                    )
                )
            ),
            server_capabilities(
                workspace=lsp.ServerCapabilitiesWorkspaceType(
                    workspace_folders=lsp.WorkspaceFoldersServerCapabilities(
                        supported=True, change_notifications=True
                    ),
                    file_operations=lsp.FileOperationOptions(
                        will_create=lsp.FileOperationRegistrationOptions(
                            filters=[
                                lsp.FileOperationFilter(
                                    pattern=lsp.FileOperationPattern(glob="**/*.py")
                                )
                            ]
                        )
                    ),
                )
            ),
        ),
        (
            lsp.WORKSPACE_DID_CREATE_FILES,
            lsp.FileOperationRegistrationOptions(
                filters=[
                    lsp.FileOperationFilter(
                        pattern=lsp.FileOperationPattern(glob="**/*.py")
                    )
                ]
            ),
            lsp.ClientCapabilities(),
            server_capabilities(),
        ),
        (
            lsp.WORKSPACE_DID_CREATE_FILES,
            lsp.FileOperationRegistrationOptions(
                filters=[
                    lsp.FileOperationFilter(
                        pattern=lsp.FileOperationPattern(glob="**/*.py")
                    )
                ]
            ),
            lsp.ClientCapabilities(
                workspace=lsp.WorkspaceClientCapabilities(
                    file_operations=lsp.FileOperationClientCapabilities(did_create=True)
                )
            ),
            server_capabilities(
                workspace=lsp.ServerCapabilitiesWorkspaceType(
                    workspace_folders=lsp.WorkspaceFoldersServerCapabilities(
                        supported=True, change_notifications=True
                    ),
                    file_operations=lsp.FileOperationOptions(
                        did_create=lsp.FileOperationRegistrationOptions(
                            filters=[
                                lsp.FileOperationFilter(
                                    pattern=lsp.FileOperationPattern(glob="**/*.py")
                                )
                            ]
                        )
                    ),
                )
            ),
        ),
        (
            lsp.WORKSPACE_WILL_DELETE_FILES,
            lsp.FileOperationRegistrationOptions(
                filters=[
                    lsp.FileOperationFilter(
                        pattern=lsp.FileOperationPattern(glob="**/*.py")
                    )
                ]
            ),
            lsp.ClientCapabilities(),
            server_capabilities(),
        ),
        (
            lsp.WORKSPACE_WILL_DELETE_FILES,
            lsp.FileOperationRegistrationOptions(
                filters=[
                    lsp.FileOperationFilter(
                        pattern=lsp.FileOperationPattern(glob="**/*.py")
                    )
                ]
            ),
            lsp.ClientCapabilities(
                workspace=lsp.WorkspaceClientCapabilities(
                    file_operations=lsp.FileOperationClientCapabilities(
                        will_delete=True
                    )
                )
            ),
            server_capabilities(
                workspace=lsp.ServerCapabilitiesWorkspaceType(
                    workspace_folders=lsp.WorkspaceFoldersServerCapabilities(
                        supported=True, change_notifications=True
                    ),
                    file_operations=lsp.FileOperationOptions(
                        will_delete=lsp.FileOperationRegistrationOptions(
                            filters=[
                                lsp.FileOperationFilter(
                                    pattern=lsp.FileOperationPattern(glob="**/*.py")
                                )
                            ]
                        )
                    ),
                )
            ),
        ),
        (
            lsp.WORKSPACE_DID_DELETE_FILES,
            lsp.FileOperationRegistrationOptions(
                filters=[
                    lsp.FileOperationFilter(
                        pattern=lsp.FileOperationPattern(glob="**/*.py")
                    )
                ]
            ),
            lsp.ClientCapabilities(),
            server_capabilities(),
        ),
        (
            lsp.WORKSPACE_DID_DELETE_FILES,
            lsp.FileOperationRegistrationOptions(
                filters=[
                    lsp.FileOperationFilter(
                        pattern=lsp.FileOperationPattern(glob="**/*.py")
                    )
                ]
            ),
            lsp.ClientCapabilities(
                workspace=lsp.WorkspaceClientCapabilities(
                    file_operations=lsp.FileOperationClientCapabilities(did_delete=True)
                )
            ),
            server_capabilities(
                workspace=lsp.ServerCapabilitiesWorkspaceType(
                    workspace_folders=lsp.WorkspaceFoldersServerCapabilities(
                        supported=True, change_notifications=True
                    ),
                    file_operations=lsp.FileOperationOptions(
                        did_delete=lsp.FileOperationRegistrationOptions(
                            filters=[
                                lsp.FileOperationFilter(
                                    pattern=lsp.FileOperationPattern(glob="**/*.py")
                                )
                            ]
                        )
                    ),
                )
            ),
        ),
        (
            lsp.WORKSPACE_WILL_RENAME_FILES,
            lsp.FileOperationRegistrationOptions(
                filters=[
                    lsp.FileOperationFilter(
                        pattern=lsp.FileOperationPattern(glob="**/*.py")
                    )
                ]
            ),
            lsp.ClientCapabilities(),
            server_capabilities(),
        ),
        (
            lsp.WORKSPACE_WILL_RENAME_FILES,
            lsp.FileOperationRegistrationOptions(
                filters=[
                    lsp.FileOperationFilter(
                        pattern=lsp.FileOperationPattern(glob="**/*.py")
                    )
                ]
            ),
            lsp.ClientCapabilities(
                workspace=lsp.WorkspaceClientCapabilities(
                    file_operations=lsp.FileOperationClientCapabilities(
                        will_rename=True
                    )
                )
            ),
            server_capabilities(
                workspace=lsp.ServerCapabilitiesWorkspaceType(
                    workspace_folders=lsp.WorkspaceFoldersServerCapabilities(
                        supported=True, change_notifications=True
                    ),
                    file_operations=lsp.FileOperationOptions(
                        will_rename=lsp.FileOperationRegistrationOptions(
                            filters=[
                                lsp.FileOperationFilter(
                                    pattern=lsp.FileOperationPattern(glob="**/*.py")
                                )
                            ]
                        )
                    ),
                )
            ),
        ),
        (
            lsp.WORKSPACE_DID_RENAME_FILES,
            lsp.FileOperationRegistrationOptions(
                filters=[
                    lsp.FileOperationFilter(
                        pattern=lsp.FileOperationPattern(glob="**/*.py")
                    )
                ]
            ),
            lsp.ClientCapabilities(),
            server_capabilities(),
        ),
        (
            lsp.WORKSPACE_DID_RENAME_FILES,
            lsp.FileOperationRegistrationOptions(
                filters=[
                    lsp.FileOperationFilter(
                        pattern=lsp.FileOperationPattern(glob="**/*.py")
                    )
                ]
            ),
            lsp.ClientCapabilities(
                workspace=lsp.WorkspaceClientCapabilities(
                    file_operations=lsp.FileOperationClientCapabilities(did_rename=True)
                )
            ),
            server_capabilities(
                workspace=lsp.ServerCapabilitiesWorkspaceType(
                    workspace_folders=lsp.WorkspaceFoldersServerCapabilities(
                        supported=True, change_notifications=True
                    ),
                    file_operations=lsp.FileOperationOptions(
                        did_rename=lsp.FileOperationRegistrationOptions(
                            filters=[
                                lsp.FileOperationFilter(
                                    pattern=lsp.FileOperationPattern(glob="**/*.py")
                                )
                            ]
                        )
                    ),
                )
            ),
        ),
        (
            lsp.WORKSPACE_SYMBOL,
            None,
            lsp.ClientCapabilities(),
            server_capabilities(
                workspace_symbol_provider=lsp.WorkspaceSymbolOptions(
                    resolve_provider=False,
                ),
            ),
        ),
        (
            lsp.TEXT_DOCUMENT_DIAGNOSTIC,
            None,
            lsp.ClientCapabilities(),
            server_capabilities(
                diagnostic_provider=lsp.DiagnosticOptions(
                    inter_file_dependencies=False,
                    workspace_diagnostics=False,
                ),
            ),
        ),
        (
            lsp.TEXT_DOCUMENT_DIAGNOSTIC,
            lsp.DiagnosticOptions(
                workspace_diagnostics=True,
                inter_file_dependencies=True,
            ),
            lsp.ClientCapabilities(),
            server_capabilities(
                diagnostic_provider=lsp.DiagnosticOptions(
                    inter_file_dependencies=True,
                    workspace_diagnostics=False,
                ),
            ),
        ),
        (
            lsp.TEXT_DOCUMENT_ON_TYPE_FORMATTING,
            None,
            lsp.ClientCapabilities(),
            server_capabilities(
                document_on_type_formatting_provider=None,
            ),
        ),
        (
            lsp.TEXT_DOCUMENT_ON_TYPE_FORMATTING,
            lsp.DocumentOnTypeFormattingOptions(first_trigger_character=":"),
            lsp.ClientCapabilities(),
            server_capabilities(
                document_on_type_formatting_provider=lsp.DocumentOnTypeFormattingOptions(
                    first_trigger_character=":",
                ),
            ),
        ),
    ],
)
def test_register_feature(
    feature_manager: FeatureManager,
    method: str,
    options: Any,
    capabilities: lsp.ClientCapabilities,
    expected: lsp.ServerCapabilities,
):
    """Ensure that we can register features while specifying their associated
    options and that `pygls` is able to correctly build the corresponding server
    capabilities.

    Parameters
    ----------
    feature_manager
       The feature manager to use

    method
       The method to register the feature handler for.

    options
       The method options to use

    capabilities
       The client capabilities to use when building the server's capabilities.

    expected
       The expected server capabilties we are expecting to see.
    """

    @feature_manager.feature(method, options)
    def _():
        pass

    actual = ServerCapabilitiesBuilder(
        capabilities,
        feature_manager.features.keys(),
        feature_manager.feature_options,
        [],
        None,
        None,
    ).build()

    assert expected == actual


def test_register_inlay_hint_resolve(feature_manager: FeatureManager):
    @feature_manager.feature(lsp.TEXT_DOCUMENT_INLAY_HINT)
    def _():
        pass

    @feature_manager.feature(lsp.INLAY_HINT_RESOLVE)
    def _():
        pass

    expected = server_capabilities(
        inlay_hint_provider=lsp.InlayHintOptions(resolve_provider=True),
    )

    actual = ServerCapabilitiesBuilder(
        lsp.ClientCapabilities(),
        feature_manager.features.keys(),
        feature_manager.feature_options,
        [],
        None,
        None,
    ).build()

    assert expected == actual


def test_register_workspace_symbol_resolve(feature_manager: FeatureManager):
    @feature_manager.feature(lsp.WORKSPACE_SYMBOL)
    def _():
        pass

    @feature_manager.feature(lsp.WORKSPACE_SYMBOL_RESOLVE)
    def _():
        pass

    expected = server_capabilities(
        workspace_symbol_provider=lsp.WorkspaceSymbolOptions(resolve_provider=True),
    )

    actual = ServerCapabilitiesBuilder(
        lsp.ClientCapabilities(),
        feature_manager.features.keys(),
        feature_manager.feature_options,
        [],
        None,
        None,
    ).build()

    assert expected == actual


def test_register_workspace_diagnostics(feature_manager: FeatureManager):
    @feature_manager.feature(
        lsp.TEXT_DOCUMENT_DIAGNOSTIC,
        lsp.DiagnosticOptions(
            identifier="example",
            inter_file_dependencies=False,
            workspace_diagnostics=False,
        ),
    )
    def _():
        pass

    @feature_manager.feature(lsp.WORKSPACE_DIAGNOSTIC)
    def _():
        pass

    expected = server_capabilities(
        diagnostic_provider=lsp.DiagnosticOptions(
            identifier="example",
            inter_file_dependencies=False,
            workspace_diagnostics=True,
        ),
    )

    actual = ServerCapabilitiesBuilder(
        lsp.ClientCapabilities(),
        feature_manager.features.keys(),
        feature_manager.feature_options,
        [],
        None,
        None,
    ).build()

    assert expected == actual
