from typing import Optional, List
from collections.abc import Sequence, Mapping

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

from debputy.architecture_support import DpkgArchitectureBuildProcessValuesTable
from debputy.deb_packaging_support import (
    install_upstream_changelog,
    compute_installed_size,
    auto_compute_multi_arch,
    generate_md5sums_file,
    install_or_generate_conffiles,
)
from debputy.filesystem_scan import build_virtual_fs, FSRootDir
from debputy.packages import BinaryPackage
from debputy.plugin.api import virtual_path_def
from debputy.plugin.api.spec import PathDef


@pytest.mark.parametrize(
    "upstream_changelog_name,other_files",
    [
        (
            "changelog.txt",
            [
                "changelog.md",
                "CHANGELOG.rst",
                "random-file",
            ],
        ),
        (
            "CHANGELOG.rst",
            [
                "doc/CHANGELOG.txt",
                "docs/CHANGELOG.md",
            ],
        ),
        (
            "docs/CHANGELOG.rst",
            [
                "docs/history.md",
            ],
        ),
        (
            "changelog",
            [],
        ),
    ],
)
def test_upstream_changelog_from_source(
    package_single_foo_arch_all_cxt_amd64,
    upstream_changelog_name,
    other_files,
) -> None:
    upstream_changelog_content = "Some upstream changelog"
    dctrl = package_single_foo_arch_all_cxt_amd64["foo"]
    data_fs_root = build_virtual_fs([], read_write_fs=True)
    upstream_fs_contents = [
        virtual_path_def("CHANGELOG", materialized_content="Some upstream changelog")
    ]
    upstream_fs_contents.extend(
        virtual_path_def(x, materialized_content="Wrong file!") for x in other_files
    )
    source_fs_root = build_virtual_fs(upstream_fs_contents)

    install_upstream_changelog(dctrl, data_fs_root, source_fs_root)

    upstream_changelog = data_fs_root.lookup(f"usr/share/doc/{dctrl.name}/changelog")
    assert upstream_changelog is not None
    assert upstream_changelog.is_file
    with upstream_changelog.open() as fd:
        content = fd.read()
    assert upstream_changelog_content == content


@pytest.mark.parametrize(
    "upstream_changelog_basename,other_data_files,other_source_files",
    [
        (
            "CHANGELOG",
            [
                "history.txt",
                "changes.md",
            ],
            [
                "changelog",
                "doc/CHANGELOG.txt",
                "docs/CHANGELOG.md",
            ],
        ),
        (
            "changelog",
            [
                "history.txt",
                "changes.md",
            ],
            [
                "changelog",
                "doc/CHANGELOG.txt",
                "docs/CHANGELOG.md",
            ],
        ),
        (
            "changes.md",
            [
                "changelog.rst",
            ],
            ["changelog"],
        ),
    ],
)
def test_upstream_changelog_from_data_fs(
    package_single_foo_arch_all_cxt_amd64,
    upstream_changelog_basename,
    other_data_files,
    other_source_files,
) -> None:
    upstream_changelog_content = "Some upstream changelog"
    dctrl = package_single_foo_arch_all_cxt_amd64["foo"]
    doc_dir = f"./usr/share/doc/{dctrl.name}"
    data_fs_contents = [
        virtual_path_def(
            f"{doc_dir}/{upstream_changelog_basename}",
            materialized_content="Some upstream changelog",
        )
    ]
    data_fs_contents.extend(
        virtual_path_def(
            f"{doc_dir}/{x}",
            materialized_content="Wrong file!",
        )
        for x in other_data_files
    )
    data_fs_root = build_virtual_fs(data_fs_contents, read_write_fs=True)
    source_fs_root = build_virtual_fs(
        [
            virtual_path_def(
                x,
                materialized_content="Wrong file!",
            )
            for x in other_source_files
        ]
    )

    install_upstream_changelog(dctrl, data_fs_root, source_fs_root)

    upstream_changelog = data_fs_root.lookup(f"usr/share/doc/{dctrl.name}/changelog")
    assert upstream_changelog is not None
    assert upstream_changelog.is_file
    with upstream_changelog.open() as fd:
        content = fd.read()
    assert upstream_changelog_content == content


def test_upstream_changelog_pre_installed_compressed(
    package_single_foo_arch_all_cxt_amd64,
) -> None:
    dctrl = package_single_foo_arch_all_cxt_amd64["foo"]
    changelog = f"./usr/share/doc/{dctrl.name}/changelog.gz"
    data_fs_root = build_virtual_fs(
        [virtual_path_def(changelog, fs_path="/nowhere/should/not/be/resolved")],
        read_write_fs=True,
    )
    source_fs_root = build_virtual_fs(
        [virtual_path_def("changelog", materialized_content="Wrong file!")]
    )

    install_upstream_changelog(dctrl, data_fs_root, source_fs_root)

    upstream_ch_compressed = data_fs_root.lookup(
        f"usr/share/doc/{dctrl.name}/changelog.gz"
    )
    assert upstream_ch_compressed is not None
    assert upstream_ch_compressed.is_file
    upstream_ch_uncompressed = data_fs_root.lookup(
        f"usr/share/doc/{dctrl.name}/changelog"
    )
    assert upstream_ch_uncompressed is None


def test_upstream_changelog_no_matches(
    package_single_foo_arch_all_cxt_amd64,
) -> None:
    dctrl = package_single_foo_arch_all_cxt_amd64["foo"]
    doc_dir = f"./usr/share/doc/{dctrl.name}"
    data_fs_root = build_virtual_fs(
        [
            virtual_path_def(
                f"{doc_dir}/random-file", materialized_content="Wrong file!"
            ),
            virtual_path_def(
                f"{doc_dir}/changelog.Debian", materialized_content="Wrong file!"
            ),
        ],
        read_write_fs=True,
    )
    source_fs_root = build_virtual_fs(
        [virtual_path_def("some-random-file", materialized_content="Wrong file!")]
    )

    install_upstream_changelog(dctrl, data_fs_root, source_fs_root)

    upstream_ch_compressed = data_fs_root.lookup(
        f"usr/share/doc/{dctrl.name}/changelog.gz"
    )
    assert upstream_ch_compressed is None
    upstream_ch_uncompressed = data_fs_root.lookup(
        f"usr/share/doc/{dctrl.name}/changelog"
    )
    assert upstream_ch_uncompressed is None


def test_upstream_changelog_salsa_issue_49(
    package_single_foo_arch_all_cxt_amd64,
) -> None:
    # https://salsa.debian.org/debian/debputy/-/issues/49
    dctrl = package_single_foo_arch_all_cxt_amd64["foo"]
    doc_dir_path = f"./usr/share/doc/{dctrl.name}"
    data_fs_root = build_virtual_fs(
        [virtual_path_def(doc_dir_path, link_target="foo-data")], read_write_fs=True
    )
    source_fs_root = build_virtual_fs(
        [virtual_path_def("changelog", materialized_content="Wrong file!")]
    )

    install_upstream_changelog(dctrl, data_fs_root, source_fs_root)

    doc_dir = data_fs_root.lookup(f"usr/share/doc/{dctrl.name}")
    assert doc_dir is not None
    assert doc_dir.is_symlink


@pytest.mark.parametrize(
    "path_defs,expected_size",
    [
        (
            [],
            1,
        ),
        # Dirs cost 1 a piece
        (
            [virtual_path_def("usr/")],
            2,
        ),
        (
            [virtual_path_def("usr/bin/")],
            3,
        ),
        # Empty file cost 0
        (
            [virtual_path_def("usr/bin/foo", content="")],
            3,
        ),
        # Non-empty files cost 1 per "started" kB
        (
            [virtual_path_def("usr/bin/foo", content="A" * 10)],
            4,
        ),
        (
            [virtual_path_def("usr/bin/foo", content="A" * 1023)],
            4,
        ),
        (
            [virtual_path_def("usr/bin/foo", content="A" * 1024)],
            4,
        ),
        (
            [virtual_path_def("usr/bin/foo", content="A" * 1025)],
            5,
        ),
        # Symlinks costs per started kB of link target
        # - in theory, an empty link target would be free, but `ln -s` does not
        #   allow these, so we are not testing it.
        (
            [virtual_path_def("usr/bin/foo", link_target="A" * 10)],
            4,
        ),
        (
            [virtual_path_def("usr/bin/foo", link_target="A" * 1023)],
            4,
        ),
        (
            [virtual_path_def("usr/bin/foo", link_target="A" * 1024)],
            4,
        ),
        (
            [virtual_path_def("usr/bin/foo", link_target="A" * 1025)],
            5,
        ),
    ],
)
def test_compute_installed_size(
    path_defs: Sequence[PathDef], expected_size: int
) -> None:
    fs = build_virtual_fs(path_defs)
    assert compute_installed_size(fs) == expected_size


@pytest.mark.parametrize(
    "package_name,package_arch,ctrl_members,data_members,expected_result",
    [
        # Arch:all defaults to "no" except when it is `-doc`
        (
            "foo",
            "all",
            [],
            [],
            None,
        ),
        (
            "foo-doc",
            "all",
            [],
            [],
            "foreign",
        ),
        # No matter the name, auto-detection for arch:all stops the moment it has a non-doc path
        (
            "foo-doc",
            "all",
            [],
            [virtual_path_def("/usr/bin/")],
            None,
        ),
        # No matter the name, auto-detection for arch:all stops the moment it has maintscript
        (
            "foo-doc",
            "all",
            [virtual_path_def("postinst")],
            [],
            None,
        ),
        # Arch:any defaults to "same" for proper multi-arch'ed paths
        # NB: This test is always run in amd64 context.
        (
            "libfoo1",
            "amd64",
            [],
            [
                virtual_path_def("/usr/lib/x86_64-linux-gnu/libfoo.so.1"),
            ],
            "same",
        ),
        # Allowed documentation that does not disable auto-detection
        (
            "libfoo1",
            "amd64",
            [],
            [
                virtual_path_def("/usr/lib/x86_64-linux-gnu/libfoo.so.1"),
                virtual_path_def("/usr/share/doc/libfoo1/copyright"),
                virtual_path_def("/usr/share/doc/libfoo1/changelog.gz"),
                virtual_path_def("/usr/share/doc/libfoo1/changelog.Debian.amd64.gz"),
                virtual_path_def("/usr/share/doc/libfoo1/NEWS.Debian.gz"),
                virtual_path_def("/usr/share/doc/libfoo1/README.Debian.gz"),
            ],
            "same",
        ),
        # No auto-detection for at non-multi-arched libs
        (
            "libfoo1",
            "amd64",
            [],
            [
                virtual_path_def("/usr/lib/libfoo.so.1"),
            ],
            None,
        ),
        # No auto-detection with unknown docs.
        (
            "libfoo1",
            "amd64",
            [],
            [
                virtual_path_def("/usr/lib/x86_64-linux-gnu/libfoo.so.1"),
                virtual_path_def("/usr/share/doc/libfoo1/custom-doc"),
            ],
            None,
        ),
        # No auto-detection with random paths docs.
        (
            "libfoo1",
            "amd64",
            [],
            [
                virtual_path_def("/usr/bin/"),
                virtual_path_def("/usr/lib/x86_64-linux-gnu/libfoo.so.1"),
            ],
            None,
        ),
    ],
)
def test_auto_compute_multi_arch(
    package_name: str,
    package_arch: str,
    ctrl_members: Sequence[PathDef],
    data_members: Sequence[PathDef],
    expected_result: str | None,
    amd64_dpkg_architecture_variables: DpkgArchitectureBuildProcessValuesTable,
    dpkg_arch_query: DpkgArchTable,
) -> None:
    pkg = BinaryPackage(
        Deb822(
            {
                "Package": package_name,
                "Architecture": package_arch,
            }
        ),
        amd64_dpkg_architecture_variables,
        dpkg_arch_query,
        is_main_package=True,
        should_be_acted_on=True,
    )
    ctrl_fs_root = build_virtual_fs(ctrl_members)
    data_fs_root = build_virtual_fs(data_members)

    assert auto_compute_multi_arch(pkg, ctrl_fs_root, data_fs_root) == expected_result


@pytest.mark.parametrize(
    "data_members,conffiles_lines,expected",
    [
        # Non-existent by default
        (
            [],
            [],
            None,
        ),
        (
            # Normally this would be a conffile, but for the sake of the test, we assume it is not.
            [virtual_path_def("etc/foo", content="test")],
            [],
            {"etc/foo": "098f6bcd4621d373cade4e832627b4f6"},
        ),
        (
            [virtual_path_def("etc/foo", content="test")],
            ["/etc/foo"],
            # Conffiles are not included in the md5sums file (not sure why, but that is
            # "how we always did it")
            None,
        ),
        (
            [virtual_path_def("usr/share/data/foo", content="test")],
            # The code should gracefully cope with dpkg conffile instructions
            ["remove-on-upgrade /etc/foo.conf"],
            {"usr/share/data/foo": "098f6bcd4621d373cade4e832627b4f6"},
        ),
    ],
)
def test_generate_md5sums_file(
    data_members: Sequence[PathDef],
    conffiles_lines: Sequence[str],
    expected: Mapping[str, str] | None,
) -> None:

    if conffiles_lines:
        conffiles_def = virtual_path_def(
            "conffiles", content="\n".join(conffiles_lines)
        )
        ctrl_fs = build_virtual_fs([conffiles_def], read_write_fs=True)
    else:
        ctrl_fs = build_virtual_fs([], read_write_fs=True)
    data_fs = build_virtual_fs(data_members)

    generate_md5sums_file(
        ctrl_fs,
        data_fs,
    )
    md5sums_file = ctrl_fs.get("md5sums")
    if expected is None:
        assert md5sums_file is None
        return

    assert md5sums_file is not None

    actual = {}
    for line in md5sums_file.open():
        checksum, fs_path = line.split()
        actual[fs_path] = checksum

    assert actual == expected


@pytest.mark.parametrize(
    "data_members,expected",
    [
        # Non-existent by default
        (
            [],
            None,
        ),
        # Empty dirs and non-etc does nothing
        (
            ["/etc/foo.d/", "/usr/bin/foo"],
            None,
        ),
        # Auto-detection works (still ignoring non-etc files)
        (
            ["/etc/bar.conf", "/etc/foo.conf", "/usr/share/foo/data.txt"],
            [
                "/etc/bar.conf",
                "/etc/foo.conf",
            ],
        ),
    ],
)
def test_install_or_generate_conffiles(
    data_members: Sequence[PathDef],
    expected: list[str] | None,
) -> None:
    root_fs = build_virtual_fs(data_members)
    ctrl_fs = FSRootDir()
    install_or_generate_conffiles(ctrl_fs, root_fs, {})
    conffiles_member = ctrl_fs.get("conffiles")
    if expected is None:
        assert conffiles_member is None
        return
    assert conffiles_member is not None
    with conffiles_member.open() as fd:
        conffiles = [x.rstrip() for x in fd]

    assert conffiles == expected
