#!/usr/bin/python3
# encoding=utf-8
#
# Copyright © 2016-2018 Simon McVittie <smcv@debian.org>
# SPDX-License-Identifier: GPL-2.0-or-later

import os
import subprocess
import sys
import tarfile
import unittest
import zipfile
from pathlib import (Path)
from tempfile import (TemporaryDirectory)

if 'GDP_UNINSTALLED' in os.environ:
    sys.path.insert(0, os.path.dirname(os.path.dirname(
                                       os.path.abspath(__file__))))
else:
    sys.path.insert(0, '/usr/share/game-data-packager')
    sys.path.insert(0, '/usr/share/games/game-data-packager')

from game_data_packager.util import (run_as_root)
from game_data_packager.version import (FORMAT, GAME_PACKAGE_VERSION)


class IntegrationTestCase(unittest.TestCase):
    def setUp(self) -> None:
        if 'GDP_BUILDDIR' in os.environ:
            self.builddir = os.environ['GDP_BUILDDIR']
        else:
            self.builddir = os.path.join(os.getcwd(), 'out')

        if 'GDP_UNINSTALLED' in os.environ:
            self.exe = os.path.join(
                os.environ.get('GDP_BUILDDIR', 'out'),
                'run-gdp-uninstalled',
            )
        else:
            self.exe = 'game-data-packager'

        if not os.path.exists(os.path.join(self.builddir, 'meson-info')):
            subprocess.check_call([
                os.environ.get('MAKE', 'make'),
                'out/tests/changelog.gz',
                'out/tests/copyright',
                'out/tests/vfs.zip',
            ])

    def test_scummvm(
        self,
        install: bool = False,
        method: str = '',
        gain_root: str = ''
    ) -> None:
        with TemporaryDirectory(prefix='gdptest.') as tmp:
            os.mkdir(os.path.join(tmp, 'in'))
            os.mkdir(os.path.join(tmp, 'out'))

            K = b'\0' * 1024

            with open(os.path.join(tmp, 'in', '1K.0'), 'wb') as writer:
                writer.write(K)

            with open(os.path.join(tmp, 'in', '4K.0'), 'wb') as writer:
                for i in range(4):
                    writer.write(K)

            with open(os.path.join(tmp, 'in', '1M.0'), 'wb') as writer:
                for i in range(1024):
                    writer.write(K)

            with open(os.path.join(tmp, 'in', 'manual.pdf'), 'wb') as writer:
                writer.write(b'this is not a PDF\n')

            with open(os.path.join(tmp, 'in', 'booklet.pdf'), 'wb') as writer:
                writer.write(b'this is not a PDF either\n')

            # Also create the alternative music file for this testing round
            with open(os.path.join(tmp, 'in', 'music.blob'), 'wb') as writer:
                for i in range(512):
                    writer.write(K)

            package_names = [
                'gdptest-doc',
                'gdptest6-data',
                'gdptest6-svga-data',
            ]
            data_version = '1.000.000+' + GAME_PACKAGE_VERSION
            doc_version = GAME_PACKAGE_VERSION

            env = os.environ.copy()
            env['GDP_PKGDATADIR'] = os.path.join(self.builddir, 'tests')

            argv = [self.exe]

            if install:
                assert os.environ.get('GDP_TEST_DESTRUCTIVE', '')
                run_as_root(['dpkg', '--purge'] + package_names, gain_root)

                argv.append('--force-install')

                if gain_root:
                    argv.extend(['--gain-root-command', gain_root])

                if method:
                    argv.extend(['--install-method', method])
            else:
                argv.extend([
                    '-d', os.path.join(tmp, 'out'),
                ])

            argv = argv + [
                '--no-compress',
                'scummvm',
                '--no-download',
                '--no-search',
                os.path.join(tmp, 'in'),
            ]

            # stdout=2 is effectively 2>&1
            subprocess.check_call(argv, env=env, stdout=2)

            if FORMAT == 'deb' and not install:
                import debian.deb822

                doc_deb = os.path.join(
                    tmp, 'out', 'gdptest-doc_%s_all.deb' % doc_version)
                vga_deb = os.path.join(
                    tmp, 'out',
                    'gdptest6-data_%s_all.deb' % data_version)
                svga_deb = os.path.join(
                    tmp, 'out',
                    'gdptest6-svga-data_%s_all.deb' % data_version,
                )
                music_deb = os.path.join(
                    tmp, 'out',
                    'gdptest6-music_%s_all.deb' % data_version
                )

                self.assertTrue(os.path.isfile(doc_deb))
                self.assertTrue(os.path.isfile(vga_deb))
                self.assertTrue(os.path.isfile(svga_deb))
                self.assertTrue(os.path.isfile(music_deb))

                blob = subprocess.check_output(
                    ['dpkg-deb', '-f', doc_deb],
                )
                meta = debian.deb822.Deb822(blob)
                description = meta.get('Description')
                self.assertIsInstance(description, str)
                assert isinstance(description, str)

                self.assertEqual(meta.get('Package'), 'gdptest-doc')
                self.assertEqual(meta.get('Version'), doc_version)
                self.assertEqual(meta.get('Priority'), 'optional')
                self.assertEqual(meta.get('Section'), 'local/doc')
                self.assertEqual(meta.get('Architecture'), 'all')
                self.assertEqual(meta.get('Multi-Arch'), 'foreign')
                self.assertEqual(
                    meta.get('Depends'),
                    ('gdptest1-data | gdptest2-data | gdptest3-data | '
                     'gdptest5-data | gdptest6-data | gdptest6-svga-data'),
                )
                self.assertEqual(meta.get('Recommends'), None)
                self.assertIn('Genre: Adventure', description)
                self.assertIn(
                    'Documentation: A ScummVM test vaguely resembling '
                    'Leisure Suit Larry 6',
                    description)
                self.assertIn(
                    'Published by: nobody',
                    description)

                blob = subprocess.check_output(
                    ['dpkg-deb', '-f', vga_deb],
                )
                meta = debian.deb822.Deb822(blob)
                description = meta.get('Description')
                self.assertIsInstance(description, str)
                assert isinstance(description, str)

                self.assertEqual(meta.get('Package'), 'gdptest6-data')
                self.assertEqual(meta.get('Version'), data_version)
                self.assertEqual(meta.get('Priority'), 'optional')
                self.assertEqual(meta.get('Section'), 'local/games')
                self.assertEqual(meta.get('Architecture'), 'all')
                self.assertEqual(meta.get('Multi-Arch'), 'foreign')
                self.assertEqual(meta.get('Depends'), None)
                self.assertEqual(
                    meta.get('Recommends'),
                    'game-data-packager-runtime (>= %s~), '
                    'scummvm' % GAME_PACKAGE_VERSION)
                self.assertIn('Genre: Adventure', description)
                self.assertIn(
                    'Game: A ScummVM test vaguely resembling '
                    'Leisure Suit Larry 6',
                    description)
                self.assertIn('Published by: nobody', description)

                blob = subprocess.check_output(
                    ['dpkg-deb', '-f', svga_deb],
                )
                meta = debian.deb822.Deb822(blob)
                description = meta.get('Description')
                self.assertIsInstance(description, str)
                assert isinstance(description, str)

                self.assertEqual(meta.get('Package'), 'gdptest6-svga-data')
                self.assertEqual(meta.get('Version'), data_version)
                self.assertEqual(meta.get('Priority'), 'optional')
                self.assertEqual(meta.get('Section'), 'local/games')
                self.assertEqual(meta.get('Architecture'), 'all')
                self.assertEqual(meta.get('Multi-Arch'), 'foreign')
                self.assertEqual(meta.get('Depends'), None)
                self.assertEqual(
                    meta.get('Recommends'),
                    'game-data-packager-runtime (>= %s~), '
                    'scummvm' % GAME_PACKAGE_VERSION)
                self.assertEqual(meta.get('Breaks'), 'scummvm (<< 2.0.0~)')
                self.assertIn('Genre: Adventure', description)
                self.assertIn(
                    'Game: A ScummVM test vaguely resembling '
                    'Leisure Suit Larry 6',
                    description)
                self.assertIn('Published by: nobody', description)

                blob = subprocess.check_output(
                    ['dpkg-deb', '-f', music_deb],
                )
                meta = debian.deb822.Deb822(blob)

                self.assertEqual(meta.get('Package'), 'gdptest6-music')
                self.assertEqual(
                    meta.get('Version'), '1.000.000+' + GAME_PACKAGE_VERSION)
                self.assertEqual(meta.get('Priority'), 'optional')
                self.assertEqual(meta.get('Section'), 'local/games')
                self.assertEqual(meta.get('Architecture'), 'all')
                self.assertEqual(meta.get('Multi-Arch'), 'foreign')
                self.assertEqual(meta.get('Depends'), 'gdptest6-data')
                self.assertEqual(meta.get('Recommends'), None)
                self.assertEqual(meta.get('Breaks'), None)
                self.assertIn(
                    'game music for',
                    meta.get('Description'))
                self.assertIn(
                    'Genre: Adventure',
                    meta.get('Description'))
                self.assertIn(
                    'Game: A ScummVM test vaguely resembling '
                    'Leisure Suit Larry 6',
                    meta.get('Description'))
                self.assertIn(
                    'Published by: nobody',
                    meta.get('Description'))

                contents = subprocess.check_output(
                    ['dpkg-deb', '--contents', doc_deb],
                    text=True,
                )
                self.assertIn(
                    './usr/share/doc/gdptest-doc/\n', contents)
                self.assertIn(
                    './usr/share/doc/gdptest-doc/booklet.pdf\n', contents)
                self.assertIn(
                    './usr/share/doc/gdptest-doc/changelog.gz\n', contents)
                self.assertIn(
                    './usr/share/doc/gdptest-doc/copyright\n', contents)
                self.assertNotIn(
                    './usr/share/applications/\n', contents)

                contents = subprocess.check_output(
                    ['dpkg-deb', '--contents', vga_deb],
                    text=True,
                )
                self.assertIn(
                    './usr/share/applications/gdptest6.desktop -> ',
                    contents,
                )
                self.assertIn(
                    './usr/share/doc/gdptest6-data/changelog.gz\n', contents)
                self.assertIn(
                    './usr/share/doc/gdptest6-data/copyright\n', contents)
                self.assertIn(
                    './usr/share/doc/gdptest6-data/manual.pdf\n', contents)
                self.assertIn(
                    './usr/share/games/gdptest6/1K.0\n', contents)
                self.assertIn(
                    './usr/share/games/gdptest6/4K.0\n', contents)
                self.assertNotIn(
                    './usr/share/games/gdptest6/1M.0\n', contents)

                contents = subprocess.check_output(
                    ['dpkg-deb', '--contents', svga_deb],
                    text=True,
                )
                self.assertIn(
                    './usr/share/applications/gdptest6-svga.desktop -> ',
                    contents)
                self.assertIn(
                    './usr/share/doc/gdptest6-svga-data/changelog.gz\n',
                    contents)
                self.assertIn(
                    './usr/share/doc/gdptest6-svga-data/copyright\n', contents)
                self.assertIn(
                  './usr/share/doc/gdptest6-svga-data/manual.pdf\n', contents)
                self.assertIn(
                    './usr/share/games/gdptest6-svga/1K.0\n', contents)
                self.assertNotIn(
                    './usr/share/games/gdptest6-svga/4K.0\n', contents)
                self.assertIn(
                    './usr/share/games/gdptest6-svga/1M.0\n', contents)

                contents = subprocess.check_output(
                    ['dpkg-deb', '--contents', music_deb],
                    universal_newlines=True,
                )
                self.assertIn(
                    './usr/share/doc/gdptest6-music/changelog.gz\n', contents)
                self.assertIn(
                    './usr/share/doc/gdptest6-music/copyright\n', contents)
                self.assertIn(
                    './usr/share/games/gdptest6/music.blob\n', contents)
                self.assertNotIn(
                    './usr/share/games/gdptest6/gdptest6_cd_track_2.ogg\n',
                    contents
                )
                self.assertNotIn(
                    './usr/share/games/gdptest6/gdptest6_cd_track_3.ogg\n',
                    contents
                )
                self.assertNotIn(
                    './usr/share/games/gdptest6/gdptest6_cd_track_4.ogg\n',
                    contents
                )

            elif FORMAT == 'deb' and install:
                completed = subprocess.run(
                    [
                        'dpkg-query', '--show',
                        '--showformat', '${Package}\\t${Version}\\n',
                    ] + package_names,
                    check=False,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                    text=True,
                )

                for line in completed.stderr.splitlines(keepends=True):
                    sys.stderr.write('# ' + line)

                if completed.returncode != 0:
                    raise AssertionError(completed.stderr)

                output = completed.stdout
                assert output is not None
                installed: dict[str, str] = {}

                for line in output.splitlines():
                    name, version = line.split('\t')
                    self.assertNotIn(name, installed)
                    installed[name] = version

                self.assertIn('gdptest6-data', installed)
                self.assertEqual(installed['gdptest6-data'], data_version)
                self.assertIn('gdptest-doc', installed)
                self.assertEqual(installed['gdptest-doc'], doc_version)
                self.assertIn('gdptest6-svga-data', installed)
                self.assertEqual(installed['gdptest6-svga-data'], data_version)

                assert os.environ.get('GDP_TEST_DESTRUCTIVE', '')
                run_as_root(['dpkg', '--purge'] + package_names, gain_root)

            if 'GDP_TEST_ALL_FORMATS' in os.environ:
                for f in 'arch deb rpm'.split():
                    subprocess.check_call(
                        argv + ['--target-format', f],
                        env=env,
                    )

    def test_scummvm_existing_music(self) -> None:
        with TemporaryDirectory(prefix='gdptest.') as tmp:
            os.mkdir(os.path.join(tmp, 'in'))
            os.mkdir(os.path.join(tmp, 'out'))

            with open(os.path.join(tmp, 'in', 'gdptest6_cd_track_2.ogg'),
                      'wb') as writer:
                writer.write(b'CD Track 2, length 2:30\n')

            with open(os.path.join(tmp, 'in', 'gdptest6_cd_track_3.ogg'),
                      'wb') as writer:
                writer.write(b'CD Track 3, length 1:15\n')

            with open(os.path.join(tmp, 'in', 'gdptest6_cd_track_4.ogg'),
                      'wb') as writer:
                writer.write(b'CD Track 4, length 3:25\n')

            data_version = '1.000.000+' + GAME_PACKAGE_VERSION
            doc_version = GAME_PACKAGE_VERSION

            env = os.environ.copy()
            env['GDP_PKGDATADIR'] = os.path.join(self.builddir, 'tests')

            argv = [self.exe]

            argv.extend([
                '-d', os.path.join(tmp, 'out'),
            ])

            argv = argv + [
                '--no-compress',
                'scummvm',
                '--no-download',
                '--no-search',
                os.path.join(tmp, 'in'),
            ]

            # stdout=2 is effectively 2>&1
            subprocess.check_call(argv, env=env, stdout=2)

            if FORMAT == 'deb':
                import debian.deb822

                doc_deb = os.path.join(
                    tmp, 'out', 'gdptest-doc_%s_all.deb' % doc_version)
                vga_deb = os.path.join(
                    tmp, 'out',
                    'gdptest6-data_%s_all.deb' % data_version)
                svga_deb = os.path.join(
                    tmp, 'out',
                    'gdptest6-svga-data_%s_all.deb' % data_version,
                )
                music_deb = os.path.join(
                    tmp, 'out',
                    'gdptest6-music_%s_all.deb' % data_version
                )

                self.assertFalse(os.path.isfile(doc_deb))
                self.assertFalse(os.path.isfile(vga_deb))
                self.assertFalse(os.path.isfile(svga_deb))
                self.assertTrue(os.path.isfile(music_deb))

                blob = subprocess.check_output(
                    ['dpkg-deb', '-f', music_deb],
                )
                meta = debian.deb822.Deb822(blob)

                self.assertEqual(meta.get('Package'), 'gdptest6-music')
                self.assertEqual(
                    meta.get('Version'), '1.000.000+' + GAME_PACKAGE_VERSION)
                self.assertEqual(meta.get('Priority'), 'optional')
                self.assertEqual(meta.get('Section'), 'local/games')
                self.assertEqual(meta.get('Architecture'), 'all')
                self.assertEqual(meta.get('Multi-Arch'), 'foreign')
                self.assertEqual(meta.get('Depends'), 'gdptest6-data')
                self.assertEqual(meta.get('Recommends'), None)
                self.assertEqual(meta.get('Breaks'), None)
                self.assertIn(
                    'game music for',
                    meta.get('Description'))
                self.assertIn(
                    'Genre: Adventure',
                    meta.get('Description'))
                self.assertIn(
                    'Game: A ScummVM test vaguely resembling '
                    'Leisure Suit Larry 6',
                    meta.get('Description'))
                self.assertIn(
                    'Published by: nobody',
                    meta.get('Description'))

                contents = subprocess.check_output(
                    ['dpkg-deb', '--contents', music_deb],
                    universal_newlines=True,
                )
                self.assertIn(
                    './usr/share/doc/gdptest6-music/changelog.gz\n', contents)
                self.assertIn(
                    './usr/share/doc/gdptest6-music/copyright\n', contents)
                self.assertNotIn(
                    './usr/share/games/gdptest6/music.blob\n', contents)
                self.assertNotIn(
                    './usr/share/games/gdptest6/gdptest6_cd_track_1.ogg\n',
                    contents
                )
                self.assertIn(
                    './usr/share/games/gdptest6/gdptest6_cd_track_2.ogg\n',
                    contents
                )
                self.assertIn(
                    './usr/share/games/gdptest6/gdptest6_cd_track_3.ogg\n',
                    contents
                )
                self.assertIn(
                    './usr/share/games/gdptest6/gdptest6_cd_track_4.ogg\n',
                    contents
                )
                self.assertIn(
                    './usr/share/games/gdptest6/track1.ogg -> ', contents)
                self.assertIn(
                    './usr/share/games/gdptest6/track2.ogg -> ', contents)
                self.assertIn(
                    './usr/share/games/gdptest6/track3.ogg -> ', contents)
                self.assertNotIn(
                    './usr/share/games/gdptest6/track4.ogg -> ', contents)

            if 'GDP_TEST_ALL_FORMATS' in os.environ:
                for f in 'arch deb rpm'.split():
                    subprocess.check_call(
                        argv + ['--target-format', f],
                        env=env,
                    )

    def test_install_defaults(self) -> None:
        if FORMAT != 'deb':
            self.skipTest('not a .deb based OS')

        if not os.environ.get('GDP_TEST_DESTRUCTIVE', ''):
            self.skipTest('GDP_TEST_DESTRUCTIVE not set')

        self.test_scummvm(install=True)

    def test_install_sudo(self) -> None:
        if FORMAT != 'deb':
            self.skipTest('not a .deb based OS')

        if not os.environ.get('GDP_TEST_DESTRUCTIVE', ''):
            self.skipTest('GDP_TEST_DESTRUCTIVE not set')

        self.test_scummvm(install=True, gain_root='sudo', method='apt')

    def test_install_pkexec(self) -> None:
        if FORMAT != 'deb':
            self.skipTest('not a .deb based OS')

        if not os.environ.get('GDP_TEST_DESTRUCTIVE', ''):
            self.skipTest('GDP_TEST_DESTRUCTIVE not set')

        self.test_scummvm(install=True, gain_root='pkexec', method='dpkg')

    def test_quack2_duplicates(
        self,
    ) -> None:
        with TemporaryDirectory(prefix='gdptest.') as tmpdir:
            tmp = Path(tmpdir)
            (tmp / 'in').mkdir()
            (tmp / 'out').mkdir()
            (tmp / 'again').mkdir()

            # Generate a mockup of Quake 2, which has some duplicate
            # files, and some non-duplicate files of the same size and
            # basename, among its player models.
            #
            # This zip file puts us onto the "unknown archive" code path.
            with zipfile.ZipFile(
                str(tmp / 'in' / 'data.zip'),
                'w',
            ) as archiver:
                for filename in (
                    'female/w_shotgun.md2',
                    'female/weapon.md2',
                ):
                    with archiver.open(filename, 'w') as writer:
                        writer.write(b'f shotgun')

                # Same size as the female version but different content
                for filename in (
                    'cyborg/w_shotgun.md2',
                    'male/w_shotgun.md2',
                    'male/weapon.md2',
                ):
                    with archiver.open(filename, 'w') as writer:
                        writer.write(b'm shotgun')

                # Same size as both the male and female versions but, again,
                # different content.
                for filename in (
                    'cyborg/weapon.md2',
                ):
                    with archiver.open(filename, 'w') as writer:
                        writer.write(b'cy weapon')

                # Different size, two different versions of content
                for filename in (
                    'cyborg/a_grenades.md2',
                    'male/a_grenades.md2',
                ):
                    with archiver.open(filename, 'w') as writer:
                        writer.write(b'm grenades')

                for filename in (
                    'female/a_grenades.md2',
                ):
                    with archiver.open(filename, 'w') as writer:
                        writer.write(b'f grenades')

            env = os.environ.copy()
            env['GDP_PKGDATADIR'] = os.path.join(self.builddir, 'tests')

            argv = [
                self.exe,
                '-d', str(tmp / 'out'),
                '--no-compress',
                '--no-download',
                '--no-search',
                'quack2',
            ]

            subprocess.check_call(
                argv + [str(tmp / 'in' / 'data.zip')],
                env=env,
                stdout=2,
            )

            if FORMAT == 'deb':
                import debian.deb822

                filename = f'quack2-data_{GAME_PACKAGE_VERSION}_all.deb'

                # Repack the .deb file as a .deb file.
                # This reproduces https://bugs.debian.org/1052371
                (tmp / 'out' / filename).rename(tmp / 'in' / filename)
                subprocess.check_call(
                    argv + [str(tmp / 'in' / filename)],
                    env=env,
                    stdout=2,
                )

                for data_deb in (
                    tmp / 'in' / filename,
                    tmp / 'out' / filename,
                ):
                    self.assertTrue(data_deb.is_file())

                    blob = subprocess.check_output(
                        ['dpkg-deb', '-f', str(data_deb)],
                    )
                    meta = debian.deb822.Deb822(blob)
                    description = meta.get('Description')
                    self.assertIsInstance(description, str)
                    assert isinstance(description, str)

                    self.assertEqual(meta.get('Package'), 'quack2-data')
                    self.assertEqual(meta.get('Version'), GAME_PACKAGE_VERSION)
                    self.assertEqual(meta.get('Priority'), 'optional')
                    self.assertEqual(meta.get('Section'), 'local/games')
                    self.assertEqual(meta.get('Architecture'), 'all')
                    self.assertEqual(meta.get('Multi-Arch'), 'foreign')
                    self.assertEqual(meta.get('Recommends'), None)

                    entries: dict[str, bytes] = {}

                    with subprocess.Popen(
                        ['dpkg-deb', '--fsys-tarfile', str(data_deb)],
                        stdout=subprocess.PIPE,
                    ) as dpkgdeb:
                        stdout = dpkgdeb.stdout
                        assert stdout is not None

                        with tarfile.open(
                            'data.tar',
                            mode='r|',
                            fileobj=stdout,
                        ) as untar:
                            for entry in untar:
                                if not entry.isfile():
                                    continue

                                if 'baseq2' not in entry.name:
                                    continue

                                reader = untar.extractfile(entry)
                                assert reader is not None

                                with reader:
                                    entries[entry.name] = reader.read()

                    p = './usr/share/games/quack2/baseq2/players'

                    self.assertEqual(
                        entries[f'{p}/cyborg/a_grenades.md2'], b'm grenades',
                    )
                    self.assertEqual(
                        entries[f'{p}/cyborg/w_shotgun.md2'], b'm shotgun',
                    )
                    self.assertEqual(
                        entries[f'{p}/cyborg/weapon.md2'], b'cy weapon',
                    )
                    self.assertEqual(
                        entries[f'{p}/female/a_grenades.md2'], b'f grenades',
                    )
                    self.assertEqual(
                        entries[f'{p}/female/w_shotgun.md2'], b'f shotgun',
                    )
                    self.assertEqual(
                        entries[f'{p}/female/weapon.md2'], b'f shotgun',
                    )
                    self.assertEqual(
                        entries[f'{p}/male/a_grenades.md2'], b'm grenades',
                    )
                    self.assertEqual(
                        entries[f'{p}/male/w_shotgun.md2'], b'm shotgun',
                    )
                    self.assertEqual(
                        entries[f'{p}/male/weapon.md2'], b'm shotgun',
                    )

    def tearDown(self) -> None:
        pass


if __name__ == '__main__':
    from gdp_test_common import main
    main()
