# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

from typing import Optional, Union

import attrs
import hamcrest
import pytest
from cattrs.errors import ClassValidationError

from lsprotocol import converters as cv
from lsprotocol import types as lsp


def test_simple():
    """Ensure that simple LSP types are serializable."""
    data = {
        "range": {
            "start": {"line": 0, "character": 0},
            "end": {"line": 0, "character": 0},
        },
        "message": "Missing module docstring",
        "severity": 3,
        "code": "C0114:missing-module-docstring",
        "source": "my_lint",
    }
    converter = cv.get_converter()
    obj = converter.structure(data, lsp.Diagnostic)
    hamcrest.assert_that(obj, hamcrest.instance_of(lsp.Diagnostic))


def test_numeric_validation():
    """Ensure that out of range numbers raise exception."""
    data = {"line": -1, "character": 0}
    converter = cv.get_converter()
    with pytest.raises((ClassValidationError, ValueError)):
        converter.structure(data, lsp.Position)


def test_forward_refs():
    """Test that forward references are handled correctly by cattrs converter."""
    data = {
        "uri": "something.py",
        "diagnostics": [
            {
                "range": {
                    "start": {"line": 0, "character": 0},
                    "end": {"line": 0, "character": 0},
                },
                "message": "Missing module docstring",
                "severity": 3,
                "code": "C0114:missing-module-docstring",
                "source": "my_lint",
            },
            {
                "range": {
                    "start": {"line": 2, "character": 6},
                    "end": {
                        "line": 2,
                        "character": 7,
                    },
                },
                "message": "Undefined variable 'x'",
                "severity": 1,
                "code": "E0602:undefined-variable",
                "source": "my_lint",
            },
            {
                "range": {
                    "start": {"line": 0, "character": 0},
                    "end": {
                        "line": 0,
                        "character": 10,
                    },
                },
                "message": "Unused import sys",
                "severity": 2,
                "code": "W0611:unused-import",
                "source": "my_lint",
            },
        ],
    }
    converter = cv.get_converter()
    obj = converter.structure(data, lsp.PublishDiagnosticsParams)
    hamcrest.assert_that(obj, hamcrest.instance_of(lsp.PublishDiagnosticsParams))


@pytest.mark.parametrize(
    "data",
    [
        {},  # No properties provided
        {"documentSelector": None},
        {"documentSelector": []},
        {"documentSelector": [{"pattern": "something/**"}]},
        {"documentSelector": [{"language": "python"}]},
        {"documentSelector": [{"scheme": "file"}]},
        {"documentSelector": [{"notebook": "jupyter"}]},
        {"documentSelector": [{"language": "python"}]},
        {"documentSelector": [{"notebook": {"notebookType": "jupyter-notebook"}}]},
        {"documentSelector": [{"notebook": {"scheme": "file"}}]},
        {"documentSelector": [{"notebook": {"pattern": "something/**"}}]},
        {
            "documentSelector": [
                {"pattern": "something/**"},
                {"language": "python"},
                {"scheme": "file"},
                {"scheme": "untitled", "language": "python"},
                {"notebook": {"pattern": "something/**"}},
                {"notebook": {"scheme": "untitled"}},
                {"notebook": {"notebookType": "jupyter-notebook"}},
                {
                    "notebook": {"notebookType": "jupyter-notebook"},
                    "language": "jupyter",
                },
            ]
        },
    ],
)
def test_union_with_complex_type(data):
    """Ensure types with multiple possible resolutions are handled correctly."""
    converter = cv.get_converter()
    obj = converter.structure(data, lsp.TextDocumentRegistrationOptions)
    hamcrest.assert_that(obj, hamcrest.instance_of(lsp.TextDocumentRegistrationOptions))


def test_keyword_field():
    """Ensure that fields same names as keywords are handled correctly."""
    data = {
        "from": {
            "name": "something",
            "kind": 5,
            "uri": "something.py",
            "range": {
                "start": {"line": 0, "character": 0},
                "end": {
                    "line": 0,
                    "character": 10,
                },
            },
            "selectionRange": {
                "start": {"line": 0, "character": 2},
                "end": {
                    "line": 0,
                    "character": 8,
                },
            },
            "data": {"something": "some other"},
        },
        "fromRanges": [
            {
                "start": {"line": 0, "character": 0},
                "end": {
                    "line": 0,
                    "character": 10,
                },
            },
            {
                "start": {"line": 12, "character": 0},
                "end": {
                    "line": 13,
                    "character": 0,
                },
            },
        ],
    }

    converter = cv.get_converter()
    obj = converter.structure(data, lsp.CallHierarchyIncomingCall)
    hamcrest.assert_that(obj, hamcrest.instance_of(lsp.CallHierarchyIncomingCall))
    rev = converter.unstructure(obj, lsp.CallHierarchyIncomingCall)
    hamcrest.assert_that(rev, hamcrest.is_(data))


@pytest.mark.parametrize(
    "data",
    [
        {"settings": None},
        {"settings": 100000},
        {"settings": 1.23456},
        {"settings": True},
        {"settings": "something"},
        {"settings": {"something": "something"}},
        {"settings": []},
        {"settings": [None, None]},
        {"settings": [None, 1, 1.23, True]},
    ],
)
def test_LSPAny(data):
    """Ensure that broad primitive and custom type alias is handled correctly."""
    converter = cv.get_converter()
    obj = converter.structure(data, lsp.DidChangeConfigurationParams)
    hamcrest.assert_that(obj, hamcrest.instance_of(lsp.DidChangeConfigurationParams))
    hamcrest.assert_that(
        converter.unstructure(obj, lsp.DidChangeConfigurationParams),
        hamcrest.is_(data),
    )


@pytest.mark.parametrize(
    "data",
    [
        {"label": "hi"},
        {"label": [0, 42]},
    ],
)
def test_ParameterInformation(data):
    converter = cv.get_converter()
    obj = converter.structure(data, lsp.ParameterInformation)
    hamcrest.assert_that(obj, hamcrest.instance_of(lsp.ParameterInformation))
    hamcrest.assert_that(
        converter.unstructure(obj, lsp.ParameterInformation),
        hamcrest.is_(data),
    )


def test_completion_item():
    data = dict(label="example", documentation="This is documented")
    converter = cv.get_converter()
    obj = converter.structure(data, lsp.CompletionItem)
    hamcrest.assert_that(obj, hamcrest.instance_of(lsp.CompletionItem))
    hamcrest.assert_that(
        converter.unstructure(obj, lsp.CompletionItem),
        hamcrest.is_(data),
    )


def test_notebook_change_event():
    data = {
        "notebookDocument": {
            "uri": "untitled:Untitled-1.ipynb?jupyter-notebook",
            "notebookType": "jupyter-notebook",
            "version": 0,
            "cells": [
                {
                    "kind": 2,
                    "document": "vscode-notebook-cell:Untitled-1.ipynb?jupyter-notebook#W0sdW50aXRsZWQ%3D",
                    "metadata": {"custom": {"metadata": {}}},
                }
            ],
            "metadata": {
                "custom": {
                    "cells": [],
                    "metadata": {
                        "orig_nbformat": 4,
                        "language_info": {"name": "python"},
                    },
                },
                "indentAmount": " ",
            },
        },
        "cellTextDocuments": [
            {
                "uri": "vscode-notebook-cell:Untitled-1.ipynb?jupyter-notebook#W0sdW50aXRsZWQ%3D",
                "languageId": "python",
                "version": 1,
                "text": "",
            }
        ],
    }

    converter = cv.get_converter()
    obj = converter.structure(data, lsp.DidOpenNotebookDocumentParams)
    hamcrest.assert_that(obj, hamcrest.instance_of(lsp.DidOpenNotebookDocumentParams))
    hamcrest.assert_that(
        converter.unstructure(obj, lsp.DidOpenNotebookDocumentParams),
        hamcrest.is_(data),
    )


def test_notebook_sync_options():
    data = {"notebookSelector": [{"cells": [{"language": "python"}]}]}

    converter = cv.get_converter()
    obj = converter.structure(data, lsp.NotebookDocumentSyncOptions)
    hamcrest.assert_that(obj, hamcrest.instance_of(lsp.NotebookDocumentSyncOptions))
    hamcrest.assert_that(
        converter.unstructure(obj, lsp.NotebookDocumentSyncOptions),
        hamcrest.is_(data),
    )


@attrs.define
class TestPosEncoding:
    """Defines the capabilities provided by a language
    server."""

    position_encoding: Optional[Union[lsp.PositionEncodingKind, str]] = attrs.field(
        default=None
    )


@pytest.mark.parametrize("e", [None, "utf-8", "utf-16", "utf-32", "something"])
def test_position_encoding_kind(e):
    data = {"positionEncoding": e}
    converter = cv.get_converter()
    obj = converter.structure(data, TestPosEncoding)
    hamcrest.assert_that(obj, hamcrest.instance_of(TestPosEncoding))

    if e is None:
        hamcrest.assert_that(
            converter.unstructure(obj, TestPosEncoding), hamcrest.is_({})
        )
    else:
        hamcrest.assert_that(
            converter.unstructure(obj, TestPosEncoding), hamcrest.is_(data)
        )
