"""This modules holds views tests for ``accounts`` app."""

from django.conf import settings
from django.contrib.messages import constants as MSG
from django.core.exceptions import NON_FIELD_ERRORS
from django.core.urlresolvers import reverse
from django.test.utils import override_settings
from django.utils import timezone

from mtp.apps.common.tests.test_cases import (
    DEFAULT_PASSWORD,
    MTPTestCase,
    SECURE_PASSWORD
)
from mtp.apps.common.utils import url_to_path

from ..constants import POTENTIAL_DUPLICATE_REASONS
from ..factories import (
    GroupFactory,
    GroupRoleFactory,
    TraineeFactory,
    AdminFactory,
)
from ..models import DuplicatedMTPUser, Group, GroupRole, MTPUser


class UserDetailsViewTestCase(MTPTestCase):
    """Test case that tests the ``user_details`` view."""

    @classmethod
    def setUpTestData(cls):
        """Set up the test data"""

        super().setUpTestData()

        # We will need another trainee
        cls.trainee_2 = TraineeFactory()
        cls.payload = {
            'username': 'testusername',
            'pass1': 'testpassword',
            'pass2': 'testpassword',
            'email': 'test@test.com',
            'natural_language': 'fr',
            'first_name': 'john',
            'last_name': 'Doe'
        }

    def test_trainee_profile_not_self_should_be_rejected(self):
        """Test that a trainee should not be allowed to edit another trainee's profile."""

        self.login_as_trainee()
        response = self.client.get(self.trainee_2.get_edit_url())

        self.assertEqual(
            response.status_code,
            404
        )

    def test_trainee_profile_is_self_should_be_accepted(self):
        """Test that a trainee should be allowed to edit his own profile."""

        self.login_as_trainee()

        response = self.client.get(self.trainee.get_edit_url())

        self.assertEqual(
            response.status_code,
            200
        )

    def test_admin_should_be_accepted(self):
        """Test that a trainer should be allowed to edit all profiles."""

        self.login_as_admin()

        # The admin can edit the trainee's details...
        response = self.client.get(self.trainee.get_edit_url())
        self.assertEqual(
            response.status_code,
            200
        )

        # ...and also the trainer's details
        response = self.client.get(self.trainer.get_edit_url())
        self.assertEqual(
            response.status_code,
            200
        )

    def test_save_form_should_correctly_set_the_created_from_attribute(self):
        """Test that creating a user from the form should set the ``created_from`` attribute"""

        self.login_as_admin()
        url = MTPUser.get_create_url()
        response = self.client.post(url, self.payload)

        self.assertEqual(response.status_code, 302)

        # The user should be created
        user = MTPUser.objects.get(username='testusername')

        # The ``created_from`` attribute should be correctly set
        self.assertEqual(user.created_from, MTPUser.ORIGINS.CMS_FORM)

    def test_admin_can_create_user_with_external_id(self):
        """Test that an admin can create a user with `external_id`."""

        self.login_as_admin()
        url = MTPUser.get_create_url()
        payload = self.payload.copy()
        payload.update({"username": 'someoneelse',
                        'external_id': 'zanzibar',
                        'email': 'someoneelse@test.com',
                        'first_name': 'someone',
                        'last_name': 'else'})
        response = self.client.post(url, payload)
        self.assertEqual(response.status_code, 302)
        user = MTPUser.objects.get(username='someoneelse')
        self.assertEqual(user.external_id, 'zanzibar')

    def test_edit_with_external_id(self):
        """Test that only admin can edit a user with `external_id`."""

        self.login_as_admin()
        url = self.trainee.get_edit_url()
        response = self.client.get(url)
        self.assertTrue(
            "external_id" in response.context['form'].fields
        )
        self.login_as_trainee()
        url = self.trainee.get_edit_url()
        response = self.client.get(url)
        self.assertFalse(
            "external_id" in response.context['form'].fields
        )

    def test_password_should_be_mandatory_when_creating_new_user(self):
        """Test that the password field should be mandatory when creating a new user"""

        self.login_as_admin()

        url = MTPUser.get_create_url()
        payload = self.payload.copy()
        payload.pop('pass1')
        payload.pop('pass2')

        response = self.client.post(url, payload)

        self.assertEqual(response.status_code, 200)
        self.assertFalse(response.context['form'].is_valid())

    def test_trainer_groups_should_be_saved_on_create(self):
        """Test that groups added to the form should be added to the new user."""

        self.login_as_admin()

        url = MTPUser.get_create_url()
        payload = self.payload.copy()

        payload.update(
            {
                'username': 'atrainerwithgroup',
                'email': 'watrainerwithgroup@test.com',
                'first_name': 'atrainerwithgroup',
                'last_name': 'withgroup',
                'mgroups': self.job_profile.pk
            }
        )

        response = self.client.post(url, payload)
        self.assertEqual(response.status_code, 302)

        user = MTPUser.objects.get(username='atrainerwithgroup')
        self.assertTrue(
            GroupRole.objects.filter(user=user, group=self.job_profile).exists()
        )

    def test_password_should_be_optional_when_editing_an_existing_user(self):
        """Test that the password field should be optional when editing an existing user"""

        self.login_as_admin()

        url = self.trainer.get_edit_url()
        payload = self.payload.copy()
        payload.pop('pass1')
        payload.pop('pass2')

        response = self.client.post(url, payload)

        self.assertEqual(response.status_code, 302)

        self.trainer.refresh_from_db()
        self.assertEqual(self.trainer.username, self.payload['username'])

    def test_view_should_redirect_to_update_view_when_user_edit_his_own_profile(self):
        """Test that the view redirect to the update view when the user edit his own profile"""

        # A user editing his own profile should be redirected to the update view
        self.login_as_admin()
        response = self.client.post(self.admin.get_edit_url(), self.payload)
        self.assertRedirects(response, self.admin.get_edit_url())

    def test_view_should_redirect_to_update_view_when_user_edit_another_user(self):
        """Test that the view redirect to the update view when the user edit another user"""

        # A user editing another user should be redirected to the user list view
        self.login_as_admin()
        response = self.client.post(self.trainer.get_edit_url(), self.payload)
        self.assertRedirects(response, MTPUser.get_list_url())


class UserWelcomeViewTestCase(MTPTestCase):
    """Test case that tests the ``user_welcome`` view."""

    @classmethod
    def setUpTestData(cls):
        super().setUpTestData()

        cls.url = cls.trainee.get_welcome_url()

    def test_trainee_should_be_rejected(self):
        """Test that a trainee should be not allowed to resent a welcome mail."""
        self.login_as_trainee()
        response = self.client.get(self.url, {}, follow=True)
        self.assertRedirects(response, settings.LOGIN_URL + '?next=' + self.trainee.get_list_url())

    def test_trainer_should_be_accepted(self):
        """Test that a trainer should be allowed to resent a welcome mail."""
        self.login_as_trainer()
        response = self.client.get(self.url, {}, follow=True)
        messages = list(response.context['messages'])

        self.assertEqual(len(messages), 1)
        self.assertEqual(messages[0].level, MSG.SUCCESS)
        self.assertEqual(response.status_code, 200)

    def test_admin_should_be_accepted(self):
        """Test that a admin should be allowed to resent a welcome mail."""
        self.login_as_admin()
        response = self.client.get(self.url, {}, follow=True)
        messages = list(response.context['messages'])

        self.assertEqual(len(messages), 1)
        self.assertEqual(messages[0].level, MSG.SUCCESS)
        self.assertEqual(response.status_code, 200)


class GroupDeleteViewTestCase(MTPTestCase):
    """Test case that tests the ``GroupDeleteView`` view."""

    @classmethod
    def setUpTestData(cls):
        super().setUpTestData()

        cls.group = GroupFactory()
        GroupRoleFactory(group=cls.group, user=cls.trainees[1])

        cls.group_delete_url = cls.group.get_delete_url()

    def test_trainee_should_be_rejected(self):
        """Test that a trainee should't be allowed to soft delete group."""
        self.login_as_trainee()
        response = self.client.post(self.group_delete_url, {})

        self.assertEqual(response.status_code, 404)

    def test_trainer_should_be_rejected(self):
        """Test that a trainer should't be allowed to soft delete group."""
        self.login_as_trainer()
        response = self.client.post(self.group_delete_url, {})

        self.assertEqual(response.status_code, 404)

    def test_admin_should_be_accepted(self):
        """Test that a admin should be allowed to soft delete group."""
        self.login_as_admin()
        response = self.client.post(self.group_delete_url, {}, follow=True)
        messages = list(response.context['messages'])

        self.assertEqual(len(messages), 1)
        self.assertEqual(messages[0].level, MSG.SUCCESS)
        self.assertEqual(response.status_code, 200)

    def test_group_is_soft_deleted(self):
        """Test that the group is soft deleted."""
        self.login_as_admin()
        self.client.post(self.group_delete_url, {})

        up_group = Group.objects.get(id=self.group.id)

        # Verify if group is soft-deleted
        self.assertTrue(up_group.is_deleted)
        # Verify if group's member is not deleted
        self.assertFalse(up_group.members.first().is_deleted)


class LoginUserViewTestCase(MTPTestCase):
    """Test the ``LoginUserView`` view"""

    def get_url(self, next_url=None):
        """Get the login url, with or without a ``next`` argument."""

        url = '%(login_url)s'
        if next_url is not None:
            url += '?next=%(next_url)s'

        return url % {
            'login_url': settings.LOGIN_URL,
            'next_url': next_url
        }

    @override_settings(INSTANCE_IS_SLAVE=False, INSTANCE_IS_MASTER=True)
    def test_failed_login(self):
        """A failed login should not redirect."""

        response = self.client.post(self.get_url(), {
            'username': 'wronglogin',
            'password': 'wrongpassword'
        })

        self.assertEqual(response.status_code, 200)
        self.assertTrue(
            response.context['form'].has_error(NON_FIELD_ERRORS, 'invalid_login')
        )

    @override_settings(INSTANCE_IS_SLAVE=False, INSTANCE_IS_MASTER=True)
    def test_successful_login_without_next_url(self):
        """A successful login without ``next`` argument should redirect to the root url."""

        response = self.client.post(self.get_url(), {
            'username': self.trainer.username,
            'password': DEFAULT_PASSWORD
        })

        self.assertRedirects(response, str(settings.ROOT_URL))

    @override_settings(INSTANCE_IS_SLAVE=False, INSTANCE_IS_MASTER=True)
    def test_successful_login_with_empty_next_url(self):
        """A successful login with empty ``next`` argument should redirect to the root url."""

        response = self.client.post(self.get_url(next_url=''), {
            'username': self.trainer.username,
            'password': DEFAULT_PASSWORD
        })

        self.assertRedirects(response, str(settings.ROOT_URL))

    @override_settings(INSTANCE_IS_SLAVE=False, INSTANCE_IS_MASTER=True)
    def test_successful_login_with_invalid_next_url(self):
        """A successful login with invalid ``next`` argument should redirect to the root url.

        An invalid url is an url with a bad scheme+host

        """
        response = self.client.post(self.get_url(next_url='http://foo.com/bar'), {
            'username': self.trainer.username,
            'password': DEFAULT_PASSWORD
        })

        self.assertRedirects(response, str(settings.ROOT_URL))

    @override_settings(INSTANCE_IS_SLAVE=False, INSTANCE_IS_MASTER=True)
    def test_successful_login_with_valid_next_url(self):
        """A successful login with valid ``next`` argument should redirect to this ``next`` url."""

        next_url = reverse('trainings:training-list')

        response = self.client.post(self.get_url(next_url=next_url), {
            'username': self.trainer.username,
            'password': DEFAULT_PASSWORD
        })

        self.assertRedirects(response, next_url)

    @override_settings(INSTANCE_IS_SLAVE=False, INSTANCE_IS_MASTER=True)
    def test_trainee_should_not_be_able_to_login(self):
        """A user who belongs to the ``trainee`` groups should not be able to login to the cms"""

        response = self.client.post(
            self.get_url(), {
                'username': self.trainee.username,
                'password': DEFAULT_PASSWORD
            }
        )

        self.assertEqual(response.status_code, 200)
        self.assertTrue(
            response.context['form'].has_error(NON_FIELD_ERRORS, 'is_trainee')
        )

    @override_settings(INSTANCE_IS_SLAVE=True, INSTANCE_IS_MASTER=False)
    def test_disable_instance_slave_login(self):
        """Test that the non-magency admin users should not be able to login on slave instances"""

        # A trainer should not be able to log-in
        response = self.client.post(
            self.get_url(), {
                'username': self.trainer.username,
                'password': DEFAULT_PASSWORD
            }
        )

        self.assertEqual(response.status_code, 200)
        self.assertTrue(
            response.context['form'].has_error(NON_FIELD_ERRORS, 'instance_is_slave')
        )
        self.assertNotIn('_auth_user_id', self.client.session)

        # The magency admin should be able to log-in
        magency_admin = AdminFactory(
            username=settings.MAGENCY_ADMIN_USERNAME,
            password=DEFAULT_PASSWORD
        )

        response = self.client.post(
            self.get_url(), {
                'username': magency_admin.username,
                'password': DEFAULT_PASSWORD
            }
        )
        self.assertRedirects(response, str(settings.ROOT_URL))
        self.assertEqual(self.client.session['_auth_user_id'], magency_admin.pk)


class UserListViewTestCase(MTPTestCase):
    """Test cases for the ``UserListView`` class"""

    @classmethod
    def setUpClass(cls):
        """Store the url of the user list"""

        super().setUpClass()

        cls.user_list_url = MTPUser.get_list_url()

    def test_only_superusers_should_have_access(self):
        """Test that only the superusers can access the user list view"""

        # Trainee should be rejected...
        self.login_as_trainee()
        response = self.client.get(self.user_list_url)

        self.assertEqual(response.status_code, 302)

        # ...So are the trainers...
        self.login_as_trainer()
        response = self.client.get(self.user_list_url)

        self.assertEqual(response.status_code, 302)

        # ...But the superusers have access
        self.login_as_admin()
        response = self.client.get(self.user_list_url)

        self.assertEqual(response.status_code, 200)

    def test_filters_should_filter_the_user_list(self):
        """Test that using the filters should correctly filter the list of user"""

        self.login_as_admin()

        # Unfiltered list
        response = self.client.get(self.user_list_url)
        self.assertEqual(len(response.context['mtpuser_list']), 4)  # 3 trainees + 1 trainer

        # Filter by username
        url = '%s?search=%s' % (self.user_list_url, self.trainee.username)
        response = self.client.get(url)
        self.assertEqual(len(response.context['mtpuser_list']), 1)  # 1 trainee

        # Filter by user group
        url = '%s?mgroups=%s' % (self.user_list_url, self.trainer_group.pk)
        response = self.client.get(url)
        self.assertEqual(len(response.context['mtpuser_list']), 1)  # 1 trainer

        # Filter by ``is_deleted``
        url = '%s?is_deleted=%s' % (self.user_list_url, 1)
        response = self.client.get(url)
        self.assertEqual(len(response.context['mtpuser_list']), 0)  # 0 user soft-deleted

        # soft-delete a user
        self.trainee.is_deleted = True
        self.trainee.save()

        response = self.client.get(url)
        self.assertEqual(len(response.context['mtpuser_list']), 1)  # 1 user soft-deleted


class UserBulkDeleteViewTestCase(MTPTestCase):
    """Test case that tests the ``UserBulkDeleteView`` view."""

    @classmethod
    def setUpTestData(cls):
        super().setUpTestData()

        cls.activated_user = TraineeFactory()
        cls.activated_user_2 = TraineeFactory()
        cls.super_user = AdminFactory()

    def test_users_should_be_deactivated(self):
        """Test that bulk delete works."""
        response = self.client.post(reverse('accounts:user-bulk-delete'), {
            'users': [self.activated_user.id, self.activated_user_2.id]
        })

        # redirect to list
        self.assertEqual(response.status_code, 302)

        # should be soft-deleted
        self.activated_user.refresh_from_db()
        self.assertTrue(self.activated_user.is_deleted)

        self.activated_user_2.refresh_from_db()
        self.assertTrue(self.activated_user_2.is_deleted)

    def test_superuser_should_not_be_deactivated(self):
        """We can not bulk delete a super user"""
        self.client.post(reverse('accounts:user-bulk-delete'), {
            'users': [self.super_user.id, ]
        })

        self.super_user.refresh_from_db()
        self.assertFalse(self.super_user.is_deleted)

    def test_current_user_should_not_be_deactivated(self):
        """We can not bulk delete self"""
        self.login_as_admin()

        self.client.post(reverse('accounts:user-bulk-delete'), {
            'users': [self.admin.id, ]
        })

        self.admin.refresh_from_db()
        self.assertFalse(self.admin.is_deleted)


class UserBulkUndeleteViewTestCase(MTPTestCase):
    """Test case that tests the ``UserBulkUndeleteView`` view."""

    @classmethod
    def setUpTestData(cls):
        super().setUpTestData()

        cls.deactivated_user = TraineeFactory(is_deleted=True)
        cls.deactivated_user_2 = TraineeFactory(is_deleted=True)
        cls.super_user = AdminFactory(is_deleted=True)

    def test_users_should_be_activated(self):
        """Test that bulk un-delete works."""
        response = self.client.post(reverse('accounts:user-bulk-undelete'), {
            'users': [self.deactivated_user.id, self.deactivated_user_2.id]
        })

        # redirect to list
        self.assertEqual(response.status_code, 302)

        # should be soft-deleted
        self.deactivated_user.refresh_from_db()
        self.assertFalse(self.deactivated_user.is_deleted)

        self.deactivated_user_2.refresh_from_db()
        self.assertFalse(self.deactivated_user_2.is_deleted)

    def test_superuser_should_not_be_activated(self):
        """We can not bulk un-delete a super user"""
        self.client.post(reverse('accounts:user-bulk-undelete'), {
            'users': [self.super_user.id, ]
        })

        self.super_user.refresh_from_db()
        self.assertTrue(self.super_user.is_deleted)

    def test_current_user_should_not_be_activated(self):
        """We can not bulk un-delete self"""
        self.login_as_admin()
        self.admin.is_deleted = True
        self.admin.save()

        self.client.post(reverse('accounts:user-bulk-undelete'), {
            'users': [self.admin.id, ]
        })

        self.admin.refresh_from_db()
        self.assertTrue(self.admin.is_deleted)


class PasswordSecurityTestCase(MTPTestCase):
    """Test password security"""

    def setUp(self):
        super().setUp()
        self.login_as_trainer()

    def test_password_security_policy(self):
        """
        Test the navigation across the CMS when the password security policy
        is active.
        """

        # So, we have a trainer who hasn't changed their password in a while...
        now = timezone.now()
        two_months_ago = now - timezone.timedelta(days=60)
        self.trainer.last_changed_password = two_months_ago
        self.trainer.save()

        # They want to go to the CMS index.
        index_url = str(settings.ROOT_URL)
        password_change_url = reverse('accounts:password_change')
        password_change_done_url = reverse('password_change_done')

        # If the password security policy is off, no problem.
        with self.settings(PASSWORD_EXTRA_SECURITY=False):
            response = self.client.get(index_url)
            self.assertEqual(response.status_code, 200)

        # If the policy is on and the password is fresh enough, no problem.
        with self.settings(PASSWORD_EXTRA_SECURITY=True, PASSWORD_TTL=90):
            response = self.client.get(index_url)
            self.assertEqual(response.status_code, 200)

        # If the policy is on and the password is old, redirect!
        with self.settings(PASSWORD_EXTRA_SECURITY=True, PASSWORD_TTL=30):
            response = self.client.get(index_url)
            self.assertEqual(response.status_code, 302)

            actual_redirect_path = url_to_path(response.url)
            expected_redirect_path = password_change_url
            self.assertEqual(actual_redirect_path, expected_redirect_path)

            # Now, if they want to go anywhere else, they are redirected to
            # the password change form.
            response = self.client.get(index_url)
            self.assertEqual(response.status_code, 302)

            actual_redirect_path = url_to_path(response.url)
            expected_redirect_path = password_change_url
            self.assertEqual(actual_redirect_path, expected_redirect_path)

            time_before_change = timezone.now()
            self.assertTrue(self.trainer.last_changed_password < time_before_change)

            # The only way to proceed is to actually fill out the form.
            response = self.client.post(password_change_url, {
                'old_password': DEFAULT_PASSWORD,
                'new_password1': SECURE_PASSWORD,
                'new_password2': SECURE_PASSWORD,
            })
            self.assertEqual(response.status_code, 302)

            # Ensure we correctly updated the ``last_changed_password`` field
            self.trainer.refresh_from_db()
            self.assertTrue(self.trainer.last_changed_password > time_before_change)

            actual_redirect_path = url_to_path(response.url)
            expected_redirect_path = password_change_done_url
            self.assertEqual(actual_redirect_path, expected_redirect_path)

            # Once this is done, they can navigate freely through the cms.
            response = self.client.get(index_url)
            self.assertEqual(response.status_code, 200)


class UserMergeViewsTestCase(MTPTestCase):
    """Test the user merge related views"""

    @classmethod
    def setUpTestData(cls):
        """Create some duplicates and "potential duplicates"."""

        super().setUpTestData()

        # Flag the second trainee as a duplicate of the first trainee
        DuplicatedMTPUser.objects.create(
            original_user=cls.trainee,
            duplicate_user=cls.trainees[1],
            origin_user=cls.trainer
        )

        # Also, we need to add the ``has_potential_duplicates`` flag
        cls.trainee.has_potential_duplicates = True
        cls.trainee.save()

        # Create a trainee 3
        cls.trainees.append(TraineeFactory())

        # Flag the trainee 4 as a potential duplicate of the trainee 3
        cls.trainees[2].flag_user_as_potential_duplicate(
            cls.trainees[3],
            POTENTIAL_DUPLICATE_REASONS.USERNAME_CONFLICT
        )

    def setUp(self):
        """Login as an admin since the merge user views are only accepting admin users"""

        super().setUp()

        self.login_as_admin()

    def test_merge_list_view_should_work(self):
        """Test that the list view of the user merge tool work"""

        response = self.client.get(reverse('accounts:user-merge-list'))

        self.assertEqual(response.status_code, 200)

        # We should have both the trainee 1 and 3 in the view queryset
        self.assertIn(self.trainee, response.context['mtpuser_list'])
        self.assertIn(self.trainees[2], response.context['mtpuser_list'])

    def test_unflag_as_duplicate_view_should_work(self):
        """Test that the view to unflag the user as a duplicate should work"""

        url = reverse('accounts:user-not-a-duplicate', kwargs={
            'user_uuid': self.trainees[2].id,
        }) + '?duplicate_user_uuid=' + self.trainees[3].id

        get_response = self.client.get(url)
        self.assertEqual(get_response.status_code, 200)

        post_response = self.client.post(url)
        self.assertRedirects(post_response, reverse('accounts:user-merge-list'))

        # The trainee 4 is no longer a potential duplicates of the trainee 3
        self.assertEqual(self.trainees[2].potential_duplicates, {})

        # The trainee 3 is no longer shown in the merge list
        list_response = self.client.get(reverse('accounts:user-merge-list'))
        self.assertNotIn(self.trainees[2], list_response.context['mtpuser_list'])

    def test_user_merge_view_should_work(self):
        """Test that the view to merge a user should work"""

        url = reverse('accounts:user-merge', kwargs={
            'user_uuid': self.trainees[2].id,
        }) + '?duplicate_user_uuid=' + self.trainees[3].id

        get_response = self.client.get(url)
        self.assertEqual(get_response.status_code, 200)

        post_response = self.client.post(url)
        self.assertRedirects(post_response, reverse('accounts:user-merge-list'))

        # The trainee 4 is now in the duplicates list of the trainee 3
        self.assertEqual(self.trainees[3].original_user, self.trainees[2])
