'''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'],
                                       universal_newlines=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=%s] file://%s %s %s' % \
                (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=%s] file://%s /' % \
                (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('%s: %s\n' % (k, v))

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

            for k, v in extra_tags.items():
                f.write('%s: %s\n' % (k, v))

        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, '%s_%s_%s.deb' % (name, version,
                                                      architecture))
        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 not type(binaries) is 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, '%s_%s.dsc' % (name, version))
        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(' %s deb admin optional arch=%s\n' % (b, a))
            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)
