import io
import textwrap
from typing import Iterable, Callable, Optional, List, Tuple, Sequence

import pytest

from debputy.dh_migration.migrators import Migrator
from debputy.dh_migration.migrators_impl import (
    migrate_tmpfile,
    migrate_lintian_overrides_files,
    detect_pam_files,
    migrate_doc_base_files,
    migrate_installexamples_file,
    migrate_installdocs_file,
    migrate_install_file,
    migrate_maintscript,
    migrate_links_files,
    detect_dh_addons,
    migrate_not_installed_file,
    migrate_installman_file,
    migrate_bash_completion,
    migrate_installinfo_file,
    migrate_dh_installsystemd_files,
)
from debputy.dh_migration.models import (
    FeatureMigration,
    AcceptableMigrationIssues,
    UnsupportedFeature,
)
from debputy.highlevel_manifest import HighLevelManifest
from debputy.highlevel_manifest_parser import YAMLManifestParser
from debputy.plugin.api import virtual_path_def, VirtualPath
from debputy.plugin.api.test_api import (
    build_virtual_file_system,
)


DEBIAN_DIR_ENTRY = virtual_path_def(".", fs_path="/nowhere/debian")


@pytest.fixture()
def manifest_parser_pkg_foo_factory(
    amd64_dpkg_architecture_variables,
    dpkg_arch_query,
    package_single_foo_arch_all_cxt_amd64,
    amd64_substitution,
    no_profiles_or_build_options,
    debputy_plugin_feature_set,
) -> Callable[[], YAMLManifestParser]:
    # We need an empty directory to avoid triggering packager provided files.
    debian_dir = build_virtual_file_system([])

    def _factory():
        return YAMLManifestParser(
            "debian/test-debputy.manifest",
            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,
            debian_dir=debian_dir,
        )

    return _factory


@pytest.fixture(scope="session")
def accept_no_migration_issues() -> AcceptableMigrationIssues:
    return AcceptableMigrationIssues(frozenset())


@pytest.fixture(scope="session")
def accept_any_migration_issues() -> AcceptableMigrationIssues:
    return AcceptableMigrationIssues(frozenset(["ALL"]))


@pytest.fixture
def empty_manifest_pkg_foo(
    manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
) -> HighLevelManifest:
    return manifest_parser_pkg_foo_factory().build_manifest()


def run_migrator(
    migrator: Migrator,
    path: VirtualPath,
    manifest: HighLevelManifest,
    acceptable_migration_issues: AcceptableMigrationIssues,
) -> FeatureMigration:
    feature_migration = FeatureMigration(migrator.__name__)
    migrator(path, manifest, acceptable_migration_issues, feature_migration)
    return feature_migration


def _assert_unsupported_feature(
    migrator: Migrator,
    path: VirtualPath,
    manifest: HighLevelManifest,
    acceptable_migration_issues: AcceptableMigrationIssues,
):
    with pytest.raises(UnsupportedFeature) as e:
        run_migrator(migrator, path, manifest, acceptable_migration_issues)
    return e


def _write_manifest(manifest: HighLevelManifest) -> str:
    with io.StringIO() as fd:
        manifest.mutable_manifest.write_to(fd)
        return fd.getvalue()


def _verify_migrator_generates_parsable_manifest(
    migrator: Migrator,
    parser_factory: Callable[[], YAMLManifestParser],
    acceptable_migration_issues: AcceptableMigrationIssues,
    dh_config_name: str,
    dh_config_content: str,
    expected_manifest_content: str,
    expected_warnings: Optional[List[str]] = None,
    expected_renamed_paths: Optional[List[Tuple[str, str]]] = None,
    expected_removals: Optional[List[str]] = None,
    required_plugins: Optional[Sequence[str]] = tuple(),
    dh_config_mode: Optional[int] = None,
) -> None:
    # No file, no changes
    empty_fs = build_virtual_file_system([DEBIAN_DIR_ENTRY])
    migration = run_migrator(
        migrator,
        empty_fs,
        parser_factory().build_manifest(),
        acceptable_migration_issues,
    )

    assert not migration.anything_to_do
    assert not migration.warnings
    assert not migration.remove_paths_on_success
    assert not migration.rename_paths_on_success
    assert not migration.required_plugins

    if dh_config_mode is None:
        if dh_config_content.startswith(("#!/usr/bin/dh-exec", "#! /usr/bin/dh-exec")):
            dh_config_mode = 0o755
        else:
            dh_config_mode = 0o644

    # Test with a dh_config file now
    fs_w_dh_config = build_virtual_file_system(
        [
            DEBIAN_DIR_ENTRY,
            virtual_path_def(
                dh_config_name,
                fs_path=f"/nowhere/debian/{dh_config_name}",
                content=dh_config_content,
                mode=dh_config_mode,
            ),
        ]
    )
    manifest = parser_factory().build_manifest()

    migration = run_migrator(
        migrator,
        fs_w_dh_config,
        manifest,
        acceptable_migration_issues,
    )

    assert migration.anything_to_do
    if expected_warnings is not None:
        assert migration.warnings == expected_warnings
    else:
        assert not migration.warnings
    assert migration.remove_paths_on_success == [f"/nowhere/debian/{dh_config_name}"]
    if expected_removals is None:
        assert migration.remove_paths_on_success == [
            f"/nowhere/debian/{dh_config_name}"
        ]
    else:
        assert migration.remove_paths_on_success == expected_removals
    if expected_renamed_paths is not None:
        assert migration.rename_paths_on_success == expected_renamed_paths
    else:
        assert not migration.rename_paths_on_success
    assert tuple(migration.required_plugins) == tuple(required_plugins)
    actual_manifest = _write_manifest(manifest)
    assert actual_manifest == expected_manifest_content

    # Verify that the output is actually parsable
    parser_factory().parse_manifest(fd=actual_manifest)


def test_migrate_tmpfile(
    empty_manifest_pkg_foo: HighLevelManifest,
    accept_no_migration_issues: AcceptableMigrationIssues,
) -> None:
    migrator = migrate_tmpfile
    empty_debian_dir = build_virtual_file_system([DEBIAN_DIR_ENTRY])
    migration = run_migrator(
        migrator,
        empty_debian_dir,
        empty_manifest_pkg_foo,
        accept_no_migration_issues,
    )

    assert not migration.anything_to_do
    assert not migration.warnings
    assert not migration.remove_paths_on_success
    assert not migration.rename_paths_on_success

    tmpfile_debian_dir = build_virtual_file_system(
        [
            DEBIAN_DIR_ENTRY,
            virtual_path_def("tmpfile", fs_path="/nowhere/debian/tmpfile"),
        ]
    )

    migration = run_migrator(
        migrator,
        tmpfile_debian_dir,
        empty_manifest_pkg_foo,
        accept_no_migration_issues,
    )

    assert migration.anything_to_do
    assert not migration.warnings
    assert not migration.remove_paths_on_success
    assert migration.rename_paths_on_success == [
        ("/nowhere/debian/tmpfile", "/nowhere/debian/tmpfiles")
    ]

    tmpfile_debian_dir = build_virtual_file_system(
        [
            DEBIAN_DIR_ENTRY,
            # Use real paths here to make `cmp -s` discover that they are the same
            virtual_path_def("tmpfile", fs_path="debian/control"),
            virtual_path_def("tmpfiles", fs_path="debian/control"),
        ]
    )

    migration = run_migrator(
        migrator,
        tmpfile_debian_dir,
        empty_manifest_pkg_foo,
        accept_no_migration_issues,
    )
    assert migration.anything_to_do
    assert not migration.warnings
    assert migration.remove_paths_on_success == ["debian/control"]
    assert not migration.rename_paths_on_success

    conflict_tmpfile_debian_dir = build_virtual_file_system(
        [
            DEBIAN_DIR_ENTRY,
            # Use real paths here to make `cmp -s` discover a difference
            virtual_path_def("tmpfile", fs_path="debian/control"),
            virtual_path_def("tmpfiles", fs_path="debian/changelog"),
        ]
    )

    migration = run_migrator(
        migrator,
        conflict_tmpfile_debian_dir,
        empty_manifest_pkg_foo,
        accept_no_migration_issues,
    )

    assert len(migration.warnings) == 1
    assert not migration.remove_paths_on_success
    assert not migration.rename_paths_on_success

    conflict_tmpfile_debian_dir = build_virtual_file_system(
        [
            DEBIAN_DIR_ENTRY,
            virtual_path_def("tmpfile", fs_path="/nowhere/debian/tmpfile"),
            virtual_path_def("tmpfiles/", fs_path="/nowhere/debian/tmpfiles"),
        ]
    )

    migration = run_migrator(
        migrator,
        conflict_tmpfile_debian_dir,
        empty_manifest_pkg_foo,
        accept_no_migration_issues,
    )

    assert len(migration.warnings) == 1
    assert not migration.remove_paths_on_success
    assert not migration.rename_paths_on_success

    conflict_tmpfile_debian_dir = build_virtual_file_system(
        [
            DEBIAN_DIR_ENTRY,
            virtual_path_def(
                "tmpfile",
                link_target="/nowhere/debian/tmpfiles",
                fs_path="/nowhere/debian/tmpfile",
            ),
        ]
    )

    migration = run_migrator(
        migrator,
        conflict_tmpfile_debian_dir,
        empty_manifest_pkg_foo,
        accept_no_migration_issues,
    )

    assert len(migration.warnings) == 1
    assert not migration.remove_paths_on_success
    assert not migration.rename_paths_on_success


def test_migrate_lintian_overrides_files(
    empty_manifest_pkg_foo: HighLevelManifest,
    accept_no_migration_issues: AcceptableMigrationIssues,
    accept_any_migration_issues: AcceptableMigrationIssues,
) -> None:
    migrator = migrate_lintian_overrides_files
    no_override_fs = build_virtual_file_system([DEBIAN_DIR_ENTRY])
    single_noexec_override_fs = build_virtual_file_system(
        [
            DEBIAN_DIR_ENTRY,
            virtual_path_def(
                "foo.lintian-overrides",
                fs_path="/nowhere/no-exec/debian/foo.lintian-overrides",
            ),
        ]
    )
    single_exec_override_fs = build_virtual_file_system(
        [
            DEBIAN_DIR_ENTRY,
            virtual_path_def(
                "foo.lintian-overrides",
                fs_path="/nowhere/exec/debian/foo.lintian-overrides",
                mode=0o755,
            ),
        ]
    )
    for no_issue_fs in [no_override_fs, single_noexec_override_fs]:
        migration = run_migrator(
            migrator,
            no_issue_fs,
            empty_manifest_pkg_foo,
            accept_no_migration_issues,
        )

        assert not migration.anything_to_do
        assert not migration.warnings
        assert not migration.remove_paths_on_success
        assert not migration.rename_paths_on_success

    _assert_unsupported_feature(
        migrator,
        single_exec_override_fs,
        empty_manifest_pkg_foo,
        accept_no_migration_issues,
    )

    migration = run_migrator(
        migrator,
        single_exec_override_fs,
        empty_manifest_pkg_foo,
        accept_any_migration_issues,
    )

    assert migration.anything_to_do
    assert len(migration.warnings) == 1
    assert not migration.remove_paths_on_success
    assert not migration.rename_paths_on_success


def test_detect_pam_files(
    empty_manifest_pkg_foo: HighLevelManifest,
    accept_no_migration_issues: AcceptableMigrationIssues,
) -> None:
    migrator = detect_pam_files
    empty_fs = build_virtual_file_system([DEBIAN_DIR_ENTRY])
    pam_fs = build_virtual_file_system(
        [
            DEBIAN_DIR_ENTRY,
            virtual_path_def("pam", fs_path="/nowhere/debian/foo.pam"),
        ]
    )

    migration = run_migrator(
        migrator,
        empty_fs,
        empty_manifest_pkg_foo,
        accept_no_migration_issues,
    )

    assert not migration.anything_to_do
    assert not migration.warnings
    assert migration.assumed_compat is None
    assert not migration.remove_paths_on_success
    assert not migration.rename_paths_on_success

    migration = run_migrator(
        migrator,
        pam_fs,
        empty_manifest_pkg_foo,
        accept_no_migration_issues,
    )

    assert not migration.anything_to_do
    assert not migration.warnings
    assert migration.assumed_compat == 14
    assert not migration.remove_paths_on_success
    assert not migration.rename_paths_on_success


def test_migrate_doc_base_files(
    empty_manifest_pkg_foo: HighLevelManifest,
    accept_no_migration_issues: AcceptableMigrationIssues,
) -> None:
    migrator = migrate_doc_base_files
    empty_fs = build_virtual_file_system([DEBIAN_DIR_ENTRY])
    doc_base_ok_fs = build_virtual_file_system(
        [
            DEBIAN_DIR_ENTRY,
            virtual_path_def("doc-base", fs_path="/nowhere/debian/doc-base"),
            virtual_path_def(
                "foo.doc-base.EX", fs_path="/nowhere/debian/foo.doc-base.EX"
            ),
        ]
    )
    doc_base_migration_fs = build_virtual_file_system(
        [
            DEBIAN_DIR_ENTRY,
            virtual_path_def(
                "foo.doc-base.bar", fs_path="/nowhere/debian/foo.doc-base.bar"
            ),
        ]
    )

    for no_change_fs in [empty_fs, doc_base_ok_fs]:
        migration = run_migrator(
            migrator,
            no_change_fs,
            empty_manifest_pkg_foo,
            accept_no_migration_issues,
        )

        assert not migration.anything_to_do
        assert not migration.warnings
        assert not migration.remove_paths_on_success
        assert not migration.rename_paths_on_success

    migration = run_migrator(
        migrator,
        doc_base_migration_fs,
        empty_manifest_pkg_foo,
        accept_no_migration_issues,
    )

    assert migration.anything_to_do
    assert not migration.warnings
    assert not migration.remove_paths_on_success
    assert migration.rename_paths_on_success == [
        ("/nowhere/debian/foo.doc-base.bar", "/nowhere/debian/foo.bar.doc-base")
    ]


def test_migrate_dh_installsystemd_files(
    empty_manifest_pkg_foo: HighLevelManifest,
    accept_no_migration_issues: AcceptableMigrationIssues,
) -> None:
    migrator = migrate_dh_installsystemd_files
    empty_fs = build_virtual_file_system([DEBIAN_DIR_ENTRY])
    files_ok_fs = build_virtual_file_system(
        [
            DEBIAN_DIR_ENTRY,
            virtual_path_def("@service", fs_path="/nowhere/debian/@service"),
            virtual_path_def("foo.@service", fs_path="/nowhere/debian/foo.@service"),
        ]
    )
    migration_fs = build_virtual_file_system(
        [
            DEBIAN_DIR_ENTRY,
            virtual_path_def("foo@.service", fs_path="/nowhere/debian/foo@.service"),
        ]
    )

    for no_change_fs in [empty_fs, files_ok_fs]:
        migration = run_migrator(
            migrator,
            no_change_fs,
            empty_manifest_pkg_foo,
            accept_no_migration_issues,
        )

        assert not migration.anything_to_do
        assert not migration.warnings
        assert not migration.remove_paths_on_success
        assert not migration.rename_paths_on_success

    migration = run_migrator(
        migrator,
        migration_fs,
        empty_manifest_pkg_foo,
        accept_no_migration_issues,
    )

    assert migration.anything_to_do
    assert not migration.warnings
    assert not migration.remove_paths_on_success
    assert migration.rename_paths_on_success == [
        ("/nowhere/debian/foo@.service", "/nowhere/debian/foo.@service")
    ]


def test_migrate_installexamples_file(
    manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
    accept_no_migration_issues: AcceptableMigrationIssues,
) -> None:
    dh_config_content = textwrap.dedent(
        """\
        foo/*
        bar
    """
    )
    expected_manifest_content = textwrap.dedent(
        """\
            manifest-version: '0.1'
            installations:
            - install-examples:
                sources:
                - foo/*
                - bar
    """
    )
    _verify_migrator_generates_parsable_manifest(
        migrate_installexamples_file,
        manifest_parser_pkg_foo_factory,
        accept_no_migration_issues,
        "examples",
        dh_config_content,
        expected_manifest_content,
    )


def test_migrate_installinfo_file(
    manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
    accept_no_migration_issues: AcceptableMigrationIssues,
) -> None:
    dh_config_content = textwrap.dedent(
        """\
        foo/*
        bar
    """
    )
    expected_manifest_content = textwrap.dedent(
        """\
            manifest-version: '0.1'
            installations:
            - install-docs:
                sources:
                - foo/*
                - bar
                dest-dir: '{{path:GNU_INFO_DIR}}'
    """
    )
    _verify_migrator_generates_parsable_manifest(
        migrate_installinfo_file,
        manifest_parser_pkg_foo_factory,
        accept_no_migration_issues,
        "info",
        dh_config_content,
        expected_manifest_content,
    )


def test_migrate_installinfo_file_conditionals(
    manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
    accept_no_migration_issues: AcceptableMigrationIssues,
) -> None:
    dh_config_content = textwrap.dedent(
        """\
        #!/usr/bin/dh-exec
        foo/* <!pkg.foo.noinfo>
        bar <!pkg.foo.noinfo>
    """
    )
    expected_manifest_content = textwrap.dedent(
        """\
            manifest-version: '0.1'
            installations:
            - install-docs:
                sources:
                - foo/*
                - bar
                dest-dir: '{{path:GNU_INFO_DIR}}'
                when:
                  build-profiles-matches: <!pkg.foo.noinfo>
    """
    )
    _verify_migrator_generates_parsable_manifest(
        migrate_installinfo_file,
        manifest_parser_pkg_foo_factory,
        accept_no_migration_issues,
        "info",
        dh_config_content,
        expected_manifest_content,
    )


def test_migrate_installexamples_file_single_source(
    manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
    accept_no_migration_issues: AcceptableMigrationIssues,
) -> None:
    dh_config_content = textwrap.dedent(
        """\
        foo/*
    """
    )
    expected_manifest_content = textwrap.dedent(
        """\
            manifest-version: '0.1'
            installations:
            - install-examples:
                source: foo/*
    """
    )
    _verify_migrator_generates_parsable_manifest(
        migrate_installexamples_file,
        manifest_parser_pkg_foo_factory,
        accept_no_migration_issues,
        "examples",
        dh_config_content,
        expected_manifest_content,
    )


def test_migrate_installdocs_file(
    manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
    accept_no_migration_issues: AcceptableMigrationIssues,
) -> None:
    dh_config_content = textwrap.dedent(
        """\
        foo/*
        bar
    """
    )
    expected_manifest_content = textwrap.dedent(
        """\
            manifest-version: '0.1'
            installations:
            - install-docs:
                sources:
                - foo/*
                - bar
    """
    )
    _verify_migrator_generates_parsable_manifest(
        migrate_installdocs_file,
        manifest_parser_pkg_foo_factory,
        accept_no_migration_issues,
        "docs",
        dh_config_content,
        expected_manifest_content,
    )


def test_migrate_installdocs_file_single_source(
    manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
    accept_no_migration_issues: AcceptableMigrationIssues,
) -> None:
    dh_config_content = textwrap.dedent(
        """\
        foo/*
    """
    )
    expected_manifest_content = textwrap.dedent(
        """\
            manifest-version: '0.1'
            installations:
            - install-docs:
                source: foo/*
    """
    )
    _verify_migrator_generates_parsable_manifest(
        migrate_installdocs_file,
        manifest_parser_pkg_foo_factory,
        accept_no_migration_issues,
        "docs",
        dh_config_content,
        expected_manifest_content,
    )


def test_migrate_install_file(
    manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
    accept_no_migration_issues: AcceptableMigrationIssues,
) -> None:
    dh_config_content = textwrap.dedent(
        """\
        bar usr/bin
    """
    )
    expected_manifest_content = textwrap.dedent(
        """\
            manifest-version: '0.1'
            installations:
            - install:
                source: bar
                dest-dir: usr/bin
    """
    )
    _verify_migrator_generates_parsable_manifest(
        migrate_install_file,
        manifest_parser_pkg_foo_factory,
        accept_no_migration_issues,
        "install",
        dh_config_content,
        expected_manifest_content,
    )


def test_migrate_install_file_conditionals_simple_arch(
    manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
    accept_no_migration_issues: AcceptableMigrationIssues,
) -> None:
    dh_config_content = textwrap.dedent(
        """\
        #!/usr/bin/dh-exec
        bar usr/bin  [linux-any]
        foo usr/bin  [linux-any]
    """
    )
    expected_manifest_content = textwrap.dedent(
        """\
            manifest-version: '0.1'
            installations:
            - install:
                sources:
                - bar
                - foo
                dest-dir: usr/bin
                when:
                  arch-matches: linux-any
    """
    )
    _verify_migrator_generates_parsable_manifest(
        migrate_install_file,
        manifest_parser_pkg_foo_factory,
        accept_no_migration_issues,
        "install",
        dh_config_content,
        expected_manifest_content,
    )


def test_migrate_install_file_util_linux_locales(
    manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
    accept_no_migration_issues: AcceptableMigrationIssues,
) -> None:
    # Parts of the `d/util-linux-locales.install` file. It uses d/tmp for most of its paths
    # and that breaks the default dest-dir (dh_install always strips --sourcedir, `debputy`
    # currently does not)
    dh_config_content = textwrap.dedent(
        """\
        #!/usr/bin/dh-exec
        usr/share/locale/*/*/util-linux.mo

        # bsdextrautils
        debian/tmp/usr/share/man/*/man1/col.1 <!nodoc>

        debian/tmp/usr/share/man/*/man3/libblkid.3 <!nodoc>
    """
    )
    expected_manifest_content = textwrap.dedent(
        """\
            manifest-version: '0.1'
            installations:
            - install:
                sources:
                - usr/share/man/*/man1/col.1
                - usr/share/man/*/man3/libblkid.3
                when:
                  build-profiles-matches: <!nodoc>
            - install:
                source: usr/share/locale/*/*/util-linux.mo
    """
    )
    _verify_migrator_generates_parsable_manifest(
        migrate_install_file,
        manifest_parser_pkg_foo_factory,
        accept_no_migration_issues,
        "install",
        dh_config_content,
        expected_manifest_content,
    )


def test_migrate_install_file_conditionals_simple_combined_cond(
    manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
    accept_no_migration_issues: AcceptableMigrationIssues,
) -> None:
    for cond in ["<!foo> <!bar> [linux-any]", "[linux-any] <!foo> <!bar>"]:
        dh_config_content = textwrap.dedent(
            """\
            #!/usr/bin/dh-exec
            bar usr/bin  {CONDITION}
            foo usr/bin  {CONDITION}
        """
        ).format(CONDITION=cond)
        expected_manifest_content = textwrap.dedent(
            """\
                manifest-version: '0.1'
                installations:
                - install:
                    sources:
                    - bar
                    - foo
                    dest-dir: usr/bin
                    when:
                      all-of:
                      - arch-matches: linux-any
                      - build-profiles-matches: <!foo> <!bar>
        """
        )
        _verify_migrator_generates_parsable_manifest(
            migrate_install_file,
            manifest_parser_pkg_foo_factory,
            accept_no_migration_issues,
            "install",
            dh_config_content,
            expected_manifest_content,
        )


def test_migrate_install_file_conditionals_unknown_subst(
    manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
    accept_any_migration_issues: AcceptableMigrationIssues,
) -> None:
    dh_config_content = textwrap.dedent(
        """\
        #!/usr/bin/dh-exec
        bar ${unknown_substvar}
    """
    )
    expected_manifest_content = textwrap.dedent(
        """\
            manifest-version: '0.1'
            definitions:
              variables:
                unknown_substvar: 'TODO: Provide variable value for unknown_substvar'
            installations:
            - install:
                source: bar
                dest-dir: '{{unknown_substvar}}'
    """
    )
    expected_warning = (
        "TODO: MANUAL MIGRATION of unresolved substitution variable {{unknown_substvar}}"
        ' from ./install line 2 token "${unknown_substvar}"'
    )
    _verify_migrator_generates_parsable_manifest(
        migrate_install_file,
        manifest_parser_pkg_foo_factory,
        accept_any_migration_issues,
        "install",
        dh_config_content,
        expected_manifest_content,
        expected_warnings=[expected_warning],
    )


def test_migrate_install_file_multidest(
    manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
    accept_no_migration_issues: AcceptableMigrationIssues,
) -> None:
    dh_config_content = textwrap.dedent(
        """\
        # Issue #66
        # - observed in kafs-client / the original install file copied in here.

        src/aklog-kafs        usr/bin
        src/kafs-check-config usr/bin
        #
        src/kafs-preload usr/sbin
        #
        src/kafs-dns     usr/libexec
        #
        conf/cellservdb.conf usr/share/kafs-client
        conf/client.conf     etc/kafs
        #
        conf/kafs_dns.conf etc/request-key.d
        #
        conf/cellservdb.conf usr/share/kafs
    """
    )
    expected_manifest_content = textwrap.dedent(
        """\
            manifest-version: '0.1'
            installations:
            - install:
                sources:
                - src/aklog-kafs
                - src/kafs-check-config
                dest-dir: usr/bin
            - install:
                source: src/kafs-preload
                dest-dir: usr/sbin
            - install:
                source: src/kafs-dns
                dest-dir: usr/libexec
            - install:
                source: conf/client.conf
                dest-dir: etc/kafs
            - install:
                source: conf/kafs_dns.conf
                dest-dir: etc/request-key.d
            - multi-dest-install:
                source: conf/cellservdb.conf
                dest-dirs:
                - usr/share/kafs-client
                - usr/share/kafs
    """
    )
    _verify_migrator_generates_parsable_manifest(
        migrate_install_file,
        manifest_parser_pkg_foo_factory,
        accept_no_migration_issues,
        "install",
        dh_config_content,
        expected_manifest_content,
    )


def test_migrate_install_file_multidest_default_dest(
    manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
    accept_no_migration_issues: AcceptableMigrationIssues,
) -> None:
    dh_config_content = textwrap.dedent(
        """\
        # Relaed to issue #66 - testing corner case not present in the original install file

        src/aklog-kafs        usr/bin
        src/kafs-check-config usr/bin
        #
        src/kafs-preload usr/sbin
        #
        src/kafs-dns     usr/libexec
        #
        usr/share/kafs-client/cellservdb.conf
        conf/client.conf     etc/kafs
        #
        conf/kafs_dns.conf etc/request-key.d
        #
        usr/share/kafs-client/cellservdb.conf usr/share/kafs
    """
    )
    expected_manifest_content = textwrap.dedent(
        """\
            manifest-version: '0.1'
            installations:
            - install:
                sources:
                - src/aklog-kafs
                - src/kafs-check-config
                dest-dir: usr/bin
            - install:
                source: src/kafs-preload
                dest-dir: usr/sbin
            - install:
                source: src/kafs-dns
                dest-dir: usr/libexec
            - install:
                source: conf/client.conf
                dest-dir: etc/kafs
            - install:
                source: conf/kafs_dns.conf
                dest-dir: etc/request-key.d
            - multi-dest-install:
                source: usr/share/kafs-client/cellservdb.conf
                dest-dirs:
                - usr/share/kafs
                - usr/share/kafs-client
    """
    )
    _verify_migrator_generates_parsable_manifest(
        migrate_install_file,
        manifest_parser_pkg_foo_factory,
        accept_no_migration_issues,
        "install",
        dh_config_content,
        expected_manifest_content,
    )


def test_migrate_install_file_multidest_default_dest_warning(
    manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
    accept_no_migration_issues: AcceptableMigrationIssues,
) -> None:
    dh_config_content = textwrap.dedent(
        """\
        # Relaed to issue #66 - testing corner case not present in the original install file

        src/aklog-kafs        usr/bin
        src/kafs-check-config usr/bin
        #
        src/kafs-preload usr/sbin
        #
        src/kafs-dns     usr/libexec
        #
        usr/share/kafs-*/cellservdb.conf
        conf/client.conf     etc/kafs
        #
        conf/kafs_dns.conf etc/request-key.d
        #
        usr/share/kafs-*/cellservdb.conf usr/share/kafs
    """
    )
    expected_manifest_content = textwrap.dedent(
        """\
            manifest-version: '0.1'
            installations:
            - install:
                sources:
                - src/aklog-kafs
                - src/kafs-check-config
                dest-dir: usr/bin
            - install:
                source: src/kafs-preload
                dest-dir: usr/sbin
            - install:
                source: src/kafs-dns
                dest-dir: usr/libexec
            - install:
                source: conf/client.conf
                dest-dir: etc/kafs
            - install:
                source: conf/kafs_dns.conf
                dest-dir: etc/request-key.d
            - multi-dest-install:
                source: usr/share/kafs-*/cellservdb.conf
                dest-dirs:
                - usr/share/kafs
                - 'FIXME: usr/share/kafs-* (could not reliably compute the dest dir)'
    """
    )
    warnings = [
        "TODO: FIXME left in dest-dir(s) of some installation rules."
        " Please review these and remove the FIXME (plus correct as necessary)"
    ]
    _verify_migrator_generates_parsable_manifest(
        migrate_install_file,
        manifest_parser_pkg_foo_factory,
        accept_no_migration_issues,
        "install",
        dh_config_content,
        expected_manifest_content,
        expected_warnings=warnings,
    )


def test_migrate_installman_file(
    manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
    accept_no_migration_issues: AcceptableMigrationIssues,
) -> None:
    dh_config_content = textwrap.dedent(
        """\
        man/foo.1 man/bar.1
        man2/*.2
        man3/bar.3 man3/bar.de.3
        man/de/man3/bar.pl.3
    """
    )
    expected_manifest_content = textwrap.dedent(
        """\
            manifest-version: '0.1'
            installations:
            - install-man:
                sources:
                - man/foo.1
                - man/bar.1
                - man2/*.2
                - man/de/man3/bar.pl.3
            - install-man:
                sources:
                - man3/bar.3
                - man3/bar.de.3
                language: derive-from-basename
    """
    )
    expected_warnings = [
        'Detected manpages that might rely on "derive-from-basename" logic.  Please double check'
        " that the generated `install-man` rules are correct"
    ]
    _verify_migrator_generates_parsable_manifest(
        migrate_installman_file,
        manifest_parser_pkg_foo_factory,
        accept_no_migration_issues,
        "manpages",
        dh_config_content,
        expected_manifest_content,
        expected_warnings=expected_warnings,
    )


def test_migrate_install_dh_exec_file(
    manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
    accept_no_migration_issues: AcceptableMigrationIssues,
) -> None:
    dh_config_content = textwrap.dedent(
        """\
        #!/usr/bin/dh-exec

        foo/script.sh => usr/bin/script
        => usr/bin/bar
        usr/bin/* usr/share/foo/extra/* usr/share/foo/extra
        another-util usr/share/foo/extra
        # This will not be merged with `=> usr/bin/bar`
        usr/share/foo/features
        usr/share/foo/bugs
        some-file.txt usr/share/foo/online-doc
        # TODO: Support migration of these
        # pathA pathB  conditional/arch [linux-anx]
        # <!pkg.foo.condition>  another-path conditional/profile
    """
    )
    expected_manifest_content = textwrap.dedent(
        """\
            manifest-version: '0.1'
            installations:
            - install:
                source: usr/bin/bar
            - install:
                source: foo/script.sh
                as: usr/bin/script
            - install:
                sources:
                - usr/bin/*
                - usr/share/foo/extra/*
                - another-util
                dest-dir: usr/share/foo/extra
            - install:
                source: some-file.txt
                dest-dir: usr/share/foo/online-doc
            - install:
                sources:
                - usr/share/foo/features
                - usr/share/foo/bugs
    """
    )
    _verify_migrator_generates_parsable_manifest(
        migrate_install_file,
        manifest_parser_pkg_foo_factory,
        accept_no_migration_issues,
        "install",
        dh_config_content,
        expected_manifest_content,
    )


def test_migrate_maintscript(
    manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
    accept_no_migration_issues: AcceptableMigrationIssues,
) -> None:
    dh_config_content = textwrap.dedent(
        """\
        rm_conffile /etc/foo.conf
        mv_conffile /etc/bar.conf /etc/new-foo.conf 1.0~ bar
    """
    )
    expected_manifest_content = textwrap.dedent(
        """\
            manifest-version: '0.1'
            packages:
              foo:
                conffile-management:
                - remove:
                    path: /etc/foo.conf
                - rename:
                    source: /etc/bar.conf
                    target: /etc/new-foo.conf
                    prior-to-version: 1.0~
                    owning-package: bar
    """
    )
    _verify_migrator_generates_parsable_manifest(
        migrate_maintscript,
        manifest_parser_pkg_foo_factory,
        accept_no_migration_issues,
        "maintscript",
        dh_config_content,
        expected_manifest_content,
    )


def test_migrate_not_installed_file(
    manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
    accept_no_migration_issues: AcceptableMigrationIssues,
) -> None:
    dh_config_content = textwrap.dedent(
        """\
        foo/*.txt bar/${DEB_HOST_MULTIARCH}/*.so*
        baz/script.sh
    """
    )
    expected_manifest_content = textwrap.dedent(
        """\
            manifest-version: '0.1'
            installations:
            - discard:
              - foo/*.txt
              - bar/{{DEB_HOST_MULTIARCH}}/*.so*
              - baz/script.sh
    """
    )
    _verify_migrator_generates_parsable_manifest(
        migrate_not_installed_file,
        manifest_parser_pkg_foo_factory,
        accept_no_migration_issues,
        "not-installed",
        dh_config_content,
        expected_manifest_content,
    )


def test_migrate_links_files(
    manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
    accept_no_migration_issues: AcceptableMigrationIssues,
) -> None:
    dh_config_content = textwrap.dedent(
        """\
        usr/share/target usr/bin/symlink
    """
    )
    expected_manifest_content = textwrap.dedent(
        """\
            manifest-version: '0.1'
            packages:
              foo:
                transformations:
                - create-symlink:
                    path: usr/bin/symlink
                    target: /usr/share/target
    """
    )
    _verify_migrator_generates_parsable_manifest(
        migrate_links_files,
        manifest_parser_pkg_foo_factory,
        accept_no_migration_issues,
        "links",
        dh_config_content,
        expected_manifest_content,
    )


def test_detect_dh_addons(
    empty_manifest_pkg_foo: HighLevelManifest,
    accept_no_migration_issues: AcceptableMigrationIssues,
    accept_any_migration_issues: AcceptableMigrationIssues,
) -> None:
    migrator = detect_dh_addons
    empty_fs = build_virtual_file_system([DEBIAN_DIR_ENTRY])

    dctrl_no_addons_content = textwrap.dedent(
        """\
    Source: foo
    Build-Depends: debhelper-compat (= 13)

    Package: foo
    Architecture: all
    Description: ...
    """
    )

    dctrl_w_addons_content = textwrap.dedent(
        """\
    Source: foo
    Build-Depends: debhelper-compat (= 13),
                   dh-sequence-debputy,
                   dh-sequence-foo,

    Package: foo
    Architecture: all
    Description: ...
    """
    )

    dctrl_w_migrateable_addons_content = textwrap.dedent(
        """\
    Source: foo
    Build-Depends: debhelper-compat (= 13),
                   dh-sequence-debputy,
                   dh-sequence-numpy3,

    Package: foo
    Architecture: all
    Description: ...
    """
    )

    dctrl_no_addons_fs = build_virtual_file_system(
        [
            DEBIAN_DIR_ENTRY,
            virtual_path_def(
                "control",
                fs_path="/nowhere/debian/control-without-addons",
                content=dctrl_no_addons_content,
            ),
        ]
    )
    dctrl_w_addons_fs = build_virtual_file_system(
        [
            DEBIAN_DIR_ENTRY,
            virtual_path_def(
                "control",
                fs_path="/nowhere/debian/control-with-addons",
                content=dctrl_w_addons_content,
            ),
        ]
    )
    dctrl_w_migrateable_addons_fs = build_virtual_file_system(
        [
            DEBIAN_DIR_ENTRY,
            virtual_path_def(
                "control",
                fs_path="/nowhere/debian/control-with-migrateable-addons",
                content=dctrl_w_migrateable_addons_content,
            ),
        ]
    )
    no_ctrl_msg = (
        "Cannot find debian/control. Detection of unsupported/missing dh-sequence addon"
        " could not be performed. Please ensure the package will Build-Depend on dh-sequence-debputy "
        " and not rely on any other debhelper sequence addons except those debputy explicitly supports."
    )
    missing_debputy_bd_msg = (
        "Missing Build-Depends on dh-sequence-debputy or dh-sequence-zz-debputy"
    )
    unsupported_sequence_msg = (
        'The dh addon "foo" is not known to work with dh-debputy and might malfunction'
    )
    migrate_numpy3_msg = (
        "TODO: MANUAL MIGRATION - Remove build-dependency on dh-sequence-numpy3"
        " (replaced by debputy-plugin-numpy3)"
    )

    migration = run_migrator(
        migrator,
        empty_fs,
        empty_manifest_pkg_foo,
        accept_no_migration_issues,
    )
    assert migration.anything_to_do
    assert migration.warnings == [no_ctrl_msg]
    assert not migration.remove_paths_on_success
    assert not migration.rename_paths_on_success

    migration = run_migrator(
        migrator,
        dctrl_no_addons_fs,
        empty_manifest_pkg_foo,
        accept_no_migration_issues,
    )
    assert migration.anything_to_do
    assert migration.warnings == [missing_debputy_bd_msg]
    assert not migration.remove_paths_on_success
    assert not migration.rename_paths_on_success

    _assert_unsupported_feature(
        migrator,
        dctrl_w_addons_fs,
        empty_manifest_pkg_foo,
        accept_no_migration_issues,
    )

    migration = run_migrator(
        migrator,
        dctrl_w_addons_fs,
        empty_manifest_pkg_foo,
        accept_any_migration_issues,
    )

    assert migration.anything_to_do
    assert migration.warnings == [unsupported_sequence_msg]
    assert not migration.remove_paths_on_success
    assert not migration.rename_paths_on_success

    migration = run_migrator(
        migrator,
        dctrl_w_migrateable_addons_fs,
        empty_manifest_pkg_foo,
        accept_any_migration_issues,
    )
    assert migration.anything_to_do
    assert migration.warnings == [migrate_numpy3_msg]
    assert not migration.remove_paths_on_success
    assert not migration.rename_paths_on_success


def test_migrate_bash_completion_file_no_changes(
    manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
    accept_no_migration_issues: AcceptableMigrationIssues,
) -> None:
    dh_config_content = textwrap.dedent(
        """\
        compgen -A
    """
    )
    dh_config_name = "bash-completion"
    fs = build_virtual_file_system(
        [
            DEBIAN_DIR_ENTRY,
            virtual_path_def(
                dh_config_name,
                fs_path=f"/nowhere/debian/{dh_config_name}",
                content=dh_config_content,
            ),
        ]
    )
    migration = run_migrator(
        migrate_bash_completion,
        fs,
        manifest_parser_pkg_foo_factory().build_manifest(),
        accept_no_migration_issues,
    )
    assert not migration.rename_paths_on_success
    assert not migration.remove_paths_on_success
    assert not migration.warnings
    assert not migration.required_plugins


def test_migrate_bash_completion_file(
    manifest_parser_pkg_foo_factory: Callable[[], YAMLManifestParser],
    accept_no_migration_issues: AcceptableMigrationIssues,
) -> None:
    dh_config_content = textwrap.dedent(
        """\
        foo/*
        bar baz
        debian/bar-completion bar
        debian/foo-completion foo
        debian/*.cmpl
    """
    )
    expected_manifest_content = textwrap.dedent(
        """\
            manifest-version: '0.1'
            installations:
            - install:
                sources:
                - foo/*
                - debian/*.cmpl
                dest-dir: '{{path:BASH_COMPLETION_DIR}}'
            - install:
                source: bar
                as: '{{path:BASH_COMPLETION_DIR}}/baz'
    """
    )
    expected_renames = [
        ("debian/bar-completion", "debian/foo.bar.bash-completion"),
        ("debian/foo-completion", "debian/foo.bash-completion"),
    ]
    _verify_migrator_generates_parsable_manifest(
        migrate_bash_completion,
        manifest_parser_pkg_foo_factory,
        accept_no_migration_issues,
        "bash-completion",
        dh_config_content,
        expected_manifest_content,
        expected_renamed_paths=expected_renames,
    )
