"""This module tests the django commands in the common app"""

import shutil
import os
from glob import glob

from io import StringIO
from django.conf import settings
from django.core.management import call_command
from mock import patch

from mtp.apps.accounts.factories import AvatarFactory
from mtp.apps.accounts.models import MTPUser
from mtp.apps.common.tests.test_cases import MTPTestCase, WithMediaOnDisk
from mtp.apps.common.management.commands import clean_media


class CleanMediaTestCase(MTPTestCase, WithMediaOnDisk):
    """Test the ``clean_media`` command and its utility functions

    As we inherit from ``MTPTestCase``, we have data created at the beginning of the class
    which will create some files: user avatars and theme files.
    Just after, we create a copy of the media directory to restore it after each test,
    to be sure to be in the exact same situation.
    Then when quitting the class we delete the copy of this directory

    """

    media_copy_path = os.path.join(settings.MEDIA_ROOT, 'default', 'copy')

    @classmethod
    def setUpClass(cls):
        """Make sure the media directory is clean when we start"""

        # keep only the default directory
        cls.clean_media()

        # create objects that may create files
        super().setUpClass()

        # make a copy of the media directory to restore it after each test

        # remove the copy directory if present
        shutil.rmtree(cls.media_copy_path, ignore_errors=True)
        # copy all media excluding the default directory
        shutil.copytree(
            src=settings.MEDIA_ROOT,
            dst=cls.media_copy_path,
            ignore=lambda src, names: ['default']
        )

    @classmethod
    def tearDownClass(cls):
        """Remove the copy of the media directory kept in the default media directory"""

        super().tearDownClass()

        shutil.rmtree(cls.media_copy_path, ignore_errors=True)

    def tearDown(self):
        """Restore the media from the copy"""

        super().tearDown()

        # start by removing all but default
        self.clean_media()

        # then restore the copy (cannot use copytree as the directory exists)

        for subpath in glob(os.path.join(self.media_copy_path, '*')):
            if os.path.isdir(subpath):
                shutil.copytree(
                    src=subpath,
                    dst=os.path.join(settings.MEDIA_ROOT, os.path.basename(subpath))
                )
            else:
                shutil.copy(subpath, self.media_copy_path)

    def test_get_media_on_disk(self):
        """Test the ``get_media_on_disk`` function"""

        all_on_disk = clean_media.exclude_default_media(
            clean_media.get_media_on_disk()
        )

        # We should have one avatar for each user
        avatars = set(MTPUser.objects.values_list('photo__img', flat=True))
        self.assertEqual(all_on_disk, avatars)

        # Create a new file
        avatar = AvatarFactory()

        all_on_disk = clean_media.exclude_default_media(
            clean_media.get_media_on_disk()
        )

        self.assertEqual(all_on_disk, avatars.union([avatar.img.name]))

    def test_get_media_on_db(self):
        """Test the ``get_media_on_db`` function"""

        all_on_db = clean_media.get_media_on_db()

        # We should have one avatar for each user
        avatars = set(MTPUser.objects.values_list('photo__img', flat=True))

        # But also some other files
        theme_files = set(
            getattr(self.theme, field)
            for field
            in ['brand_logo', 'favicon', 'header_image', 'home_diffusion_image', 'reveal_logo']
        )

        expected_files = avatars.union(theme_files)

        self.assertEqual(all_on_db, expected_files)

        # Create a new file
        avatar = AvatarFactory()

        all_on_db = clean_media.get_media_on_db()

        self.assertEqual(all_on_db, expected_files.union([avatar.img.name]))

    def test_clean_media_command(self):
        """Test the ``clean_media`` django command"""

        # start clean, all files on disk should be the one on db

        # first in dry-run
        out = StringIO()
        call_command('clean_media', dry_run=True, stdout=out)
        self.assertEqual(out.getvalue(), '\nNo orphan media files detected to delete.\n')

        # then without dry-run
        out = StringIO()
        call_command('clean_media', stdout=out)
        self.assertEqual(out.getvalue(), '\nNo orphan media files detected to delete.\n')

        # then replace some avatars (keep file paths to check deletion)
        deleted = tuple(sorted([self.admin.photo.img.name, self.trainer.photo.img.name]))
        self.admin.photo.delete()
        self.admin.photo = AvatarFactory()
        self.admin.save()
        self.trainer.photo.delete()
        self.trainer.photo = AvatarFactory()
        self.trainer.save()

        # we should have two files to delete

        # first in dry-run
        out = StringIO()
        call_command('clean_media', dry_run=True, stdout=out)
        self.assertEqual(
            out.getvalue(),
            '\n'.join([
                '',
                '2 orphan media files detected to delete (dry-run):',
                '',
                '%s',
                '%s',
                '',
                'No orphan media files deleted (2 found) because of dry-run mode.',
                '',
            ]) % deleted
        )

        # both files should still be there
        self.assertTrue(os.path.isfile(os.path.join(settings.MEDIA_ROOT, deleted[0])))
        self.assertTrue(os.path.isfile(os.path.join(settings.MEDIA_ROOT, deleted[1])))

        # then without dry-run
        out = StringIO()
        call_command('clean_media', stdout=out)
        self.assertEqual(
            out.getvalue(),
            '\n'.join([
                '',
                '2 orphan media files detected to delete:',
                '',
                '%s  OK',
                '%s  OK',
                '',
                '2 orphan media files deleted.',
                '',
            ]) % deleted
        )

        # both file should be deleted
        self.assertFalse(os.path.exists(os.path.join(settings.MEDIA_ROOT, deleted[0])))
        self.assertFalse(os.path.exists(os.path.join(settings.MEDIA_ROOT, deleted[1])))

        # now try error management

        # replace again some avatars (keep file paths to check deletion)
        deleted = tuple(sorted([self.admin.photo.img.name, self.trainer.photo.img.name]))
        self.admin.photo.delete()
        self.admin.photo = AvatarFactory()
        self.admin.save()
        self.trainer.photo.delete()
        self.trainer.photo = AvatarFactory()
        self.trainer.save()

        # we'll use mock to patch the `get_on_disk_not_on_db` to simulate
        # a file that cannot be deleted, to generate an error
        simulate_deleted = set(deleted).union(['foo/bar.baz'])

        # the command should succeed but with the error shown

        out = StringIO()
        err = StringIO()

        with patch(
            'mtp.apps.common.management.commands.clean_media.get_on_disk_not_on_db',
            return_value=simulate_deleted
        ):
            call_command('clean_media', stdout=out, stderr=err)

        # error indication on stdout
        self.assertEqual(
            out.getvalue(),
            '\n'.join([
                '',
                '3 orphan media files detected to delete:',
                '',
                '%s  OK',
                '%s  OK',
                '%s  ERROR',
                '',
                "2 orphan media files deleted, and 1 couldn't.",
                '',
            ]) % tuple(sorted(simulate_deleted))
        )

        # real error on stderr
        self.assertEqual(
            err.getvalue(),
            "-> ERROR while removing %(path)s: "
            "[Errno 2] No such file or directory: '%(path)s'\n" % {
                'path': os.path.join(settings.MEDIA_ROOT, 'foo/bar.baz')
            }
        )

        # both file should be deleted
        self.assertFalse(os.path.exists(os.path.join(settings.MEDIA_ROOT, deleted[0])))
        self.assertFalse(os.path.exists(os.path.join(settings.MEDIA_ROOT, deleted[1])))
