import dataclasses
import textwrap
from typing import Mapping, Dict, Self

import pytest
from debian.deb822 import Deb822
from debian.debian_support import DpkgArchTable

from debputy._deb_options_profiles import DebBuildOptionsAndProfiles
from debputy.architecture_support import DpkgArchitectureBuildProcessValuesTable
from debputy.highlevel_manifest import (
    HighLevelManifest,
    PathNotCoveredByInstallRulesError,
)
from debputy.highlevel_manifest_parser import YAMLManifestParser
from debputy.installations import (
    InstallSearchDirContext,
    NoMatchForInstallPatternError,
    SearchDir,
)
from debputy.packages import BinaryPackage, SourcePackage
from debputy.plugin.api import virtual_path_def, VirtualPath
from debputy.plugin.api.feature_set import PluginProvidedFeatureSet
from debputy.plugin.api.spec import INTEGRATION_MODE_DH_DEBPUTY
from debputy.plugin.api.test_api import build_virtual_file_system
from debputy.substitution import Substitution


@pytest.fixture()
def manifest_parser_pkg_foo(
    amd64_dpkg_architecture_variables,
    dpkg_arch_query,
    source_package,
    package_single_foo_arch_all_cxt_amd64,
    amd64_substitution,
    no_profiles_or_build_options,
    debputy_plugin_feature_set,
) -> YAMLManifestParser:
    # We need an empty directory to avoid triggering packager provided files.
    debian_dir = build_virtual_file_system([])
    return YAMLManifestParser(
        "debian/test-debputy.manifest",
        source_package,
        package_single_foo_arch_all_cxt_amd64,
        amd64_substitution,
        amd64_dpkg_architecture_variables,
        dpkg_arch_query,
        no_profiles_or_build_options,
        debputy_plugin_feature_set,
        "full",
        debian_dir=debian_dir,
    )


@pytest.fixture()
def manifest_parser_pkg_foo_w_udeb(
    amd64_dpkg_architecture_variables,
    dpkg_arch_query,
    source_package,
    package_foo_w_udeb_arch_any_cxt_amd64,
    amd64_substitution,
    no_profiles_or_build_options,
    debputy_plugin_feature_set,
) -> YAMLManifestParser:
    # We need an empty directory to avoid triggering packager provided files.
    debian_dir = build_virtual_file_system([])
    return YAMLManifestParser(
        "debian/test-debputy.manifest",
        source_package,
        package_foo_w_udeb_arch_any_cxt_amd64,
        amd64_substitution,
        amd64_dpkg_architecture_variables,
        dpkg_arch_query,
        no_profiles_or_build_options,
        debputy_plugin_feature_set,
        "full",
        debian_dir=debian_dir,
    )


@dataclasses.dataclass(slots=True, frozen=True)
class FlexibleManifestParser:
    dpkg_architecture_variables: DpkgArchitectureBuildProcessValuesTable
    dpkg_arch_query: DpkgArchTable
    source_package: SourcePackage
    substitution: Substitution
    no_profiles_or_build_options: DebBuildOptionsAndProfiles
    debputy_plugin_feature_set: PluginProvidedFeatureSet
    debian_dir: VirtualPath
    binary_packages: Dict[str, BinaryPackage] = dataclasses.field(default_factory=dict)

    def binary_package(
        self,
        fields: Mapping[str, str],
        *,
        is_main_package: bool = False,
        should_be_acted_on: bool = True,
    ) -> Self:
        pkg = BinaryPackage(
            fields,
            self.dpkg_architecture_variables,
            self.dpkg_arch_query,
            is_main_package=is_main_package,
            should_be_acted_on=should_be_acted_on,
        )
        self.binary_packages[pkg.name] = pkg
        return self

    def parse_manifest(self, manifest_data: str) -> HighLevelManifest:
        binary_packages = self.binary_packages
        if not binary_packages:
            raise ValueError("No binary packages defined!?")
        return YAMLManifestParser(
            "debian/test-debputy.manifest",
            self.source_package,
            binary_packages,
            self.substitution,
            self.dpkg_architecture_variables,
            self.dpkg_arch_query,
            self.no_profiles_or_build_options,
            self.debputy_plugin_feature_set,
            "full",
            debian_dir=self.debian_dir,
        ).parse_manifest(fd=manifest_data)


@pytest.fixture()
def flexible_manifest_parser(
    amd64_dpkg_architecture_variables: DpkgArchitectureBuildProcessValuesTable,
    dpkg_arch_query: DpkgArchTable,
    source_package: SourcePackage,
    amd64_substitution: Substitution,
    no_profiles_or_build_options: DebBuildOptionsAndProfiles,
    debputy_plugin_feature_set: PluginProvidedFeatureSet,
) -> FlexibleManifestParser:
    return FlexibleManifestParser(
        amd64_dpkg_architecture_variables,
        dpkg_arch_query,
        source_package,
        amd64_substitution,
        no_profiles_or_build_options,
        debputy_plugin_feature_set,
        build_virtual_file_system([]),
    )


def test_install_rules(manifest_parser_pkg_foo) -> None:
    debian_source_root_dir = build_virtual_file_system(
        [virtual_path_def(".", fs_path="/nowhere")]
    )
    debian_tmp_dir = build_virtual_file_system(
        [
            virtual_path_def(".", fs_path="/nowhere/debian/tmp"),
            virtual_path_def("usr/", fs_path="/nowhere/debian/tmp/usr"),
            virtual_path_def("usr/bin/", fs_path="/nowhere/debian/tmp/usr/bin"),
            virtual_path_def(
                "usr/bin/foo",
                fs_path="/nowhere/debian/tmp/usr/bin/foo",
                content="#!/bin/sh\n",
                mtime=10,
            ),
            virtual_path_def(
                "usr/bin/foo-util",
                fs_path="/nowhere/debian/tmp/usr/bin/foo-util",
                content="#!/bin/sh\n",
                mtime=10,
            ),
            virtual_path_def(
                "usr/bin/tool.sh",
                fs_path="/nowhere/debian/tmp/usr/bin/tool.sh",
                link_target="./foo",
            ),
            virtual_path_def("usr/share/", fs_path="/nowhere/debian/tmp/usr/share"),
            virtual_path_def(
                "usr/share/foo/", fs_path="/nowhere/debian/tmp/usr/share/foo"
            ),
            virtual_path_def(
                "usr/share/foo/foo.txt",
                fs_path="/nowhere/debian/tmp/usr/share/foo/foo.txt",
                content="A text file",
            ),
        ]
    )
    manifest_content = textwrap.dedent(
        """\
        manifest-version: "0.1"
        installations:
          - install:
              - /usr/share/foo
              - /usr/bin/foo
              - /usr/bin/foo-util
          - install:
              source: usr/bin/tool.sh
              as: usr/bin/tool
        """
    )
    manifest = manifest_parser_pkg_foo.parse_manifest(fd=manifest_content)
    all_pkgs = frozenset(manifest.all_packages)

    result = manifest.perform_installations(
        INTEGRATION_MODE_DH_DEBPUTY,
        install_request_context=InstallSearchDirContext(
            [
                SearchDir(debian_tmp_dir, all_pkgs),
                SearchDir(debian_source_root_dir, all_pkgs),
            ],
            [debian_tmp_dir],
        ),
    )
    assert "foo" in result
    foo_fs_root = result["foo"].fs_root

    ub_dir = foo_fs_root.lookup("/usr/bin")
    assert ub_dir is not None
    assert ub_dir.is_dir
    assert not ub_dir.has_fs_path  # This will be "generated"

    tool = ub_dir.get("tool")
    assert tool is not None
    assert tool.is_symlink
    assert tool.readlink() == "./foo"

    assert {"foo", "foo-util", "tool"} == {p.name for p in ub_dir.iterdir}
    for n in ["foo", "foo-util"]:
        assert ub_dir[n].mtime == 10
    usf_dir = foo_fs_root.lookup("/usr/share/foo")
    assert usf_dir is not None
    assert usf_dir.is_dir
    # Here we are installing an actual directory, so it should be present too
    assert usf_dir.has_fs_path
    assert usf_dir.fs_path == "/nowhere/debian/tmp/usr/share/foo"
    assert {"foo.txt"} == {p.name for p in usf_dir.iterdir}


def test_multi_dest_install_rules(manifest_parser_pkg_foo) -> None:
    debian_source_root_dir = build_virtual_file_system(
        [
            virtual_path_def(".", fs_path="/nowhere"),
            virtual_path_def("source/", fs_path="/nowhere/source"),
            virtual_path_def("source/foo/", fs_path="/nowhere/foo"),
            virtual_path_def(
                "source/foo/foo-a.data",
                fs_path="/nowhere/foo/foo-a.data",
                content="data file",
            ),
            virtual_path_def(
                "source/foo/foo-b.data",
                fs_path="/nowhere/foo/foo-b.data",
                link_target="./foo-a.data",
            ),
            virtual_path_def("source/bar/", fs_path="/nowhere/bar"),
            virtual_path_def(
                "source/bar/bar-a.data",
                fs_path="/nowhere/bar/bar-a.data",
                content="data file",
            ),
            virtual_path_def(
                "source/bar/bar-b.data",
                fs_path="/nowhere/bar/bar-b.data",
                content="data file",
            ),
            virtual_path_def(
                "source/tool.sh",
                fs_path="/nowhere/source/tool.sh",
                content="#!/bin/sh\n# run some command ...",
            ),
        ]
    )
    manifest_content = textwrap.dedent(
        """\
        manifest-version: "0.1"
        installations:
          - multi-dest-install:
              sources:
              - source/foo/*
              - source/bar
              dest-dirs:
              - usr/share/foo
              - usr/share/foo2
          - multi-dest-install:
              source: source/tool.sh
              as:
               - usr/share/foo/tool
               - usr/share/foo2/tool
        """
    )
    manifest = manifest_parser_pkg_foo.parse_manifest(fd=manifest_content)
    all_pkgs = frozenset(manifest.all_packages)

    result = manifest.perform_installations(
        INTEGRATION_MODE_DH_DEBPUTY,
        install_request_context=InstallSearchDirContext(
            [
                SearchDir(debian_source_root_dir, all_pkgs),
            ],
            [],
        ),
    )
    assert "foo" in result
    foo_fs_root = result["foo"].fs_root

    for stem in ["foo", "foo2"]:
        foo_dir = foo_fs_root.lookup(f"/usr/share/{stem}")
        assert foo_dir is not None
        assert foo_dir.is_dir

        assert {"foo-a.data", "foo-b.data", "bar", "tool"} == {
            p.name for p in foo_dir.iterdir
        }

        tool = foo_dir["tool"]
        assert tool.is_file
        with tool.open() as fd:
            content = fd.read()
        assert content.startswith("#!/bin/sh")
        foo_a = foo_dir["foo-a.data"]
        assert foo_a.is_file
        assert foo_a.fs_path == "/nowhere/foo/foo-a.data"
        with foo_a.open() as fd:
            content = fd.read()
        assert "data" in content
        foo_b = foo_dir["foo-b.data"]
        assert foo_b.is_symlink
        assert foo_b.readlink() == "./foo-a.data"

        bar = foo_dir["bar"]
        assert bar.is_dir
        assert {"bar-a.data", "bar-b.data"} == {p.name for p in bar.iterdir}
        assert {"/nowhere/bar/bar-a.data", "/nowhere/bar/bar-b.data"} == {
            p.fs_path for p in bar.iterdir
        }


def test_install_rules_with_glob(manifest_parser_pkg_foo) -> None:
    debian_source_root_dir = build_virtual_file_system(
        [virtual_path_def(".", fs_path="/nowhere")]
    )
    debian_tmp_dir = build_virtual_file_system(
        [
            virtual_path_def(".", fs_path="/nowhere/debian/tmp"),
            virtual_path_def("usr/", fs_path="/nowhere/debian/tmp/usr"),
            virtual_path_def("usr/bin/", fs_path="/nowhere/debian/tmp/usr/bin"),
            virtual_path_def(
                "usr/bin/foo",
                fs_path="/nowhere/debian/tmp/usr/bin/foo",
                content="#!/bin/sh\n",
            ),
            virtual_path_def(
                "usr/bin/foo-util",
                fs_path="/nowhere/debian/tmp/usr/bin/foo-util",
                content="#!/bin/sh\n",
            ),
            virtual_path_def(
                "usr/bin/tool.sh",
                fs_path="/nowhere/debian/tmp/usr/bin/tool.sh",
                link_target="./foo",
            ),
            virtual_path_def("usr/share/", fs_path="/nowhere/debian/tmp/usr/share"),
            virtual_path_def(
                "usr/share/foo/", fs_path="/nowhere/debian/tmp/usr/share/foo"
            ),
            virtual_path_def(
                "usr/share/foo/foo.txt",
                fs_path="/nowhere/debian/tmp/usr/share/foo/foo.txt",
                content="A text file",
            ),
        ]
    )
    manifest_content = textwrap.dedent(
        """\
        manifest-version: "0.1"
        installations:
          - install:
              source: usr/bin/tool.sh
              as: usr/bin/tool
          - install:
              - /usr/share/foo
              - /usr/bin/foo*
        """
    )
    manifest = manifest_parser_pkg_foo.parse_manifest(fd=manifest_content)
    all_pkgs = frozenset(manifest.all_packages)

    result = manifest.perform_installations(
        INTEGRATION_MODE_DH_DEBPUTY,
        install_request_context=InstallSearchDirContext(
            [
                SearchDir(debian_tmp_dir, all_pkgs),
                SearchDir(debian_source_root_dir, all_pkgs),
            ],
            [debian_tmp_dir],
        ),
    )
    assert "foo" in result
    foo_fs_root = result["foo"].fs_root

    ub_dir = foo_fs_root.lookup("/usr/bin")
    assert ub_dir is not None
    assert ub_dir.is_dir
    assert not ub_dir.has_fs_path  # This will be "generated"

    tool = ub_dir.get("tool")
    assert tool is not None
    assert tool.is_symlink
    assert tool.readlink() == "./foo"

    assert {"foo", "foo-util", "tool"} == {p.name for p in ub_dir.iterdir}
    usf_dir = foo_fs_root.lookup("/usr/share/foo")
    assert usf_dir is not None
    assert usf_dir.is_dir
    # Here we are installing an actual directory, so it should be present too
    assert usf_dir.has_fs_path
    assert usf_dir.fs_path == "/nowhere/debian/tmp/usr/share/foo"
    assert {"foo.txt"} == {p.name for p in usf_dir.iterdir}


def test_install_rules_auto_discard_rules_dir(manifest_parser_pkg_foo) -> None:
    debian_source_root_dir = build_virtual_file_system(
        [virtual_path_def(".", fs_path="/nowhere")]
    )
    debian_tmp_dir = build_virtual_file_system(
        [
            virtual_path_def(".", fs_path="/nowhere/debian/tmp"),
            virtual_path_def("usr/", fs_path="/nowhere/debian/tmp/usr"),
            virtual_path_def("usr/lib/", fs_path="/nowhere/debian/tmp/usr/lib"),
            virtual_path_def(
                "usr/lib/libfoo.so.1.0.0",
                fs_path="/nowhere/debian/tmp/usr/lib/libfoo.so.1.0.0",
                content="Not really an ELF FILE",
            ),
            virtual_path_def(
                "usr/lib/libfoo.la",
                fs_path="/nowhere/debian/tmp/usr/lib/libfoo.la",
                content="Not really a LA FILE",
            ),
        ]
    )
    manifest_content = textwrap.dedent(
        """\
        manifest-version: "0.1"
        installations:
          - install:
              - /usr/lib
        """
    )
    manifest = manifest_parser_pkg_foo.parse_manifest(fd=manifest_content)

    all_pkgs = frozenset(manifest.all_packages)

    result = manifest.perform_installations(
        INTEGRATION_MODE_DH_DEBPUTY,
        install_request_context=InstallSearchDirContext(
            [
                SearchDir(debian_tmp_dir, all_pkgs),
                SearchDir(debian_source_root_dir, all_pkgs),
            ],
            [debian_tmp_dir],
        ),
    )
    assert "foo" in result
    foo_fs_root = result["foo"].fs_root

    lib_dir = foo_fs_root.lookup("/usr/lib")
    assert lib_dir is not None
    assert lib_dir.is_dir
    assert lib_dir.has_fs_path
    assert lib_dir.fs_path == "/nowhere/debian/tmp/usr/lib"

    so_file = lib_dir.get("libfoo.so.1.0.0")
    assert so_file is not None
    assert so_file.is_file
    assert so_file.has_fs_path
    assert so_file.fs_path == "/nowhere/debian/tmp/usr/lib/libfoo.so.1.0.0"

    assert {"libfoo.so.1.0.0"} == {p.name for p in lib_dir.iterdir}


def test_install_rules_auto_discard_rules_glob(manifest_parser_pkg_foo) -> None:
    debian_source_root_dir = build_virtual_file_system(
        [virtual_path_def(".", fs_path="/nowhere")]
    )
    debian_tmp_dir = build_virtual_file_system(
        [
            virtual_path_def(".", fs_path="/nowhere/debian/tmp"),
            virtual_path_def("usr/", fs_path="/nowhere/debian/tmp/usr"),
            virtual_path_def("usr/lib/", fs_path="/nowhere/debian/tmp/usr/lib"),
            virtual_path_def(
                "usr/lib/libfoo.so.1.0.0",
                fs_path="/nowhere/debian/tmp/usr/lib/libfoo.so.1.0.0",
                content="Not really an ELF FILE",
            ),
            virtual_path_def(
                "usr/lib/libfoo.la",
                fs_path="/nowhere/debian/tmp/usr/lib/libfoo.la",
                content="Not really an ELF FILE",
            ),
        ]
    )
    manifest_content = textwrap.dedent(
        """\
        manifest-version: "0.1"
        installations:
          - install:
              - /usr/lib/*
        """
    )
    manifest = manifest_parser_pkg_foo.parse_manifest(fd=manifest_content)
    all_pkgs = frozenset(manifest.all_packages)

    result = manifest.perform_installations(
        INTEGRATION_MODE_DH_DEBPUTY,
        install_request_context=InstallSearchDirContext(
            [
                SearchDir(debian_tmp_dir, all_pkgs),
                SearchDir(debian_source_root_dir, all_pkgs),
            ],
            [debian_tmp_dir],
        ),
    )
    assert "foo" in result
    foo_fs_root = result["foo"].fs_root

    lib_dir = foo_fs_root.lookup("/usr/lib")
    assert lib_dir is not None
    assert lib_dir.is_dir
    assert not lib_dir.has_fs_path

    so_file = lib_dir.get("libfoo.so.1.0.0")
    assert so_file is not None
    assert so_file.is_file
    assert so_file.has_fs_path
    assert so_file.fs_path == "/nowhere/debian/tmp/usr/lib/libfoo.so.1.0.0"

    assert {"libfoo.so.1.0.0"} == {p.name for p in lib_dir.iterdir}


def test_install_rules_auto_discard_rules_overruled_by_explicit_install_rule(
    manifest_parser_pkg_foo,
) -> None:
    debian_source_root_dir = build_virtual_file_system(
        [virtual_path_def(".", fs_path="/nowhere")]
    )
    debian_tmp_dir = build_virtual_file_system(
        [
            virtual_path_def(".", fs_path="/nowhere/debian/tmp"),
            virtual_path_def("usr/", fs_path="/nowhere/debian/tmp/usr"),
            virtual_path_def("usr/lib/", fs_path="/nowhere/debian/tmp/usr/lib"),
            virtual_path_def(
                "usr/lib/libfoo.so.1.0.0",
                fs_path="/nowhere/debian/tmp/usr/lib/libfoo.so.1.0.0",
                content="Not really an ELF FILE",
            ),
            virtual_path_def(
                "usr/lib/libfoo.la",
                fs_path="/nowhere/debian/tmp/usr/lib/libfoo.la",
                content="Not really an ELF FILE",
            ),
            virtual_path_def(
                "usr/lib/libfoo.so",
                fs_path="/nowhere/debian/tmp/usr/lib/libfoo.so",
                link_target="libfoo.so.1.0.0",
            ),
        ]
    )
    manifest_content = textwrap.dedent(
        """\
        manifest-version: "0.1"
        installations:
          - install:
              - /usr/lib
              - /usr/lib/libfoo.la
        """
    )
    manifest = manifest_parser_pkg_foo.parse_manifest(fd=manifest_content)
    all_pkgs = frozenset(manifest.all_packages)

    result = manifest.perform_installations(
        INTEGRATION_MODE_DH_DEBPUTY,
        install_request_context=InstallSearchDirContext(
            [
                SearchDir(debian_tmp_dir, all_pkgs),
                SearchDir(debian_source_root_dir, all_pkgs),
            ],
            [debian_tmp_dir],
        ),
    )
    assert "foo" in result
    foo_fs_root = result["foo"].fs_root

    lib_dir = foo_fs_root.lookup("/usr/lib")
    assert lib_dir is not None
    assert lib_dir.is_dir
    assert lib_dir.has_fs_path
    assert lib_dir.fs_path == "/nowhere/debian/tmp/usr/lib"

    so_file = lib_dir.get("libfoo.so.1.0.0")
    assert so_file is not None
    assert so_file.is_file
    assert so_file.has_fs_path
    assert so_file.fs_path == "/nowhere/debian/tmp/usr/lib/libfoo.so.1.0.0"

    la_file = lib_dir.get("libfoo.la")
    assert la_file is not None
    assert la_file.is_file
    assert la_file.has_fs_path
    assert la_file.fs_path == "/nowhere/debian/tmp/usr/lib/libfoo.la"

    so_link = lib_dir.get("libfoo.so")
    assert so_link is not None
    assert so_link.is_symlink
    assert so_link.readlink() == "libfoo.so.1.0.0"

    assert {"libfoo.so.1.0.0", "libfoo.so", "libfoo.la"} == {
        p.name for p in lib_dir.iterdir
    }


def test_install_rules_install_as_with_var(manifest_parser_pkg_foo) -> None:
    debian_source_root_dir = build_virtual_file_system(
        [
            virtual_path_def(".", fs_path="/nowhere"),
            virtual_path_def("build/", fs_path="/nowhere/build"),
            virtual_path_def(
                "build/private-arch-tool.sh",
                fs_path="/nowhere/build/private-arch-tool.sh",
                content="#!/bin/sh\n",
            ),
        ]
    )
    debian_tmp_dir = build_virtual_file_system(
        [
            virtual_path_def(".", fs_path="/nowhere/debian/tmp"),
        ]
    )
    manifest_content = textwrap.dedent(
        """\
        manifest-version: "0.1"
        installations:
          - install:
              source: build/private-arch-tool.sh
              as: /usr/lib/{{DEB_HOST_MULTIARCH}}/foo/private-arch-tool
        """
    )
    manifest = manifest_parser_pkg_foo.parse_manifest(fd=manifest_content)
    all_pkgs = frozenset(manifest.all_packages)

    result = manifest.perform_installations(
        INTEGRATION_MODE_DH_DEBPUTY,
        install_request_context=InstallSearchDirContext(
            [
                SearchDir(debian_tmp_dir, all_pkgs),
                SearchDir(debian_source_root_dir, all_pkgs),
            ],
            [debian_tmp_dir],
        ),
    )
    assert "foo" in result
    foo_fs_root = result["foo"].fs_root

    # The variable is always resolved in amd64 context, so we can hard code the resolved
    # variable
    tool = foo_fs_root.lookup("/usr/lib/x86_64-linux-gnu/foo/private-arch-tool")
    assert tool is not None
    assert tool.is_file
    assert tool.fs_path == "/nowhere/build/private-arch-tool.sh"


def test_install_rules_no_matches(manifest_parser_pkg_foo) -> None:
    debian_source_root_dir = build_virtual_file_system(
        [
            virtual_path_def(".", fs_path="/nowhere"),
            virtual_path_def("build/", fs_path="/nowhere/build"),
            virtual_path_def(
                "build/private-arch-tool.sh",
                fs_path="/nowhere/build/private-arch-tool.sh",
                content="#!/bin/sh\n",
            ),
        ]
    )
    debian_tmp_dir = build_virtual_file_system(
        [
            virtual_path_def(".", fs_path="/nowhere/debian/tmp"),
        ]
    )
    manifest_content = textwrap.dedent(
        """\
        manifest-version: "0.1"
        installations:
          - install:
              # Typo: the path should have ended with ".sh"
              source: build/private-arch-tool
              as: /usr/lib/foo/private-arch-tool
        """
    )
    manifest = manifest_parser_pkg_foo.parse_manifest(fd=manifest_content)
    all_pkgs = frozenset(manifest.all_packages)

    with pytest.raises(NoMatchForInstallPatternError) as e_info:
        manifest.perform_installations(
            INTEGRATION_MODE_DH_DEBPUTY,
            install_request_context=InstallSearchDirContext(
                [
                    SearchDir(debian_tmp_dir, all_pkgs),
                    SearchDir(debian_source_root_dir, all_pkgs),
                ],
                [debian_tmp_dir],
            ),
        )
    expected_msg = (
        "There were no matches for build/private-arch-tool in /nowhere/debian/tmp, /nowhere"
        " (definition: installations[0].install [Line 5 column 6])."
        " Match rule: ./build/private-arch-tool (the exact path / no globbing)"
    )
    assert e_info.value.message == expected_msg


def test_install_rules_per_package_search_dirs(manifest_parser_pkg_foo_w_udeb) -> None:
    debian_source_root_dir = build_virtual_file_system(
        [virtual_path_def(".", fs_path="/nowhere")]
    )
    debian_tmp_deb_dir = build_virtual_file_system(
        [
            virtual_path_def(".", fs_path="/nowhere/debian/tmp-deb"),
            virtual_path_def("usr/", fs_path="/nowhere/debian/tmp-deb/usr"),
            virtual_path_def("usr/bin/", fs_path="/nowhere/debian/tmp-deb/usr/bin"),
            virtual_path_def(
                "usr/bin/foo",
                fs_path="/nowhere/debian/tmp-deb/usr/bin/foo",
                content="#!/bin/sh\ndeb",
            ),
            virtual_path_def(
                "usr/bin/foo-util",
                fs_path="/nowhere/debian/tmp-deb/usr/bin/foo-util",
                content="#!/bin/sh\ndeb",
            ),
            virtual_path_def(
                "usr/bin/tool.sh",
                fs_path="/nowhere/debian/tmp-deb/usr/bin/tool.sh",
                link_target="./foo",
            ),
            virtual_path_def("usr/share/", fs_path="/nowhere/debian/tmp-deb/usr/share"),
            virtual_path_def(
                "usr/share/foo/", fs_path="/nowhere/debian/tmp-deb/usr/share/foo"
            ),
            virtual_path_def(
                "usr/share/foo/foo.txt",
                fs_path="/nowhere/debian/tmp-deb/usr/share/foo/foo.txt",
                content="A deb text file",
            ),
        ]
    )
    debian_tmp_udeb_dir = build_virtual_file_system(
        [
            virtual_path_def(".", fs_path="/nowhere/debian/tmp-udeb"),
            virtual_path_def("usr/", fs_path="/nowhere/debian/tmp-udeb/usr"),
            virtual_path_def("usr/bin/", fs_path="/nowhere/debian/tmp-udeb/usr/bin"),
            virtual_path_def(
                "usr/bin/foo",
                fs_path="/nowhere/debian/tmp-udeb/usr/bin/foo",
                content="#!/bin/sh\nudeb",
            ),
            virtual_path_def(
                "usr/bin/foo-util",
                fs_path="/nowhere/debian/tmp-udeb/usr/bin/foo-util",
                content="#!/bin/sh\nudeb",
            ),
            virtual_path_def(
                "usr/bin/tool.sh",
                fs_path="/nowhere/debian/tmp-udeb/usr/bin/tool.sh",
                link_target="./foo",
            ),
            virtual_path_def(
                "usr/share/", fs_path="/nowhere/debian/tmp-udeb/usr/share"
            ),
            virtual_path_def(
                "usr/share/foo/", fs_path="/nowhere/debian/tmp-udeb/usr/share/foo"
            ),
            virtual_path_def(
                "usr/share/foo/foo.txt",
                fs_path="/nowhere/debian/tmp-udeb/usr/share/foo/foo.txt",
                content="A udeb text file",
            ),
        ]
    )
    manifest_content = textwrap.dedent(
        """\
        manifest-version: "0.1"
        installations:
          - install:
              source: usr/bin/tool.sh
              as: usr/bin/tool
              into:
                - foo
                - foo-udeb
          - install:
              sources:
                - /usr/share/foo
                - /usr/bin/foo*
              into:
                - foo
                - foo-udeb
        """
    )
    manifest = manifest_parser_pkg_foo_w_udeb.parse_manifest(fd=manifest_content)
    all_pkgs = frozenset(manifest.all_packages)
    all_deb_pkgs = frozenset({p for p in all_pkgs if not p.is_udeb})
    all_udeb_pkgs = frozenset({p for p in all_pkgs if p.is_udeb})

    result = manifest.perform_installations(
        INTEGRATION_MODE_DH_DEBPUTY,
        install_request_context=InstallSearchDirContext(
            [
                SearchDir(debian_tmp_deb_dir, all_deb_pkgs),
                SearchDir(debian_tmp_udeb_dir, all_udeb_pkgs),
                SearchDir(debian_source_root_dir, all_pkgs),
            ],
            [debian_tmp_deb_dir],
        ),
    )
    for pkg, ptype in [
        ("foo", "deb"),
        ("foo-udeb", "udeb"),
    ]:
        assert pkg in result
        fs_root = result[pkg].fs_root

        ub_dir = fs_root.lookup("/usr/bin")
        assert ub_dir is not None
        assert ub_dir.is_dir
        assert not ub_dir.has_fs_path  # This will be "generated"

        tool = ub_dir.get("tool")
        assert tool is not None
        assert tool.is_symlink
        assert tool.readlink() == "./foo"

        assert {"foo", "foo-util", "tool"} == {p.name for p in ub_dir.iterdir}

        for p in ub_dir.iterdir:
            assert p.has_fs_path
            assert f"/nowhere/debian/tmp-{ptype}/usr/bin" in p.fs_path

        usf_dir = fs_root.lookup("/usr/share/foo")
        assert usf_dir is not None
        assert usf_dir.is_dir
        # Here we are installing an actual directory, so it should be present too
        assert usf_dir.has_fs_path
        assert usf_dir.fs_path == f"/nowhere/debian/tmp-{ptype}/usr/share/foo"
        assert {"foo.txt"} == {p.name for p in usf_dir.iterdir}
        foo_txt = usf_dir["foo.txt"]
        assert foo_txt.fs_path == f"/nowhere/debian/tmp-{ptype}/usr/share/foo/foo.txt"


def test_install_rules_multi_into(manifest_parser_pkg_foo_w_udeb) -> None:
    debian_source_root_dir = build_virtual_file_system(
        [
            virtual_path_def(".", fs_path="/nowhere"),
            virtual_path_def("source/", fs_path="/nowhere/source"),
            virtual_path_def("source/foo/", fs_path="/nowhere/foo"),
            virtual_path_def(
                "source/foo/foo-a.data",
                fs_path="/nowhere/foo/foo-a.data",
                content="data file",
            ),
            virtual_path_def(
                "source/foo/foo-b.data",
                fs_path="/nowhere/foo/foo-b.data",
                link_target="./foo-a.data",
            ),
            virtual_path_def("source/bar/", fs_path="/nowhere/bar"),
            virtual_path_def(
                "source/bar/bar-a.data",
                fs_path="/nowhere/bar/bar-a.data",
                content="data file",
            ),
            virtual_path_def(
                "source/bar/bar-b.data",
                fs_path="/nowhere/bar/bar-b.data",
                content="data file",
            ),
            virtual_path_def(
                "source/tool.sh",
                fs_path="/nowhere/source/tool.sh",
                content="#!/bin/sh\n# run some command ...",
            ),
        ]
    )
    manifest_content = textwrap.dedent(
        """\
        manifest-version: "0.1"
        installations:
          - install:
              sources:
              - source/foo/*
              - source/bar
              dest-dir: usr/share/foo
              into:
              - foo
              - foo-udeb
          - install:
              source: source/tool.sh
              as: usr/share/foo/tool
              into:
              - foo
              - foo-udeb
        """
    )
    manifest = manifest_parser_pkg_foo_w_udeb.parse_manifest(fd=manifest_content)
    all_pkgs = frozenset(manifest.all_packages)

    result = manifest.perform_installations(
        INTEGRATION_MODE_DH_DEBPUTY,
        install_request_context=InstallSearchDirContext(
            [
                SearchDir(debian_source_root_dir, all_pkgs),
            ],
            [],
        ),
    )
    for pkg in ["foo", "foo-udeb"]:
        assert pkg in result
        foo_fs_root = result[pkg].fs_root

        foo_dir = foo_fs_root.lookup("/usr/share/foo")
        assert foo_dir is not None
        assert foo_dir.is_dir

        assert {"foo-a.data", "foo-b.data", "bar", "tool"} == {
            p.name for p in foo_dir.iterdir
        }

        tool = foo_dir["tool"]
        assert tool.is_file
        with tool.open() as fd:
            content = fd.read()
        assert content.startswith("#!/bin/sh")
        foo_a = foo_dir["foo-a.data"]
        assert foo_a.is_file
        assert foo_a.fs_path == "/nowhere/foo/foo-a.data"
        with foo_a.open() as fd:
            content = fd.read()
        assert "data" in content
        foo_b = foo_dir["foo-b.data"]
        assert foo_b.is_symlink
        assert foo_b.readlink() == "./foo-a.data"

        bar = foo_dir["bar"]
        assert bar.is_dir
        assert {"bar-a.data", "bar-b.data"} == {p.name for p in bar.iterdir}
        assert {"/nowhere/bar/bar-a.data", "/nowhere/bar/bar-b.data"} == {
            p.fs_path for p in bar.iterdir
        }


def test_auto_install_d_pkg(manifest_parser_pkg_foo_w_udeb) -> None:
    debian_source_root_dir = build_virtual_file_system(
        [virtual_path_def(".", fs_path="/nowhere")]
    )
    debian_foo_dir = build_virtual_file_system(
        [
            virtual_path_def(".", fs_path="/nowhere/debian/foo"),
            virtual_path_def("usr/", fs_path="/nowhere/debian/foo/usr"),
            virtual_path_def("usr/bin/", fs_path="/nowhere/debian/foo/usr/bin"),
            virtual_path_def(
                "usr/bin/foo",
                fs_path="/nowhere/debian/foo/usr/bin/foo",
                content="#!/bin/sh\ndeb",
            ),
            virtual_path_def(
                "usr/bin/foo-util",
                fs_path="/nowhere/debian/foo/usr/bin/foo-util",
                content="#!/bin/sh\ndeb",
            ),
            virtual_path_def(
                "usr/bin/tool",
                fs_path="/nowhere/debian/foo/usr/bin/tool",
                link_target="./foo",
            ),
            virtual_path_def("usr/share/", fs_path="/nowhere/debian/foo/usr/share"),
            virtual_path_def(
                "usr/share/foo/", fs_path="/nowhere/debian/foo/usr/share/foo"
            ),
            virtual_path_def(
                "usr/share/foo/foo.txt",
                fs_path="/nowhere/debian/foo/usr/share/foo/foo.txt",
                content="A deb text file",
            ),
        ]
    )
    debian_foo_udeb_dir = build_virtual_file_system(
        [
            virtual_path_def(".", fs_path="/nowhere/debian/foo-udeb"),
            virtual_path_def("usr/", fs_path="/nowhere/debian/foo-udeb/usr"),
            virtual_path_def("usr/bin/", fs_path="/nowhere/debian/foo-udeb/usr/bin"),
            virtual_path_def(
                "usr/bin/foo",
                fs_path="/nowhere/debian/foo-udeb/usr/bin/foo",
                content="#!/bin/sh\nudeb",
            ),
            virtual_path_def(
                "usr/bin/foo-util",
                fs_path="/nowhere/debian/foo-udeb/usr/bin/foo-util",
                content="#!/bin/sh\nudeb",
            ),
            virtual_path_def(
                "usr/bin/tool",
                fs_path="/nowhere/debian/foo-udeb/usr/bin/tool",
                link_target="./foo",
            ),
            virtual_path_def(
                "usr/share/", fs_path="/nowhere/debian/foo-udeb/usr/share"
            ),
            virtual_path_def(
                "usr/share/foo/", fs_path="/nowhere/debian/foo-udeb/usr/share/foo"
            ),
            virtual_path_def(
                "usr/share/foo/foo.txt",
                fs_path="/nowhere/debian/foo-udeb/usr/share/foo/foo.txt",
                content="A udeb text file",
            ),
        ]
    )
    manifest_content = textwrap.dedent(
        """\
        manifest-version: "0.1"
        """
    )
    manifest = manifest_parser_pkg_foo_w_udeb.parse_manifest(fd=manifest_content)
    all_pkgs = frozenset(manifest.all_packages)

    result = manifest.perform_installations(
        INTEGRATION_MODE_DH_DEBPUTY,
        install_request_context=InstallSearchDirContext(
            [
                SearchDir(debian_source_root_dir, all_pkgs),
            ],
            [debian_foo_dir],
            {
                "foo": debian_foo_dir,
                "foo-udeb": debian_foo_udeb_dir,
            },
        ),
    )
    for pkg in ["foo", "foo-udeb"]:
        assert pkg in result
        fs_root = result[pkg].fs_root
        ub_dir = fs_root.lookup("/usr/bin")
        assert ub_dir is not None
        assert ub_dir.is_dir
        assert ub_dir.has_fs_path
        assert ub_dir.fs_path == f"/nowhere/debian/{pkg}/usr/bin"

        assert {"foo", "foo-util", "tool"} == {p.name for p in ub_dir.iterdir}

        tool = ub_dir.get("tool")
        assert tool is not None
        assert tool.is_symlink
        assert tool.readlink() == "./foo"

        for p in ub_dir.iterdir:
            assert p.has_fs_path
            assert f"/nowhere/debian/{pkg}/usr/bin" in p.fs_path

        usf_dir = fs_root.lookup("/usr/share/foo")
        assert usf_dir is not None
        assert usf_dir.is_dir
        # Here we are installing an actual directory, so it should be present too
        assert usf_dir.has_fs_path
        assert usf_dir.fs_path == f"/nowhere/debian/{pkg}/usr/share/foo"
        assert {"foo.txt"} == {p.name for p in usf_dir.iterdir}
        foo_txt = usf_dir["foo.txt"]
        assert foo_txt.fs_path == f"/nowhere/debian/{pkg}/usr/share/foo/foo.txt"


def test_install_doc_rules_ignore_udeb(manifest_parser_pkg_foo_w_udeb) -> None:
    debian_source_root_dir = build_virtual_file_system(
        [
            virtual_path_def(".", fs_path="/nowhere"),
            virtual_path_def("source/", fs_path="/nowhere/source"),
            virtual_path_def("source/foo/", fs_path="/nowhere/foo"),
            virtual_path_def(
                "source/foo/foo-a.txt",
                fs_path="/nowhere/foo/foo-a.txt",
                content="data file",
            ),
            virtual_path_def(
                "source/foo/foo-b.txt",
                fs_path="/nowhere/foo/foo-b.txt",
                link_target="./foo-a.txt",
            ),
            virtual_path_def("source/html/", fs_path="/nowhere/html"),
            virtual_path_def(
                "source/html/bar-a.html",
                fs_path="/nowhere/html/bar-a.html",
                content="data file",
            ),
            virtual_path_def(
                "source/html/bar-b.html",
                fs_path="/nowhere/html/bar-b.html",
                content="data file",
            ),
        ]
    )
    manifest_content = textwrap.dedent(
        """\
        manifest-version: "0.1"
        installations:
          - install-doc:
              sources:
              - source/foo/*
              - source/html
        """
    )
    manifest = manifest_parser_pkg_foo_w_udeb.parse_manifest(fd=manifest_content)
    all_pkgs = frozenset(manifest.all_packages)

    result = manifest.perform_installations(
        INTEGRATION_MODE_DH_DEBPUTY,
        install_request_context=InstallSearchDirContext(
            [
                SearchDir(debian_source_root_dir, all_pkgs),
            ],
            [],
        ),
    )
    assert "foo" in result
    foo_fs_root = result["foo"].fs_root

    foo_dir = foo_fs_root.lookup("/usr/share/doc/foo")
    assert foo_dir is not None
    assert foo_dir.is_dir

    assert {"foo-a.txt", "foo-b.txt", "html"} == {p.name for p in foo_dir.iterdir}

    foo_a = foo_dir["foo-a.txt"]
    assert foo_a.is_file
    assert foo_a.fs_path == "/nowhere/foo/foo-a.txt"
    foo_b = foo_dir["foo-b.txt"]
    assert foo_b.is_symlink
    assert foo_b.readlink() == "./foo-a.txt"

    html_dir = foo_dir["html"]
    assert html_dir.is_dir
    assert {"bar-a.html", "bar-b.html"} == {p.name for p in html_dir.iterdir}
    assert {"/nowhere/html/bar-a.html", "/nowhere/html/bar-b.html"} == {
        p.fs_path for p in html_dir.iterdir
    }

    assert "foo-udeb" in result
    foo_udeb_fs_root = result["foo-udeb"].fs_root

    udeb_doc_dir = foo_udeb_fs_root.lookup("/usr/share/doc")
    assert udeb_doc_dir is None


def test_bts_1093680_crash_inactive_package(
    flexible_manifest_parser: FlexibleManifestParser,
) -> None:
    debian_source_root_dir = build_virtual_file_system(
        [
            virtual_path_def(".", fs_path="/nowhere"),
            virtual_path_def("examples/", fs_path="/nowhere/examples"),
            virtual_path_def("examples/case-a/", fs_path="/nowhere/examples/case-a"),
            virtual_path_def(
                "examples/case-a/data/", fs_path="/nowhere/examples/case-a/data"
            ),
            virtual_path_def(
                "examples/case-a/data/foo.txt",
                fs_path="/nowhere/examples/case-a/data/foo.txt",
                content="data file",
            ),
            virtual_path_def("examples/case-b/", fs_path="/nowhere/examples/case-b"),
            virtual_path_def(
                "examples/case-b/data/", fs_path="/nowhere/examples/case-b/data"
            ),
            virtual_path_def(
                "examples/case-b/data/foo.txt",
                fs_path="/nowhere/examples/case-b/data/foo.txt",
                content="data file",
            ),
        ]
    )
    manifest_content = textwrap.dedent(
        """\
        manifest-version: "0.1"
        installations:
          - install-examples:
              source: "examples/*"
              into: foo-data
        """
    )

    foo_fields = Deb822(
        {
            "Package": "foo",
            "Architecture": "any",
        }
    )
    foo_data_fields = Deb822(
        {
            "Package": "foo-data",
            "Architecture": "all",
        }
    )
    # An arch:any build (-B => foo is active, foo-data is inactive)
    flexible_manifest_parser.binary_package(foo_fields, is_main_package=True)
    flexible_manifest_parser.binary_package(foo_data_fields, should_be_acted_on=False)

    manifest = flexible_manifest_parser.parse_manifest(manifest_content)
    all_pkgs = frozenset(manifest.all_packages)

    manifest.perform_installations(
        INTEGRATION_MODE_DH_DEBPUTY,
        install_request_context=InstallSearchDirContext(
            [
                SearchDir(debian_source_root_dir, all_pkgs),
            ],
            [debian_source_root_dir],
        ),
    )


@pytest.mark.parametrize(
    "should_act_on_foo,should_act_on_foo_data",
    [
        (True, True),
        (True, False),
        (False, False),
        (False, False),
    ],
)
def test_debputy_132(
    flexible_manifest_parser: FlexibleManifestParser,
    should_act_on_foo: bool,
    should_act_on_foo_data: bool,
) -> None:
    debian_source_root_dir = build_virtual_file_system(
        [
            virtual_path_def(".", fs_path="/nowhere"),
            virtual_path_def("built/", fs_path="/nowhere/built"),
            virtual_path_def("built/data/", fs_path="/nowhere/built/data"),
            virtual_path_def("built/data/foo.txt", fs_path="/nowhere/data/foo.txt"),
            virtual_path_def("built/bin/", fs_path="/nowhere/built/bin"),
            virtual_path_def(
                "built/bin/foo",
                fs_path="/nowhere/built/bin/foo",
                content="#!/bin/sh\n\nexit 0",
            ),
        ]
    )
    manifest_content = textwrap.dedent(
        """\
        manifest-version: "0.1"
        installations:
          - install:
              source: "built/data"
              dest-dir: "usr/share/foo"
              into: foo-data
          - install:
              source: "built/bin"
              dest-dir: "usr"
              into: foo
        """
    )

    foo_fields = Deb822(
        {
            "Package": "foo",
            "Architecture": "any",
        }
    )
    foo_data_fields = Deb822(
        {
            "Package": "foo-data",
            "Architecture": "all",
        }
    )
    # An arch:any build (-B => foo is active, foo-data is inactive)
    flexible_manifest_parser.binary_package(
        foo_fields,
        is_main_package=True,
        should_be_acted_on=should_act_on_foo,
    )
    flexible_manifest_parser.binary_package(
        foo_data_fields,
        should_be_acted_on=should_act_on_foo_data,
    )

    manifest = flexible_manifest_parser.parse_manifest(manifest_content)
    all_pkgs = frozenset(manifest.all_packages)

    manifest.perform_installations(
        INTEGRATION_MODE_DH_DEBPUTY,
        install_request_context=InstallSearchDirContext(
            [
                SearchDir(debian_source_root_dir, all_pkgs),
            ],
            [debian_source_root_dir],
        ),
    )


def test_uncovered_installation(
    manifest_parser_pkg_foo: YAMLManifestParser,
) -> None:
    debian_source_root_dir = build_virtual_file_system(
        [
            virtual_path_def(".", fs_path="/nowhere"),
            virtual_path_def("usr/", fs_path="/nowhere/debian/tmp/usr"),
            virtual_path_def("usr/bin/", fs_path="/nowhere/debian/tmp/usr/bin"),
            virtual_path_def(
                "usr/bin/foo",
                fs_path="/nowhere/debian/tmp/usr/bin/foo",
                content="#!/bin/sh\n\nexit 0",
            ),
            virtual_path_def(
                "usr/bin/bar",
                fs_path="/nowhere/debian/tmp/usr/bin/bar",
                content="#!/bin/sh\n\nexit 0",
            ),
        ]
    )
    manifest_content = textwrap.dedent(
        """\
        manifest-version: "0.1"
        installations:
          - install:
              source: "usr/bin/foo"
              into: foo
        """
    )

    manifest = manifest_parser_pkg_foo.parse_manifest(fd=manifest_content)
    all_pkgs = frozenset(manifest.all_packages)

    with pytest.raises(PathNotCoveredByInstallRulesError) as exec_info:
        manifest.perform_installations(
            INTEGRATION_MODE_DH_DEBPUTY,
            install_request_context=InstallSearchDirContext(
                [
                    SearchDir(debian_source_root_dir, all_pkgs),
                ],
                [debian_source_root_dir],
            ),
        )

    assert len(exec_info.value.unmatched_paths) == 1
    assert exec_info.value.unmatched_paths[0].path == "./usr/bin/bar"
    assert exec_info.value.search_dir is debian_source_root_dir
