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

from django.core.exceptions import ValidationError
from django.db.utils import IntegrityError
from django.utils import timezone

from mtp.apps.assessments.models import CompetencyTree
from mtp.apps.likes.constants import LIKE_DEFAULT_WEIGHT
from mtp.apps.likes.tests.likeable_testapp.models import Proposition
from mtp.apps.common.tests.test_cases import MTPTestCase
from mtp.apps.common.tests.waffle_test_utils import override_switch

from ..constants import (
    GROUPTYPE_JOB_PROFILE,
    POTENTIAL_DUPLICATE_REASONS,
)
from ..factories import (
    GroupFactory,
    GroupRoleFactory,
    GroupTypeFactory,
    JobProfileFactory,
    TraineeFactory,
    TrainerFactory,
)
from ..models import (
    GroupRole,
    MTPUser,
    DuplicatedMTPUser,
)
from ..utils import init_user


class MTPUserModelTestCase(MTPTestCase):
    """Test case to test the ``MTPUser`` model"""

    @classmethod
    def setUpTestData(cls):
        """Create a likeable object."""

        super().setUpTestData()

        cls.likeable_object = Proposition.objects.create(text='fake_proposition')

    def setUp(self):
        """Remove cached properties for ``original_user`` between test runs"""

        super().setUp()

        if self.trainee.original_user:
            del self.trainee.original_user

        if self.trainees[1].original_user:
            del self.trainees[1].original_user

    def test_init_user_should_correctly_set_the_password(self):
        """
        Test the ``mtp.apps.account.utils.init_user`` method.
        """

        # We create a user with an empty password
        user = MTPUser.objects.create(username='test')
        user, password = init_user(user, password=None, groupset=None, is_staff=False)

        # The user should have a fresh new password :)
        self.assertEqual(
            len(password),
            8
        )

        # We should test the login with this new password.
        self.assertTrue(
            self.client.login(username=user.username, password=password)
        )

    def test_check_password_expiry(self):
        """
        Test the password expiry checking function.
        """

        # 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)
        trainer = TrainerFactory(last_changed_password=two_months_ago)

        # The password security policy is off.
        with self.settings(PASSWORD_EXTRA_SECURITY=False):
            self.assertFalse(trainer.check_password_expiry())

        # The password security policy is on, and the password is not too old..
        with self.settings(PASSWORD_EXTRA_SECURITY=True, PASSWORD_TTL=90):
            self.assertFalse(trainer.check_password_expiry())

        # The password security policy is on, and the password is too old.
        with self.settings(PASSWORD_EXTRA_SECURITY=True, PASSWORD_TTL=30):
            self.assertTrue(trainer.check_password_expiry())

    def test_is_trainer_method(self):
        """Test the ``is_trainer`` method of the ``MTPUser`` model"""

        # a trainer is a trainer
        trainer = TrainerFactory()
        self.assertTrue(trainer.is_trainer)

        # a trainee is not
        trainee = TraineeFactory()
        self.assertFalse(trainee.is_trainer)

    def test_is_trainee_method(self):
        """Test the ``is_trainee`` method of the ``MTPUser`` model"""

        # a trainee is a trainee
        trainee = TraineeFactory()
        self.assertTrue(trainee.is_trainee)

        # a trainer is not
        trainer = TrainerFactory()
        self.assertFalse(trainer.is_trainee)

    def test_managed_groups(self):
        """Test the ``managed_groups`` property of the ``MTPUser`` model"""

        # The trainer is not managing any groups
        self.assertEqual(
            self.trainee.managed_groups.count(),
            0
        )

        # The admin is managing two groups
        self.assertEqual(
            self.admin.managed_groups.count(),
            2
        )

    def test_like(self):
        """Test the ``like`` method."""

        # The trainee likes the object
        self.trainee.like(self.likeable_object)

        # We should have 1 like for the trainee
        like = self.trainee.likes.non_deleted().get()
        self.assertEqual(like.author.id, self.trainee.id)
        self.assertEqual(like.liked_object.id, self.likeable_object.id)
        self.assertEqual(like.weight, LIKE_DEFAULT_WEIGHT)

        # The trainee unlikes and likes again the object with new weight
        self.trainee.unlike(self.likeable_object)
        self.trainee.like(self.likeable_object, 2)

        # We should have 1 like for the trainee
        like = self.trainee.likes.non_deleted().get()
        self.assertEqual(like.author.id, self.trainee.id)
        self.assertEqual(like.liked_object.id, self.likeable_object.id)
        self.assertEqual(like.weight, 2)

        # The trainee likes the object with new weight
        self.trainee.like(self.likeable_object, 3)

        # We should have 1 like for the trainee
        like = self.trainee.likes.non_deleted().get()
        self.assertEqual(like.author.id, self.trainee.id)
        self.assertEqual(like.liked_object.id, self.likeable_object.id)
        self.assertEqual(like.weight, 3)

    def test_unlike(self):
        """Test the ``unlike`` method."""

        # The trainee unlikes a not liked object
        self.trainee.unlike(self.likeable_object)

        # We should have no like for the trainee
        self.assertEqual(len(self.trainee.likes.non_deleted()), 0)

        # The trainee likes the object and unlikes it
        self.trainee.like(self.likeable_object)
        self.trainee.unlike(self.likeable_object)

        # We should have no like for the trainee
        self.assertEqual(len(self.trainee.likes.non_deleted()), 0)

    def test_original_or_self_external_id_property_should_work(self):
        """Test the ``original_or_self_external_id`` property"""

        self.trainee.external_id = 'XXX'
        self.trainee.save()

        self.trainees[1].external_id = 'YYY'
        self.trainees[1].save()

        self.assertEqual(self.trainees[1].original_or_self_external_id, 'YYY')

        # Flag this user as a duplicate of "trainee"
        self.trainee.flag_user_as_duplicate(self.trainees[1], self.admin)

        del self.trainees[1].original_user
        del self.trainees[1].original_or_self_external_id

        # We correctly get the original's external_id
        self.assertEqual(self.trainees[1].original_or_self_external_id, 'XXX')


class GroupRoleModelTestCase(MTPTestCase):
    """Test cases for the through ``GroupRole`` model."""

    def test_grouprole_create(self):
        """Test the creation of a new ``GroupRole`` model."""

        # We create a new trainer
        trainer = TrainerFactory()

        # Create a new group
        group = GroupFactory()

        # Add the trainer to a new group
        GroupRoleFactory(user=trainer, group=group)

        # The trainer is in the group
        self.assertIn(
            group,
            trainer.mgroups.all()
        )

        # He is a ``MEMBER`` of the group
        self.assertEqual(
            trainer.group_roles.get(group=group).role,
            GroupRole.ROLES.MEMBER
        )

        # We can't re-add the trainer to the group
        with self.assertRaises(IntegrityError):
            GroupRoleFactory(user=trainer, group=group)

    def test_grouprole_manager(self):
        """Test the unicity of a given group user role ``MANAGER``."""

        # Create a trainer
        trainer = TrainerFactory()

        # Create a new group
        group = GroupFactory()

        # Add the trainer to a new group, as a ``MANAGER``.
        group_role = GroupRoleFactory(user=trainer, group=group, role=GroupRole.ROLES.MANAGER)
        # Subsequant call to the save method don't raise an exception
        group_role.save()

        # The manager is a member
        self.assertEqual(
            group.members.count(),
            1
        )

        # The trainer is the manager of this group
        self.assertEqual(
            group.manager_member,
            trainer
        )

        # Adding another manager to the group will raise a ``ValidationError``.
        with self.assertRaises(ValidationError):
            GroupRoleFactory(
                user=TrainerFactory(),
                group=group,
                role=GroupRole.ROLES.MANAGER
            )


class GroupModelTestCase(MTPTestCase):
    """Test the ``Group`` model."""

    @classmethod
    def setUpTestData(cls):
        """Create a new group."""

        super().setUpTestData()

        cls.group = GroupFactory()

    def test_set_manager(self):
        """Test the ``set_manager`` method."""

        # The group shouldn't have a manager
        self.assertIsNone(self.group.manager_member)

        # We attribute the trainer as manager of the group
        self.group.set_manager(self.trainer)

        # The manager member should be the trainer
        self.assertEqual(
            self.group.manager_member.id,
            self.trainer.id
        )

        # We attribute the trainee as manager of the group
        self.group.set_manager(self.trainee)

        # The manager member should be the trainee
        self.assertEqual(
            self.group.manager_member.id,
            self.trainee.id
        )


class JobProfileTestCase(MTPTestCase):
    """Test case to test behaviors related to job profile groups."""

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

        # We need the "job profile" group type...
        cls.group_type_job_profile = GroupTypeFactory(name=GROUPTYPE_JOB_PROFILE)

        # ... and a job profile.
        cls.job_profile = JobProfileFactory()

    def test_set_job_profile(self):
        """Test set job profile"""
        self.trainee.job_profile = self.job_profile
        self.assertIn(self.job_profile, self.trainee.mgroups.all())

    def test_set_job_profile_raises_if_group_is_not_a_job_profile(self):
        """Test fail if set job profile is not job profile"""
        not_a_job_profile = GroupFactory()
        with self.assertRaises(ValueError):
            self.trainee.job_profile = not_a_job_profile

    def test_get_users_job_profile(self):
        """Test get users job profile"""
        self.trainee.job_profile = self.job_profile
        self.assertEqual(self.trainee.job_profile, self.job_profile)

    def test_automatic_competency_tree_creation(self):
        """Test automatic competency tree creation"""
        test_job_profile_names = ['test1', 'test2']

        # If the assessments are disabled, no competency tree creation.
        with override_switch('enable_assessments', False):
            JobProfileFactory(name=test_job_profile_names[0])
            self.assertFalse(
                CompetencyTree.objects.filter(name=test_job_profile_names[0]).exists()
            )

        # Otherwise, there should be a new competency tree for the job profile.
        with override_switch('enable_assessments', True):
            JobProfileFactory(name=test_job_profile_names[1])
            self.assertTrue(
                CompetencyTree.objects.filter(name=test_job_profile_names[1]).exists()
            )


class MTPUserDuplicateTestCase(MTPTestCase):
    """Test case related to the "duplicate" feature of the ``MTPUser`` model"""

    def setUp(self):
        """Make sure the cached properties and the redis model are clean"""

        super().setUp()

        # The code below is ugly... Is there a way to del several cached_property at once,
        # while catching the ``AttributeError`` exception ?
        try:
            del self.trainee.original_user
        except AttributeError:
            pass

        try:
            del self.trainee.duplicate_users
        except AttributeError:
            pass

        try:
            del self.trainee.potential_duplicates
        except AttributeError:
            pass

        self.trainee.redis_instance.potential_duplicates_ids.delete()

    def test_flag_user_as_duplicate_should_work(self):
        """Test that the ``flag_user_as_duplicate`` should mark a user as a duplicate"""

        self.trainee.flag_user_as_duplicate(self.trainees[1], self.admin)

        # We got a new ``MTPUserDuplicate`` object
        self.assertEqual(
            len(DuplicatedMTPUser.objects.filter(
                original_user=self.trainee,
                duplicate_user=self.trainees[1],
                origin_user=self.admin
            )),
            1
        )

    def test_flag_user_as_duplicate_should_raise_if_user_is_a_duplicate(self):
        """Test that the ``flag_user_as_duplicate`` should raise if user is a duplicate"""

        # Create manually a duplicate
        DuplicatedMTPUser.objects.create(
            original_user=self.trainee,
            duplicate_user=self.trainees[1],
            origin_user=self.admin
        )

        with self.assertRaises(ValidationError) as exception_context:
            self.trainees[1].flag_user_as_duplicate(self.trainee, self.admin)

        self.assertIn(
            "The original user is already flagged as a duplicate",
            str(exception_context.exception)
        )

    def test_flag_user_as_duplicate_should_raise_if_user_is_same_as_original(self):
        """Test that the ``flag_user_as_duplicate`` should raise if user is the same as original"""

        with self.assertRaises(ValidationError) as exception_context:
            self.trainee.flag_user_as_duplicate(self.trainee, self.admin)

        self.assertIn(
            "The original user and the duplicated one should be different users",
            str(exception_context.exception)
        )

    def test_flag_user_as_duplicate_should_raise_if_user_is_already_a_duplicate(self):
        """Test that the ``flag_user_as_duplicate`` should raise if user a duplicate"""

        # Create manually a duplicate
        DuplicatedMTPUser.objects.create(
            original_user=self.trainee,
            duplicate_user=self.trainees[1],
            origin_user=self.admin
        )

        with self.assertRaises(ValidationError) as exception_context:
            self.trainees[2].flag_user_as_duplicate(self.trainees[1], self.admin)

        self.assertIn(
            "The duplicate user is already flagged as a duplicate for another user",
            str(exception_context.exception)
        )

    def test_potential_duplicates_property_should_work(self):
        """Test that the ``potential_duplicates`` property should work"""

        # At first, the trainee should not have any duplicate
        self.assertEqual(self.trainee.potential_duplicates, {})

        # We add a duplicate via the tied redis model
        self.trainee.redis_instance.flag_user_as_potential_duplicate(
            self.trainees[1],
            POTENTIAL_DUPLICATE_REASONS.SAME_LAST_AND_FIRST_NAME
        )

        del self.trainee.potential_duplicates

        # We should now have a potential duplicate in the property
        self.assertEqual(
            self.trainee.potential_duplicates,
            {
                self.trainees[1]: POTENTIAL_DUPLICATE_REASONS.for_constant(
                    'SAME_LAST_AND_FIRST_NAME'
                )
            }
        )

    def test_original_user_property_should_work(self):
        """Test that the ``original_user`` property should work"""

        # At first, the property should be None
        self.assertIsNone(self.trainees[1].original_user)

        # Flag this user as a duplicate of "trainee"
        self.trainee.flag_user_as_duplicate(self.trainees[1], self.admin)

        del self.trainees[1].original_user

        # We correctly get the original
        self.assertEqual(self.trainees[1].original_user, self.trainee)

    def test_duplicate_users_property_should_work(self):
        """Test that the ``duplicate_users`` property should work"""

        # At first, the property should be empty
        self.assertListEqual(list(self.trainee.duplicate_users), [])

        # Flag this user as a duplicate of "trainee"
        self.trainee.flag_user_as_duplicate(self.trainees[1], self.admin)

        del self.trainee.duplicate_users

        # We correctly get the duplicates
        self.assertListEqual(list(self.trainee.duplicate_users), [self.trainees[1]])

    def test_flag_user_as_potential_duplicate_should_work_when_no_potential_or_duplicates(self):
        """Test that the ``flag_user_as_potential_duplicate`` should work"""

        # At first, the user has not duplicates nor potential duplicates
        self.assertEqual(list(self.trainee.duplicate_users), [])
        self.assertEqual(self.trainee.potential_duplicates, {})

        # We flag a user as a potential duplicate
        self.trainee.flag_user_as_potential_duplicate(
            self.trainees[1],
            POTENTIAL_DUPLICATE_REASONS.SAME_LAST_AND_FIRST_NAME
        )

        del self.trainee.potential_duplicates

        # Now, the user should have a potential duplicate
        self.assertEqual(
            self.trainee.potential_duplicates,
            {
                self.trainees[1]: POTENTIAL_DUPLICATE_REASONS.for_value('same_last_and_first_name')
            }
        )

    def test_flag_user_as_potential_duplicate_should_work_when_potentials(self):
        """Test that the ``flag_user_as_potential_duplicate`` should work"""

        # At first, the user have already a duplicate
        DuplicatedMTPUser.objects.create(
            original_user=self.trainee,
            duplicate_user=self.trainees[1],
            origin_user=self.admin
        )

        self.assertEqual(self.trainee.potential_duplicates, {})

        # Try to flag the same user as a potential duplicate
        self.trainee.flag_user_as_potential_duplicate(
            self.trainees[1],
            POTENTIAL_DUPLICATE_REASONS.SAME_LAST_AND_FIRST_NAME
        )

        del self.trainee.potential_duplicates

        # Since the user is already a duplicate, he is not added to the potentials list
        self.assertEqual(self.trainee.potential_duplicates, {})
