"""Test the models and managers of the ``surveys`` app"""

from django.core.exceptions import ValidationError

from mtp.apps.accounts.factories import AdminFactory, TraineeFactory, TrainerFactory
from mtp.apps.common.tests.test_cases import run_tests_concurrently
from mtp.apps.trainings.factories import TrainingFactory, TrainingSessionFactory

from ..factories import PoolFactory, QuestionFactory
from ..models import Choice, Pool, Question, Survey
from .test_cases import SurveyTestCase, SurveyTransactionTestCase


class PoolModelTestCase(SurveyTestCase):
    """Test case that tests the ``Pool`` model."""

    def test_should_set_is_global_field(self):
        """Test that the ``save`` method correctly sets the ``is_global`` field."""

        # Should be set to True if created by an admin.
        self.assertTrue(self.admin_pool.is_global)

        # Should be set to False if created by an trainer.
        self.assertFalse(self.trainer_pool.is_global)


class PoolsManagerTestCase(SurveyTestCase):
    """Test case that tests the ``PoolsManager`` manager."""

    def test_queryset_should_return_all_instances(self):
        """Test that the default queryset returns all instances, including ones marked as deleted"""

        # And some deleted ones.
        PoolFactory.create_batch(2, is_deleted=True)

        # We should have 4 pools (2 delete + ``self.admin_pool`` and ``self.trainer_pool``..
        self.assertEqual(Pool.objects.count(), 4)

    def test_get_for_trainer(self):
        """Test that the ``filter_for_trainer`` method returns all expected pools for a trainer."""

        # Add another pool owned by the trainer.
        PoolFactory(author=self.trainer)

        # Add another global pool.
        PoolFactory(author=self.admin)

        # Get the pools for the trainer, including globals (default behavior).
        pools = Pool.objects.filter_for_trainer(self.trainer)

        # Check we've got 4 pools.
        self.assertEqual(len(pools), 4)

        # And that global ones come first.
        self.assertTrue(pools[0].is_global)
        self.assertTrue(pools[1].is_global)
        self.assertFalse(pools[2].is_global)
        self.assertEqual(pools[2].author, self.trainer)
        self.assertFalse(pools[3].is_global)
        self.assertEqual(pools[3].author, self.trainer)

        # Get the pools without globals.
        pools = Pool.objects.filter_for_trainer(self.trainer, include_globals=False)

        # Check we've got 2 pools
        self.assertEqual(len(pools), 2)

        # And that we only have non-global ones.
        self.assertFalse(pools[0].is_global)
        self.assertFalse(pools[1].is_global)

        # Check we can have the number of questions
        self.create_open_question(pools[1])

        pools = Pool.objects.filter_for_trainer(self.trainer, include_globals=False,
                                                with_questions_count=True)
        self.assertEqual(pools[0].nb_questions, 3)  # create in the setup
        self.assertEqual(pools[1].nb_questions, 1)

    def test_get_for_admin(self):
        """Test that the ``filter_for_admin`` method returns all expected pools for an admin."""

        # Add another pool owned by the trainer.
        PoolFactory(author=self.trainer)

        # Add another global pool.
        PoolFactory(author=self.admin)

        # Create some global pools.for another admin
        admin2 = AdminFactory()
        PoolFactory.create_batch(2, author=admin2)

        # Get the pools for the admin, including ones they're not the author of (default behavior).
        pools = Pool.objects.filter_for_admin(self.admin)

        # Check we've got 4 pools (not the ones from the trainer)
        self.assertEqual(len(pools), 4)

        # All globals
        self.assertTrue(pools[0].is_global)
        self.assertTrue(pools[1].is_global)
        self.assertTrue(pools[2].is_global)
        self.assertTrue(pools[3].is_global)

        # Get the pools without ones they're not the author of.
        pools = Pool.objects.filter_for_admin(self.admin, include_others=False)

        # Check we've got 2 pools.
        self.assertEqual(len(pools), 2)

        # And that we only have the ones the admin is the author of..
        self.assertEqual(pools[0].author, self.admin)
        self.assertEqual(pools[1].author, self.admin)

        # Check we can have the number of questions
        self.create_open_question(pools[0])
        self.create_open_question(pools[0])
        self.create_open_question(pools[1])

        pools = Pool.objects.filter_for_admin(self.admin, include_others=False,
                                              with_questions_count=True)
        self.assertEqual(pools[0].nb_questions, 2)
        self.assertEqual(pools[1].nb_questions, 1)

    def test_count_available_for_user(self):
        """Test that the ``count_valid_for_user`` method returns the correct nb of questions."""

        # Test for the current trainer
        # Only one pool owned by the trainer with questions. The global pool doesn't have questions
        self.assertEqual(Pool.objects.count_valid_for_user(self.trainer), 1)

        # Test with a new trainer
        trainer = TrainerFactory()
        # Still no global pool with question, and this trainer doesn't own any pool
        self.assertEqual(Pool.objects.count_valid_for_user(trainer), 0)

        # Test with the admin
        # There is no global pool with questions
        self.assertEqual(Pool.objects.count_valid_for_user(self.admin), 0)

        # Add a global pool with a question
        admin_pool = PoolFactory(author=self.admin)
        admin_question = self.create_open_question(admin_pool)

        # Soft-deleting the question should not count the pool
        admin_question.remove()
        self.assertEqual(Pool.objects.count_valid_for_user(self.admin), 0)

        # Adding another one...
        self.create_open_question(admin_pool)

        # Now the admin has one pool available
        self.assertEqual(Pool.objects.count_valid_for_user(self.admin), 1)

        # The first trainer has two
        self.assertEqual(Pool.objects.count_valid_for_user(self.trainer), 2)

        # And the new trainer has one
        self.assertEqual(Pool.objects.count_valid_for_user(trainer), 1)

        # But another admin has one two: the global one
        admin = AdminFactory()
        self.assertEqual(Pool.objects.count_valid_for_user(admin), 1)

        # Soft-delete this pool
        admin_pool.remove()

        # Using `objects` should still count one pool
        self.assertEqual(Pool.objects.count_valid_for_user(admin), 1)

        # But using `non_deleted` should count none
        self.assertEqual(Pool.non_deleted.count_valid_for_user(admin), 0)


class NonDeletedPoolsManagerTestCase(SurveyTestCase):
    """Test case that tests the ``NonDeletedPoolsManager`` manager."""

    def test_queryset_should_not_return_deleted_instances(self):
        """Test that the default queryset does not returns instances marked as deleted."""

        # And some deleted pools.
        PoolFactory.create_batch(2, is_deleted=True)

        # We should have 2 non-deleted pools.
        self.assertEqual(Pool.non_deleted.count(), 2)

    def test_get_for_trainer_should_returns_deleted_instances_depending_on_the_manager(self):
        """Test that the ``filter_for_trainer`` method returns deleted only when asked.

        It should returns deleted ones for the default manager, but not for the "non-deleted"
        one.

        """
        # Add a deleted pool for the trainer.
        PoolFactory(author=self.trainer, is_deleted=True)

        # Add a deleted pool for the admin.
        PoolFactory(author=self.admin, is_deleted=True)

        # Default manager should return all
        self.assertEqual(Pool.objects.filter_for_trainer(self.trainer).count(), 4)
        self.assertEqual(Pool.objects.filter_for_trainer(self.trainer,
                                                         include_globals=False).count(), 2)

        # Non-deleted manager should not return non-deleted ones.
        self.assertEqual(Pool.non_deleted.filter_for_trainer(self.trainer).count(), 2)
        self.assertEqual(Pool.non_deleted.filter_for_trainer(self.trainer,
                                                             include_globals=False).count(), 1)


class QuestionsManagerTestCase(SurveyTestCase):
    """Test case that tests the ``QuestionsManager`` manager."""

    def test_get_for_pool_should_returns_all_questions_in_a_pool(self):
        """Test that the ``filter_for_pool`` methods returns all questions of a pool."""

        # And another pool with other questions.
        QuestionFactory.create_batch(2, pool=PoolFactory())

        # Retrieve the questions of the trainer.
        questions = Question.objects.filter_for_pool(self.trainer_pool)

        # And check we only have ones for the pool.
        self.assertEqual(len(questions), 3)

        questions_pks = {self.single_choice_question.pk,
                         self.multi_choices_question.pk,
                         self.open_question.pk}

        expected_pks = set([q.pk for q in questions])
        self.assertSetEqual(questions_pks, expected_pks)


class NonDeletedQuestionsManagerTestCase(SurveyTestCase):
    """Test case that tests the ``NonDeletedQuestionsManager`` manager."""

    def test_get_for_pool_should_not_return_deleted_questions(self):
        """Test that the ``filter_for_pool`` methods does not returns deleted questions."""

        # And a deleted question.
        QuestionFactory(pool=self.trainer_pool, is_deleted=True)

        # And another pool with other questions.
        pool2 = PoolFactory()
        QuestionFactory.create_batch(2, pool=pool2)
        QuestionFactory(pool=pool2, is_deleted=True)

        # Retrieve the questions.
        questions = Question.non_deleted.filter_for_pool(self.trainer_pool)

        # And check we only have ones for the pool.
        self.assertEqual(len(questions), 3)

        questions_pks = {self.single_choice_question.pk,
                         self.multi_choices_question.pk,
                         self.open_question.pk}

        expected_pks = set([q.pk for q in questions])
        self.assertSetEqual(questions_pks, expected_pks)


class SurveysManagerTestCase(SurveyTestCase):
    """Test case that tests the ``SurveysManager`` manager."""

    def test_get_or_create_for_training_raise_if_not_a_trainer(self):
        """Test that ``get_or_create_for_training`` raises if the user is not a trainer."""

        # Creating a survey for the trainee should fail.
        with self.assertRaises(ValidationError):
            Survey.objects.get_or_create_for_training(self.training, self.trainee)

    def test_get_or_create_for_training_creates_if_needed(self):
        """Test that the ``get_or_create_for_training`` method creates a survey if needed."""

        # First call should create the survey.
        survey1, created = Survey.objects.get_or_create_for_training(self.training, self.trainer)
        self.assertTrue(created)

        # Second call should return the existing survey
        survey2, created = Survey.objects.get_or_create_for_training(self.training, self.trainer)
        self.assertFalse(created)

        # Both surveys should be the same.
        self.assertEqual(survey1, survey2)

    def test_creating_a_survey_should_add_the_pools(self):
        """Test that the ``get_or_create_for_training`` method creates the survey and its pools."""

        # Create the survey.
        survey, __ = Survey.objects.get_or_create_for_training(self.training, self.trainer)

        # The survey should have the correct given attributes
        self.assertEqual(survey.training, self.training)
        self.assertEqual(survey.trainer, self.trainer)

        # And should have our pools
        self.assertEqual(len(survey.pools.all()), 2)
        survey_pools = set([pool.id for pool in survey.pools.all()])
        expected_pools = {self.admin_pool.pk, self.trainer_pool.pk}
        self.assertSetEqual(survey_pools, expected_pools)

    def test_getting_a_survey_should_add_the_missing_pools(self):
        """Test that the ``get_or_create_for_training`` method add the missing_ pools."""

        # Create the survey.
        survey, __ = Survey.objects.get_or_create_for_training(self.training, self.trainer)

        # The survey should have our pools.
        self.assertEqual(len(survey.pools.all()), 2)

        # Add some new global pools
        PoolFactory.create_batch(2, author=self.admin)

        # And some owned by the trainer.
        PoolFactory.create_batch(2, author=self.trainer)

        # Now we can get the survey again.
        survey, __ = Survey.objects.get_or_create_for_training(self.training, self.trainer)

        # The survey should have our 2 previous pools and the 4 new ones
        self.assertEqual(len(survey.pools.all()), 6)

    def test_survey_is_not_created_if_no_pool_with_questions(self):
        """`get_or_create_for_training` should not return a survey if no pool exists for it."""

        # Remove only questions, we shouldn't have a survey
        Choice.objects.all().delete()
        Question.objects.all().delete()

        # Try to get a survey
        survey, __ = Survey.objects.get_or_create_for_training(self.training_session.training,
                                                               self.training_session.trainer)

        # We should not have a survey
        self.assertIsNone(survey)

        # Removing the pool shouldn't, obviously, return a survey
        Pool.objects.all().delete()

        # Try to get a survey
        survey, __ = Survey.objects.get_or_create_for_training(self.training_session.training,
                                                               self.training_session.trainer)

        # We should not have a survey
        self.assertIsNone(survey)


class SurveysManagerTransactionTestCase(SurveyTransactionTestCase):
    """Test case that tests the ``SurveysManager`` manager.

    This test case is based on ``TransactionTestCase`` to manage correctly the threaded calls.

    """

    def test_concurrent_creation_of_survey(self):
        """Test that it will not fail if many requests try to create a survey."""

        # Will hold results to check them later.
        results = []

        # We'll simulate many concurrent calls to ``get_or_create_for_training``.
        @run_tests_concurrently(3)
        def get_or_create_survey():
            results.append(Survey.objects.get_or_create_for_training(self.training, self.trainer))

        get_or_create_survey()

        # Check that all surveys are the same.
        self.assertTrue(results[0][0] == results[1][0] == results[2][0])

        # And only one was marked as created.
        self.assertEqual([r[1] for r in results].count(True), 1)

        # And that all return the correct number of pools.
        for survey, __ in results:
            self.assertEqual(len(survey.pools.all()), 2)

    def test_concurrent_update_of_survey(self):
        """Test that it will not fail if many requests try to update a survey."""

        # Create the survey.
        survey, __ = Survey.objects.get_or_create_for_training(self.training, self.trainer)

        # The survey should have our 2 pools.
        self.assertEqual(len(survey.pools.all()), 2)

        # Add some new global pools
        PoolFactory.create_batch(2, author=self.admin)

        # And some owned by the trainer.
        PoolFactory.create_batch(2, author=self.trainer)

        # Will hold results to check them later.
        results = []

        # We'll simulate many concurrent calls to ``get_or_create_for_training``.
        @run_tests_concurrently(3)
        def get_or_create_survey():
            results.append(Survey.objects.get_or_create_for_training(self.training, self.trainer))

        get_or_create_survey()

        # Check that all surveys are the same.
        self.assertTrue(results[0][0] == results[1][0] == results[2][0])

        # And no one was marked as created.
        self.assertEqual([r[1] for r in results].count(True), 0)

        # And that all return the correct number of pools.
        for survey, __ in results:
            self.assertEqual(len(survey.pools.all()), 6)


class AnswerManagerSaveAnswerTestCase(SurveyTestCase):
    """Test case for the ``save_answer`` method of ``AnswerManager``."""

    @classmethod
    def setUpTestData(cls):
        """Create a survey in addition to existing test data."""

        super().setUpTestData()

        cls.survey, __ = Survey.objects.get_or_create_for_training(cls.training, cls.trainer)

    def do_save_answer(self, question, choices, **kwargs):
        answer = self.save_answer(question, choices, **kwargs)

        self.assertEqual(answer.training_session, self.training_session)
        self.assertEqual(answer.survey, self.survey)
        self.assertEqual(answer.trainee, self.trainee)
        self.assertEqual(answer.question, question)

        if choices:
            self.assertQuerysetEqual(
                answer.choices.all(),
                [choice.pk for choice in choices],
                transform=lambda obj: obj.id,
                ordered=False
            )
        else:
            self.assertQuerysetEqual(answer.choices.all(), [])

        if 'text' in kwargs:
            self.assertEqual(answer.text, kwargs['text'])

    def test_answer_to_single_choice_question_is_saved(self):
        self.do_save_answer(
            question=self.single_choice_question,
            choices=[self.single_choice_question.choices.first()],
            text='',
        )

    def test_answer_to_multiple_choices_question_should_be_saved(self):
        self.do_save_answer(
            question=self.multi_choices_question,
            choices=self.multi_choices_question.choices.all()[1:3],
            text='',
        )

    def test_answer_to_open_question_should_be_saved(self):
        self.do_save_answer(
            question=self.open_question,
            choices=[],
            text='dummy text',
        )

    def test_no_choices_for_single_choice_question_should_raise(self):
        with self.assertRaises(ValidationError) as raise_context:
            self.do_save_answer(
                question=self.single_choice_question,
                choices=[],
                text='',
            )
        self.assertEqual(raise_context.exception.code, 'expected-choice')

    def test_many_choices_for_single_choice_question_should_raise(self):
        with self.assertRaises(ValidationError) as raise_context:
            self.do_save_answer(
                question=self.single_choice_question,
                choices=self.single_choice_question.choices.all()[1:3],
                text='',
            )
        self.assertEqual(raise_context.exception.code, 'too-many-choices')

    def test_no_choices_for_multi_choices_question_should_raise(self):
        with self.assertRaises(ValidationError) as raise_context:
            self.do_save_answer(
                question=self.multi_choices_question,
                choices=[],
                text='',
            )
        self.assertEqual(raise_context.exception.code, 'expected-choices')

    def test_text_for_single_choice_question_should_raise(self):
        with self.assertRaises(ValidationError) as raise_context:
            self.do_save_answer(
                question=self.single_choice_question,
                choices=[self.single_choice_question.choices.first()],
                text='dummy text',
            )
        self.assertEqual(raise_context.exception.code, 'unexpected-text')

    def test_text_for_multi_choices_question_should_raise(self):
        with self.assertRaises(ValidationError) as raise_context:
            self.do_save_answer(
                question=self.multi_choices_question,
                choices=self.multi_choices_question.choices.all()[1:3],
                text='dummy text',
            )
        self.assertEqual(raise_context.exception.code, 'unexpected-text')

    def test_choices_not_in_the_single_choice_question_should_raise(self):
        with self.assertRaises(ValidationError) as raise_context:
            self.do_save_answer(
                question=self.single_choice_question,
                choices=[self.multi_choices_question.choices.first()],
                text='',
            )
        self.assertEqual(raise_context.exception.code, 'invalid-choice')

    def test_choices_not_in_the_multi_choices_question_should_raise(self):
        with self.assertRaises(ValidationError) as raise_context:
            self.do_save_answer(
                question=self.multi_choices_question,
                choices=self.single_choice_question.choices.all()[1:3],
                text='',
            )
        self.assertEqual(raise_context.exception.code, 'invalid-choice')

    def test_choices_for_open_questions_should_raise(self):
        with self.assertRaises(ValidationError) as raise_context:
            self.do_save_answer(
                question=self.open_question,
                choices=self.multi_choices_question.choices.all()[1:3],
                text='dummy text',
            )
        self.assertEqual(raise_context.exception.code, 'unexpected-choices')

    def test_invalid_survey_should_raise(self):

        # Override ``self.training_session`` with a session from another training to have one not
        # matching ``self.survey``
        other_training = TrainingFactory()
        training_session = TrainingSessionFactory(training=other_training,
                                                  trainer=self.trainer)

        with self.assertRaises(ValidationError) as raise_context:
            self.do_save_answer(
                question=self.open_question,
                choices=[],
                training_session=training_session,
                text='dummy text',
            )
        self.assertEqual(raise_context.exception.code, 'invalid-survey')

    def test_should_fail_if_user_not_trainee(self):
        with self.assertRaises(ValidationError) as raise_context:
            self.do_save_answer(
                question=self.open_question,
                choices=[],
                text='dummy text',
                trainee=self.trainer,
            )
        self.assertEqual(raise_context.exception.code, 'invalid-user')

    def test_question_should_exist_and_not_deleted(self):
        self.open_question.is_deleted = True
        self.open_question.save()

        with self.assertRaises(ValidationError) as raise_context:
            self.do_save_answer(
                question=self.open_question,
                choices=[],
                text='dummy text',
            )
        self.assertEqual(raise_context.exception.code, 'invalid-question')

    def test_question_should_be_in_the_survey(self):

        other_trainer = TraineeFactory()
        other_pool = PoolFactory(author=other_trainer)
        other_question = QuestionFactory(
            question_type=Question.QUESTION_TYPES.OPEN,
            pool=other_pool
        )

        with self.assertRaises(ValidationError) as raise_context:
            self.do_save_answer(
                question=other_question,
                choices=[],
                text='dummy text',
            )
        self.assertEqual(raise_context.exception.code, 'invalid-question')
