import datetime
import hashlib

from django.template import Context, Template
from django.test import TestCase, override_settings
from django.urls import reverse
from django.utils import timezone
from djconfig.utils import override_djconfig

from ...category.models import Category
from ...comment.bookmark.models import CommentBookmark
from ...core.conf import settings
from ...core.tests import utils
from .. import utils as utils_topic
from ..models import Topic
from ..notification.models import TopicNotification
from . import views as private_views
from .forms import (
    TopicForPrivateForm,
    TopicPrivateInviteForm,
    TopicPrivateJoinForm,
    TopicPrivateManyForm,
)
from .models import TopicPrivate
from .tags import render_invite_form
from .utils import notify_access


class TopicPrivateViewTest(TestCase):
    def setUp(self):
        utils.cache_clear()
        self.user = utils.create_user()
        self.user2 = utils.create_user()

    @override_settings(ST_TESTS_RATELIMIT_NEVER_EXPIRE=True)
    def test_private_publish(self):
        """
        POST, create private topic
        """
        self.assertEqual(len(Topic.objects.all()), 0)

        utils.login(self)
        form_data = {"comment": "foo", "title": "foobar", "users": self.user2.username}
        response = self.client.post(reverse("spirit:topic:private:publish"), form_data)
        private = TopicPrivate.objects.last()
        expected_url = private.get_absolute_url()
        self.assertRedirects(response, expected_url, status_code=302)
        self.assertEqual(len(Topic.objects.all()), 1)

        # ratelimit
        form_data["title"] = "new foobar"
        form_data["comment"] = "new foo"
        response = self.client.post(reverse("spirit:topic:private:publish"), form_data)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(Topic.objects.all()), 1)

        response = self.client.get(reverse("spirit:topic:private:publish"))
        self.assertEqual(response.status_code, 200)

    @override_settings(ST_TESTS_RATELIMIT_NEVER_EXPIRE=True)
    def test_private_publishvalidate(self):
        """
        Should validate all forms even when errors
        """
        self.assertEqual(len(Topic.objects.all()), 0)

        utils.login(self)
        no_data = {}
        response = self.client.post(reverse("spirit:topic:private:publish"), no_data)
        self.assertEqual(len(Topic.objects.all()), 0)
        self.assertTrue(bool(response.context["tform"].errors))
        self.assertTrue(bool(response.context["cform"].errors))
        self.assertTrue(bool(response.context["tpform"].errors))
        self.assertEqual(len(list(response.context["messages"])), 0)

        # No rate-limit
        form_data = {"comment": "foo", "title": "foobar", "users": self.user2.username}
        self.client.post(reverse("spirit:topic:private:publish"), form_data)
        self.assertEqual(len(Topic.objects.all()), 1)

    def test_private_publish_create_notifications(self):
        """
        Should create notifications for invited members
        """
        utils.login(self)
        user = utils.create_user()
        form_data = {"comment": "foo", "title": "foobar", "users": [user.username]}
        response = self.client.post(reverse("spirit:topic:private:publish"), form_data)
        self.assertEqual(response.status_code, 302)

        topic_private = TopicPrivate.objects.last()
        self.assertEqual(
            len(TopicNotification.objects.filter(topic=topic_private.topic)), 2
        )
        self.assertEqual(
            len(TopicNotification.objects.filter(topic=topic_private.topic, user=user)),
            1,
        )
        self.assertEqual(
            len(
                TopicNotification.objects.filter(
                    topic=topic_private.topic, user=self.user
                )
            ),
            1,
        )

    def test_private_publish_user(self):
        """
        create private topic with user as initial value
        """
        utils.login(self)
        response = self.client.get(
            reverse("spirit:topic:private:publish", kwargs={"user_id": self.user2.pk})
        )
        self.assertEqual(
            response.context["tpform"].initial["users"], [self.user2.username]
        )

    @override_settings(ST_DOUBLE_POST_THRESHOLD_MINUTES=10)
    def test_private_publish_double_post(self):
        """
        Should prevent double posts
        """
        utils.login(self)
        category_private = Category.objects.get(
            pk=settings.ST_TOPIC_PRIVATE_CATEGORY_PK
        )
        topic_title = "title foobar"

        # First post
        self.client.post(
            reverse("spirit:topic:private:publish"),
            {"comment": "foo", "title": topic_title, "users": [self.user2.username]},
        )
        self.assertEqual(len(Topic.objects.all()), 1)

        # Double post
        utils.cache_clear()  # Clear rate limit
        response = self.client.post(
            reverse("spirit:topic:private:publish"),
            {
                "comment": "new foo",
                "title": topic_title,
                "users": [self.user2.username],
            },
        )
        self.assertEqual(len(Topic.objects.all()), 1)  # Prevented!

        self.assertRedirects(
            response,
            expected_url=category_private.get_absolute_url(),
            status_code=302,
            target_status_code=200,
        )

        # New post
        utils.cache_clear()  # Clear rate limit
        self.client.post(
            reverse("spirit:topic:private:publish"),
            {"comment": "foo", "title": "new topic", "users": [self.user2.username]},
        )
        self.assertEqual(len(Topic.objects.all()), 2)

    def test_private_detail(self):
        """
        private topic detail
        """
        utils.login(self)
        private = utils.create_private_topic(user=self.user)

        comment1 = utils.create_comment(topic=private.topic)
        comment2 = utils.create_comment(topic=private.topic)

        category = utils.create_category()
        topic2 = utils.create_topic(category=category)
        utils.create_comment(topic=topic2)

        response = self.client.get(
            reverse(
                "spirit:topic:private:detail",
                kwargs={"topic_id": private.topic.pk, "slug": private.topic.slug},
            )
        )
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.context["topic"], private.topic)
        self.assertEqual(list(response.context["comments"]), [comment1, comment2])

    @override_djconfig(comments_per_page=2)
    def test_private_detail_view_paginate(self):
        """
        should display topic with comments, page 1
        """
        utils.login(self)
        private = utils.create_private_topic(user=self.user)

        comment1 = utils.create_comment(topic=private.topic)
        comment2 = utils.create_comment(topic=private.topic)
        utils.create_comment(topic=private.topic)  # comment3

        response = self.client.get(
            reverse(
                "spirit:topic:private:detail",
                kwargs={"topic_id": private.topic.pk, "slug": private.topic.slug},
            )
        )
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.context["topic"], private.topic)
        self.assertEqual(list(response.context["comments"]), [comment1, comment2])

    def test_topic_private_detail_viewed(self):
        """
        Calls utils.topic_viewed
        """

        def mocked_topic_viewed(request, topic):
            self._user = request.user
            self._topic = topic

        org_viewed, utils_topic.topic_viewed = (
            utils_topic.topic_viewed,
            mocked_topic_viewed,
        )
        try:
            utils.login(self)
            category = utils.create_category()
            topic = utils.create_topic(category=category, user=self.user)
            response = self.client.get(
                reverse(
                    "spirit:topic:detail", kwargs={"pk": topic.pk, "slug": topic.slug}
                )
            )
            self.assertEqual(response.status_code, 200)
            self.assertEqual(self._topic, topic)
            self.assertEqual(self._user, self.user)
        finally:
            utils_topic.topic_viewed = org_viewed

    def test_private_access_create(self):
        """
        private topic access creation
        """
        utils.login(self)
        private = utils.create_private_topic(user=self.user)
        utils.create_comment(topic=private.topic)
        form_data = {"user": self.user2.username}
        response = self.client.post(
            reverse(
                "spirit:topic:private:access-create",
                kwargs={"topic_id": private.topic.pk},
            ),
            form_data,
        )
        expected_url = private.get_absolute_url()
        self.assertRedirects(response, expected_url, status_code=302)
        self.assertEqual(
            len(TopicPrivate.objects.filter(user=self.user2, topic=private.topic)), 1
        )

    def test_private_access_create_invalid(self):
        """
        Only the topic owner should be able to invite
        """
        utils.login(self)
        private = utils.create_private_topic(user=self.user2)
        TopicPrivate.objects.create(user=self.user, topic=private.topic)
        user = utils.create_user()
        form_data = {"user": user.username}
        response = self.client.post(
            reverse(
                "spirit:topic:private:access-create",
                kwargs={"topic_id": private.topic.pk},
            ),
            form_data,
        )
        self.assertEqual(response.status_code, 404)

    def test_private_access_create_notify_access(self):
        """
        notify_access
        """

        def mocked_notify_access(user, topic_private):
            self._user = user
            self._topic_private = topic_private

        org_access, private_views.notify_access = (
            private_views.notify_access,
            mocked_notify_access,
        )
        try:
            utils.login(self)
            private = utils.create_private_topic(user=self.user)
            form_data = {"user": self.user2.username}
            response = self.client.post(
                reverse(
                    "spirit:topic:private:access-create",
                    kwargs={"topic_id": private.topic.pk},
                ),
                form_data,
            )
            self.assertEqual(response.status_code, 302)
            self.assertEqual(self._topic_private, TopicPrivate.objects.last())
            self.assertEqual(self._user, self.user2)
        finally:
            private_views.notify_access = org_access

    def test_private_access_delete(self):
        """
        private topic access deletion
        """
        utils.login(self)
        private = utils.create_private_topic(user=self.user)
        private2 = TopicPrivate.objects.create(user=self.user2, topic=private.topic)
        form_data = {}
        response = self.client.post(
            reverse("spirit:topic:private:access-remove", kwargs={"pk": private2.pk}),
            form_data,
        )
        expected_url = private.get_absolute_url()
        self.assertRedirects(response, expected_url, status_code=302)

    def test_private_access_delete_invalid(self):
        """
        Only the topic owner should be able to remove accesses
        """
        utils.login(self)
        private = utils.create_private_topic(user=self.user2)
        TopicPrivate.objects.create(user=self.user, topic=private.topic)
        form_data = {}
        response = self.client.post(
            reverse("spirit:topic:private:access-remove", kwargs={"pk": private.pk}),
            form_data,
        )
        self.assertEqual(response.status_code, 404)

    def test_private_access_delete_leave(self):
        """
        user should be able to remove himself
        """
        utils.login(self)
        private = utils.create_private_topic(user=self.user2)
        private2_leave = TopicPrivate.objects.create(
            user=self.user, topic=private.topic
        )
        form_data = {}
        response = self.client.post(
            reverse(
                "spirit:topic:private:access-remove", kwargs={"pk": private2_leave.pk}
            ),
            form_data,
        )
        expected_url = reverse("spirit:topic:private:index")
        self.assertRedirects(response, expected_url, status_code=302)

    def test_private_list(self):
        """
        private topic list
        """
        private = utils.create_private_topic(user=self.user)
        # dont show private topics from other users
        TopicPrivate.objects.create(user=self.user2, topic=private.topic)
        # dont show topics from other categories
        category = utils.create_category()
        utils.create_topic(category, user=self.user)

        utils.login(self)
        response = self.client.get(reverse("spirit:topic:private:index"))
        self.assertEqual(list(response.context["topics"]), [private.topic])

    def test_private_list_order_topics(self):
        """
        private topic list ordered by last active
        """
        private_a = utils.create_private_topic(user=self.user)
        private_b = utils.create_private_topic(user=self.user)
        private_c = utils.create_private_topic(user=self.user)

        Topic.objects.filter(pk=private_a.topic.pk).update(
            last_active=timezone.now() - datetime.timedelta(days=10)
        )
        Topic.objects.filter(pk=private_c.topic.pk).update(
            last_active=timezone.now() - datetime.timedelta(days=5)
        )

        utils.login(self)
        response = self.client.get(reverse("spirit:topic:private:index"))
        self.assertEqual(
            list(response.context["topics"]),
            [private_b.topic, private_c.topic, private_a.topic],
        )

    def test_private_list_bookmarks(self):
        """
        private topic list with bookmarks
        """
        private = utils.create_private_topic(user=self.user)
        bookmark = CommentBookmark.objects.create(topic=private.topic, user=self.user)

        utils.login(self)
        response = self.client.get(reverse("spirit:topic:private:index"))
        self.assertEqual(list(response.context["topics"]), [private.topic])
        self.assertEqual(response.context["topics"][0].bookmark, bookmark)

    @override_djconfig(topics_per_page=1)
    def test_private_list_paginated(self):
        """
        private topic list paginated
        """
        utils.create_private_topic(user=self.user)
        private = utils.create_private_topic(user=self.user)

        utils.login(self)
        response = self.client.get(reverse("spirit:topic:private:index"))
        self.assertEqual(list(response.context["topics"]), [private.topic])

    def test_private_join(self):
        """
        private topic join
        """
        private = utils.create_private_topic(user=self.user)
        utils.create_comment(topic=private.topic)
        private.delete()

        utils.login(self)
        form_data = {}
        response = self.client.post(
            reverse("spirit:topic:private:join", kwargs={"topic_id": private.topic.pk}),
            form_data,
        )
        expected_url = private.get_absolute_url()
        self.assertRedirects(response, expected_url, status_code=302)

        response = self.client.get(
            reverse("spirit:topic:private:join", kwargs={"topic_id": private.topic.pk})
        )
        self.assertEqual(response.status_code, 200)

    def test_private_join_invalid_regular_topic(self):
        """
        Only topics from the category private can be rejoined
        """
        category = utils.create_category()
        topic = utils.create_topic(category, user=self.user)

        utils.login(self)
        form_data = {}
        response = self.client.post(
            reverse("spirit:topic:private:join", kwargs={"topic_id": topic.pk}),
            form_data,
        )
        self.assertEqual(response.status_code, 404)

    def test_private_join_invalid_not_owner(self):
        """
        Only topic creators/owners can rejoin
        """
        private = utils.create_private_topic(user=self.user2)

        utils.login(self)
        form_data = {}
        response = self.client.post(
            reverse("spirit:topic:private:join", kwargs={"topic_id": private.topic.pk}),
            form_data,
        )
        self.assertEqual(response.status_code, 404)

    def test_private_join_access_notify_access(self):
        """
        notify_access
        """

        def mocked_notify_access(user, topic_private):
            self._user = user
            self._topic_private = topic_private

        org_access, private_views.notify_access = (
            private_views.notify_access,
            mocked_notify_access,
        )
        try:
            private = utils.create_private_topic(user=self.user)
            private.delete()

            utils.login(self)
            form_data = {}
            response = self.client.post(
                reverse(
                    "spirit:topic:private:join", kwargs={"topic_id": private.topic.pk}
                ),
                form_data,
            )
            self.assertEqual(response.status_code, 302)
            self.assertEqual(self._topic_private, TopicPrivate.objects.last())
            self.assertEqual(self._user, self.user)
        finally:
            private_views.notify_access = org_access

    def test_private_created_list(self):
        """
        private topic created list, shows only the private topics the user is no longer participating
        """
        category = utils.create_category()
        utils.create_topic(category, user=self.user)
        # it's the owner, left the topic
        private = utils.create_private_topic(user=self.user)
        private.delete()
        # has access and is the owner
        utils.create_private_topic(user=self.user)
        # does not has access
        utils.create_private_topic(user=self.user2)
        # has access but it's not owner
        private4 = utils.create_private_topic(user=self.user2)
        TopicPrivate.objects.create(user=self.user, topic=private4.topic)

        utils.login(self)
        response = self.client.get(reverse("spirit:topic:private:index-author"))
        self.assertEqual(list(response.context["topics"]), [private.topic])

    def test_private_created_list_order_topics(self):
        """
        private topic created list ordered by last active
        """
        private_a = utils.create_private_topic(user=self.user)
        private_b = utils.create_private_topic(user=self.user)
        private_c = utils.create_private_topic(user=self.user)
        private_a.delete()
        private_b.delete()
        private_c.delete()

        Topic.objects.filter(pk=private_a.topic.pk).update(
            last_active=timezone.now() - datetime.timedelta(days=10)
        )
        Topic.objects.filter(pk=private_c.topic.pk).update(
            last_active=timezone.now() - datetime.timedelta(days=5)
        )

        utils.login(self)
        response = self.client.get(reverse("spirit:topic:private:index-author"))
        self.assertEqual(
            list(response.context["topics"]),
            [private_b.topic, private_c.topic, private_a.topic],
        )

    @override_djconfig(topics_per_page=1)
    def test_private_created_list_paginate(self):
        """
        private topic created list paginated
        """
        private = utils.create_private_topic(user=self.user)
        private.delete()
        private2 = utils.create_private_topic(user=self.user)
        private2.delete()

        utils.login(self)
        response = self.client.get(reverse("spirit:topic:private:index-author"))
        self.assertEqual(list(response.context["topics"]), [private2.topic])


class TopicPrivateFormTest(TestCase):
    def setUp(self):
        utils.cache_clear()
        self.user = utils.create_user()
        self.user2 = utils.create_user()

    def test_private_publish(self):
        """
        create simple topic
        """
        form_data = {"title": "foo"}
        form = TopicForPrivateForm(data=form_data)
        self.assertEqual(form.is_valid(), True)

    def test_private_publish_category(self):
        """
        Should return the private category
        """
        category_private = Category.objects.get(
            pk=settings.ST_TOPIC_PRIVATE_CATEGORY_PK
        )
        form = TopicForPrivateForm()
        self.assertEqual(form.category, category_private)
        self.assertEqual(form.category, category_private)  # Cached

    def test_private_publish_get_topic_hash(self):
        """
        Should return the topic hash
        """
        category_private = Category.objects.get(
            pk=settings.ST_TOPIC_PRIVATE_CATEGORY_PK
        )
        title = "title foobar"
        form = TopicForPrivateForm(data={"title": title})
        self.assertTrue(form.is_valid())
        self.assertEqual(
            form.get_topic_hash(),
            hashlib.md5(
                "{}category-{}".format(title, category_private.pk).encode("utf-8")
            ).hexdigest(),
        )

    def test_private_publish_get_topic_hash_from_field(self):
        """
        Should return the topic hash from form field
        """
        topic_hash = "1" * 32
        form = TopicForPrivateForm(data={"title": "foobar", "topic_hash": topic_hash})
        self.assertTrue(form.is_valid())
        self.assertEqual(form.get_topic_hash(), topic_hash)

    def test_private_create_many(self):
        """
        create many private topics accesses
        """
        users = f"{self.user.username}, {self.user2.username}"
        form_data = {"users": users}
        form = TopicPrivateManyForm(self.user, data=form_data)
        self.assertEqual(form.is_valid(), True)

        category = Category.objects.get(pk=settings.ST_TOPIC_PRIVATE_CATEGORY_PK)
        topic = utils.create_topic(category=category, user=self.user)
        form.topic = topic
        privates_saved = form.save_m2m()
        self.assertEqual(len(privates_saved), 2)
        privates = TopicPrivate.objects.all()
        self.assertEqual([p.user.pk for p in privates], [self.user2.pk, self.user.pk])

    @override_settings(ST_CASE_INSENSITIVE_USERNAMES=True)
    def test_private_invite_case_insensitive(self):
        user1 = utils.create_user(username="UnIQuEfOo")
        self.assertNotEqual(user1.username, user1.username.upper())
        form_data = {"user": user1.username.upper()}
        private = utils.create_private_topic(user=self.user)
        form = TopicPrivateInviteForm(private.topic, data=form_data)
        self.assertEqual(form.is_valid(), True)

        # regular username should still work
        form_data = {"user": user1.username}
        private = utils.create_private_topic(user=self.user)
        form = TopicPrivateInviteForm(private.topic, data=form_data)
        self.assertEqual(form.is_valid(), True)

    @override_settings(ST_CASE_INSENSITIVE_USERNAMES=False)
    def test_private_invite_case_insensitive_off(self):
        user1 = utils.create_user(username="UnIQuEfOo")
        self.assertNotEqual(user1.username, user1.username.upper())
        form_data = {"user": user1.username.upper()}
        private = utils.create_private_topic(user=self.user)
        form = TopicPrivateInviteForm(private.topic, data=form_data)
        self.assertEqual(form.is_valid(), False)

        # regular username should still work
        form_data = {"user": user1.username}
        private = utils.create_private_topic(user=self.user)
        form = TopicPrivateInviteForm(private.topic, data=form_data)
        self.assertEqual(form.is_valid(), True)

    @override_settings(ST_CASE_INSENSITIVE_USERNAMES=True)
    def test_private_create_many_case_insensitive(self):
        """
        create many private topics accesses
        """
        user1 = utils.create_user(username="UnIQuEfOo")
        user2 = utils.create_user(username="uniquebar")
        self.assertNotEqual(user1.username, user1.username.upper())
        self.assertEqual(user2.username, "uniquebar")
        users = "{}, {}".format(user1.username.upper(), "uniquebar")
        form_data = {"users": users}
        form = TopicPrivateManyForm(self.user, data=form_data)
        self.assertEqual(form.is_valid(), True)

    @override_settings(ST_CASE_INSENSITIVE_USERNAMES=False)
    def test_private_create_many_case_insensitive_off(self):
        """
        create many private topics accesses
        """
        user1 = utils.create_user(username="UnIQuEfOo")
        user2 = utils.create_user(username="uniquebar")
        self.assertNotEqual(user1.username, user1.username.upper())
        self.assertEqual(user2.username, "uniquebar")
        users = "{}, {}".format(user1.username.upper(), "uniquebar")
        form_data = {"users": users}
        form = TopicPrivateManyForm(self.user, data=form_data)
        self.assertEqual(form.is_valid(), False)

        users = "UnIQuEfOo, uniquebar"
        form_data = {"users": users}
        form = TopicPrivateManyForm(self.user, data=form_data)
        self.assertEqual(form.is_valid(), True)

    def test_private_create(self):
        """
        create single private topic access
        """
        category = Category.objects.get(pk=settings.ST_TOPIC_PRIVATE_CATEGORY_PK)
        topic = utils.create_topic(category=category, user=self.user)
        form_data = {"user": self.user.username}
        form = TopicPrivateInviteForm(data=form_data)
        form.topic = topic
        self.assertEqual(form.is_valid(), True)

    def test_private_join(self):
        """
        re-join private topic if user is the creator
        """
        private = utils.create_private_topic(user=self.user)
        private.delete()

        form_data = {}
        form = TopicPrivateJoinForm(user=self.user, topic=private.topic, data=form_data)
        self.assertTrue(form.is_valid())
        private_topic = form.save()
        self.assertEqual(private_topic.topic, private.topic)
        self.assertEqual(private_topic.user, private.user)

        # topic private exists
        private = utils.create_private_topic(user=self.user)
        form = TopicPrivateJoinForm(user=self.user, topic=private.topic, data=form_data)
        self.assertFalse(form.is_valid())


class TopicTemplateTagsTest(TestCase):
    def setUp(self):
        utils.cache_clear()
        self.user = utils.create_user()
        self.category = Category.objects.get(pk=settings.ST_TOPIC_PRIVATE_CATEGORY_PK)
        self.topic = utils.create_topic(category=self.category, user=self.user)

    def test_render_invite_form(self):
        """
        display invite form
        """
        out = Template("{% load spirit_tags %}{% render_invite_form topic %}").render(
            Context({"topic": self.topic})
        )
        self.assertNotEqual(out, "")
        context = render_invite_form(self.topic)
        self.assertEqual(context["next"], None)
        self.assertIsInstance(context["form"], TopicPrivateInviteForm)
        self.assertEqual(context["topic"], self.topic)


class TopicPrivateUtilsTest(TestCase):
    def setUp(self):
        utils.cache_clear()
        self.user = utils.create_user()
        self.user2 = utils.create_user()
        self.category = utils.create_category()
        self.topic = utils.create_topic(self.category)
        self.comment = utils.create_comment(topic=self.topic)

    def test_topic_private_notify_access(self):
        """
        Should create a notification
        """
        private = utils.create_private_topic()
        comment = utils.create_comment(topic=private.topic)
        notify_access(user=private.user, topic_private=private)
        notification = TopicNotification.objects.get(
            user=private.user, topic=private.topic
        )
        self.assertTrue(notification.is_active)
        self.assertFalse(notification.is_read)
        self.assertEqual(notification.comment, comment)
