#!/usr/bin/env python3
# Copyright 2020 Collabora Ltd.
#
# SPDX-License-Identifier: MIT

import os
import subprocess
import sys
import tempfile
from pathlib import Path


try:
    import typing
    typing      # placate pyflakes
except ImportError:
    pass

from testutils import (
    BaseTest,
    test_main,
)


class TestCheapCopy(BaseTest):
    def setUp(self) -> None:
        super().setUp()
        os.environ['G_MESSAGES_DEBUG'] = 'all'
        self.cheap_copy = os.path.join(self.G_TEST_BUILDDIR, 'test-cheap-copy')

    def assert_tree_is_superset(
        self,
        superset,
        subset,
        require_hard_links: bool = True,
        require_same_mtime: bool = True,
    ):
        for path, dirs, files in os.walk(subset):
            equivalent = os.path.join(superset, os.path.relpath(path, subset))

            for d in dirs:
                in_subset = os.path.join(path, d)
                in_superset = os.path.join(equivalent, d)

                if not os.path.isdir(in_superset):
                    raise AssertionError(
                        '%r should be a directory', in_superset)

                info = os.stat(in_subset)
                info2 = os.stat(in_superset)
                self.assertEqual(info.st_mode, info2.st_mode)

            for f in files:
                in_subset = os.path.join(path, f)
                in_superset = os.path.join(equivalent, f)

                if (
                    os.path.islink(in_subset)
                    or not os.path.exists(in_subset)
                ):
                    target = os.readlink(in_subset)
                    target2 = os.readlink(in_superset)
                    self.assertEqual(target, target2)
                else:
                    info = os.stat(in_subset)
                    info2 = os.stat(in_superset)

                    if require_hard_links:
                        self.assertEqual(info.st_ino, info2.st_ino)
                        self.assertEqual(info.st_dev, info2.st_dev)

                    self.assertEqual(info.st_mode, info2.st_mode)
                    self.assertEqual(info.st_size, info2.st_size)

                    if require_same_mtime:
                        # Different filesystems don't necessarily have
                        # the same resolution, so we only assert that the
                        # integer part is equal
                        self.assertEqual(
                            int(info.st_mtime),
                            int(info2.st_mtime),
                        )

    def assert_tree_is_same(
        self,
        left,
        right,
        require_hard_links=True,
        require_same_mtime=True,
    ):
        for pair in (left, right), (right, left):
            self.assert_tree_is_superset(
                pair[0],
                pair[1],
                require_hard_links=require_hard_links,
                require_same_mtime=require_same_mtime,
            )

    def test_empty(self) -> None:
        with tempfile.TemporaryDirectory(
        ) as source, tempfile.TemporaryDirectory(
        ) as dest:
            subprocess.run(
                [
                    self.cheap_copy,
                    source,
                    dest,
                ],
                check=True,
                stdout=2,       # >&2, i.e. stderr
            )
            self.assert_tree_is_same(source, dest)

    def test_create(self) -> None:
        with tempfile.TemporaryDirectory(
        ) as source, tempfile.TemporaryDirectory(
        ) as parent:
            dest = os.path.join(parent, 'dest')
            subprocess.run(
                [
                    self.cheap_copy,
                    source,
                    dest,
                ],
                check=True,
                stdout=2,       # >&2, i.e. stderr
            )
            self.assert_tree_is_same(source, dest)

    def test_populated(
        self,
        dir1=None,      # type: typing.Optional[str]
        dir2=None,      # type: typing.Optional[str]
        require_hard_links=True,
    ) -> None:
        with tempfile.TemporaryDirectory(
            dir=dir1,
        ) as source, tempfile.TemporaryDirectory(
            dir=dir2,
        ) as parent:
            os.makedirs(os.path.join(source, 'a', 'b', 'c'))
            os.makedirs(os.path.join(source, 'files'))

            with open(os.path.join(source, 'x'), 'w') as writer:
                writer.write('hello')

            # Use unusual (but safe) permissions to assert that they get
            # copied
            os.chmod(source, 0o2740)
            os.chmod(os.path.join(source, 'x'), 0o651)

            with open(os.path.join(source, 'files', 'y'), 'w') as writer:
                writer.write('hello')

            os.symlink('y', os.path.join(source, 'files', 'exists'))
            os.symlink('/dev', os.path.join(source, 'files', 'dev'))
            os.symlink('no', os.path.join(source, 'files', 'not here'))

            dest = os.path.join(parent, 'dest')
            subprocess.run(
                [
                    self.cheap_copy,
                    source,
                    dest,
                ],
                check=True,
                stdout=2,       # >&2, i.e. stderr
            )
            self.assert_tree_is_same(source, dest, require_hard_links)

    def test_cannot_hard_link(self):
        """
        Assert that we can copy a directory hierarchy between directories
        that might be on different filesystems.

        If /tmp and /var/tmp are both on the same mount point, this is
        equivalent to test_populated(), but if they are on different
        mount points (/tmp is often a tmpfs) then this exercises different
        code paths.
        """
        self.test_populated('/tmp', '/var/tmp', require_hard_links=False)

    def test_usrmerge(self):
        with tempfile.TemporaryDirectory(
        ) as source, tempfile.TemporaryDirectory(
        ) as parent, tempfile.TemporaryDirectory(
        ) as expected:
            (Path(source) / 'bin').mkdir(parents=True)
            (Path(source) / 'lib').mkdir(parents=True)
            (Path(source) / 'lib/x86_64-linux-gnu').mkdir(parents=True)
            (Path(source) / 'lib32').mkdir(parents=True)
            (Path(source) / 'usr/bin').mkdir(parents=True)
            (Path(source) / 'usr/bin/which').touch()
            (Path(source) / 'bin/which').symlink_to('/usr/bin/which')
            (Path(source) / 'bin/less').touch()
            (Path(source) / 'usr/bin/less').symlink_to('/bin/less')
            (Path(source) / 'bin/more').touch()
            (Path(source) / 'usr/bin/more').symlink_to('../../bin/more')
            (Path(source) / 'usr/bin/env').touch()
            (Path(source) / 'bin/env').symlink_to('../usr/bin/env')
            (Path(source) / 'usr/bin/gcc').symlink_to('gcc-9')
            (Path(source) / 'usr/bin/foo').symlink_to('/bin/foo-1')
            (Path(source) / 'usr/bin/bar').symlink_to('../../bin/bar-2')
            (Path(source) / 'usr/lib/x86_64-linux-gnu').mkdir(parents=True)
            (Path(source) / 'bin/x').symlink_to('/usr/bin/x-1')
            (Path(source) / 'bin/y').symlink_to('../usr/bin/x-2')
            (Path(source) / 'lib/x86_64-linux-gnu/libpng12.so.0').symlink_to(
                'libpng12.so.0.46.0')
            (Path(source) / 'lib/x86_64-linux-gnu/libpng12.so.0.46.0').touch()
            (
                Path(source) / 'usr/lib/x86_64-linux-gnu/libpng12.so.0'
            ).symlink_to('/lib/x86_64-linux-gnu/libpng12.so.0')
            (
                Path(source) / 'usr/lib/x86_64-linux-gnu/libpng12.so'
            ).symlink_to('libpng12.so.0')

            (Path(expected) / 'bin').symlink_to('usr/bin')
            (Path(expected) / 'lib').symlink_to('usr/lib')
            (Path(expected) / 'lib32').symlink_to('usr/lib32')
            (Path(expected) / 'usr/lib').mkdir(parents=True)
            (Path(expected) / 'usr/lib/x86_64-linux-gnu').mkdir(parents=True)
            (Path(expected) / 'usr/lib32').mkdir(parents=True)
            (Path(expected) / 'usr/bin').mkdir(parents=True)
            (Path(expected) / 'usr/bin/which').touch()
            (Path(expected) / 'usr/bin/less').touch()
            (Path(expected) / 'usr/bin/more').touch()
            (Path(expected) / 'usr/bin/env').touch()
            (Path(expected) / 'usr/bin/gcc').symlink_to('gcc-9')
            (Path(expected) / 'usr/bin/foo').symlink_to('/bin/foo-1')
            (Path(expected) / 'usr/bin/bar').symlink_to('../../bin/bar-2')
            (Path(expected) / 'bin/x').symlink_to('/usr/bin/x-1')
            (Path(expected) / 'bin/y').symlink_to('../usr/bin/x-2')
            (
                Path(expected) / 'usr/lib/x86_64-linux-gnu/libpng12.so.0'
            ).symlink_to('libpng12.so.0.46.0')
            (
                Path(expected) / 'usr/lib/x86_64-linux-gnu/libpng12.so.0.46.0'
            ).touch()
            (
                Path(expected) / 'usr/lib/x86_64-linux-gnu/libpng12.so'
            ).symlink_to('libpng12.so.0')

            dest = os.path.join(parent, 'dest')
            subprocess.run(
                [
                    self.cheap_copy,
                    '--usrmerge',
                    source,
                    dest,
                ],
                check=True,
                stdout=2,       # >&2, i.e. stderr
            )
            self.assert_tree_is_same(
                expected,
                dest,
                require_hard_links=False,
                # We are not directly comparing source with dest, so the
                # mtime seen in expected is not necessarily the same as
                # the mtime in dest, if the time spent creating the two
                # trees crossed the boundary between one second and the next.
                require_same_mtime=False,
            )

    def tearDown(self) -> None:
        super().tearDown()


if __name__ == '__main__':
    assert sys.version_info >= (3, 5), \
        'Python 3.5+ is required (configure with -Dpython=python3.5 ' \
        'if necessary)'

    test_main()

# vi: set sw=4 sts=4 et:
