import os
import textwrap
from typing import Sequence, Any, List, Optional

import pytest

from debputy.exceptions import (
    DebputyManifestVariableRequiresDebianDirError,
    DebputySubstitutionError,
)
from debputy.manifest_parser.base_types import SymbolicMode
from debputy.manifest_parser.util import AttributePath
from debputy.plugin.api import virtual_path_def
from debputy.plugin.api.spec import DSD
from debputy.plugin.api.test_api import (
    build_virtual_file_system,
    package_metadata_context,
)
from debputy.plugin.api.test_api import manifest_variable_resolution_context
from debputy.plugin.api.test_api.test_impl import initialize_plugin_under_test_preloaded
from debputy.plugin.api.test_api.test_spec import DetectedService
from debputy.plugin.debputy.build_system_rules import (
    AutoconfBuildSystemRule,
    MakefileBuildSystemRule,
    PerlBuildBuildSystemRule,
    PerlMakeMakerBuildSystemRule,
    QmakeBuildSystemRule,
    Qmake6BuildSystemRule,
    CMakeBuildSystemRule,
    MesonBuildSystemRule,
)
from debputy.plugin.debputy.debputy_plugin import initialize_debputy_features
from debputy.plugin.debputy.private_api import load_libcap
from debputy.plugin.debputy.service_management import SystemdServiceContext
from debputy.plugin.debputy.types import DebputyCapability


def test_debputy_packager_provided_files():
    plugin = initialize_plugin_under_test_preloaded(
        1,
        initialize_debputy_features,
        plugin_name="debputy",
        load_debputy_plugin=False,
    )
    ppf_by_stem = plugin.packager_provided_files_by_stem()
    # Verify that all the files are loaded
    assert set(ppf_by_stem.keys()) == {
        "tmpfiles",
        "sysusers",
        "bash-completion",
        "pam",
        "ppp.ip-up",
        "ppp.ip-down",
        "logrotate",
        "logcheck.cracking",
        "logcheck.violations",
        "logcheck.violations.ignore",
        "logcheck.ignore.workstation",
        "logcheck.ignore.server",
        "logcheck.ignore.paranoid",
        "mime",
        "sharedmimeinfo",
        "if-pre-up",
        "if-up",
        "if-down",
        "if-post-down",
        "cron.hourly",
        "cron.daily",
        "cron.weekly",
        "cron.monthly",
        "cron.yearly",
        "cron.d",
        "initramfs-hook",
        "modprobe",
        "gsettings-override",
        "lintian-overrides",
        "bug-script",
        "bug-control",
        "bug-presubj",
        "changelog",
        "NEWS",
        "copyright",
        "README.Debian",
        "TODO",
        "doc-base",
        "shlibs",
        "symbols",
        "alternatives",
        "init",
        "default",
        "templates",
        # dh_installsytemd
        "mount",
        "path",
        "service",
        "socket",
        "target",
        "timer",
        "@path",
        "@service",
        "@socket",
        "@target",
        "@timer",
    }
    # Verify the post_rewrite_hook
    assert (
        ppf_by_stem["logcheck.ignore.paranoid"].compute_dest("foo.bar")[1] == "foo_bar"
    )
    # Verify custom formats work
    assert ppf_by_stem["tmpfiles"].compute_dest("foo.bar")[1] == "foo.bar.conf"
    assert ppf_by_stem["sharedmimeinfo"].compute_dest("foo.bar")[1] == "foo.bar.xml"
    assert ppf_by_stem["modprobe"].compute_dest("foo.bar")[1] == "foo.bar.conf"
    assert (
        ppf_by_stem["gsettings-override"].compute_dest("foo.bar", assigned_priority=20)[
            1
        ]
        == "20_foo.bar.gschema.override"
    )


def test_debputy_docbase_naming() -> None:
    plugin = initialize_plugin_under_test_preloaded(
        1,
        initialize_debputy_features,
        plugin_name="debputy",
        load_debputy_plugin=False,
    )
    doc_base_pff = plugin.packager_provided_files_by_stem()["doc-base"]
    fs_root = build_virtual_file_system(
        [virtual_path_def("foo.doc-base", content="Document: bar")]
    )
    _, basename = doc_base_pff.compute_dest("foo", path=fs_root["foo.doc-base"])
    assert basename == "foo.bar"


def test_debputy_adr_examples() -> None:
    plugin = initialize_plugin_under_test_preloaded(
        1,
        initialize_debputy_features,
        plugin_name="debputy",
        load_debputy_plugin=False,
    )
    issues = plugin.automatic_discard_rules_examples_with_issues()
    assert not issues


def test_debputy_metadata_detector_gsettings_dependencies():
    plugin = initialize_plugin_under_test_preloaded(
        1,
        initialize_debputy_features,
        plugin_name="debputy",
        load_debputy_plugin=False,
    )
    # By default, the plugin will not add a substvars
    fs_root = build_virtual_file_system(["./bin/ls"])
    metadata = plugin.run_metadata_detector("gsettings-dependencies", fs_root)
    assert "misc:Depends" not in metadata.substvars

    # It will not react if there is only directories or non-files
    fs_root = build_virtual_file_system(["./usr/share/glib-2.0/schemas/some-dir/"])
    metadata = plugin.run_metadata_detector("gsettings-dependencies", fs_root)
    assert "misc:Depends" not in metadata.substvars

    # However, it will if there is a file beneath the schemas dir
    fs_root = build_virtual_file_system(["./usr/share/glib-2.0/schemas/foo.xml"])
    metadata = plugin.run_metadata_detector("gsettings-dependencies", fs_root)
    assert (
        metadata.substvars["misc:Depends"]
        == "dconf-gsettings-backend | gsettings-backend"
    )


def test_debputy_metadata_detector_initramfs_hooks():
    plugin = initialize_plugin_under_test_preloaded(
        1,
        initialize_debputy_features,
        plugin_name="debputy",
        load_debputy_plugin=False,
    )
    metadata_detector_id = "initramfs-hooks"

    # By default, the plugin will not add a trigger
    fs_root = build_virtual_file_system(["./bin/ls"])
    metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
    assert metadata.triggers == []

    # It will not react if the directory is empty
    fs_root = build_virtual_file_system(
        [
            # Use an absolute path to verify that also work (it should and third-party plugin are likely
            # use absolute paths)
            "/usr/share/initramfs-tools/hooks/"
        ]
    )
    metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
    assert metadata.triggers == []

    # However, it will if there is a file beneath the schemas dir
    fs_root = build_virtual_file_system(["./usr/share/initramfs-tools/hooks/some-hook"])
    metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
    result = [t.serialized_format() for t in metadata.triggers]
    assert result == ["activate-noawait update-initramfs"]


def test_debputy_metadata_detector_systemd_tmpfiles():
    plugin = initialize_plugin_under_test_preloaded(
        1,
        initialize_debputy_features,
        plugin_name="debputy",
        load_debputy_plugin=False,
    )
    metadata_detector_id = "systemd-tmpfiles"

    # By default, the plugin will not add anything
    fs_root = build_virtual_file_system(["./bin/ls"])
    metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
    assert metadata.maintscripts() == []

    # It only reacts to ".conf" files
    fs_root = build_virtual_file_system(["./usr/lib/tmpfiles.d/foo"])
    metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
    assert metadata.maintscripts() == []

    fs_root = build_virtual_file_system(
        [
            "./usr/lib/tmpfiles.d/foo.conf",
            "./etc/tmpfiles.d/foo.conf",
        ]
    )
    metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
    snippets = metadata.maintscripts()
    assert len(snippets) == 1
    snippet = snippets[0]
    assert snippet.maintscript == "postinst"
    assert snippet.registration_method == "on_configure"
    # The snippet should use "systemd-tmpfiles [...] --create foo.conf ..."
    assert "--create foo.conf" in snippet.plugin_provided_script
    # The "foo.conf" should only be listed once
    assert snippet.plugin_provided_script.count("foo.conf") == 1


def test_debputy_metadata_detector_systemd_sysusers():
    plugin = initialize_plugin_under_test_preloaded(
        1,
        initialize_debputy_features,
        plugin_name="debputy",
        load_debputy_plugin=False,
    )
    metadata_detector_id = "systemd-sysusers"

    # By default, the plugin will not add anything
    fs_root = build_virtual_file_system(["./bin/ls"])
    metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
    assert metadata.maintscripts() == []

    # It only reacts to ".conf" files
    fs_root = build_virtual_file_system(["./usr/lib/sysusers.d/foo"])
    metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
    assert metadata.maintscripts() == []

    fs_root = build_virtual_file_system(["./usr/lib/sysusers.d/foo.conf"])
    metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
    snippets = metadata.maintscripts()
    assert len(snippets) == 1
    snippet = snippets[0]
    assert snippet.maintscript == "postinst"
    assert snippet.registration_method == "on_configure"
    # The snippet should use "systemd-sysusers [...] foo.conf ..."
    assert "systemd-sysusers" in snippet.plugin_provided_script
    assert "foo.conf" in snippet.plugin_provided_script
    # The "foo.conf" should only be listed once
    assert snippet.plugin_provided_script.count("foo.conf") == 1


def test_debputy_metadata_detector_xfonts():
    plugin = initialize_plugin_under_test_preloaded(
        1,
        initialize_debputy_features,
        plugin_name="debputy",
        load_debputy_plugin=False,
    )
    metadata_detector_id = "xfonts"

    # By default, the plugin will not add anything
    fs_root = build_virtual_file_system(["./bin/ls"])
    metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
    assert metadata.maintscripts() == []
    assert "misc:Depends" not in metadata.substvars

    # It ignores files in the X11 dir and directories starting with ".".
    fs_root = build_virtual_file_system(
        ["./usr/share/fonts/X11/foo", "./usr/share/fonts/X11/.a/"]
    )
    metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
    assert metadata.maintscripts() == []
    assert "misc:Depends" not in metadata.substvars

    fs_root = build_virtual_file_system(
        [
            "./usr/share/fonts/X11/some-font-dir/",
        ]
    )
    metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
    snippets = metadata.maintscripts()
    assert metadata.substvars["misc:Depends"] == "xfonts-utils"
    assert len(snippets) == 2
    assert set(s.maintscript for s in snippets) == {"postinst", "postrm"}
    postinst_snippet = metadata.maintscripts(maintscript="postinst")[0]
    postrm_snippet = metadata.maintscripts(maintscript="postrm")[0]

    assert postinst_snippet.maintscript == "postinst"
    assert postinst_snippet.registration_method == "unconditionally_in_script"
    assert (
        "update-fonts-scale some-font-dir"
        not in postinst_snippet.plugin_provided_script
    )
    assert "--x11r7-layout some-font-dir" in postinst_snippet.plugin_provided_script
    assert (
        f"update-fonts-alias --include" not in postinst_snippet.plugin_provided_script
    )

    assert postrm_snippet.maintscript == "postrm"
    assert postrm_snippet.registration_method == "unconditionally_in_script"
    assert (
        "update-fonts-scale some-font-dir" not in postrm_snippet.plugin_provided_script
    )
    assert "--x11r7-layout some-font-dir" in postrm_snippet.plugin_provided_script
    assert f"update-fonts-alias --exclude" not in postrm_snippet.plugin_provided_script


def test_debputy_metadata_detector_xfonts_scale_and_alias():
    plugin = initialize_plugin_under_test_preloaded(
        1,
        initialize_debputy_features,
        plugin_name="debputy",
        load_debputy_plugin=False,
    )

    metadata_detector_id = "xfonts"
    package_name = "bar"
    fs_root = build_virtual_file_system(
        [
            "./usr/share/fonts/X11/some-font-dir/",
            f"./etc/X11/xfonts/some-font-dir/{package_name}.scale",
            f"./etc/X11/xfonts/some-font-dir/{package_name}.alias",
        ]
    )
    metadata = plugin.run_metadata_detector(
        metadata_detector_id,
        fs_root,
        package_metadata_context(
            package_fields={
                "Package": package_name,
            }
        ),
    )
    snippets = metadata.maintscripts()
    assert metadata.substvars["misc:Depends"] == "xfonts-utils"
    assert len(snippets) == 2
    assert set(s.maintscript for s in snippets) == {"postinst", "postrm"}
    postinst_snippet = metadata.maintscripts(maintscript="postinst")[0]
    postrm_snippet = metadata.maintscripts(maintscript="postrm")[0]

    assert postinst_snippet.maintscript == "postinst"
    assert postinst_snippet.registration_method == "unconditionally_in_script"
    assert "update-fonts-scale some-font-dir" in postinst_snippet.plugin_provided_script
    assert "--x11r7-layout some-font-dir" in postinst_snippet.plugin_provided_script
    assert (
        f"update-fonts-alias --include /etc/X11/xfonts/some-font-dir/{package_name}.alias some-font-dir"
        in postinst_snippet.plugin_provided_script
    )

    assert postrm_snippet.maintscript == "postrm"
    assert postrm_snippet.registration_method == "unconditionally_in_script"
    assert "update-fonts-scale some-font-dir" in postrm_snippet.plugin_provided_script
    assert "--x11r7-layout some-font-dir" in postrm_snippet.plugin_provided_script
    assert (
        f"update-fonts-alias --exclude /etc/X11/xfonts/some-font-dir/{package_name}.alias some-font-dir"
        in postrm_snippet.plugin_provided_script
    )


def test_debputy_metadata_detector_icon_cache():
    plugin = initialize_plugin_under_test_preloaded(
        1,
        initialize_debputy_features,
        plugin_name="debputy",
        load_debputy_plugin=False,
    )
    metadata_detector_id = "icon-cache"
    icon_dir = "usr/share/icons"

    # By default, the plugin will not add anything
    fs_root = build_virtual_file_system(["./bin/ls"])
    metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
    assert metadata.maintscripts() == []

    fs_root = build_virtual_file_system(
        [
            # Ignored subdirs (dh_icons ignores them too)
            f"./{icon_dir}/gnome/foo.png",
            f"./{icon_dir}/hicolor/foo.png",
            # Unknown image format, so it does not trigger the update-icon-caches call
            f"./{icon_dir}/subdir-a/unknown-image-format.img",
        ]
    )
    metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
    assert metadata.maintscripts() == []

    fs_root = build_virtual_file_system(
        [
            f"./{icon_dir}/subdir-a/foo.png",
            f"./{icon_dir}/subdir-b/subsubdir/bar.svg",
        ]
    )
    metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
    snippets = metadata.maintscripts()
    assert len(snippets) == 2
    assert set(s.maintscript for s in snippets) == {"postinst", "postrm"}
    postinst_snippet = metadata.maintscripts(maintscript="postinst")[0]
    postrm_snippet = metadata.maintscripts(maintscript="postrm")[0]

    assert postinst_snippet.registration_method == "on_configure"
    assert postrm_snippet.registration_method == "unconditionally_in_script"

    # Directory order is stable according to the BinaryPackagePath API.
    assert (
        f"update-icon-caches /{icon_dir}/subdir-a /{icon_dir}/subdir-b"
        in postinst_snippet.plugin_provided_script
    )
    assert (
        f"update-icon-caches /{icon_dir}/subdir-a /{icon_dir}/subdir-b"
        in postrm_snippet.plugin_provided_script
    )


def test_debputy_metadata_detector_kernel_modules():
    plugin = initialize_plugin_under_test_preloaded(
        1,
        initialize_debputy_features,
        plugin_name="debputy",
        load_debputy_plugin=False,
    )
    metadata_detector_id = "kernel-modules"
    module_dir = "lib/modules"

    # By default, the plugin will not add anything
    fs_root = build_virtual_file_system(["./bin/ls"])
    metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
    assert metadata.maintscripts() == []

    fs_root = build_virtual_file_system(
        [
            # Ignore files directly in the path or with wrong extension
            f"./{module_dir}/README",
            f"./{module_dir}/3.11/ignored-file.txt",
        ]
    )
    metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
    assert metadata.maintscripts() == []

    fs_root = build_virtual_file_system(
        [
            f"./{module_dir}/3.11/foo.ko",
            f"./usr/{module_dir}/3.12/bar.ko.xz",
        ]
    )
    metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
    snippets = metadata.maintscripts()
    assert len(snippets) == 4  # Two for each version
    assert set(s.maintscript for s in snippets) == {"postinst", "postrm"}
    postinst_snippets = metadata.maintscripts(maintscript="postinst")
    postrm_snippets = metadata.maintscripts(maintscript="postrm")

    assert len(postinst_snippets) == 2
    assert len(postrm_snippets) == 2
    assert {s.registration_method for s in postinst_snippets} == {"on_configure"}
    assert {s.registration_method for s in postrm_snippets} == {
        "unconditionally_in_script"
    }

    assert (
        "depmod -a -F /boot/System.map-3.11 3.11"
        in postinst_snippets[0].plugin_provided_script
    )
    assert (
        "depmod -a -F /boot/System.map-3.12 3.12"
        in postinst_snippets[1].plugin_provided_script
    )

    assert (
        "depmod -a -F /boot/System.map-3.11 3.11"
        in postrm_snippets[0].plugin_provided_script
    )
    assert (
        "depmod -a -F /boot/System.map-3.12 3.12"
        in postrm_snippets[1].plugin_provided_script
    )


def test_debputy_metadata_detector_dpkg_shlibdeps():
    plugin = initialize_plugin_under_test_preloaded(
        1,
        initialize_debputy_features,
        plugin_name="debputy",
        load_debputy_plugin=False,
    )
    metadata_detector_id = "dpkg-shlibdeps"
    skip_root_dir = "usr/lib/debug/"

    # By default, the plugin will not add anything
    fs_root = build_virtual_file_system(
        [
            "./usr/share/doc/foo/copyright",
            virtual_path_def("./usr/lib/debputy/test.py", fs_path=__file__),
        ]
    )
    metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
    assert "shlibs:Depends" not in metadata.substvars

    fs_root = build_virtual_file_system(
        [
            # Verify that certain directories are skipped as promised
            virtual_path_def(f"./{skip_root_dir}/bin/ls", fs_path="/bin/ls")
        ]
    )
    metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
    assert "shlibs:Depends" not in metadata.substvars

    # But we detect ELF binaries elsewhere
    fs_root = build_virtual_file_system(
        [virtual_path_def(f"./bin/ls", fs_path="/bin/ls")]
    )
    metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
    # Do not make assertions about the content of `shlibs:Depends` as
    # package name and versions change over time.
    assert "shlibs:Depends" in metadata.substvars

    # Re-run to verify it runs for udebs as well
    metadata = plugin.run_metadata_detector(
        metadata_detector_id,
        fs_root,
        context=package_metadata_context(
            package_fields={"Package-Type": "udeb"},
        ),
    )
    assert "shlibs:Depends" in metadata.substvars


def test_debputy_metadata_detector_pycompile_files():
    plugin = initialize_plugin_under_test_preloaded(
        1,
        initialize_debputy_features,
        plugin_name="debputy",
        load_debputy_plugin=False,
    )
    metadata_detector_id = "pycompile-files"
    module_dir = "usr/lib/python3/dist-packages"

    # By default, the plugin will not add anything
    fs_root = build_virtual_file_system(["./bin/ls"])
    metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
    assert metadata.maintscripts() == []

    fs_root = build_virtual_file_system(
        [
            # Ignore files in unknown directories by default
            "./random-dir/foo.py",
            # Must be in "dist-packages" to count
            "./usr/lib/python3/foo.py",
        ]
    )
    metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
    assert metadata.maintscripts() == []

    fs_root = build_virtual_file_system(
        [
            f"./{module_dir}/debputy/foo.py",
        ]
    )
    metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
    snippets = metadata.maintscripts()
    assert len(snippets) == 2
    assert set(s.maintscript for s in snippets) == {"postinst", "prerm"}
    postinst_snippets = metadata.maintscripts(maintscript="postinst")
    prerm_snippets = metadata.maintscripts(maintscript="prerm")

    assert len(postinst_snippets) == 1
    assert len(prerm_snippets) == 1
    assert {s.registration_method for s in postinst_snippets} == {"on_configure"}
    assert {s.registration_method for s in prerm_snippets} == {
        "unconditionally_in_script"
    }

    assert "py3compile -p foo" in postinst_snippets[0].plugin_provided_script

    assert "py3clean -p foo" in prerm_snippets[0].plugin_provided_script


def test_debputy_metadata_detector_pycompile_files_private_package_dir():
    plugin = initialize_plugin_under_test_preloaded(
        1,
        initialize_debputy_features,
        plugin_name="debputy",
        load_debputy_plugin=False,
    )
    metadata_detector_id = "pycompile-files"
    module_dir = "usr/share/foo"

    fs_root = build_virtual_file_system(
        [
            f"./{module_dir}/debputy/foo.py",
        ]
    )
    metadata = plugin.run_metadata_detector(metadata_detector_id, fs_root)
    snippets = metadata.maintscripts()
    assert len(snippets) == 2
    assert set(s.maintscript for s in snippets) == {"postinst", "prerm"}
    postinst_snippets = metadata.maintscripts(maintscript="postinst")
    prerm_snippets = metadata.maintscripts(maintscript="prerm")

    assert len(postinst_snippets) == 1
    assert len(prerm_snippets) == 1
    assert {s.registration_method for s in postinst_snippets} == {"on_configure"}
    assert {s.registration_method for s in prerm_snippets} == {
        "unconditionally_in_script"
    }

    assert (
        f"py3compile -p foo /{module_dir}"
        in postinst_snippets[0].plugin_provided_script
    )

    assert "py3clean -p foo" in prerm_snippets[0].plugin_provided_script


def _extract_service(
    services: Sequence[DetectedService[DSD]], name: str
) -> DetectedService[DSD]:
    v = [s for s in services if name in s.names]
    assert len(v) == 1
    return v[0]


def test_system_service_detection() -> None:
    plugin = initialize_plugin_under_test_preloaded(
        1,
        initialize_debputy_features,
        plugin_name="debputy",
        load_debputy_plugin=False,
    )
    systemd_service_root_dir = "usr/lib/systemd"
    systemd_service_system_dir = f"{systemd_service_root_dir}/system"
    systemd_service_user_dir = f"{systemd_service_root_dir}/user"

    services: List[DetectedService[Optional[object]]]

    services, _ = plugin.run_service_detection_and_integrations(
        "systemd", build_virtual_file_system([])
    )
    assert not services

    services, _ = plugin.run_service_detection_and_integrations(
        "systemd",
        build_virtual_file_system(
            [f"{systemd_service_system_dir}/", f"{systemd_service_user_dir}/"]
        ),
    )
    assert not services

    fs_root = build_virtual_file_system(
        [
            virtual_path_def(
                f"{systemd_service_system_dir}/foo.service",
                content=textwrap.dedent(
                    """\
            Alias="myname.service"
            [Install]
            """
                ),
            ),
            virtual_path_def(
                f"{systemd_service_system_dir}/foo@.service",
                content=textwrap.dedent(
                    """\
            # dh_installsystemd ignores template services - we do for now as well.
            Alias="ignored.service"
            [Install]
            """
                ),
            ),
            virtual_path_def(
                f"{systemd_service_system_dir}/alias.service", link_target="foo.service"
            ),
            virtual_path_def(f"{systemd_service_system_dir}/bar.timer", content=""),
        ]
    )
    services, metadata = plugin.run_service_detection_and_integrations(
        "systemd",
        fs_root,
        service_context_type_hint=SystemdServiceContext,
    )
    assert len(services) == 2
    assert {s.names[0] for s in services} == {"foo.service", "bar.timer"}
    foo_service = _extract_service(services, "foo.service")
    assert set(foo_service.names) == {
        "foo.service",
        "foo",
        "alias",
        "alias.service",
        "myname.service",
        "myname",
    }
    assert foo_service.type_of_service == "service"
    assert foo_service.service_scope == "system"
    assert foo_service.enable_by_default
    assert foo_service.start_by_default
    assert foo_service.default_upgrade_rule == "restart"
    foo_service_context = foo_service.service_context
    assert isinstance(foo_service_context, SystemdServiceContext)
    assert foo_service_context.had_install_section

    bar_timer = _extract_service(services, "bar.timer")
    assert set(bar_timer.names) == {"bar.timer"}
    assert bar_timer.type_of_service == "timer"
    assert bar_timer.service_scope == "system"
    assert not bar_timer.enable_by_default
    assert bar_timer.start_by_default
    assert bar_timer.default_upgrade_rule == "restart"
    bar_service_context = bar_timer.service_context
    assert isinstance(bar_service_context, SystemdServiceContext)
    assert not bar_service_context.had_install_section

    snippets = metadata.maintscripts()
    assert len(snippets) == 4
    postinsts = metadata.maintscripts(maintscript="postinst")
    assert len(postinsts) == 2
    enable_postinst, start_postinst = postinsts
    assert (
        "deb-systemd-helper debian-installed foo.service"
        in enable_postinst.plugin_provided_script
    )
    assert (
        "deb-systemd-invoke start foo.service" in start_postinst.plugin_provided_script
    )
    assert (
        "deb-systemd-invoke restart foo.service"
        in start_postinst.plugin_provided_script
    )


def test_sysv_service_detection() -> None:
    plugin = initialize_plugin_under_test_preloaded(
        1,
        initialize_debputy_features,
        plugin_name="debputy",
        load_debputy_plugin=False,
    )
    init_dir = "etc/init.d"

    services: List[DetectedService[Optional[object]]]

    services, _ = plugin.run_service_detection_and_integrations(
        "sysvinit", build_virtual_file_system([])
    )
    assert not services

    services, _ = plugin.run_service_detection_and_integrations(
        "sysvinit",
        build_virtual_file_system(
            [
                f"{init_dir}/",
            ]
        ),
    )
    assert not services

    services, _ = plugin.run_service_detection_and_integrations(
        "sysvinit",
        build_virtual_file_system(
            [
                virtual_path_def(
                    f"{init_dir}/README",
                    mode=0o644,
                ),
            ]
        ),
    )
    assert not services

    fs_root = build_virtual_file_system(
        [
            virtual_path_def(
                f"{init_dir}/foo",
                mode=0o755,
            ),
        ]
    )
    services, metadata = plugin.run_service_detection_and_integrations(
        "sysvinit", fs_root
    )
    assert len(services) == 1
    assert {s.names[0] for s in services} == {"foo"}
    foo_service = _extract_service(services, "foo")
    assert set(foo_service.names) == {"foo"}
    assert foo_service.type_of_service == "service"
    assert foo_service.service_scope == "system"
    assert foo_service.enable_by_default
    assert foo_service.start_by_default
    assert foo_service.default_upgrade_rule == "restart"

    snippets = metadata.maintscripts()
    assert len(snippets) == 4
    postinsts = metadata.maintscripts(maintscript="postinst")
    assert len(postinsts) == 1
    postinst = postinsts[0]
    assert postinst.registration_method == "on_configure"
    assert "" in postinst.plugin_provided_script
    assert "update-rc.d foo defaults" in postinst.plugin_provided_script
    assert (
        "invoke-rc.d --skip-systemd-native foo start" in postinst.plugin_provided_script
    )
    assert (
        "invoke-rc.d --skip-systemd-native foo restart"
        in postinst.plugin_provided_script
    )


def test_debputy_manifest_variables() -> None:
    plugin = initialize_plugin_under_test_preloaded(
        1,
        initialize_debputy_features,
        plugin_name="debputy",
        load_debputy_plugin=False,
    )
    manifest_variables_no_dch = plugin.manifest_variables()
    assert manifest_variables_no_dch.keys() == {
        "DEB_BUILD_ARCH",
        "DEB_BUILD_ARCH_ABI",
        "DEB_BUILD_ARCH_BITS",
        "DEB_BUILD_ARCH_CPU",
        "DEB_BUILD_ARCH_ENDIAN",
        "DEB_BUILD_ARCH_LIBC",
        "DEB_BUILD_ARCH_OS",
        "DEB_BUILD_GNU_CPU",
        "DEB_BUILD_GNU_SYSTEM",
        "DEB_BUILD_GNU_TYPE",
        "DEB_BUILD_MULTIARCH",
        "DEB_HOST_ARCH",
        "DEB_HOST_ARCH_ABI",
        "DEB_HOST_ARCH_BITS",
        "DEB_HOST_ARCH_CPU",
        "DEB_HOST_ARCH_ENDIAN",
        "DEB_HOST_ARCH_LIBC",
        "DEB_HOST_ARCH_OS",
        "DEB_HOST_GNU_CPU",
        "DEB_HOST_GNU_SYSTEM",
        "DEB_HOST_GNU_TYPE",
        "DEB_HOST_MULTIARCH",
        "DEB_SOURCE",
        "DEB_TARGET_ARCH",
        "DEB_TARGET_ARCH_ABI",
        "DEB_TARGET_ARCH_BITS",
        "DEB_TARGET_ARCH_CPU",
        "DEB_TARGET_ARCH_ENDIAN",
        "DEB_TARGET_ARCH_LIBC",
        "DEB_TARGET_ARCH_OS",
        "DEB_TARGET_GNU_CPU",
        "DEB_TARGET_GNU_SYSTEM",
        "DEB_TARGET_GNU_TYPE",
        "DEB_TARGET_MULTIARCH",
        "DEB_VERSION",
        "DEB_VERSION_EPOCH_UPSTREAM",
        "DEB_VERSION_UPSTREAM",
        "DEB_VERSION_UPSTREAM_REVISION",
        "PACKAGE",
        "SOURCE_DATE_EPOCH",
        "_DEBPUTY_INTERNAL_NON_BINNMU_SOURCE",
        "_DEBPUTY_SND_SOURCE_DATE_EPOCH",
        "path:BASH_COMPLETION_DIR",
        "path:GNU_INFO_DIR",
        "token:CLOSE_CURLY_BRACE",
        "token:DOUBLE_CLOSE_CURLY_BRACE",
        "token:DOUBLE_OPEN_CURLY_BRACE",
        "token:NEWLINE",
        "token:NL",
        "token:OPEN_CURLY_BRACE",
        "token:TAB",
    }

    for v in [
        "DEB_SOURCE",
        "DEB_VERSION",
        "DEB_VERSION_EPOCH_UPSTREAM",
        "DEB_VERSION_UPSTREAM",
        "DEB_VERSION_UPSTREAM_REVISION",
        "SOURCE_DATE_EPOCH",
        "_DEBPUTY_INTERNAL_NON_BINNMU_SOURCE",
        "_DEBPUTY_SND_SOURCE_DATE_EPOCH",
    ]:
        with pytest.raises(DebputyManifestVariableRequiresDebianDirError):
            manifest_variables_no_dch[v]

    with pytest.raises(DebputySubstitutionError):
        manifest_variables_no_dch["PACKAGE"]

    dch_content = textwrap.dedent(
        """\
        mscgen (1:0.20-15) unstable; urgency=medium

          * Irrelevant stuff here...
          * Also, some details have been tweaked for better testing

         -- Niels Thykier <niels@thykier.net>  Mon, 09 Oct 2023 14:50:06 +0000
        """
    )

    debian_dir = build_virtual_file_system(
        [virtual_path_def("changelog", content=dch_content)]
    )
    resolution_context = manifest_variable_resolution_context(debian_dir=debian_dir)
    manifest_variables = plugin.manifest_variables(
        resolution_context=resolution_context
    )

    assert manifest_variables["DEB_SOURCE"] == "mscgen"
    assert manifest_variables["DEB_VERSION"] == "1:0.20-15"
    assert manifest_variables["_DEBPUTY_INTERNAL_NON_BINNMU_SOURCE"] == "1:0.20-15"

    assert manifest_variables["DEB_VERSION_EPOCH_UPSTREAM"] == "1:0.20"
    assert manifest_variables["DEB_VERSION_UPSTREAM"] == "0.20"
    assert manifest_variables["DEB_VERSION_UPSTREAM_REVISION"] == "0.20-15"
    assert manifest_variables["SOURCE_DATE_EPOCH"] == "1696863006"
    assert manifest_variables["_DEBPUTY_SND_SOURCE_DATE_EPOCH"] == "1696863006"

    # This one remains unresolvable
    with pytest.raises(DebputySubstitutionError):
        manifest_variables["PACKAGE"]

    static_values = {
        "path:BASH_COMPLETION_DIR": "/usr/share/bash-completion/completions",
        "path:GNU_INFO_DIR": "/usr/share/info",
    }

    for k, v in static_values.items():
        assert manifest_variables[k] == v

    dch_content_bin_nmu = textwrap.dedent(
        """\
        mscgen (1:0.20-15+b4) unstable; urgency=medium, binary-only=yes

          * Some binNMU entry here

         -- Niels Thykier <niels@thykier.net>  Mon, 10 Nov 2023 16:01:17 +0000

        mscgen (1:0.20-15) unstable; urgency=medium

          * Irrelevant stuff here...
          * Also, some details have been tweaked for better testing

         -- Niels Thykier <niels@thykier.net>  Mon, 09 Oct 2023 14:50:06 +0000
        """
    )

    debian_dir_bin_nmu = build_virtual_file_system(
        [virtual_path_def("changelog", content=dch_content_bin_nmu)]
    )
    resolution_context_bin_nmu = manifest_variable_resolution_context(
        debian_dir=debian_dir_bin_nmu
    )
    manifest_variables_bin_nmu = plugin.manifest_variables(
        resolution_context=resolution_context_bin_nmu
    )

    assert manifest_variables_bin_nmu["DEB_SOURCE"] == "mscgen"
    assert manifest_variables_bin_nmu["DEB_VERSION"] == "1:0.20-15+b4"
    assert (
        manifest_variables_bin_nmu["_DEBPUTY_INTERNAL_NON_BINNMU_SOURCE"] == "1:0.20-15"
    )

    assert manifest_variables_bin_nmu["DEB_VERSION_EPOCH_UPSTREAM"] == "1:0.20"
    assert manifest_variables_bin_nmu["DEB_VERSION_UPSTREAM"] == "0.20"
    assert manifest_variables_bin_nmu["DEB_VERSION_UPSTREAM_REVISION"] == "0.20-15+b4"
    assert manifest_variables_bin_nmu["SOURCE_DATE_EPOCH"] == "1699632077"
    assert manifest_variables_bin_nmu["_DEBPUTY_SND_SOURCE_DATE_EPOCH"] == "1696863006"


def test_cap_validator() -> None:
    has_libcap, _, is_valid_cap = load_libcap()

    if not has_libcap:
        if os.environ.get("DEBPUTY_REQUIRE_LIBCAP", "") != "":
            pytest.fail("Could not load libcap, but DEBPUTY_REQUIRE_CAP was non-empty")
        pytest.skip("Could not load libcap.so")
    assert not is_valid_cap("foo")
    assert is_valid_cap("cap_dac_override,cap_bpf,cap_net_admin=ep")


def test_clean_la_files() -> None:
    plugin = initialize_plugin_under_test_preloaded(
        1,
        initialize_debputy_features,
        plugin_name="debputy",
        load_debputy_plugin=False,
    )
    fs_root = build_virtual_file_system(
        [virtual_path_def("usr/bin/foo", content="#!/bin/sh\n")]
    )
    # Does nothing by default
    plugin.run_package_processor(
        "clean-la-files",
        fs_root,
    )

    la_file_content = textwrap.dedent(
        """\
    dependency_libs = 'foo bar'
    another_line = 'foo bar'
    """
    )
    expected_content = textwrap.dedent(
        """\
        dependency_libs = ''
        another_line = 'foo bar'
        """
    )
    la_file_content_no_change = expected_content
    expected_content = textwrap.dedent(
        """\
        dependency_libs = ''
        another_line = 'foo bar'
        """
    )

    fs_root = build_virtual_file_system(
        [
            virtual_path_def("usr/lib/libfoo.la", materialized_content=la_file_content),
            virtual_path_def(
                "usr/lib/libfoo-unchanged.la",
                content=la_file_content_no_change,
            ),
        ]
    )

    plugin.run_package_processor(
        "clean-la-files",
        fs_root,
    )
    for basename in ("libfoo.la", "libfoo-unchanged.la"):
        la_file = fs_root.lookup(f"usr/lib/{basename}")
        assert la_file is not None and la_file.is_file
        if basename == "libfoo-unchanged.la":
            # it should never have been rewritten
            assert not la_file.has_fs_path
        else:
            assert la_file.has_fs_path
        with la_file.open() as fd:
            rewritten_content = fd.read()
            assert rewritten_content == expected_content


def test_strip_nondeterminism() -> None:
    plugin = initialize_plugin_under_test_preloaded(
        1,
        initialize_debputy_features,
        plugin_name="debputy",
        load_debputy_plugin=False,
    )

    fs_root = build_virtual_file_system(
        [
            # Note, we are only testing a negative example as a positive example crashes
            # because we do not have a SOURCE_DATE_EPOCH value/substitution
            virtual_path_def("test/not-really-a-png.png", content="Not a PNG")
        ]
    )

    plugin.run_package_processor(
        "strip-nondeterminism",
        fs_root,
    )


def test_translate_capabilities() -> None:
    attribute_path = AttributePath.test_path()
    plugin = initialize_plugin_under_test_preloaded(
        1,
        initialize_debputy_features,
        plugin_name="debputy",
        load_debputy_plugin=False,
    )

    fs_root = build_virtual_file_system([virtual_path_def("usr/bin/foo", mode=0o4755)])

    foo = fs_root.lookup("usr/bin/foo")
    assert foo is not None
    assert foo.is_file
    assert foo.is_read_write

    metadata_no_cap = plugin.run_metadata_detector(
        "translate-capabilities",
        fs_root,
    )

    assert not metadata_no_cap.maintscripts(maintscript="postinst")

    cap = foo.metadata(DebputyCapability)
    assert not cap.is_present
    assert cap.can_write
    cap.value = DebputyCapability(
        capabilities="cap_net_raw+ep",
        capability_mode=SymbolicMode.parse_filesystem_mode(
            "u-s",
            attribute_path["cap_mode"],
        ),
        definition_source="test",
    )
    metadata_w_cap = plugin.run_metadata_detector(
        "translate-capabilities",
        fs_root,
    )

    postinsts = metadata_w_cap.maintscripts(maintscript="postinst")
    assert len(postinsts) == 1
    postinst = postinsts[0]
    assert postinst.registration_method == "on_configure"
    assert "setcap cap_net_raw+ep " in postinst.plugin_provided_script
    assert "chmod u-s " in postinst.plugin_provided_script


def test_pam_auth_update() -> None:
    plugin = initialize_plugin_under_test_preloaded(
        1,
        initialize_debputy_features,
        plugin_name="debputy",
        load_debputy_plugin=False,
    )

    fs_root = build_virtual_file_system(["usr/bin/foo"])

    empty_metadata = plugin.run_metadata_detector("pam-auth-update", fs_root)
    assert not empty_metadata.maintscripts()

    fs_root = build_virtual_file_system(["/usr/share/pam-configs/foo-pam"])

    pam_metadata = plugin.run_metadata_detector("pam-auth-update", fs_root)
    postinsts = pam_metadata.maintscripts(maintscript="postinst")
    assert len(postinsts) == 1
    prerms = pam_metadata.maintscripts(maintscript="prerm")
    assert len(prerms) == 1

    postinst = postinsts[0]
    assert postinst.registration_method == "on_configure"
    assert "pam-auth-update --package" in postinst.plugin_provided_script

    prerm = prerms[0]
    assert prerm.registration_method == "on_before_removal"
    assert "pam-auth-update --package --remove foo-pam" in prerm.plugin_provided_script


def test_auto_depends_solink() -> None:
    plugin = initialize_plugin_under_test_preloaded(
        1,
        initialize_debputy_features,
        plugin_name="debputy",
        load_debputy_plugin=False,
    )

    fs_root = build_virtual_file_system(["usr/bin/foo"])

    empty_metadata = plugin.run_metadata_detector(
        "auto-depends-arch-any-solink",
        fs_root,
    )
    assert "misc:Depends" not in empty_metadata.substvars
    fs_root = build_virtual_file_system(
        [
            "usr/lib/x86_64-linux-gnu/libfoo.la",
            virtual_path_def(
                "usr/lib/x86_64-linux-gnu/libfoo.so", link_target="libfoo.so.1"
            ),
        ]
    )

    still_empty_metadata = plugin.run_metadata_detector(
        "auto-depends-arch-any-solink",
        fs_root,
    )
    assert "misc:Depends" not in still_empty_metadata.substvars

    libfoo1_fs_root = build_virtual_file_system(
        [
            virtual_path_def(
                "usr/lib/x86_64-linux-gnu/libfoo.so.1", link_target="libfoo.so.1.0.0"
            ),
        ]
    )

    context_correct = package_metadata_context(
        package_fields={
            "Package": "libfoo-dev",
        },
        accessible_package_roots=[
            (
                {
                    "Package": "libfoo1",
                    "Architecture": "any",
                },
                libfoo1_fs_root,
            )
        ],
    )
    sodep_metadata = plugin.run_metadata_detector(
        "auto-depends-arch-any-solink",
        fs_root,
        context=context_correct,
    )
    assert "misc:Depends" in sodep_metadata.substvars
    assert sodep_metadata.substvars["misc:Depends"] == "libfoo1 (= ${binary:Version})"

    context_incorrect = package_metadata_context(
        package_fields={"Package": "libfoo-dev", "Architecture": "all"},
        accessible_package_roots=[
            (
                {
                    "Package": "foo",
                    "Architecture": "all",
                },
                build_virtual_file_system([]),
            )
        ],
    )
    sodep_metadata = plugin.run_metadata_detector(
        "auto-depends-arch-any-solink",
        fs_root,
        context=context_incorrect,
    )
    assert "misc:Depends" not in sodep_metadata.substvars

    context_too_many_matches = package_metadata_context(
        package_fields={"Package": "libfoo-dev"},
        accessible_package_roots=[
            (
                {
                    "Package": "libfoo1-a",
                    "Architecture": "any",
                },
                libfoo1_fs_root,
            ),
            (
                {
                    "Package": "libfoo1-b",
                    "Architecture": "any",
                },
                libfoo1_fs_root,
            ),
        ],
    )
    sodep_metadata = plugin.run_metadata_detector(
        "auto-depends-arch-any-solink",
        fs_root,
        context=context_too_many_matches,
    )
    assert "misc:Depends" not in sodep_metadata.substvars


@pytest.mark.parametrize(
    "filename,expected,mode,content",
    [
        ("configure.ac", True, 0o0644, None),
        ("configure.in", True, 0o0644, "AC_INIT"),
        ("configure.in", True, 0o0644, "AC_PREREQ"),
        ("configure.in", False, 0o0644, "None of the above"),
        ("configure", True, 0o0755, "GNU Autoconf"),
        ("configure", False, 0o0644, "GNU Autoconf"),
        ("configure", False, 0o0755, "No magic keyword"),
        ("random-file", False, 0o0644, "No configure at all"),
    ],
)
def test_auto_detect_autoconf_build_system(
    filename: str,
    expected: bool,
    mode: int,
    content: Optional[str],
) -> None:
    fs_root = build_virtual_file_system(
        [virtual_path_def(filename, mode=mode, content=content)]
    )
    detected = AutoconfBuildSystemRule.auto_detect_build_system(fs_root)
    assert detected == expected


@pytest.mark.parametrize(
    "filename,expected",
    [
        ("GNUmakefile", True),
        ("Makefile", True),
        ("makefile", True),
        ("random-file", False),
    ],
)
def test_auto_detect_make_build_system(
    filename: str,
    expected: bool,
) -> None:
    fs_root = build_virtual_file_system([filename])
    detected = MakefileBuildSystemRule.auto_detect_build_system(fs_root)
    assert detected == expected


@pytest.mark.parametrize(
    "filename,expected",
    [
        ("Build.PL", True),
        ("random-file", False),
    ],
)
def test_auto_detect_perl_build_build_system(
    filename: str,
    expected: bool,
) -> None:
    fs_root = build_virtual_file_system([filename])
    detected = PerlBuildBuildSystemRule.auto_detect_build_system(fs_root)
    assert detected == expected


@pytest.mark.parametrize(
    "filename,expected",
    [
        ("Makefile.PL", True),
        ("random-file", False),
    ],
)
def test_auto_detect_perl_makemaker_build_system(
    filename: str,
    expected: bool,
) -> None:
    fs_root = build_virtual_file_system([filename])
    detected = PerlMakeMakerBuildSystemRule.auto_detect_build_system(fs_root)
    assert detected == expected


@pytest.mark.parametrize(
    "filename,expected",
    [
        ("foo.pro", True),
        ("random-file", False),
    ],
)
def test_auto_detect_qmake_build_systems(
    filename: str,
    expected: bool,
) -> None:
    fs_root = build_virtual_file_system([filename])
    detected_qmake = QmakeBuildSystemRule.auto_detect_build_system(fs_root)
    detected_qmake6 = Qmake6BuildSystemRule.auto_detect_build_system(fs_root)
    assert detected_qmake == expected
    assert detected_qmake6 == expected


@pytest.mark.parametrize(
    "filename,expected",
    [
        ("CMakeLists.txt", True),
        ("random-file", False),
    ],
)
def test_auto_detect_cmake_build_systems(
    filename: str,
    expected: bool,
) -> None:
    fs_root = build_virtual_file_system([filename])
    detected = CMakeBuildSystemRule.auto_detect_build_system(fs_root)
    assert detected == expected


@pytest.mark.parametrize(
    "filename,expected",
    [
        ("meson.build", True),
        ("random-file", False),
    ],
)
def test_auto_detect_meson_build_systems(
    filename: str,
    expected: bool,
) -> None:
    fs_root = build_virtual_file_system([filename])
    detected = MesonBuildSystemRule.auto_detect_build_system(fs_root)
    assert detected == expected
