"debputy: test generation of the (static-)built-using field."

import typing

import pytest

from conftest import ManifestParserFactory
from debputy.filesystem_scan import FSRootDir
from debputy.plugin.api import virtual_path_def
from debputy.plugin.api.impl_types import (
    PackageProcessingContextProvider,
    PackageDataTable,
)
from debputy.plugin.api.test_api import build_virtual_file_system
from debputy.plugin.api.test_api.test_impl import initialize_plugin_under_test_preloaded
from debputy.plugins.debputy.debputy_plugin import initialize_debputy_features
from debputy.util import PKGNAME_REGEX
from tutil import is_pkg_installed


class TDefinition(typing.NamedTuple):
    name: str
    manifest: str
    expected: set[str]
    build_depends: str
    architecture: str = "any"
    field: str = "Built-Using"
    required_pkgs: tuple[str, ...] = ()


# More or less in sync with dh-builtusing unit-tests.
tests = (
    TDefinition(
        name="01basic",
        build_depends="dpkg",
        manifest="sources-for: dpkg",
        expected={"dpkg"},
    ),
    TDefinition(
        name="01static",
        build_depends="dpkg",
        manifest="sources-for: dpkg",
        expected={"dpkg"},
        field="Static-Built-Using",
    ),
    TDefinition(
        name="20build-depends-arch-restriction",
        build_depends="dpkg [i386]",
        manifest="sources-for: dpkg",
        expected={},
    ),
    TDefinition(
        name="20build-depends-profile-restriction",
        build_depends="dpkg <dummy>",
        manifest="sources-for: dpkg",
        expected={},
    ),
    TDefinition(
        name="20build-depends-arch",
        required_pkgs=("libc6", "libdpkg-perl", "libbinutils"),
        build_depends="""libc6
Build-Depends-Arch: libdpkg-perl
Build-Depends-Indep: libbinutils""",
        manifest="sources-for: 'lib*'",
        expected={"glibc", "dpkg"},
    ),
    TDefinition(
        name="20build-depends-indep",
        required_pkgs=("libc6", "libdpkg-perl", "libbinutils"),
        architecture="all",
        build_depends="""libc6
Build-Depends-Arch: libdpkg-perl
Build-Depends-Indep: libbinutils""",
        manifest="sources-for: 'lib*'",
        expected={"glibc", "binutils"},
    ),
    TDefinition(
        name="30or-dependency-first",
        build_depends="dpkg | dummy",
        manifest="sources-for: dpkg",
        expected={"dpkg"},
    ),
    TDefinition(
        name="30or-dependency-not-first",
        build_depends="dummy | dpkg",
        manifest="sources-for: dpkg",
        expected={"dpkg"},
    ),
    TDefinition(
        name="40pattern-star-initial",
        build_depends="dpkg",
        manifest="sources-for: '*kg'",
        expected={"dpkg"},
    ),
    TDefinition(
        name="40pattern-star-final",
        build_depends="dpkg",
        manifest="sources-for: 'dp*'",
        expected={"dpkg"},
    ),
    TDefinition(
        name="40pattern-star-empty",
        build_depends="dpkg",
        manifest="sources-for: 'dp*kg'",
        expected={"dpkg"},
    ),
    TDefinition(
        name="40pattern-star-one-char",
        build_depends="dpkg",
        manifest="sources-for: 'dpk*g'",
        expected={"dpkg"},
    ),
    TDefinition(
        name="40pattern-re-char-plus",
        required_pkgs=("g++",),
        build_depends="g++",
        manifest="sources-for: g*+",
        expected={"gcc-defaults"},
    ),
    TDefinition(
        name="40pattern-ambiguous-all",
        required_pkgs=("libbinutils", "libc6"),
        build_depends="""
Build-Depends-Arch: libbinutils
Build-Depends-Indep: libc6""",
        architecture="all",
        manifest="sources-for: 'lib*'",
        expected={"glibc"},
    ),
    TDefinition(
        name="40pattern-ambiguous-any",
        required_pkgs=("libbinutils", "libc6"),
        build_depends="""
Build-Depends-Arch: libbinutils
Build-Depends-Indep: libc6""",
        manifest="sources-for: 'lib*'",
        expected={"binutils"},
    ),
    TDefinition(
        name="45multiple-matches",
        required_pkgs=("dpkg-dev", "autotools-dev"),
        build_depends="dpkg-dev, autotools-dev",
        manifest="sources-for: '*-dev'",
        expected={"autotools-dev", "dpkg"},
    ),
    TDefinition(
        name="50same-source",
        required_pkgs=("dpkg-dev",),
        build_depends="dpkg, dpkg-dev",
        manifest="sources-for: dpkg, sources-for: dpkg-dev",
        expected={"dpkg"},
    ),
    TDefinition(
        name="70arch-profile-restrictions-arch-yes",
        build_depends="dpkg",
        manifest="{sources-for: dpkg, when: {arch-matches: amd64}}",
        expected={"dpkg"},
    ),
    TDefinition(
        name="70arch-profile-restrictions-arch-no",
        build_depends="dpkg",
        manifest="{sources-for: dpkg, when: {arch-matches: '!amd64'}}",
        expected={},
    ),
    TDefinition(
        name="70arch-profile-restrictions-profile-no",
        build_depends="dpkg",
        manifest="{sources-for: dpkg, when: {build-profiles-matches: '<dummy>'}}",
        expected={},
    ),
    TDefinition(
        name="70arch-suffix-all",
        required_pkgs=("debhelper",),
        build_depends="debhelper:all",
        manifest="sources-for: debhelper",
        expected={"debhelper"},
    ),
    TDefinition(
        name="70arch-suffix-native",
        required_pkgs=("gcc",),
        build_depends="gcc:amd64",
        manifest="sources-for: gcc",
        expected={"gcc-defaults"},
    ),
    TDefinition(
        name="70arch-suffix-same",
        required_pkgs=("libc6",),
        build_depends="libc6:amd64",
        manifest="sources-for: libc6",
        expected={"glibc"},
    ),
    TDefinition(
        name="70multiarch-no",
        required_pkgs=("gcc",),
        build_depends="gcc",
        manifest="sources-for: gcc",
        expected={"gcc-defaults"},
    ),
    # 70multiarch-foreign would duplicate 01basic
    TDefinition(
        name="70multiarch-same",
        required_pkgs=("libc6",),
        build_depends="libc6",
        manifest="sources-for: libc6",
        expected={"glibc"},
    ),
    TDefinition(
        name="70multiarch-allowed",
        required_pkgs=("make",),
        build_depends="make",
        manifest="sources-for: make",
        expected={"make-dfsg"},
    ),
)


def _pkg_names_from_substvar(resolve_substvar: str) -> set[str]:
    return set(
        PKGNAME_REGEX.match(p.strip()).group(0) for p in resolve_substvar.split(",")
    )


@pytest.mark.parametrize("test_definition", tests, ids=[t.name for t in tests])
def test_built_using(
    manifest_parser_amd64_factory: ManifestParserFactory,
    test_definition: TDefinition,
) -> None:

    for p in test_definition.required_pkgs:
        if not is_pkg_installed(p):
            pytest.skip(f"The test requires the {p} package installed")
            return

    dctrl_contents = f"""\
Source: foo
Build-Depends: {test_definition.build_depends}

Package: foo
Architecture: {test_definition.architecture}
"""

    manifest_contents = f"""\
manifest-version: '0.1'
packages:
  foo:
    {test_definition.field.lower()}: [{test_definition.manifest}]
"""

    # A bit of debugging output in case the test fails
    print(" --- d/control ---")
    print(dctrl_contents)
    print(" --- d/debputy.manifest ---")
    print(manifest_contents)

    manifest = manifest_parser_amd64_factory(
        build_virtual_file_system(
            (virtual_path_def("./control", content=dctrl_contents),),
        )
    ).parse_manifest(fd=manifest_contents)
    binary_package = manifest.package_state_for("foo").binary_package
    assert binary_package.should_be_acted_on
    substvars = (
        initialize_plugin_under_test_preloaded(
            1,
            initialize_debputy_features,
            plugin_name="debputy",
            load_debputy_plugin=False,
        )
        .run_metadata_detector(
            metadata_detector_id="built-using-relations",
            fs_root=FSRootDir(),
            context=PackageProcessingContextProvider(
                manifest=manifest,
                binary_package=binary_package,
                related_udeb_package=None,
                package_data_table=PackageDataTable({}),
            ),
        )
        .substvars
    )
    key = f"debputy:{test_definition.field}"
    if test_definition.expected:
        actual = _pkg_names_from_substvar(substvars[key])
        assert actual == test_definition.expected
    else:
        assert key not in substvars
