import textwrap
from typing import List, Optional, Callable

import pytest

from debputy.lsp.lsp_debian_debputy_manifest import _lint_debian_debputy_manifest
from lint_tests.lint_tutil import (
    run_linter,
    group_diagnostics_by_severity,
    requires_levenshtein,
)

try:
    from lsprotocol.types import Diagnostic, DiagnosticSeverity
except ImportError:
    pass


TestLinter = Callable[[List[str]], Optional[List["Diagnostic"]]]


@pytest.fixture
def line_linter() -> TestLinter:
    path = "/nowhere/debian/debputy.manifest"

    def _linter(lines: List[str]) -> Optional[List["Diagnostic"]]:
        return run_linter(path, lines, _lint_debian_debputy_manifest)

    return _linter


def test_debputy_lint_unknown_keys(line_linter: TestLinter) -> None:
    lines = textwrap.dedent(
        """\
    manifest-version: 0.1
    installations:
    - install-something:
        sources:
        - abc
        - def
    - install-docs:
        source: foo
        puff: true   # Unknown keyword (assuming install-docs)
        when:
          negated: cross-compiling
    - install-docs:
        source: bar
        when: ross-compiling  # Typo of "cross-compiling"; FIXME not caught
    packages:
      foo:
        blah: qwe    # Unknown keyword
    """
    ).splitlines(keepends=True)

    diagnostics = line_linter(lines)
    by_severity = group_diagnostics_by_severity(diagnostics)
    # This example triggers errors only
    assert DiagnosticSeverity.Error in by_severity

    assert DiagnosticSeverity.Warning not in by_severity
    assert DiagnosticSeverity.Hint not in by_severity
    assert DiagnosticSeverity.Information not in by_severity

    errors = by_severity[DiagnosticSeverity.Error]
    print(errors)
    assert len(errors) == 4

    first_error, second_error, third_error, fourth_error = errors

    msg = 'Unknown or unsupported key "install-something".'
    assert first_error.message == msg
    assert f"{first_error.range}" == "2:2-2:19"

    msg = 'Unknown or unsupported key "puff".'
    assert second_error.message == msg
    assert f"{second_error.range}" == "8:4-8:8"

    msg = 'Unknown or unsupported key "negated".'
    assert third_error.message == msg
    assert f"{third_error.range}" == "10:6-10:13"

    msg = 'Unknown or unsupported key "blah".'
    assert fourth_error.message == msg
    assert f"{fourth_error.range}" == "16:4-16:8"


@requires_levenshtein
def test_debputy_lint_unknown_keys_spelling(line_linter: TestLinter) -> None:
    lines = textwrap.dedent(
        """\
    manifest-version: 0.1
    installations:
    - install-dcoss:  # typo
        sources:
        - abc
        - def
        puff: true   # Unknown keyword (assuming install-docs)
        when:
          nut: cross-compiling  # Typo of "not"
    - install-docs:
        source: bar
        when: ross-compiling  # Typo of "cross-compiling"; FIXME not caught
    """
    ).splitlines(keepends=True)

    diagnostics = line_linter(lines)
    by_severity = group_diagnostics_by_severity(diagnostics)
    # This example triggers errors only
    assert DiagnosticSeverity.Error in by_severity

    assert DiagnosticSeverity.Warning not in by_severity
    assert DiagnosticSeverity.Hint not in by_severity
    assert DiagnosticSeverity.Information not in by_severity

    errors = by_severity[DiagnosticSeverity.Error]
    print(errors)
    assert len(errors) == 3

    first_error, second_error, third_error = errors

    msg = 'Unknown or unsupported key "install-dcoss". It looks like a typo of "install-docs".'
    assert first_error.message == msg
    assert f"{first_error.range}" == "2:2-2:15"

    msg = 'Unknown or unsupported key "puff".'
    assert second_error.message == msg
    assert f"{second_error.range}" == "6:4-6:8"

    msg = 'Unknown or unsupported key "nut". It looks like a typo of "not".'
    assert third_error.message == msg
    assert f"{third_error.range}" == "8:6-8:9"


def test_debputy_lint_conflicting_keys(line_linter: TestLinter) -> None:
    lines = textwrap.dedent(
        """\
    manifest-version: 0.1
    installations:
    - install-docs:
        sources:
        - foo
        - bar
        as: baz      # Conflicts with "sources" (#85)
    - install:
        source: foo
        sources:     # Conflicts with "source" (#85)
        - bar
        - baz
    """
    ).splitlines(keepends=True)

    diagnostics = line_linter(lines)
    by_severity = group_diagnostics_by_severity(diagnostics)
    # This example triggers errors only
    assert DiagnosticSeverity.Error in by_severity

    assert DiagnosticSeverity.Warning not in by_severity
    assert DiagnosticSeverity.Hint not in by_severity
    assert DiagnosticSeverity.Information not in by_severity

    errors = by_severity[DiagnosticSeverity.Error]
    print(errors)
    assert len(errors) == 4

    first_error, second_error, third_error, fourth_error = errors

    msg = 'The "sources" cannot be used with "as".'
    assert first_error.message == msg
    assert f"{first_error.range}" == "3:4-3:11"

    msg = 'The "as" cannot be used with "sources".'
    assert second_error.message == msg
    assert f"{second_error.range}" == "6:4-6:6"

    msg = 'The "source" cannot be used with "sources".'
    assert third_error.message == msg
    assert f"{third_error.range}" == "8:4-8:10"

    msg = 'The "sources" cannot be used with "source".'
    assert fourth_error.message == msg
    assert f"{fourth_error.range}" == "9:4-9:11"
