import textwrap

import pytest

from debputy.lsp.languages.lsp_debian_debputy_manifest import (
    _lint_debian_debputy_manifest,
)
from debputy.packages import DctrlParser
from debputy.plugin.api.feature_set import PluginProvidedFeatureSet
from lint_tests.lint_tutil import (
    requires_levenshtein,
    LintWrapper,
    group_diagnostics_by_severity,
    diag_range_to_text,
)

from debputy.lsprotocol.types import DiagnosticSeverity


@pytest.fixture
def line_linter(
    debputy_plugin_feature_set: PluginProvidedFeatureSet,
    lint_dctrl_parser: DctrlParser,
) -> LintWrapper:
    return LintWrapper(
        "/nowhere/debian/debputy.manifest",
        _lint_debian_debputy_manifest,
        debputy_plugin_feature_set,
        lint_dctrl_parser,
    )


def test_debputy_lint_syntax_error_a(line_linter: LintWrapper) -> None:
    lines = textwrap.dedent(
        """\
    manifest-version: '0.1'
    foo
    """
    ).splitlines(keepends=True)

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

    # We do not check the error message here, since it is directly from the YAML library
    # and therefore an implementation detail.
    assert f"{issue.range}" == "1:0-1:3"
    assert issue.severity == DiagnosticSeverity.Error
    assert diag_range_to_text(lines, issue.range) == "foo"


def test_debputy_lint_syntax_error_b(line_linter: LintWrapper) -> None:
    # Using `§` as "end of line" marker
    lines = (
        textwrap.dedent(
            """\
    manifest-version: '0.1'
    packages:
      dh-\t§
    """
        )
        .replace("§", "")
        .splitlines(keepends=True)
    )

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

    # We do not check the error message here, since it is directly from the YAML library
    # and therefore an implementation detail.
    assert f"{issue.range}" == "2:5-2:6"
    assert issue.severity == DiagnosticSeverity.Error
    assert diag_range_to_text(lines, issue.range) == "\t"


def test_debputy_lint_invalid_package_name_crash(line_linter: LintWrapper) -> None:
    lines = textwrap.dedent(
        """\
    manifest-version: '0.1'
    packages:
      :
    """
    ).splitlines(keepends=True)

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

    # We do not check the error message here, since it is directly from the YAML library
    # and therefore an implementation detail.
    assert f"{issue.range}" == "2:2-2:3"
    assert issue.severity == DiagnosticSeverity.Error
    assert diag_range_to_text(lines, issue.range) == ":"


def test_debputy_lint_unknown_keys(line_linter: LintWrapper) -> 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
        # Typo of `cross-compiling` that is too far for the typo detection to catch it.
        when: ross-compile
    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) == 5

    first_error, second_error, third_error, fourth_error, fifth_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 value "ross-compile".'
    assert fourth_error.message == msg
    assert f"{fourth_error.range}" == "14:10-14:22"

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


def test_debputy_lint_null_keys(line_linter: LintWrapper) -> None:
    lines = textwrap.dedent(
        """\
    manifest-version: '0.1'
    installations:
    - install-docs:
        :
        - GETTING-STARTED-WITH-dh-debputy.md
        - MANIFEST-FORMAT.md
        - MIGRATING-A-DH-PLUGIN.md
    """
    ).splitlines(keepends=True)

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

    msg = "Missing key"
    assert issue.message == msg
    assert f"{issue.range}" == "3:4-3:5"
    assert issue.severity == DiagnosticSeverity.Error


@requires_levenshtein
def test_debputy_lint_unknown_keys_spelling(line_linter: LintWrapper) -> 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"
    """
    ).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-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"

    msg = 'Unknown or unsupported value "ross-compiling". It looks like a typo of "cross-compiling".'
    assert fourth_error.message == msg
    assert f"{fourth_error.range}" == "11:10-11:24"


@requires_levenshtein
def test_debputy_lint_crash_on_typo_fix(line_linter: LintWrapper) -> None:
    lines = textwrap.dedent(
        """\
    manifest-version: '0.1'
    packages:
      foo:
        conffile-management:
        - rename:
            sources: "/etc/foo"
    """
    ).splitlines(keepends=True)
    line_linter.dctrl_lines = textwrap.dedent(
        """\
    Source: foo
    Build-Depends: dh-sequence-zz-debputy,

    Package: foo
    """
    ).splitlines(keepends=True)

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

    msg = 'Unknown or unsupported key "sources". It looks like a typo of "source".'
    assert issue.message == msg
    assert f"{issue.range}" == "5:8-5:15"


def test_debputy_lint_check_package_names(line_linter: LintWrapper) -> None:
    lines = textwrap.dedent(
        """\
    manifest-version: '0.1'
    packages:
        unknown-package:
            binary-version: '1:{{DEB_VERSION_UPSTREAM_REVISION}}'
    """
    ).splitlines(keepends=True)

    line_linter.dctrl_lines = None
    diagnostics = line_linter(lines)
    print(diagnostics)
    # Does nothing without a control file
    assert not diagnostics

    line_linter.dctrl_lines = textwrap.dedent(
        """\
    Source: foo

    Package: foo
    """
    ).splitlines(keepends=True)

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

    msg = 'Unknown package "unknown-package".'
    assert diag.message == msg
    assert f"{diag.range}" == "2:4-2:19"


def test_debputy_lint_integration_mode(line_linter: LintWrapper) -> None:
    lines = textwrap.dedent(
        """\
    manifest-version: '0.1'
    installations: []
    packages:
        foo:
            services:
            - service: foo
    """
    ).splitlines(keepends=True)
    line_linter.dctrl_lines = textwrap.dedent(
        """\
    Source: foo
    Build-Depends: dh-sequence-zz-debputy-rrr,

    Package: foo
    """
    ).splitlines(keepends=True)

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

    msg = 'Feature "installations" not supported in integration mode dh-sequence-zz-debputy-rrr'
    assert first_issue.message == msg
    assert f"{first_issue.range}" == "1:0-1:13"
    assert first_issue.severity == DiagnosticSeverity.Error

    msg = 'Feature "services" not supported in integration mode dh-sequence-zz-debputy-rrr'
    assert second_issue.message == msg
    assert f"{second_issue.range}" == "4:8-4:16"
    assert second_issue.severity == DiagnosticSeverity.Error

    # Changing the integration mode should fix both
    line_linter.dctrl_lines = textwrap.dedent(
        """\
    Source: foo
    Build-Depends: dh-sequence-zz-debputy,

    Package: foo
    """
    ).splitlines(keepends=True)
    diagnostics = line_linter(lines)
    assert not diagnostics


def test_debputy_lint_attr_value_checks(line_linter: LintWrapper) -> None:
    lines = textwrap.dedent(
        """\
    manifest-version: '0.1'
    packages:
        foo:
            services:
            - service: foo
              enable-on-install: "true"
              on-upgrade: "bar"
    """
    ).splitlines(keepends=True)

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

    msg = 'Not a supported value for "enable-on-install"'
    assert first_issue.message == msg
    assert f"{first_issue.range}" == "5:29-5:35"
    assert first_issue.severity == DiagnosticSeverity.Error

    msg = 'Not a supported value for "on-upgrade"'
    assert second_issue.message == msg
    assert f"{second_issue.range}" == "6:22-6:27"
    assert second_issue.severity == DiagnosticSeverity.Error


def test_debputy_lint_attr_value_crash_check(line_linter: LintWrapper) -> None:
    lines = textwrap.dedent(
        """\
    manifest-version: '0.1'
    installations:
    - install-docs:
        sources:
        - GETTING-STARTED-WITH-dh-debputy.md
        - MANIFEST-FORMAT.md
        - MIGRATING-A-DH-PLUGIN.md
        - GETTING-STARTED-WITH-LINT-AND-LSP-FEATURES.md
        dest-dir:
    """
    ).splitlines(keepends=True)

    diagnostics = line_linter(lines)
    # There is actually a problem (`dest-dir` must have a value) but the code
    # currently finds an empty range and bails out rather than sending empty
    # problem ranges to the editor.
    assert not diagnostics
