""" This module tests all our quiz models"""

import os
import shutil
import tempfile

from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.management import call_command
from django.utils.translation import get_language, activate
from django.urls import reverse
from django.test import TestCase, override_settings
from rest_framework.test import APITestCase
from django.conf import settings
from .models import (
    Sport,
    Criterion,
    CriterionRating,
    CallToMove,
    KnowledgeSnack,
    Question,
)


class SportModelTest(TestCase):
    """Tests the sport model"""

    def setUp(self):
        self.name = "HIIT"
        self.url = (
            "https://www.buchsys.de/fu-berlin/angebote/aktueller_zeitraum/_HIIT_"
            "-_High_Intensity_Interval_Training___HOME.html "
        )

        self.test_sport = Sport(
            name=self.name,
            url=self.url,
        )

        self.test_sport.save()

    def test_sport_can_be_created(self):
        """New sport is written to the database"""
        test_sport = Sport.objects.first()
        self.assertEqual(test_sport.name, self.name)
        self.assertEqual(test_sport.url, self.url)


class CriterionRatingTest(TestCase):
    """Tests the Relation between Sport and Criterion"""

    def setUp(self):
        self.name = "HIIT"
        self.url = (
            "https://www.buchsys.de/fu-berlin/angebote/aktueller_zeitraum/_HIIT_"
            "-_High_Intensity_Interval_Training___HOME.html "
        )

        self.test_sport = Sport(
            name=self.name,
            url=self.url,
        )

        self.test_sport.save()

        self.criterion = Criterion(name="Einzelsport")
        self.criterion.save()

    def test_can_rate_criterion_for_sport(self):
        """A rating for a specific criterion can be added to a sport"""
        self.test_sport.rate(self.criterion, 10)
        self.assertEqual(self.test_sport.criteria_ratings.first(), self.criterion)
        self.assertEqual(self.test_sport.get_rating(self.criterion), 10)

    def test_rating_can_be_changed(self):
        """
        If a sport is rated again then the corresponding relation is changed,
        instead of recreated
        """
        first_rating_object = self.test_sport.rate(self.criterion, 10)
        second_rating_object = self.test_sport.rate(self.criterion, 8)
        self.assertEqual(first_rating_object, second_rating_object)
        self.assertEqual(self.test_sport.get_rating(criterion=self.criterion), 8)


FIXTURE_IMAGES = os.path.join(settings.BASE_DIR, "quiz", "fixtures", "images")
MEDIA_ROOT = tempfile.mkdtemp(
    suffix="testing"
)  # Create a temp directory for files created during tests


@override_settings(MEDIA_ROOT=MEDIA_ROOT)
class CallToMoveTest(TestCase):
    """Tests the Model for Call To Move"""

    def setUp(self):
        tempfile.mkdtemp(suffix="testing")  # recreate tmp folder before each test

        self.text = "Kreise deine Arme vor der nächsten Frage 3x nach hinten"
        self.image_name = "test_image.png"
        self.image_path = os.path.join(FIXTURE_IMAGES, self.image_name)
        self.image = SimpleUploadedFile(
            name=self.image_name,
            content=open(self.image_path, "rb").read(),
            content_type="image/png",
        )
        self.call_to_move = CallToMove(text=self.text, image=self.image)
        self.call_to_move.save()

    def tearDown(self) -> None:
        """Delete the temp dir after each test"""
        shutil.rmtree(MEDIA_ROOT, ignore_errors=True)

    def test_can_create_call_to_move(self):
        """A call to move can be correctly created"""
        self.assertEqual(self.call_to_move.text, self.text)
        self.assertEqual(self.call_to_move.image.name, self.image.name)

    def test_can_save_and_load_call_to_move(self):
        """A saved Call to Move can be loaded"""
        call_to_move = CallToMove.objects.first()
        self.assertEqual(call_to_move.text, self.text)
        self.assertEqual(call_to_move.image.name, self.image.name)


@override_settings(MEDIA_ROOT=MEDIA_ROOT)
class KnowledgeSnackTest(TestCase):
    """Tests the Model for Knowledge Snack"""

    def setUp(self):
        tempfile.mkdtemp(suffix="testing")  # recreate tmp folder before each test

        self.text = (
            "Dass Treppensteigen fast 5x so viele Kalorien verbrennt,"
            "als bei der Nutzung des Aufzuges?"
        )
        self.image_name = "test_image.png"
        self.image_path = os.path.join(FIXTURE_IMAGES, self.image_name)
        self.image = SimpleUploadedFile(
            name=self.image_name,
            content=open(self.image_path, "rb").read(),
            content_type="image/png",
        )
        self.knowledge_snack = KnowledgeSnack(text=self.text, image=self.image)
        self.knowledge_snack.save()

    def tearDown(self) -> None:
        """Delete the temp dir after each test"""
        shutil.rmtree(MEDIA_ROOT, ignore_errors=True)

    def test_can_create_knowledge_snack(self):
        """A knowledge snack can be correctly created"""
        self.assertEqual(self.knowledge_snack.text, self.text)
        self.assertEqual(self.knowledge_snack.image.name, self.image.name)

    def test_can_save_and_load_call_to_move(self):
        """A saved Knowledge Snack can be loaded"""
        knowledge_snack = KnowledgeSnack.objects.first()
        self.assertEqual(knowledge_snack.text, self.text)
        self.assertEqual(knowledge_snack.image.name, self.image.name)


class CriterionAndQuestionModelTest(TestCase):
    """Tests the Criterion and the Question model which have a One to One Relation"""

    def setUp(self):
        self.name = "Einzelsport"
        self.criterion = Criterion(name=self.name)
        self.criterion.save()

    def test_criterion_can_be_saved_and_loaded(self):
        """New criterion can be loaded from the db"""
        test_criterion = Criterion.objects.first()
        self.assertEqual(test_criterion.name, self.name)

    def test_question_can_be_added(self):
        """
        If a question is added to a criterion, then it is accessible through the criterion.
        """
        text = "Ich trainiere gerne mit anderen im Team"
        question = Question(text=text, criterion=self.criterion)
        question.save()
        self.criterion.question = question
        self.assertEqual(question, self.criterion.question)

    def test_criterion_stays_if_question_deleted(self):
        """If assigned question is deleted the foreign key is set None"""
        text = "Ich trainiere gerne mit anderen im Team"
        question = Question(text=text, criterion=self.criterion)
        question.save()
        question.delete()
        self.criterion = Criterion.objects.first()

        with self.assertRaises(Criterion.question.RelatedObjectDoesNotExist):
            self.criterion.question

    def test_question_can_be_saved_and_loaded(self):
        """New Question is saved to the db and can be loaded"""
        text = "Ich trainiere gerne mit anderen im Team"
        Question(text=text, criterion=self.criterion).save()
        test_question = Question.objects.first()
        self.assertEqual(test_question.text, text)


class FixturesTest(TestCase):
    """
    These are the tests for the fixtures in quiz/fixtures.
    Fixtures can be used to populate the database with test data.
    They can be used in automated tests, but also in development.
    """

    fixtures = [
        "sports.json",
        "criteria.json",
        "criterion_ratings.json",
        "questions.json",
        "calls_to_move.json",
        "knowledge_snacks.json",
    ]

    def test_sports_created_by_fixture(self):
        """If the sports fixture is loaded there exists a sport with the given data."""
        sport = Sport.objects.get(pk=1)
        self.assertEqual(sport.name, "Jiu Jitsu")
        self.assertEqual(sport.url, "http://www.test.de")

    def test_criterion_created_by_fixture(self):
        """If the criteria fixture is loaded there exists a criterion with the given data"""
        criterion = Criterion.objects.get(pk=1)
        self.assertEqual(criterion.name, "Outdoorsport")

    def test_criterion_rating_created_by_fixture(self):
        """If the criterion_ratings fixture is loaded the given sport has a corresponding rating"""
        criterion = Criterion.objects.get(name="Outdoorsport")
        sport = Sport.objects.get(name="Jiu Jitsu")
        self.assertEqual(sport.get_rating(criterion), 1)

    def test_question_created_by_fixture(self):
        """If the questions fixture is loaded there exists a question with the given data"""
        question = Question.objects.get(pk=1)
        criterion = Criterion.objects.get(name="Outdoorsport")
        self.assertEqual(question.text, "Ich mache am liebsten draußen Sport")
        self.assertEqual(question.criterion, criterion)

    def test_call_to_move_created_by_fixture(self):
        """If the call to move fixture is loaded there exists a call to move with the given data"""
        call_to_move = CallToMove.objects.get(pk=1)
        self.assertEqual(
            call_to_move.text, "Kreise deine Arme vor der nächsten Frage 3x nach hinten"
        )
        self.assertEqual(call_to_move.image.name, "test_image.png")

    def test_knowledge_snack_created_by_fixture(self):
        """
        If the knowledge snack fixture is loaded there exists a knowledge snack with the given data
        """
        knowledge_snack = KnowledgeSnack.objects.get(pk=1)
        self.assertEqual(
            knowledge_snack.text,
            "Dass Treppensteigen fast 5x so viele Kalorien verbrennt, "
            "als die Nutzung des Aufzuges?",
        )
        self.assertEqual(knowledge_snack.image.name, "logo.png")


class SeedingTest(TestCase):
    """Tests the seed_db command in quiz/management/commands"""

    def test_seed_with_complete_data(self):
        """If seed_db is called then there exists a certain number of elements per model"""

        # call the seed command without asking for confirmation
        call_command("seed_db", ["--yes", "--no-superuser"])

        n_sports = 5
        n_criteria = 5
        self.assertEqual(Sport.objects.count(), n_sports)
        self.assertEqual(Criterion.objects.count(), n_criteria)
        self.assertEqual(CriterionRating.objects.count(), n_sports * n_criteria)
        self.assertEqual(Question.objects.count(), n_criteria)
        self.assertEqual(CallToMove.objects.count(), 3)
        self.assertEqual(KnowledgeSnack.objects.count(), 3)


class ModeltranslationFallbackTest(TestCase):
    """
    Tests Behaviour of Modeltranslation when no translation is given
    Also tests Default Language
    """

    def setUp(self):
        """
        Creates a Question and fills the german Translation
        """
        self.criterion = Criterion(name="test")
        self.question = Question(text="", criterion=self.criterion)
        self.criterion.save()
        self.question.save()

    def test_default_language(self):
        """
        Checks whether the Default Language is German
        """
        cur_language = get_language()
        self.assertEqual(cur_language, "de")

    def test_vallback_value(self):
        """
        Checks whether if no Translation is set, the Fallbackvalue is used
        """
        self.assertEqual(self.question.text, ("No Translation for this Field",))


class Modeltranslation_One_Language_Test(TestCase):
    """
    Tests Behaviour when only one language is defined
    """

    def setUp(self):
        """
        Creates a Question and fills the german Translation
        """
        self.criterion = Criterion(name="test")
        self.question = Question(text="", criterion=self.criterion)

        self.question.text = "de_text"

        self.criterion.save()
        self.question.save()

    def test_german_translation(self):
        """
        Check whether obj.text returns the German Translation
        """
        self.assertEqual(self.question.text, "de_text")

    def test_fallback_value_translations(self):
        """
        English Translation is not filled out, check whether german text is returned
        """

        activate("en")
        self.assertNotEqual(self.question.text, self.question.text_en)
        self.assertEqual(self.question.text, self.question.text_de)


class Modeltranslation_Two_Languages_Test(TestCase):
    """
    Tests Behaviour when two languages are defined
    """

    def setUp(self):
        """
        Creates a Question and fills both the german and english Translations
        """
        self.criterion = Criterion(name="test")
        self.question = Question(text="", criterion=self.criterion)

        activate("de")
        self.question.text = "de_text"
        activate("en")
        self.question.text = "en_text"
        activate("de")

        self.criterion.save()
        self.question.save()

    def test_german_translation(self):
        """
        Tests whether German Translation is returned
        """
        activate("de")

        self.assertEqual(self.question.text, self.question.text_de)
        self.assertNotEqual(self.question.text, self.question.text_en)

    def test_english_translation(self):
        """
        Tests whether English Translation is returned
        """
        activate("en")

        self.assertEqual(self.question.text, self.question.text_en)
        self.assertNotEqual(self.question.text, self.question.text_de)


class APITest(APITestCase):
    """Tests the Django API"""

    fixtures = [
        "sports.json",
        "criteria.json",
        "criterion_ratings.json",
        "questions.json",
    ]

    def test_get_sport_returns_correct_data(self):
        """Test the API endpoint /sport/{id}"""
        response = self.client.get(reverse("small-sport-list-detail", kwargs={"pk": 1}))
        sport_data = {
            "id": 1,
            "name": "Jiu Jitsu",
            "url": "http://www.test.de",
            "criteria": [{"id": 1, "name": "Outdoorsport", "value": 1}],
        }
        self.assertDictEqual(response.data, sport_data)

    def test_put_sport_makes_correct_changes(self):
        """
        Test the API endpoint /sport/{id} for put requests
        """
        sport_data = {
            "name": "Karate",
            "url": "http://www.test2.de",
            "criteria": [{"id": 1, "name": "Outdoorsport", "value": 1}],
        }

        response = self.client.put(
            reverse("small-sport-list-detail", kwargs={"pk": 1}),
            data=sport_data,
            format="json",
        )
        self.assertEqual(response.data["name"], sport_data["name"])
        self.assertEqual(response.data["url"], sport_data["url"])
        self.assertEqual(len(response.data["criteria"]), Criterion.objects.count())
        self.assertDictEqual(
            response.data["criteria"][0], {"id": 1, "name": "Outdoorsport", "value": 1}
        )

    def test_patch_sport_makes_correct_changes(self):
        """
        Test the API endpoint /sport/{id} for patch requests
        """
        sport_data = {
            "criteria": [{"id": 1, "value": 3}],
        }

        response = self.client.patch(
            reverse("small-sport-list-detail", kwargs={"pk": 1}),
            data=sport_data,
            format="json",
        )
        self.assertEqual(response.data["name"], "Jiu Jitsu")
        self.assertEqual(response.data["url"], "http://www.test.de")
        self.assertEqual(len(response.data["criteria"]), Criterion.objects.count())
        self.assertDictEqual(
            response.data["criteria"][0], {"id": 1, "name": "Outdoorsport", "value": 3}
        )

    def test_get_sports_returns_correct_data(self):
        """Test if API endpoint /sport returns correct sports list"""

        response = self.client.get(reverse("small-sport-list-list"))

        sport_data = [
            {
                "id": 1,
                "is_filled": True,
                "name": "Jiu Jitsu",
                "url": "http://www.test.de",
            }
        ]
        self.assertListEqual(response.data["results"], sport_data)
        self.assertEqual(response.data["count"], 1)

    def test_post_sports_creates_correct_entry(self):
        """Test if post to /sport creates a correct object"""
        sport_data_new = {
            "name": "Karate",
            "url": "http://www.test2.de",
        }

        response = self.client.post(
            reverse("small-sport-list-list"), data=sport_data_new
        )
        self.assertEqual(response.data["name"], sport_data_new["name"])
        self.assertEqual(response.data["url"], sport_data_new["url"])
        self.assertEqual(len(response.data["criteria"]), Criterion.objects.count())
        self.assertEqual(Sport.objects.count(), 2)

    def test_delete_sports_creates_correct_entry(self):
        """Test if delete to /sports deletes an object"""

        response = self.client.delete(
            reverse("small-sport-list-detail", kwargs={"pk": 1})
        )

        self.assertEqual(Sport.objects.count(), 0)

    def test_get_incomplete_sport_list(self):
        """
        Tests if get to /sport/incomplete/ returns a list of incomplete sports
        """

        # Set Up Incomplete Sport
        response = self.client.patch(
            reverse("small-sport-list-detail", kwargs={"pk": 1}),
            data={"criteria": [{"id": 1, "value": -1}]},
            format="json",
        )

        response = self.client.get(reverse("incomplete"))

        self.assertEqual(len(response.data["results"]), 1)
        self.assertEqual(response.data["results"][0]["name"], "Jiu Jitsu")

    def test_get_criteria(self):
        """
        Tests if get /critera/ returns a list of all criteria
        """

        # Set Up Values
        response = self.client.patch(
            reverse("small-sport-list-detail", kwargs={"pk": 1}),
            data={"criteria": [{"id": 1, "value": 7}]},
            format="json",
        )

        response = self.client.get(reverse("criteria"))

        self.assertEqual(len(response.data), Criterion.objects.count())
        self.assertEqual(response.data[0]["number_of_sports_active"], 1)
        self.assertEqual(response.data[0]["sum_of_weights"], 7)