"""Provide a fake package archive for testing"""

# (C) 2012-2015 Martin Pitt <martin.pitt@ubuntu.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.

import tempfile
import shutil
import os
import subprocess
import atexit
import textwrap


class Archive:
    def __init__(
        self,
        *,
        path=None,
        pooldir="pool",
        suite=None,
        codename=None,
        component=None,
        notautomatic=False,
        butautomaticupgrades=False,
        codename_symlink=False,
    ):
        """Construct a local package test archive.

        By default, Packages.gz is created in the root of the target directory,
        so that the apt source will just use '/' for the suite and no
        components. If suite, codename and component are given, Packages.gz
        will be placed into 'dists/suite/component/binary-<arch>/', and the
        Suite and Codename fields of the Release file of this archive will be
        set to the specified values.

        If the notautomatic argument is true, "NotAutomatic: yes" will be set
        in the Release file. In a similar fashion, the butautomaticupgrades
        argument controls ButAutomaticUpgrades.

        If codename_symlink is true, the archive will be accessible by using
        the specified codename in the APT sources. This is accomplished by
        creating a dists/codename -> dists/suite symlink. This is useful to
        create archives with a Debian-like layout, where "unstable" is also
        available via its codename "sid".

        The archive is initially empty. You can create new packages with
        create_deb(). self.path contains the path of the archive, and
        self.apt_source provides an apt source "deb" line.

        If path is None (default), it is kept in a temporary directory which
        gets removed when the Archive object gets deleted. Otherwise the given
        path is used, which is useful for creating multiple suites/components;
        then you should use a different pooldir.
        """
        if path:
            self.path = path
        else:
            self.path = tempfile.mkdtemp(prefix="testarchive.")
            atexit.register(shutil.rmtree, self.path)
        self.pooldir = pooldir

        arch = subprocess.check_output(
            ["dpkg", "--print-architecture"], text=True
        ).strip()
        self.suite = suite
        self.codename = codename
        self.component = component
        self.notautomatic = notautomatic
        self.butautomaticupgrades = butautomaticupgrades
        if suite or codename or component:
            assert suite and codename and component, (
                "must specify all of none of: suite, codename, component"
            )
            self.index_dir = os.path.join("dists", suite, component, "binary-" + arch)
            os.makedirs(os.path.join(self.path, self.index_dir))
            self.apt_source = "deb [trusted=yes arch={}] file://{} {} {}".format(
                arch,
                self.path,
                suite,
                component,
            )

            if codename_symlink and codename != suite:
                os.symlink(suite, os.path.join(self.path, "dists", codename))
        else:
            self.apt_source = "deb [trusted=yes arch={}] file://{} /".format(
                arch,
                self.path,
            )
            self.index_dir = ""

    def create_deb(
        self,
        name,
        version="1",
        architecture="all",
        dependencies={},
        description="test package",
        extra_tags={},
        files={},
        component="main",
        srcpkg=None,
        update_index=True,
    ):
        """Build a deb package and add it to the archive.

        The only mandatory argument is the package name. You can additionally
        specify the package version (default '1'), architecture (default
        'all'), a dictionary with dependencies (empty by default; for example
        {'Depends': 'foo, bar', 'Conflicts: baz'}, a short description
        (default: 'test package'), and arbitrary extra tags.

        By default the package is empty. It can get files by specifying a
        path -> contents dictionary in 'files'. Paths must be relative.
        Example: files={'etc/foo.conf': 'enable=true'}

        The newly created deb automatically gets added to the "Packages" index,
        unless update_index is False.

        Return the path to the newly created deb package, in case you only need
        the deb itself, not the archive.
        """
        d = tempfile.mkdtemp()
        os.mkdir(os.path.join(d, "DEBIAN"))
        with open(os.path.join(d, "DEBIAN", "control"), "w") as f:
            f.write(
                """Package: %s
Maintainer: Test User <test@example.com>
Version: %s
Priority: optional
Section: devel
Architecture: %s
"""
                % (name, version, architecture)
            )

            if srcpkg:
                f.write("Source: %s\n" % srcpkg)

            for k, v in dependencies.items():
                f.write(f"{k}: {v}\n")

            f.write(
                """Description: %s
 Test dummy package.
"""
                % description
            )

            for k, v in extra_tags.items():
                f.write(f"{k}: {v}\n")

        for path, contents in files.items():
            if isinstance(contents, bytes):
                mode = "wb"
            else:
                mode = "w"
            pathdir = os.path.join(d, os.path.dirname(path))
            if not os.path.isdir(pathdir):
                os.makedirs(pathdir)
            with open(os.path.join(d, path), mode) as f:
                f.write(contents)

        if srcpkg is None:
            srcpkg = name
        if srcpkg.startswith("lib"):
            prefix = srcpkg[:4]
        else:
            prefix = srcpkg[0]
        dir = os.path.join(self.path, self.pooldir, component, prefix, srcpkg)
        if not os.path.isdir(dir):
            os.makedirs(dir)

        debpath = os.path.join(dir, f"{name}_{version}_{architecture}.deb")
        subprocess.check_call(
            ["dpkg-deb", "-Zgzip", "-b", d, debpath],
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
        )

        shutil.rmtree(d)
        assert os.path.exists(debpath)

        if update_index:
            self.update_index()

        return debpath

    def add_sources(self, name, binaries, version="1", component="main"):
        """Add source package entry to the Sources index"""

        if type(binaries) is not dict:
            binaries = {k: "any" for k in binaries}

        if name.startswith("lib"):
            prefix = name[:4]
        else:
            prefix = name[0]
        poolsubdir = os.path.join(self.pooldir, self.component, prefix, name)

        # create a dsc
        dir = os.path.join(self.path, self.pooldir, component, prefix, name)
        if not os.path.isdir(dir):
            os.makedirs(dir)
        dscpath = os.path.join(dir, f"{name}_{version}.dsc")
        with open(dscpath, mode="w"):
            # For now we only care about the file being present,
            # so let's create it empty.
            pass

        if self.index_dir:
            srcindex_dir = os.path.join(
                self.path, os.path.dirname(self.index_dir), "source"
            )
        else:
            srcindex_dir = self.index_dir
        if not os.path.isdir(srcindex_dir):
            os.makedirs(srcindex_dir)

        with open(os.path.join(srcindex_dir, "Sources"), "a") as f:
            f.write(
                textwrap.dedent(
                    f"""\
                    Package: {name}
                    Binary: {", ".join(binaries.keys())}
                    Version: {version}
                    Priority: optional
                    Architecture: any all
                    Format: 1.0
                    Directory: {poolsubdir}
                    Files:
                     d41d8cd98f00b204e9800998ecf8427e 0 {name}_{version}.dsc
                    Package-List:
                    """
                )
            )
            for b, a in binaries.items():
                f.write(f" {b} deb admin optional arch={a}\n")
            f.write("Standards-Version: 1.0\n\n")

    def update_index(self):
        """Update the Packages index and Release file.

        This usually gets done automatically by create_deb(), but needs to be
        done if you manually copy debs into the archive or call create_deb with
        update_index==False.
        """
        old_cwd = os.getcwd()
        try:
            os.chdir(self.path)
            with open(os.path.join(self.index_dir, "Packages"), "wb") as f:
                subprocess.check_call(
                    ["apt-ftparchive", "packages", self.pooldir], stdout=f
                )
            if self.suite:
                rp = os.path.join(self.path, "dists", self.suite, "Release")
                try:
                    os.unlink(rp)
                except OSError:
                    pass
                release_cmd = [
                    "apt-ftparchive",
                    "release",
                    "-o",
                    "APT::FTPArchive::Release::Suite=" + self.suite,
                    "-o",
                    "APT::FTPArchive::Release::Codename=" + self.codename,
                ]
                if self.notautomatic:
                    release_cmd += ["-o", "APT::FTPArchive::Release::NotAutomatic=yes"]
                if self.butautomaticupgrades:
                    release_cmd += [
                        "-o",
                        "APT::FTPArchive::Release::ButAutomaticUpgrades=yes",
                    ]
                release_cmd += [os.path.dirname(rp)]
                release = subprocess.check_output(release_cmd)
                with open(rp, "wb") as f:
                    f.write(release)
        finally:
            os.chdir(old_cwd)
        subprocess.check_call(["chmod", "-R", "a+rX", "--", self.path])


if __name__ == "__main__":
    r = Archive(suite="testy", component="main")
    r.create_deb("vanilla")
    r.create_deb("libvanilla0", srcpkg="vanilla")
    r.create_deb("chocolate", dependencies={"Depends": "vanilla"})
    print(r.apt_source)
    r.add_sources("vanilla", ["vanilla", "libvanilla0"])

    p = Archive(
        path=r.path,
        pooldir="pool-proposed",
        suite="testy-proposed",
        codename="testy",
        component="main",
    )
    p.create_deb("vanilla", "2")
    r.create_deb("libvanilla0", "2", srcpkg="vanilla")
    p.create_deb("chocolate", "2", dependencies={"Depends": "vanilla (>= 2)"})
    print(p.apt_source)
    p.add_sources("vanilla", ["vanilla", "libvanilla0"], "2")

    subprocess.call(["bash", "-i"], cwd=r.path)
