import textwrap
from typing import List, Optional

import pytest

from debputy.lsp.lsp_debian_control import _lint_debian_control
from debputy.lsp.lsp_debian_control_reference_data import CURRENT_STANDARDS_VERSION
from debputy.packages import DctrlParser
from debputy.plugin.api.feature_set import PluginProvidedFeatureSet
from lint_tests.lint_tutil import (
    group_diagnostics_by_severity,
    requires_levenshtein,
    LintWrapper,
)

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


class DctrlLintWrapper(LintWrapper):

    def __call__(self, lines: List[str]) -> Optional[List["Diagnostic"]]:
        try:
            self.dctrl_lines = lines
            return super().__call__(lines)
        finally:
            self.dctrl_lines = None


@pytest.fixture
def line_linter(
    debputy_plugin_feature_set: PluginProvidedFeatureSet,
    lint_dctrl_parser: DctrlParser,
) -> LintWrapper:
    return DctrlLintWrapper(
        "/nowhere/debian/control",
        _lint_debian_control,
        debputy_plugin_feature_set,
        lint_dctrl_parser,
    )


def test_dctrl_lint(line_linter: LintWrapper) -> None:
    lines = textwrap.dedent(
        """\
    Source: foo
    Some-Other-Field: bar
    Build-Depends: debhelper-compat (= 13)

    Package: foo
    Architecture: all
    # Unknown section
    Section: base
    """
    ).splitlines(keepends=True)

    diagnostics = line_linter(lines)
    by_severity = group_diagnostics_by_severity(diagnostics)
    # This example triggers errors and warnings, but no hint of info
    assert DiagnosticSeverity.Error in by_severity
    assert DiagnosticSeverity.Warning 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 = "Stanza is missing field Standards-Version"
    assert first_error.message == msg
    assert f"{first_error.range}" == "0:0-1:0"

    msg = "Stanza is missing field Maintainer"
    assert second_error.message == msg
    assert f"{second_error.range}" == "0:0-1:0"

    msg = "Stanza is missing field Priority"
    assert third_error.message == msg
    assert f"{third_error.range}" == "4:0-5:0"

    warnings = by_severity[DiagnosticSeverity.Warning]
    assert len(warnings) == 2

    first_warn, second_warn = warnings

    msg = "Stanza is missing field Description"
    assert first_warn.message == msg
    assert f"{first_warn.range}" == "4:0-5:0"

    msg = 'The value "base" is not supported in Section.'
    assert second_warn.message == msg
    assert f"{second_warn.range}" == "7:9-7:13"


@requires_levenshtein
def test_dctrl_lint_typos(line_linter: LintWrapper) -> None:
    lines = textwrap.dedent(
        f"""\
    Source: foo
    Standards-Version: {CURRENT_STANDARDS_VERSION}
    Priority: optional
    Section: devel
    Maintainer: Jane Developer <jane@example.com>
    # Typo
    Build-Dpends: debhelper-compat (= 13)

    Package: foo
    Architecture: all
    Description: Some very interesting synopsis
     A very interesting description
     that spans multiple lines
     .
     Just so be clear, this is for a test.
    """
    ).splitlines(keepends=True)

    diagnostics = line_linter(lines)
    print(diagnostics)
    assert diagnostics and len(diagnostics) == 1
    diag = diagnostics[0]

    msg = 'The "Build-Dpends" looks like a typo of "Build-Depends".'
    assert diag.message == msg
    assert diag.severity == DiagnosticSeverity.Warning
    assert f"{diag.range}" == "6:0-6:12"


@requires_levenshtein
def test_dctrl_lint_mx_value_with_typo(line_linter: LintWrapper) -> None:
    lines = textwrap.dedent(
        f"""\
    Source: foo
    Standards-Version: {CURRENT_STANDARDS_VERSION}
    Priority: optional
    Section: devel
    Maintainer: Jane Developer <jane@example.com>
    Build-Depends: debhelper-compat (= 13)

    Package: foo
    # Typo of `all`
    Architecture: linux-any alle
    Description: Some very interesting synopsis
     A very interesting description
     that spans multiple lines
     .
     Just so be clear, this is for a test.
    """
    ).splitlines(keepends=True)

    diagnostics = line_linter(lines)
    print(diagnostics)
    assert diagnostics is not None
    assert len(diagnostics) == 2
    by_severity = group_diagnostics_by_severity(diagnostics)
    assert DiagnosticSeverity.Error in by_severity
    assert DiagnosticSeverity.Warning in by_severity

    typo_diag = by_severity[DiagnosticSeverity.Warning][0]
    mx_diag = by_severity[DiagnosticSeverity.Error][0]
    mx_msg = 'The value "all" cannot be used with other values.'
    typo_msg = 'It is possible that the value is a typo of "all".'
    assert mx_diag.message == mx_msg
    assert typo_diag.message == typo_msg
    assert f"{mx_diag.range}" == "9:24-9:28"
    assert f"{typo_diag.range}" == "9:24-9:28"


def test_dctrl_lint_mx_value(line_linter: LintWrapper) -> None:
    lines = textwrap.dedent(
        f"""\
    Source: foo
    Standards-Version: {CURRENT_STANDARDS_VERSION}
    Priority: optional
    Section: devel
    Maintainer: Jane Developer <jane@example.com>
    Build-Depends: debhelper-compat (= 13)

    Package: foo
    Architecture: all linux-any
    Description: Some very interesting synopsis
     A very interesting description
     that spans multiple lines
     .
     Just so be clear, this is for a test.
    """
    ).splitlines(keepends=True)

    diagnostics = line_linter(lines)
    print(diagnostics)
    assert diagnostics and len(diagnostics) == 1
    diag = diagnostics[0]

    msg = 'The value "all" cannot be used with other values.'
    assert diag.message == msg
    assert diag.severity == DiagnosticSeverity.Error
    assert f"{diag.range}" == "8:14-8:17"

    lines = textwrap.dedent(
        f"""\
    Source: foo
    Standards-Version: {CURRENT_STANDARDS_VERSION}
    Priority: optional
    Section: devel
    Maintainer: Jane Developer <jane@example.com>
    Build-Depends: debhelper-compat (= 13)

    Package: foo
    Architecture: linux-any any
    Description: Some very interesting synopsis
     A very interesting description
     that spans multiple lines
     .
     Just so be clear, this is for a test.
    """
    ).splitlines(keepends=True)

    diagnostics = line_linter(lines)
    print(diagnostics)
    assert diagnostics and len(diagnostics) == 1
    diag = diagnostics[0]

    msg = 'The value "any" cannot be used with other values.'
    assert diag.message == msg
    assert diag.severity == DiagnosticSeverity.Error
    assert f"{diag.range}" == "8:24-8:27"


def test_dctrl_lint_dup_sep(line_linter: LintWrapper) -> None:
    lines = textwrap.dedent(
        f"""\
    Source: foo
    Section: devel
    Priority: optional
    Standards-Version: {CURRENT_STANDARDS_VERSION}
    Maintainer: Jane Developer <jane@example.com>
    Build-Depends: debhelper-compat (= 13)

    Package: foo
    Architecture: all
    Depends: bar,
     , baz
    Description: Some very interesting synopsis
     A very interesting description
     that spans multiple lines
     .
     Just so be clear, this is for a test.
    """
    ).splitlines(keepends=True)

    diagnostics = line_linter(lines)
    print(diagnostics)
    assert diagnostics and len(diagnostics) == 1
    error = diagnostics[0]

    msg = "Duplicate separator"
    assert error.message == msg
    assert f"{error.range}" == "10:1-10:2"
    assert error.severity == DiagnosticSeverity.Error


def test_dctrl_lint_ma(line_linter: LintWrapper) -> None:
    lines = textwrap.dedent(
        f"""\
    Source: foo
    Section: devel
    Priority: optional
    Standards-Version: {CURRENT_STANDARDS_VERSION}
    Maintainer: Jane Developer <jane@example.com>
    Build-Depends: debhelper-compat (= 13)

    Package: foo
    Architecture: all
    Multi-Arch: same
    Depends: bar, baz
    Description: Some very interesting synopsis
     A very interesting description
     that spans multiple lines
     .
     Just so be clear, this is for a test.
    """
    ).splitlines(keepends=True)

    diagnostics = line_linter(lines)
    print(diagnostics)
    assert diagnostics and len(diagnostics) == 1
    error = diagnostics[0]

    msg = "Multi-Arch: same is not valid for Architecture: all packages. Maybe you want foreign?"
    assert error.message == msg
    assert f"{error.range}" == "9:12-9:16"
    assert error.severity == DiagnosticSeverity.Error


def test_dctrl_lint_udeb(line_linter: LintWrapper) -> None:
    lines = textwrap.dedent(
        f"""\
    Source: foo
    Section: devel
    Priority: optional
    Standards-Version: {CURRENT_STANDARDS_VERSION}
    Maintainer: Jane Developer <jane@example.com>
    Build-Depends: debhelper-compat (= 13)

    Package: foo
    Architecture: all
    XB-Installer-Menu-Item: 1234
    Depends: bar, baz
    Description: Some very interesting synopsis
     A very interesting description
     that spans multiple lines
     .
     Just so be clear, this is for a test.

    Package: bar-udeb
    Architecture: all
    Section: debian-installer
    Package-Type: udeb
    XB-Installer-Menu-Item: golf
    Description: Some very interesting synopsis
     A very interesting description
     that spans multiple lines
     .
     Just so be clear, this is for a test.
    """
    ).splitlines(keepends=True)

    diagnostics = line_linter(lines)
    print(diagnostics)
    assert diagnostics and len(diagnostics) == 2
    first, second = diagnostics

    msg = "The XB-Installer-Menu-Item field is only applicable to udeb packages (`Package-Type: udeb`)"
    assert first.message == msg
    assert f"{first.range}" == "9:0-9:22"
    assert first.severity == DiagnosticSeverity.Warning

    msg = r'The value "golf" does not match the regex ^[1-9]\d{3,4}$.'
    assert second.message == msg
    assert f"{second.range}" == "21:24-21:28"
    assert second.severity == DiagnosticSeverity.Error


def test_dctrl_lint_arch_only_fields(line_linter: LintWrapper) -> None:
    lines = textwrap.dedent(
        f"""\
    Source: foo
    Section: devel
    Priority: optional
    Standards-Version: {CURRENT_STANDARDS_VERSION}
    Maintainer: Jane Developer <jane@example.com>
    Build-Depends: debhelper-compat (= 13)

    Package: foo
    Architecture: all
    X-DH-Build-For-Type: target
    Depends: bar, baz
    Description: Some very interesting synopsis
     A very interesting description
     that spans multiple lines
     .
     Just so be clear, this is for a test.
    """
    ).splitlines(keepends=True)

    diagnostics = line_linter(lines)
    print(diagnostics)
    assert diagnostics and len(diagnostics) == 1
    issue = diagnostics[0]

    msg = "The X-DH-Build-For-Type field is not applicable to arch:all packages (`Architecture: all`)"
    assert issue.message == msg
    assert f"{issue.range}" == "9:0-9:19"
    assert issue.severity == DiagnosticSeverity.Warning


def test_dctrl_lint_sv(line_linter: LintWrapper) -> None:
    lines = textwrap.dedent(
        f"""\
    Source: foo
    Section: devel
    Priority: optional
    Standards-Version: 4.6.2
    Maintainer: Jane Developer <jane@example.com>
    Build-Depends: debhelper-compat (= 13)

    Package: foo
    Architecture: all
    Depends: bar, baz
    Description: Some very interesting synopsis
     A very interesting description
     that spans multiple lines
     .
     Just so be clear, this is for a test.
    """
    ).splitlines(keepends=True)

    diagnostics = line_linter(lines)
    print(diagnostics)
    assert diagnostics and len(diagnostics) == 1
    issue = diagnostics[0]

    msg = f"Latest Standards-Version is {CURRENT_STANDARDS_VERSION}"
    assert issue.message == msg
    assert f"{issue.range}" == "3:19-3:24"
    assert issue.severity == DiagnosticSeverity.Information

    lines = textwrap.dedent(
        f"""\
    Source: foo
    Section: devel
    Priority: optional
    Standards-Version: Golf
    Maintainer: Jane Developer <jane@example.com>
    Build-Depends: debhelper-compat (= 13)

    Package: foo
    Architecture: all
    Depends: bar, baz
    Description: Some very interesting synopsis
     A very interesting description
     that spans multiple lines
     .
     Just so be clear, this is for a test.
    """
    ).splitlines(keepends=True)

    diagnostics = line_linter(lines)
    print(diagnostics)
    assert diagnostics and len(diagnostics) == 1
    issue = diagnostics[0]

    msg = f'Not a valid version. Current version is "{CURRENT_STANDARDS_VERSION}"'
    assert issue.message == msg
    assert f"{issue.range}" == "3:19-3:23"
    assert issue.severity == DiagnosticSeverity.Warning

    lines = textwrap.dedent(
        f"""\
    Source: foo
    Section: devel
    Priority: optional
    Standards-Version: {CURRENT_STANDARDS_VERSION}.0
    Maintainer: Jane Developer <jane@example.com>
    Build-Depends: debhelper-compat (= 13)

    Package: foo
    Architecture: all
    Depends: bar, baz
    Description: Some very interesting synopsis
     A very interesting description
     that spans multiple lines
     .
     Just so be clear, this is for a test.
    """
    ).splitlines(keepends=True)

    diagnostics = line_linter(lines)
    print(diagnostics)
    assert diagnostics and len(diagnostics) == 1
    issue = diagnostics[0]

    msg = "Unnecessary version segment. This part of the version is only used for editorial changes"
    assert issue.message == msg
    assert f"{issue.range}" == "3:24-3:26"
    assert issue.severity == DiagnosticSeverity.Information


def test_dctrl_lint_sv_udeb_only(line_linter: LintWrapper) -> None:
    lines = textwrap.dedent(
        f"""\
    Source: foo
    Section: devel
    Priority: optional
    Maintainer: Jane Developer <jane@example.com>
    Build-Depends: debhelper-compat (= 13)

    Package: foo-udeb
    Architecture: all
    Package-Type: udeb
    Section: debian-installer
    Depends: bar, baz
    Description: Some very interesting synopsis
     A very interesting description
     that spans multiple lines
     .
     Just so be clear, this is for a test.
    """
    ).splitlines(keepends=True)

    diagnostics = line_linter(lines)
    print(diagnostics)
    assert not diagnostics


def test_dctrl_lint_udeb_menu_iten(line_linter: LintWrapper) -> None:
    lines = textwrap.dedent(
        """\
    Source: foo
    Section: devel
    Priority: optional
    Maintainer: Jane Developer <jane@example.com>
    Build-Depends: debhelper-compat (= 13)

    Package: foo-udeb
    Architecture: all
    Package-Type: udeb
    Section: debian-installer
    XB-Installer-Menu-Item: 12345
    Description: Some very interesting synopsis
     A very interesting description
     that spans multiple lines
     .
     Just so be clear, this is for a test.

    Package: bar-udeb
    Architecture: all
    Package-Type: udeb
    Section: debian-installer
    XB-Installer-Menu-Item: ${foo}
    Description: Some very interesting synopsis
     A very interesting description
     that spans multiple lines
     .
     Just so be clear, this is for a test.
    """
    ).splitlines(keepends=True)

    diagnostics = line_linter(lines)
    print(diagnostics)
    assert not diagnostics


def test_dctrl_lint_multiple_vcs(line_linter: LintWrapper) -> None:
    lines = textwrap.dedent(
        f"""\
    Source: foo
    Section: devel
    Priority: optional
    Standards-Version: {CURRENT_STANDARDS_VERSION}
    Maintainer: Jane Developer <jane@example.com>
    Build-Depends: debhelper-compat (= 13)
    Vcs-Git: https://salsa.debian.org/debian/foo
    Vcs-Svn: https://svn.debian.org/debian/foo
    Vcs-Browser: https://salsa.debian.org/debian/foo

    Package: foo
    Architecture: all
    Depends: bar, baz
    Description: Some very interesting synopsis
     A very interesting description
     that spans multiple lines
     .
     Just so be clear, this is for a test.
    """
    ).splitlines(keepends=True)

    diagnostics = line_linter(lines)
    print(diagnostics)
    assert diagnostics and len(diagnostics) == 2
    first_issue, second_issue = diagnostics

    msg = f'Multiple Version Control fields defined ("Vcs-Git")'
    assert first_issue.message == msg
    assert f"{first_issue.range}" == "6:0-7:0"
    assert first_issue.severity == DiagnosticSeverity.Warning

    msg = f'Multiple Version Control fields defined ("Vcs-Svn")'
    assert second_issue.message == msg
    assert f"{second_issue.range}" == "7:0-8:0"
    assert second_issue.severity == DiagnosticSeverity.Warning


def test_dctrl_lint_synopsis_empty(line_linter: LintWrapper) -> None:
    lines = textwrap.dedent(
        f"""\
    Source: foo
    Section: devel
    Priority: optional
    Standards-Version: {CURRENT_STANDARDS_VERSION}
    Maintainer: Jane Developer <jane@example.com>
    Build-Depends: debhelper-compat (= 13)

    Package: foo
    Architecture: all
    Depends: bar, baz
    Description:
     A very interesting description
     without a synopsis
     .
     Just so be clear, this is for a test.
    """
    ).splitlines(keepends=True)

    diagnostics = line_linter(lines)
    print(diagnostics)
    assert diagnostics and len(diagnostics) == 1
    issue = diagnostics[0]

    msg = "Package synopsis is missing"
    assert issue.message == msg
    assert f"{issue.range}" == "10:0-10:11"
    assert issue.severity == DiagnosticSeverity.Warning


def test_dctrl_lint_synopsis_basis(line_linter: LintWrapper) -> None:
    lines = textwrap.dedent(
        f"""\
    Source: foo
    Section: devel
    Priority: optional
    Standards-Version: {CURRENT_STANDARDS_VERSION}
    Maintainer: Jane Developer <jane@example.com>
    Build-Depends: debhelper-compat (= 13)

    Package: foo
    Architecture: all
    Depends: bar, baz
    Description: The synopsis is not the best because it starts with an article and also the synopsis goes on and on
     A very interesting description
     with a poor synopsis
     .
     Just so be clear, this is for a test.
    """
    ).splitlines(keepends=True)

    diagnostics = line_linter(lines)
    print(diagnostics)
    assert diagnostics and len(diagnostics) == 2
    first_issue, second_issue = diagnostics

    msg = "Package synopsis starts with an article (a/an/the)."
    assert first_issue.message == msg
    assert f"{first_issue.range}" == "10:13-10:16"
    assert first_issue.severity == DiagnosticSeverity.Warning

    msg = "Package synopsis is too long."
    assert second_issue.message == msg
    assert f"{second_issue.range}" == "10:92-10:112"
    assert second_issue.severity == DiagnosticSeverity.Warning


def test_dctrl_lint_synopsis_template(line_linter: LintWrapper) -> None:
    lines = textwrap.dedent(
        f"""\
    Source: foo
    Section: devel
    Priority: optional
    Standards-Version: {CURRENT_STANDARDS_VERSION}
    Maintainer: Jane Developer <jane@example.com>
    Build-Depends: debhelper-compat (= 13)

    Package: foo
    Architecture: all
    Depends: bar, baz
    Description: <insert up to 60 chars description>
     A very interesting description
     with a poor synopsis
     .
     Just so be clear, this is for a test.
    """
    ).splitlines(keepends=True)

    diagnostics = line_linter(lines)
    print(diagnostics)
    assert diagnostics and len(diagnostics) == 1
    issue = diagnostics[0]

    msg = "Package synopsis is a placeholder"
    assert issue.message == msg
    assert f"{issue.range}" == "10:13-10:48"
    assert issue.severity == DiagnosticSeverity.Warning


def test_dctrl_lint_synopsis_too_short(line_linter: LintWrapper) -> None:
    lines = textwrap.dedent(
        f"""\
    Source: foo
    Section: devel
    Priority: optional
    Standards-Version: {CURRENT_STANDARDS_VERSION}
    Maintainer: Jane Developer <jane@example.com>
    Build-Depends: debhelper-compat (= 13)

    Package: foo
    Architecture: all
    Depends: bar, baz
    Description: short
     A very interesting description
     with a poor synopsis
     .
     Just so be clear, this is for a test.
    """
    ).splitlines(keepends=True)

    diagnostics = line_linter(lines)
    print(diagnostics)
    assert diagnostics and len(diagnostics) == 1
    issue = diagnostics[0]

    msg = "Package synopsis is too short"
    assert issue.message == msg
    assert f"{issue.range}" == "10:13-10:18"
    assert issue.severity == DiagnosticSeverity.Warning
