"""This module holds the test cases for the commands of the ``sync`` app"""

import json
import responses
import requests_mock
from io import StringIO
from mock import patch
from operator import itemgetter

from django.conf import settings
from django.core import serializers
from django.core.management import call_command, CommandError
from django.core.serializers.json import DjangoJSONEncoder
from django.core.urlresolvers import reverse
from django.test.testcases import TestCase
from django.test.utils import override_settings

from waffle.models import Switch

import mtp
from mtp.apps.sync.management.commands.update_up import Command
from mtp.apps.accounts.factories import AvatarFactory, MTPUserFactory

from ..factories import UpdateFactory

from .test_cases import DownloadWaffleBaseTestCase


class UpdateWaffleManagementCommantTestCase(DownloadWaffleBaseTestCase):
    """Test the ``update_waffle_from_master`` management command.

    Attributes
    ----------
    download_waffle_url : string
        The url to download waffle data on the master server.
    master_data : dict
        A dict that mimics the format returned by the ``DownloadWaffle`` view.
        Used to mock the call to the master endpoint.

    Notes
    -----
    As this command is doing a request on a master server, we mock the request to avoid
    the need to have a test server.

    """

    @classmethod
    def setUpTestData(cls):
        """Prepare the url to download waffle and mock data."""

        super().setUpTestData()
        cls.download_waffle_url = settings.MASTER_INSTANCE_URL + reverse('sync:download_waffle')

        json_switches = serializers.serialize('json', [cls.switch1, cls.switch2])
        cls.master_data = {'waffle': {'switch': json_switches}}

    def call_command(self):
        """Call the command faking the response from master and return the output.

        Returns
        -------
        str
            The output of the call to the ``update_waffle_from_master`` management command.

        """

        responses.add(
            responses.POST, self.download_waffle_url,
            body=json.dumps(self.master_data, cls=DjangoJSONEncoder),
            status=200,
        )

        out = StringIO()
        call_command('update_waffle_from_master', stdout=out)
        return out.getvalue()

    def check_output(self, output, expected_lines):
        """Check that the output contains the same lines as the expected ones.

        Parameters
        ----------
        output : str
            The string to test that will be stripped and splitted into lines.
        expected_lines : list
            The list of lines we expect.

        """
        output_lines = output.strip().split('\n')
        self.assertListEqual(output_lines, expected_lines)

    def check_switches(self):
        """Check that the switches are the same than the ones created in ``setUpTestData``.

        The switches created in ``setUpTestData`` are used

        """

        expected_switches = [
            {'name': 'test1', 'active': False},
            {'name': 'test2', 'active': True}
        ]

        existing_switches = sorted([
            {'name': s.name, 'active': s.active}
            for s in Switch.objects.all()
        ], key=itemgetter('name'))

        self.assertListEqual(existing_switches, expected_switches)

    @responses.activate
    def test_nothing_new(self):
        """Test updating switches from master that are the same on local server."""

        output = self.call_command()

        expected = [
            'Switch "test1" is still NOT active',
            'Switch "test2" is still active'
        ]

        self.check_output(output, expected)

    @responses.activate
    def test_all_new(self):
        """Test updating switches from master with no switches on local server."""

        # Delete all switches to have all switches from master appear as new.
        Switch.objects.all().delete()

        output = self.call_command()

        expected = [
            'Switch "test1" is new and is NOT active',
            'Switch "test2" is new and is active',
            'Note: no switches existed before'
        ]

        self.check_output(output, expected)
        self.check_switches()

    @responses.activate
    def test_updated_switches(self):
        """Test updating switches from master with different active state on local server."""

        # Update the switches to make then updated by the management command.
        self.switch1.active = True
        self.switch1.save()
        self.switch2.active = False
        self.switch2.save()

        output = self.call_command()

        expected = [
            'Switch "test1" is now NOT active',
            'Switch "test2" is now active',
        ]

        self.check_output(output, expected)
        self.check_switches()

    @responses.activate
    def test_deleted_switches(self):
        """Test updating switches from master with more switches on local server."""

        # Add new switches that should be deleted after the call.
        Switch.objects.create(name='test3', active=False)
        Switch.objects.create(name='test4', active=True)

        output = self.call_command()

        expected = [
            'Switch "test1" is still NOT active',
            'Switch "test2" is still active',
            'Switch "test3" (NOT active) was removed',
            'Switch "test4" (active) was removed',
        ]

        self.check_output(output, expected)
        self.check_switches()

    @responses.activate
    def test_no_switches_on_master(self):
        """Test updating switches from master which has no switches."""

        # Mimic no switches on the server
        self.master_data = {'waffle': {'switch': serializers.serialize('json', [])}}

        output = self.call_command()

        expected = [
            'Switch "test1" (NOT active) was removed',
            'Switch "test2" (active) was removed',
            'Note: no switches retrieved from master'
        ]

        self.check_output(output, expected)

        # We shouldn't have switches anymore.
        self.assertEqual(Switch.objects.count(), 0)


class UpdateUpCommandTestCase(TestCase):
    """Test the ``update_up`` management command."""

    @classmethod
    def setUpClass(cls):
        """Prepare the urls called by the command and fake responses."""

        super().setUpClass()
        cls.update_up_url = settings.MASTER_INSTANCE_URL + reverse('sync:upload')
        cls.upload_url = settings.MASTER_INSTANCE_URL + reverse('sync:upload_media_archive')
        cls.settings_url = settings.MASTER_INSTANCE_URL + reverse('apiv2:client_settings')

        cls.adapter = requests_mock.Adapter()

    def call_command(self, ignore_version_check=True):
        """Call the command and return the output.

        Parameters
        ----------
        ignore_version_check: bool
            If False, the version will be checked when calling the command,
            Otherwise, the version check is ignored.

        Returns
        -------
        str
            The output of the call to the ``update_up`` management command.

        """

        out = StringIO()
        with patch('mtp.apps.sync.management.commands.update_up.run_post_sync_hooks'):

            if ignore_version_check:
                with patch.object(Command, 'check_version'):
                    call_command('update_up', stdout=out)
            else:
                call_command('update_up', stdout=out)

        return out.getvalue()

    def check_output(self, output, expected_lines):
        """Check that the output contains the same lines as the expected ones.

        Parameters
        ----------
        output : str
            The string to test that will be stripped and splitted into lines.
        expected_lines : list
            The list of lines we expect.

        """

        output_lines = output.strip().split('\n')
        self.assertListEqual(output_lines, expected_lines)

    def create_local_update(self, with_media=False):
        """Helper method to create a sample local update

        The local update is simply a new user.

        Parameters
        ----------
        with_media: bool
            If True, an avatar will be created additionally to the user,
            if False, only the user will be created.

        """

        # Brand new user
        user = MTPUserFactory(photo=None)

        if with_media:
            user.photo = AvatarFactory()

    @override_settings(INSTANCE_IS_SLAVE=False, INSTANCE_IS_MASTER=True)
    def test_command_should_raise_if_called_on_master_instance(self):
        """Test that the command should raise if called on a "MASTER" instance"""

        with self.assertRaisesRegex(  # pylint:disable=deprecated-method
            CommandError,
            'This command can only be run from a "SLAVE" instance'
        ):
            call_command('update_up')

    @override_settings(INSTANCE_IS_SLAVE=True, INSTANCE_IS_MASTER=False)
    def test_command_should_raise_if_called_with_a_different_version(self):
        """Test that the command should raise if the slave and master versions mismatch"""

        with requests_mock.Mocker() as mock:
            mock.register_uri('GET', self.settings_url, json={'server_version': 'pouet'})

            with self.assertRaisesRegex(  # pylint:disable=deprecated-method
                CommandError,
                'MTP version mismatch. The slave version is %(slave_version)s and '
                'the master version is %(master_version)s' % {
                    'slave_version': mtp.__version__,
                    'master_version': 'pouet'
                }
            ):
                self.call_command(ignore_version_check=False)

            # The settings url was called
            self.assertTrue(mock.called)

    @override_settings(INSTANCE_IS_SLAVE=True, INSTANCE_IS_MASTER=False)
    def test_command_without_local_updates_should_ouptut_a_message(self):
        """Test that the command without any local updates should output a message"""

        with requests_mock.Mocker() as mock:
            mock.register_uri('POST', self.update_up_url)
            mock.register_uri('POST', self.upload_url)

            output = self.call_command()

            # We don't have any requests to update endpoints
            self.assertFalse(mock.called)

        self.check_output(
            output,
            [
                'Running pre-sync hooks',
                '=> DONE!',
                'No data to upload'
            ]
        )

    @override_settings(INSTANCE_IS_SLAVE=True, INSTANCE_IS_MASTER=False)
    def test_command_with_local_updates_should_call_the_update_up_url(self):
        """Test that the command with local updates should call the update up url"""

        # Create the local update
        self.create_local_update()

        with requests_mock.Mocker() as mock:
            mock.register_uri(
                'POST',
                self.update_up_url,
                text=serializers.serialize('json', (UpdateFactory(),))
            )

            self.call_command()

            # The update up url is called
            self.assertTrue(mock.called)

    @override_settings(INSTANCE_IS_SLAVE=True, INSTANCE_IS_MASTER=False,
                       DEFAULT_FILE_STORAGE=settings.DJANGO_DEFAULT_FILE_STORAGE)
    def test_command_with_local_media_updates_should_call_the_upload_url(self):
        """Test that the command with local media updates should call the upload url"""

        # Create the local update with medias
        self.create_local_update(with_media=True)

        with requests_mock.Mocker() as mock:
            mock.register_uri(
                'POST',
                self.update_up_url,
                text=serializers.serialize('json', (UpdateFactory(),))
            )
            mock.register_uri('POST', self.upload_url)

            self.call_command()

            # We got two requests
            self.assertTrue(mock.called)
            self.assertEqual(mock.call_count, 2)

            # The two urls were called, in this order:
            self.assertEqual(
                mock.request_history[0].url,
                self.upload_url
            )
            self.assertEqual(
                mock.request_history[1].url,
                self.update_up_url
            )

    @override_settings(INSTANCE_IS_SLAVE=True, INSTANCE_IS_MASTER=False,
                       DEFAULT_FILE_STORAGE=settings.DJANGO_DEFAULT_FILE_STORAGE)
    def test_command_should_raise_if_bad_response_from_upload_media_up_url(self):
        """Test that the command should raise if bad response from the media upload url"""

        # Create the local update
        self.create_local_update(with_media=True)

        with requests_mock.Mocker() as mock:
            mock.register_uri('POST', self.upload_url, status_code=400)

            with self.assertRaisesRegex(  # pylint:disable=deprecated-method
                CommandError,
                'Failed to upload the media. '
                'Got unexpected response with status 400 from the master instance'
            ):
                self.call_command()

        with requests_mock.Mocker() as mock:
            mock.register_uri(
                'POST',
                self.upload_url,
                status_code=400,
                json={'message': 'Bad Zip file'}
            )

            with self.assertRaisesRegex(  # pylint:disable=deprecated-method
                CommandError,
                'Failed to upload the media. The master instance answer was "Bad Zip file"'
            ):
                self.call_command()

    @override_settings(INSTANCE_IS_SLAVE=True, INSTANCE_IS_MASTER=False)
    def test_command_should_raise_if_bad_response_from_update_up_url(self):
        """Test that the command should raise if bad response from the upload url"""

        # Create the local update
        self.create_local_update()

        with requests_mock.Mocker() as mock:
            mock.register_uri('POST', self.update_up_url, status_code=500)

            with self.assertRaisesRegex(  # pylint:disable=deprecated-method
                CommandError,
                'Failed to upload the data. '
                'Got unexpected response with status 500 from the master instance'
            ):

                self.call_command()

        with requests_mock.Mocker() as mock:
            mock.register_uri(
                'POST',
                self.update_up_url,
                status_code=400,
                json={
                    'message': 'Bad Zip file'
                }
            )

            with self.assertRaisesRegex(  # pylint:disable=deprecated-method
                CommandError,
                'Failed to upload the data. '
                'The master instance answer was "Bad Zip file"'
            ):
                self.call_command()
