import datetime
import time
from decimal import Decimal
from unittest import mock

from django.core.cache import cache
from django.db import IntegrityError
from django.shortcuts import reverse
from django.test import TestCase, TransactionTestCase, override_settings
from django.utils import timezone

from dictionary.conf import settings
from dictionary.models import (
    Author,
    Category,
    Conversation,
    Entry,
    Memento,
    Message,
    Topic,
    TopicFollowing,
    UserVerification,
)


class AuthorModelTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.generic_superuser = Author.objects.create(
            username=settings.GENERIC_SUPERUSER_USERNAME, email="gsu", is_active=True
        )
        cls.author = Author.objects.create(username="user", email="0", is_active=True, is_novice=False)
        cls.topic = Topic.objects.create_topic("test_topic")
        cls.entry_base = {"topic": cls.topic, "author": cls.author}

    def test_profile_entry_counts(self):
        Entry.objects.create(**self.entry_base)  # created now (today)
        # dates to be mocked for auto now add field 'date_created'
        mock_31 = timezone.now() - datetime.timedelta(minutes=43201)  # month upper limit
        mock_30 = timezone.now() - datetime.timedelta(minutes=43199)  # created in 1 month period
        mock_14 = timezone.now() - datetime.timedelta(minutes=20159)  # created in 2 weeks period
        mock_8 = timezone.now() - datetime.timedelta(minutes=10081)  # week upper limit
        mock_7 = timezone.now() - datetime.timedelta(minutes=10079)  # created in 1 week period
        mock_2 = timezone.now() - datetime.timedelta(minutes=1441)  # today upper limit
        mock_1 = timezone.now() - datetime.timedelta(minutes=1439)  # created today

        mock_these = (mock_1, mock_2, mock_7, mock_8, mock_14, mock_30, mock_31)

        with mock.patch("django.utils.timezone.now") as mock_now:
            for retval in mock_these:
                mock_now.return_value = retval
                Entry.objects.create(**self.entry_base)

        self.assertEqual(self.author.entry_count, 8)
        self.assertEqual(self.author.entry_count_day, 2)
        self.assertEqual(self.author.entry_count_month, 7)
        self.assertEqual(self.author.entry_count_week, 4)

    def test_last_entry_date(self):
        Entry.objects.create(**self.entry_base, is_draft=True)
        self.assertIsNone(self.author.last_entry_date)

        del self.author.last_entry_date

        entry = Entry.objects.create(**self.entry_base)
        self.assertEqual(self.author.last_entry_date, entry.date_created)

    def test_followers(self):
        self.assertEqual(self.author.followers.count(), 0)  # no follower supplied yet
        follower = Author.objects.create(username="1", email="1")
        some_other_follower = Author.objects.create(username="2", email="2")
        follower.following.add(self.author)
        some_other_follower.following.add(self.author)
        self.assertIn(follower, self.author.followers)
        self.assertEqual(self.author.followers.count(), 2)

    def test_novice_list_join_retreat(self):
        """
        10 published entries needed in order an user to be in the novice list,
        if the number of entries drop to < 10
        user is removed from novice list

        del novice.entry_count -> Normally we don't expect multiple entry creation
        in one request; we need to delete cached_property to simulate that.
        """

        novice = Author.objects.create(username="noviceuser", email="noviceuser", is_active=True, is_novice=True)
        entry_base = {"topic": self.topic, "author": novice, "content": "123"}

        # Initial status
        self.assertEqual(novice.application_status, Author.Status.ON_HOLD)
        self.assertIsNone(novice.application_date)

        # Add NINE entries
        for _ in range(9):
            Entry.objects.create(**entry_base)
            del novice.entry_count

        # Add an entry which is a draft
        Entry.objects.create(**entry_base, is_draft=True)
        del novice.entry_count

        # There are 10 PUBLISHED entries required, 9 present, so everything should be the same
        self.assertEqual(novice.application_status, Author.Status.ON_HOLD)
        self.assertIsNone(novice.application_date)

        # Add 10th entry (user joins the novice list)
        final_entry = Entry.objects.create(**entry_base)
        del novice.entry_count

        self.assertEqual(novice.application_status, Author.Status.PENDING)
        self.assertIsNotNone(novice.application_date)

        final_entry.delete()  # delete 10th entry to retreat from novice list

        self.assertEqual(novice.application_status, Author.Status.ON_HOLD)
        self.assertIsNone(novice.application_date)

    def test_message_preferences(self):
        some_author = Author.objects.create(username="author", email="3", is_novice=False, is_active=True)
        some_novice = Author.objects.create(username="novice", email="4", is_active=True)
        frozen_account = Author.objects.create(username="frozen", email="5", is_frozen=True, is_active=True)
        private_account = Author.objects.create(username="private", email="6", is_private=True, is_active=True)
        inactive_account = Author.objects.create(username="inactive", email="7")

        # ALL users (database default)
        can_msg_sent_by_novice_public = Message.objects.compose(some_novice, self.author, "test")
        can_msg_sent_by_author_public = Message.objects.compose(some_author, self.author, "test")
        self.assertNotEqual(can_msg_sent_by_author_public, False)
        self.assertNotEqual(can_msg_sent_by_novice_public, False)

        # Disabled
        self.author.message_preference = Author.MessagePref.DISABLED
        can_msg_sent_by_novice_disabled = Message.objects.compose(some_novice, self.author, "test-")
        can_msg_sent_by_author_disabled = Message.objects.compose(some_author, self.author, "test-")
        can_msg_sent_by_gsuper_disabled = Message.objects.compose(self.generic_superuser, self.author, "test")
        self.assertEqual(can_msg_sent_by_author_disabled, False)
        self.assertEqual(can_msg_sent_by_novice_disabled, False)
        self.assertNotEqual(can_msg_sent_by_gsuper_disabled, False)

        # Authors (non-novices) only
        self.author.message_preference = Author.MessagePref.AUTHOR_ONLY
        msg_sent_by_novice = Message.objects.compose(some_novice, self.author, "test")
        msg_sent_by_author = Message.objects.compose(some_author, self.author, "test")
        msg_sent_by_gsuper = Message.objects.compose(self.generic_superuser, self.author, "test")
        self.assertNotEqual(msg_sent_by_author, False)
        self.assertEqual(msg_sent_by_novice, False)
        self.assertNotEqual(msg_sent_by_gsuper, False)

        # Following only
        self.author.message_preference = Author.MessagePref.FOLLOWING_ONLY
        msg_sent_by_non_follower = Message.objects.compose(some_author, self.author, "test")
        msg_sent_by_gsuper = Message.objects.compose(self.generic_superuser, self.author, "test")
        self.assertEqual(msg_sent_by_non_follower, False)
        self.assertNotEqual(msg_sent_by_gsuper, False)
        self.author.following.add(some_author)  # add following to send message
        msg_sent_by_follower = Message.objects.compose(some_author, self.author, "test")
        self.assertNotEqual(msg_sent_by_follower, False)

        # Blocking tests
        self.author.message_preference = Author.MessagePref.ALL_USERS
        self.author.blocked.add(some_author)
        can_recieve_msg_from_blocked_user = Message.objects.compose(some_author, self.author, "test")
        self.assertEqual(can_recieve_msg_from_blocked_user, False)
        can_send_msg_to_blocked_user = Message.objects.compose(self.author, some_author, "test")
        self.assertEqual(can_send_msg_to_blocked_user, False)

        self.author.blocked.add(self.generic_superuser)
        can_recieve_msg_from_blocked_gsuper = Message.objects.compose(self.generic_superuser, self.author, "test")
        self.assertNotEqual(can_recieve_msg_from_blocked_gsuper, False)

        # No self-messaging allowed
        can_send_message_to_self = Message.objects.compose(self.author, self.author, "test")
        self.assertEqual(can_send_message_to_self, False)

        # Frozen and private accounts can't be messaged
        can_send_message_to_frozen = Message.objects.compose(self.author, frozen_account, "test")
        can_send_message_to_private = Message.objects.compose(self.author, private_account, "test")
        can_send_message_to_frozen_gsuper = Message.objects.compose(self.generic_superuser, frozen_account, "test")
        can_send_message_to_private_gsuper = Message.objects.compose(self.generic_superuser, private_account, "test")
        self.assertEqual(can_send_message_to_frozen, False)
        self.assertEqual(can_send_message_to_private, False)
        self.assertNotEqual(can_send_message_to_frozen_gsuper, False)
        self.assertNotEqual(can_send_message_to_private_gsuper, False)

        # Inactive accounts can't be messaged
        can_send_message_to_inactive = Message.objects.compose(self.author, inactive_account, "test")
        can_send_message_to_inactive_gsuper = Message.objects.compose(self.generic_superuser, inactive_account, "test")
        self.assertEqual(can_send_message_to_inactive, False)
        self.assertNotEqual(can_send_message_to_inactive_gsuper, False)

    def test_follow_all_categories_on_creation(self):
        category_1 = Category.objects.create(name="test")
        Category.objects.create(name="test2")
        some_user = Author.objects.create(username="some_user", email="5")
        self.assertIn(category_1, some_user.following_categories.all())
        self.assertEqual(some_user.following_categories.all().count(), 2)

    def test_absolute_url(self):
        absolute_url = reverse("user-profile", kwargs={"slug": self.author.slug})
        self.assertEqual(absolute_url, self.author.get_absolute_url())

    def test_entry_nice(self):
        # No entry = No nice entry
        cache.clear()
        author = self.author
        self.assertEqual(None, author.entry_nice)

        # Entry with low vote rate
        entry = Entry.objects.create(topic=self.topic, author=author)
        del author.entry_nice
        cache.clear()

        self.assertEqual(None, author.entry_nice)

        # Entry with enough vote rate
        entry.vote_rate = Decimal("1.1")
        entry.save()

        del author.entry_nice
        cache.clear()
        self.assertEqual(entry, author.entry_nice)

    def test_username_change_simple(self):
        self.author.username = "hello world"
        with self.assertNumQueries(3):
            self.author.save()
        self.author.refresh_from_db()
        self.assertEqual("hello-world", self.author.slug)

    def test_username_change_update_fields(self):
        self.author.username = "hello world"
        with self.assertNumQueries(3):
            self.author.save(update_fields=["username"])
        self.author.refresh_from_db()
        self.assertEqual("hello-world", self.author.slug)

    def test_username_change_update_fields_ignored(self):
        with self.assertNumQueries(1):
            self.author.save(update_fields=["date_joined"])
        self.author.refresh_from_db()
        self.assertEqual("user", self.author.slug)

    def test_username_change_unchanged_ignored(self):
        with self.assertNumQueries(2):
            self.author.save()
        self.author.refresh_from_db()
        self.assertEqual("user", self.author.slug)


class CategoryModelTests(TransactionTestCase):
    @classmethod
    def setUp(cls):
        cls.category = Category.objects.create(name="şeker")

    def test_absolute_url(self):
        absolute_url = reverse("topic_list", kwargs={"slug": self.category.slug})
        self.assertEqual(absolute_url, self.category.get_absolute_url())

    def test_uniqueness(self):
        with self.assertRaises(IntegrityError):
            Category.objects.create(name="şeker")

        similar_category = Category.objects.create(name="seker")
        self.assertNotEqual(similar_category.slug, self.category.slug)


class EntryModelTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.author = Author.objects.create(username="user", email="0")
        cls.topic = Topic.objects.create_topic("test_topic")
        cls.entry_base = {"topic": cls.topic, "author": cls.author}
        with override_settings(LANGUAGE_CODE="tr"):
            cls.entry = Entry.objects.create(**cls.entry_base, content="CONtent İŞçI")

    def test_absolute_url(self):
        absolute_url = reverse("entry-permalink", kwargs={"entry_id": self.entry.pk})
        self.assertEqual(absolute_url, self.entry.get_absolute_url())

    def test_content_lower(self):
        self.assertEqual("content işçı", self.entry.content)

    def test_topic_ownership(self):
        self.assertEqual(self.author, self.topic.created_by)

        topic_with_no_ownership = Topic.objects.create_topic("test_topic2")
        self.assertIsNone(topic_with_no_ownership.created_by)

        new_entry = Entry.objects.create(author=self.author, topic=topic_with_no_ownership, is_draft=True)
        self.assertIsNone(topic_with_no_ownership.created_by)

        new_entry.is_draft = False
        new_entry.save()
        self.assertEqual(self.author, topic_with_no_ownership.created_by)

    def test_votes(self):
        # Initial vote should be 0
        self.assertEqual(self.entry.vote_rate.conjugate(), Decimal("0"))

        # Increase by .2
        self.entry.update_vote(Decimal(".2"))
        self.entry.refresh_from_db()
        self.assertEqual(self.entry.vote_rate, Decimal(".2"))

        # Increase by .2 again to ensure that it is incremental (not a replacement)
        self.entry.update_vote(Decimal(".2"))
        self.entry.refresh_from_db()
        self.assertEqual(self.entry.vote_rate, Decimal(".4"))

        # Increase by -.2 (decrease .2)
        self.entry.update_vote(Decimal("-.2"))
        self.entry.refresh_from_db()
        self.assertEqual(self.entry.vote_rate, Decimal(".2"))

        # Increase by change
        self.entry.update_vote(Decimal(".2"), change=True)
        self.entry.refresh_from_db()
        self.assertEqual(self.entry.vote_rate, Decimal(".6"))


class MementoModelTests(TransactionTestCase):
    @classmethod
    def setUp(cls):
        cls.author_1 = Author.objects.create(username="user1", email="1")
        cls.author_2 = Author.objects.create(username="user2", email="2")

    def test_unique_constraint(self):
        Memento.objects.create(holder=self.author_1, patient=self.author_2)

        # Make sure that the fields are evaluated differently
        Memento.objects.create(holder=self.author_2, patient=self.author_1)

        # Creating a non-unique object
        with self.assertRaises(IntegrityError):
            Memento.objects.create(holder=self.author_1, patient=self.author_2)


class UserVerificationModelTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.author = Author.objects.create(username="user", email="0")

    def test_no_multiple_verifications(self):
        UserVerification.objects.create(author=self.author, expiration_date=timezone.now())
        latest_uv = UserVerification.objects.create(author=self.author, expiration_date=timezone.now())
        list_uv_for_author = UserVerification.objects.filter(author=self.author)
        self.assertEqual(list_uv_for_author.count(), 1)
        self.assertEqual(latest_uv, list_uv_for_author.first())

    def test_email_confirmed_status(self):
        # For profile page email change status indicator
        # Create pending email confirmation
        uv_author = UserVerification.objects.create(
            author=self.author, expiration_date=timezone.now() + datetime.timedelta(hours=12)
        )
        self.assertEqual(self.author.email_confirmed, False)

        # No pending email confirmation
        uv_author.delete()
        self.assertEqual(self.author.email_confirmed, True)

        # There is a email confirmation sent, but it has been expired
        UserVerification.objects.create(
            author=self.author, expiration_date=timezone.now() - datetime.timedelta(hours=30)
        )
        self.assertEqual(self.author.email_confirmed, True)


class MessageModelTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.generic_superuser = Author.objects.create(
            username=settings.GENERIC_SUPERUSER_USERNAME, email="gsu", is_active=True
        )
        cls.author_1 = Author.objects.create(username="user1", email="1", is_active=True)
        cls.author_2 = Author.objects.create(username="user2", email="2", is_active=True)

    def test_read_at_time(self):
        some_message = Message.objects.compose(self.author_1, self.author_2, "body")
        self.assertIsNone(some_message.read_at)
        some_message.mark_read()
        self.assertIsNotNone(some_message.read_at)


class ConversationModelTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.generic_superuser = Author.objects.create(
            username=settings.GENERIC_SUPERUSER_USERNAME, email="gsu", is_active=True
        )
        cls.author_1 = Author.objects.create(username="user1", email="1", is_active=True)
        cls.author_2 = Author.objects.create(username="user2", email="2", is_active=True)

    def test_conversation_creation_on_messaging(self):
        # Check initial status
        conversation_count = Conversation.objects.all().count()
        self.assertEqual(conversation_count, 0)

        # A conversation started
        some_msg = Message.objects.compose(self.author_1, self.author_2, "gelmiyorsun artık günah çıkarmaya?")

        conversation_count = Conversation.objects.all().count()
        self.assertEqual(conversation_count, 2)

        # Get that conversation and check if the previous message is in it
        current_conversation_1_2 = Conversation.objects.get(holder=self.author_1, target=self.author_2)
        current_conversation_2_1 = Conversation.objects.get(holder=self.author_2, target=self.author_1)
        self.assertIn(some_msg, current_conversation_1_2.messages.all())
        self.assertIn(some_msg, current_conversation_2_1.messages.all())

        # Reply message, check also if that message in conversation and check no extra conversation is created
        # for it (it should append to newly created conversation)

        some_other_msg = Message.objects.compose(self.author_2, self.author_1, "işlemiyorum ki, evdeyim hep.")
        conversation_count = Conversation.objects.all().count()
        self.assertEqual(conversation_count, 2)
        self.assertIn(some_other_msg, current_conversation_1_2.messages.all())
        self.assertIn(some_other_msg, current_conversation_2_1.messages.all())

        # Check participants
        self.assertEqual(self.author_1, current_conversation_1_2.holder)
        self.assertEqual(self.author_1, current_conversation_2_1.target)
        self.assertEqual(self.author_2, current_conversation_2_1.holder)
        self.assertEqual(self.author_2, current_conversation_1_2.target)

    def test_last_message(self):
        some_msg = Message.objects.compose(self.author_1, self.author_2, "baapoçun çen?!!")
        current_conversation_1_2 = Conversation.objects.get(holder=self.author_1, target=self.author_2)
        current_conversation_2_1 = Conversation.objects.get(holder=self.author_2, target=self.author_1)
        self.assertEqual(some_msg, current_conversation_1_2.last_message)
        self.assertEqual(some_msg, current_conversation_2_1.last_message)

        time.sleep(0.01)  # apparently auto_now_add fields will be exactly the same in the same block.
        some_other_msg = Message.objects.compose(self.author_1, self.author_2, "ya bi sktr git allah allah")
        self.assertEqual(some_other_msg, current_conversation_1_2.last_message)
        self.assertEqual(some_other_msg, current_conversation_2_1.last_message)


class TopicModelTest(TransactionTestCase):
    @classmethod
    def setUp(cls):
        cls.some_topic = Topic.objects.create_topic("zeki müren")
        cls.author = Author.objects.create(username="user", email="0", is_novice=False)

    def test_uniqueness(self):
        with self.assertRaises(IntegrityError):
            Topic.objects.create_topic("zeki müren")

        similar_topic = Topic.objects.create_topic("zeki muren")
        self.assertNotEqual(similar_topic.slug, self.some_topic.slug)

    def test_existence(self):
        self.assertEqual(self.some_topic.exists, True)

    def test_valid(self):
        self.assertEqual(self.some_topic.valid, True)

    def test_has_entries(self):
        # Initial status
        self.assertEqual(self.some_topic.has_entries, False)

        # Add non-published entry
        Entry.objects.create(topic=self.some_topic, author=self.author, is_draft=True)
        self.assertEqual(self.some_topic.has_entries, False)

        # Add published entry
        Entry.objects.create(topic=self.some_topic, author=self.author)
        self.assertEqual(self.some_topic.has_entries, True)

    def test_follow_check(self):
        # Initial status
        self.assertEqual(self.some_topic.follow_check(self.author), False)

        # Follow
        TopicFollowing.objects.create(topic=self.some_topic, author=self.author)
        self.assertEqual(self.some_topic.follow_check(self.author), True)

    @override_settings(LANGUAGE_CODE="tr")
    def test_title_lower(self):
        weird_topic = Topic.objects.create_topic("WINTER")
        self.assertEqual("wınter", weird_topic.title)

    def test_title_lower_case_en(self):
        weird_topic = Topic.objects.create_topic("WINTER")
        self.assertEqual("winter", weird_topic.title)

    def test_absolute_url(self):
        absolute_url = reverse("topic", kwargs={"slug": self.some_topic.slug})
        self.assertEqual(absolute_url, self.some_topic.get_absolute_url())
