"""
Tests for Discussion API serializers
"""
import itertools
from urlparse import urlparse

import ddt
import httpretty
import mock

from django.test.client import RequestFactory

from discussion_api.serializers import CommentSerializer, ThreadSerializer, get_context
from discussion_api.tests.utils import (
    CommentsServiceMockMixin,
    make_minimal_cs_thread,
    make_minimal_cs_comment,
)
from django_comment_common.models import (
    FORUM_ROLE_ADMINISTRATOR,
    FORUM_ROLE_COMMUNITY_TA,
    FORUM_ROLE_MODERATOR,
    FORUM_ROLE_STUDENT,
    Role,
)
from lms.lib.comment_client.comment import Comment
from lms.lib.comment_client.thread import Thread
from student.tests.factories import UserFactory
from util.testing import UrlResetMixin
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory


@ddt.ddt
class SerializerTestMixin(CommentsServiceMockMixin, UrlResetMixin):
    @mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
    def setUp(self):
        super(SerializerTestMixin, self).setUp()
        httpretty.reset()
        httpretty.enable()
        self.addCleanup(httpretty.disable)
        self.maxDiff = None  # pylint: disable=invalid-name
        self.user = UserFactory.create()
        self.register_get_user_response(self.user)
        self.request = RequestFactory().get("/dummy")
        self.request.user = self.user
        self.course = CourseFactory.create()
        self.author = UserFactory.create()

    def create_role(self, role_name, users, course=None):
        """Create a Role in self.course with the given name and users"""
        course = course or self.course
        role = Role.objects.create(name=role_name, course_id=course.id)
        role.users = users

    @ddt.data(
        (FORUM_ROLE_ADMINISTRATOR, True, False, True),
        (FORUM_ROLE_ADMINISTRATOR, False, True, False),
        (FORUM_ROLE_MODERATOR, True, False, True),
        (FORUM_ROLE_MODERATOR, False, True, False),
        (FORUM_ROLE_COMMUNITY_TA, True, False, True),
        (FORUM_ROLE_COMMUNITY_TA, False, True, False),
        (FORUM_ROLE_STUDENT, True, False, True),
        (FORUM_ROLE_STUDENT, False, True, True),
    )
    @ddt.unpack
    def test_anonymity(self, role_name, anonymous, anonymous_to_peers, expected_serialized_anonymous):
        """
        Test that content is properly made anonymous.

        Content should be anonymous iff the anonymous field is true or the
        anonymous_to_peers field is true and the requester does not have a
        privileged role.

        role_name is the name of the requester's role.
        anonymous is the value of the anonymous field in the content.
        anonymous_to_peers is the value of the anonymous_to_peers field in the
          content.
        expected_serialized_anonymous is whether the content should actually be
          anonymous in the API output when requested by a user with the given
          role.
        """
        self.create_role(role_name, [self.user])
        serialized = self.serialize(
            self.make_cs_content({"anonymous": anonymous, "anonymous_to_peers": anonymous_to_peers})
        )
        actual_serialized_anonymous = serialized["author"] is None
        self.assertEqual(actual_serialized_anonymous, expected_serialized_anonymous)

    @ddt.data(
        (FORUM_ROLE_ADMINISTRATOR, False, "staff"),
        (FORUM_ROLE_ADMINISTRATOR, True, None),
        (FORUM_ROLE_MODERATOR, False, "staff"),
        (FORUM_ROLE_MODERATOR, True, None),
        (FORUM_ROLE_COMMUNITY_TA, False, "community_ta"),
        (FORUM_ROLE_COMMUNITY_TA, True, None),
        (FORUM_ROLE_STUDENT, False, None),
        (FORUM_ROLE_STUDENT, True, None),
    )
    @ddt.unpack
    def test_author_labels(self, role_name, anonymous, expected_label):
        """
        Test correctness of the author_label field.

        The label should be "staff", "staff", or "community_ta" for the
        Administrator, Moderator, and Community TA roles, respectively, but
        the label should not be present if the content is anonymous.

        role_name is the name of the author's role.
        anonymous is the value of the anonymous field in the content.
        expected_label is the expected value of the author_label field in the
          API output.
        """
        self.create_role(role_name, [self.author])
        serialized = self.serialize(self.make_cs_content({"anonymous": anonymous}))
        self.assertEqual(serialized["author_label"], expected_label)

    def test_abuse_flagged(self):
        serialized = self.serialize(self.make_cs_content({"abuse_flaggers": [str(self.user.id)]}))
        self.assertEqual(serialized["abuse_flagged"], True)

    def test_voted(self):
        thread_id = "test_thread"
        self.register_get_user_response(self.user, upvoted_ids=[thread_id])
        serialized = self.serialize(self.make_cs_content({"id": thread_id}))
        self.assertEqual(serialized["voted"], True)


@ddt.ddt
class ThreadSerializerSerializationTest(SerializerTestMixin, ModuleStoreTestCase):
    """Tests for ThreadSerializer serialization."""
    def make_cs_content(self, overrides):
        """
        Create a thread with the given overrides, plus some useful test data.
        """
        merged_overrides = {
            "course_id": unicode(self.course.id),
            "user_id": str(self.author.id),
            "username": self.author.username,
        }
        merged_overrides.update(overrides)
        return make_minimal_cs_thread(merged_overrides)

    def serialize(self, thread):
        """
        Create a serializer with an appropriate context and use it to serialize
        the given thread, returning the result.
        """
        return ThreadSerializer(thread, context=get_context(self.course, self.request)).data

    def test_basic(self):
        thread = {
            "type": "thread",
            "id": "test_thread",
            "course_id": unicode(self.course.id),
            "commentable_id": "test_topic",
            "group_id": None,
            "user_id": str(self.author.id),
            "username": self.author.username,
            "anonymous": False,
            "anonymous_to_peers": False,
            "created_at": "2015-04-28T00:00:00Z",
            "updated_at": "2015-04-28T11:11:11Z",
            "thread_type": "discussion",
            "title": "Test Title",
            "body": "Test body",
            "pinned": True,
            "closed": False,
            "abuse_flaggers": [],
            "votes": {"up_count": 4},
            "comments_count": 5,
            "unread_comments_count": 3,
        }
        expected = {
            "id": "test_thread",
            "course_id": unicode(self.course.id),
            "topic_id": "test_topic",
            "group_id": None,
            "group_name": None,
            "author": self.author.username,
            "author_label": None,
            "created_at": "2015-04-28T00:00:00Z",
            "updated_at": "2015-04-28T11:11:11Z",
            "type": "discussion",
            "title": "Test Title",
            "raw_body": "Test body",
            "rendered_body": "<p>Test body</p>",
            "pinned": True,
            "closed": False,
            "following": False,
            "abuse_flagged": False,
            "voted": False,
            "vote_count": 4,
            "comment_count": 5,
            "unread_comment_count": 3,
            "comment_list_url": "http://testserver/api/discussion/v1/comments/?thread_id=test_thread",
            "endorsed_comment_list_url": None,
            "non_endorsed_comment_list_url": None,
            "editable_fields": ["abuse_flagged", "following", "voted"],
        }
        self.assertEqual(self.serialize(thread), expected)

        thread["thread_type"] = "question"
        expected.update({
            "type": "question",
            "comment_list_url": None,
            "endorsed_comment_list_url": (
                "http://testserver/api/discussion/v1/comments/?thread_id=test_thread&endorsed=True"
            ),
            "non_endorsed_comment_list_url": (
                "http://testserver/api/discussion/v1/comments/?thread_id=test_thread&endorsed=False"
            ),
        })
        self.assertEqual(self.serialize(thread), expected)

    def test_pinned_missing(self):
        """
        Make sure that older threads in the comments service without the pinned
        field do not break serialization
        """
        thread_data = self.make_cs_content({})
        del thread_data["pinned"]
        self.register_get_thread_response(thread_data)
        serialized = self.serialize(Thread(id=thread_data["id"]))
        self.assertEqual(serialized["pinned"], False)

    def test_group(self):
        cohort = CohortFactory.create(course_id=self.course.id)
        serialized = self.serialize(self.make_cs_content({"group_id": cohort.id}))
        self.assertEqual(serialized["group_id"], cohort.id)
        self.assertEqual(serialized["group_name"], cohort.name)

    def test_following(self):
        thread_id = "test_thread"
        self.register_get_user_response(self.user, subscribed_thread_ids=[thread_id])
        serialized = self.serialize(self.make_cs_content({"id": thread_id}))
        self.assertEqual(serialized["following"], True)


@ddt.ddt
class CommentSerializerTest(SerializerTestMixin, ModuleStoreTestCase):
    """Tests for CommentSerializer."""
    def setUp(self):
        super(CommentSerializerTest, self).setUp()
        self.endorser = UserFactory.create()
        self.endorsed_at = "2015-05-18T12:34:56Z"

    def make_cs_content(self, overrides=None, with_endorsement=False):
        """
        Create a comment with the given overrides, plus some useful test data.
        """
        merged_overrides = {
            "user_id": str(self.author.id),
            "username": self.author.username
        }
        if with_endorsement:
            merged_overrides["endorsement"] = {
                "user_id": str(self.endorser.id),
                "time": self.endorsed_at
            }
        merged_overrides.update(overrides or {})
        return make_minimal_cs_comment(merged_overrides)

    def serialize(self, comment, thread_data=None):
        """
        Create a serializer with an appropriate context and use it to serialize
        the given comment, returning the result.
        """
        context = get_context(self.course, self.request, make_minimal_cs_thread(thread_data))
        return CommentSerializer(comment, context=context).data

    def test_basic(self):
        comment = {
            "type": "comment",
            "id": "test_comment",
            "thread_id": "test_thread",
            "user_id": str(self.author.id),
            "username": self.author.username,
            "anonymous": False,
            "anonymous_to_peers": False,
            "created_at": "2015-04-28T00:00:00Z",
            "updated_at": "2015-04-28T11:11:11Z",
            "body": "Test body",
            "endorsed": False,
            "abuse_flaggers": [],
            "votes": {"up_count": 4},
            "children": [],
        }
        expected = {
            "id": "test_comment",
            "thread_id": "test_thread",
            "parent_id": None,
            "author": self.author.username,
            "author_label": None,
            "created_at": "2015-04-28T00:00:00Z",
            "updated_at": "2015-04-28T11:11:11Z",
            "raw_body": "Test body",
            "rendered_body": "<p>Test body</p>",
            "endorsed": False,
            "endorsed_by": None,
            "endorsed_by_label": None,
            "endorsed_at": None,
            "abuse_flagged": False,
            "voted": False,
            "vote_count": 4,
            "children": [],
            "editable_fields": ["abuse_flagged", "voted"],
        }
        self.assertEqual(self.serialize(comment), expected)

    @ddt.data(
        *itertools.product(
            [
                FORUM_ROLE_ADMINISTRATOR,
                FORUM_ROLE_MODERATOR,
                FORUM_ROLE_COMMUNITY_TA,
                FORUM_ROLE_STUDENT,
            ],
            [True, False]
        )
    )
    @ddt.unpack
    def test_endorsed_by(self, endorser_role_name, thread_anonymous):
        """
        Test correctness of the endorsed_by field.

        The endorser should be anonymous iff the thread is anonymous to the
        requester, and the endorser is not a privileged user.

        endorser_role_name is the name of the endorser's role.
        thread_anonymous is the value of the anonymous field in the thread.
        """
        self.create_role(endorser_role_name, [self.endorser])
        serialized = self.serialize(
            self.make_cs_content(with_endorsement=True),
            thread_data={"anonymous": thread_anonymous}
        )
        actual_endorser_anonymous = serialized["endorsed_by"] is None
        expected_endorser_anonymous = endorser_role_name == FORUM_ROLE_STUDENT and thread_anonymous
        self.assertEqual(actual_endorser_anonymous, expected_endorser_anonymous)

    @ddt.data(
        (FORUM_ROLE_ADMINISTRATOR, "staff"),
        (FORUM_ROLE_MODERATOR, "staff"),
        (FORUM_ROLE_COMMUNITY_TA, "community_ta"),
        (FORUM_ROLE_STUDENT, None),
    )
    @ddt.unpack
    def test_endorsed_by_labels(self, role_name, expected_label):
        """
        Test correctness of the endorsed_by_label field.

        The label should be "staff", "staff", or "community_ta" for the
        Administrator, Moderator, and Community TA roles, respectively.

        role_name is the name of the author's role.
        expected_label is the expected value of the author_label field in the
          API output.
        """
        self.create_role(role_name, [self.endorser])
        serialized = self.serialize(self.make_cs_content(with_endorsement=True))
        self.assertEqual(serialized["endorsed_by_label"], expected_label)

    def test_endorsed_at(self):
        serialized = self.serialize(self.make_cs_content(with_endorsement=True))
        self.assertEqual(serialized["endorsed_at"], self.endorsed_at)

    def test_children(self):
        comment = self.make_cs_content({
            "id": "test_root",
            "children": [
                self.make_cs_content({
                    "id": "test_child_1",
                    "parent_id": "test_root",
                }),
                self.make_cs_content({
                    "id": "test_child_2",
                    "parent_id": "test_root",
                    "children": [
                        self.make_cs_content({
                            "id": "test_grandchild",
                            "parent_id": "test_child_2"
                        })
                    ],
                }),
            ],
        })
        serialized = self.serialize(comment)
        self.assertEqual(serialized["children"][0]["id"], "test_child_1")
        self.assertEqual(serialized["children"][0]["parent_id"], "test_root")
        self.assertEqual(serialized["children"][1]["id"], "test_child_2")
        self.assertEqual(serialized["children"][1]["parent_id"], "test_root")
        self.assertEqual(serialized["children"][1]["children"][0]["id"], "test_grandchild")
        self.assertEqual(serialized["children"][1]["children"][0]["parent_id"], "test_child_2")


@ddt.ddt
class ThreadSerializerDeserializationTest(CommentsServiceMockMixin, UrlResetMixin, ModuleStoreTestCase):
    """Tests for ThreadSerializer deserialization."""
    @mock.patch.dict("django.conf.settings.FEATURES", {"ENABLE_DISCUSSION_SERVICE": True})
    def setUp(self):
        super(ThreadSerializerDeserializationTest, self).setUp()
        httpretty.reset()
        httpretty.enable()
        self.addCleanup(httpretty.disable)
        self.course = CourseFactory.create()
        self.user = UserFactory.create()
        self.register_get_user_response(self.user)
        self.request = RequestFactory().get("/dummy")
        self.request.user = self.user
        self.minimal_data = {
            "course_id": unicode(self.course.id),
            "topic_id": "test_topic",
            "type": "discussion",
            "title": "Test Title",
            "raw_body": "Test body",
        }
        self.existing_thread = Thread(**make_minimal_cs_thread({
            "id": "existing_thread",
            "course_id": unicode(self.course.id),
            "commentable_id": "original_topic",
            "thread_type": "discussion",
            "title": "Original Title",
            "body": "Original body",
            "user_id": str(self.user.id),
        }))

    def save_and_reserialize(self, data, instance=None):
        """
        Create a serializer with the given data and (if updating) instance,
        ensure that it is valid, save the result, and return the full thread
        data from the serializer.
        """
        serializer = ThreadSerializer(
            instance,
            data=data,
            partial=(instance is not None),
            context=get_context(self.course, self.request)
        )
        self.assertTrue(serializer.is_valid())
        serializer.save()
        return serializer.data

    def test_create_minimal(self):
        self.register_post_thread_response({"id": "test_id"})
        saved = self.save_and_reserialize(self.minimal_data)
        self.assertEqual(
            urlparse(httpretty.last_request().path).path,
            "/api/v1/test_topic/threads"
        )
        self.assertEqual(
            httpretty.last_request().parsed_body,
            {
                "course_id": [unicode(self.course.id)],
                "commentable_id": ["test_topic"],
                "thread_type": ["discussion"],
                "title": ["Test Title"],
                "body": ["Test body"],
                "user_id": [str(self.user.id)],
            }
        )
        self.assertEqual(saved["id"], "test_id")

    def test_create_all_fields(self):
        self.register_post_thread_response({"id": "test_id"})
        data = self.minimal_data.copy()
        data["group_id"] = 42
        self.save_and_reserialize(data)
        self.assertEqual(
            httpretty.last_request().parsed_body,
            {
                "course_id": [unicode(self.course.id)],
                "commentable_id": ["test_topic"],
                "thread_type": ["discussion"],
                "title": ["Test Title"],
                "body": ["Test body"],
                "user_id": [str(self.user.id)],
                "group_id": ["42"],
            }
        )

    def test_create_missing_field(self):
        for field in self.minimal_data:
            data = self.minimal_data.copy()
            data.pop(field)
            serializer = ThreadSerializer(data=data)
            self.assertFalse(serializer.is_valid())
            self.assertEqual(
                serializer.errors,
                {field: ["This field is required."]}
            )

    @ddt.data("", " ")
    def test_create_empty_string(self, value):
        data = self.minimal_data.copy()
        data.update({field: value for field in ["topic_id", "title", "raw_body"]})
        serializer = ThreadSerializer(data=data, context=get_context(self.course, self.request))
        self.assertEqual(
            serializer.errors,
            {field: ["This field is required."] for field in ["topic_id", "title", "raw_body"]}
        )

    def test_create_type(self):
        self.register_post_thread_response({"id": "test_id"})
        data = self.minimal_data.copy()
        data["type"] = "question"
        self.save_and_reserialize(data)

        data["type"] = "invalid_type"
        serializer = ThreadSerializer(data=data)
        self.assertFalse(serializer.is_valid())

    def test_update_empty(self):
        self.register_put_thread_response(self.existing_thread.attributes)
        self.save_and_reserialize({}, self.existing_thread)
        self.assertEqual(
            httpretty.last_request().parsed_body,
            {
                "course_id": [unicode(self.course.id)],
                "commentable_id": ["original_topic"],
                "thread_type": ["discussion"],
                "title": ["Original Title"],
                "body": ["Original body"],
                "anonymous": ["False"],
                "anonymous_to_peers": ["False"],
                "closed": ["False"],
                "pinned": ["False"],
                "user_id": [str(self.user.id)],
            }
        )

    def test_update_all(self):
        self.register_put_thread_response(self.existing_thread.attributes)
        data = {
            "topic_id": "edited_topic",
            "type": "question",
            "title": "Edited Title",
            "raw_body": "Edited body",
        }
        saved = self.save_and_reserialize(data, self.existing_thread)
        self.assertEqual(
            httpretty.last_request().parsed_body,
            {
                "course_id": [unicode(self.course.id)],
                "commentable_id": ["edited_topic"],
                "thread_type": ["question"],
                "title": ["Edited Title"],
                "body": ["Edited body"],
                "anonymous": ["False"],
                "anonymous_to_peers": ["False"],
                "closed": ["False"],
                "pinned": ["False"],
                "user_id": [str(self.user.id)],
            }
        )
        for key in data:
            self.assertEqual(saved[key], data[key])

    @ddt.data("", " ")
    def test_update_empty_string(self, value):
        serializer = ThreadSerializer(
            self.existing_thread,
            data={field: value for field in ["topic_id", "title", "raw_body"]},
            partial=True,
            context=get_context(self.course, self.request)
        )
        self.assertEqual(
            serializer.errors,
            {field: ["This field is required."] for field in ["topic_id", "title", "raw_body"]}
        )

    def test_update_course_id(self):
        serializer = ThreadSerializer(
            self.existing_thread,
            data={"course_id": "some/other/course"},
            partial=True,
            context=get_context(self.course, self.request)
        )
        self.assertEqual(
            serializer.errors,
            {"course_id": ["This field is not allowed in an update."]}
        )


@ddt.ddt
class CommentSerializerDeserializationTest(CommentsServiceMockMixin, ModuleStoreTestCase):
    """Tests for ThreadSerializer deserialization."""
    def setUp(self):
        super(CommentSerializerDeserializationTest, self).setUp()
        httpretty.reset()
        httpretty.enable()
        self.addCleanup(httpretty.disable)
        self.course = CourseFactory.create()
        self.user = UserFactory.create()
        self.register_get_user_response(self.user)
        self.request = RequestFactory().get("/dummy")
        self.request.user = self.user
        self.minimal_data = {
            "thread_id": "test_thread",
            "raw_body": "Test body",
        }
        self.existing_comment = Comment(**make_minimal_cs_comment({
            "id": "existing_comment",
            "thread_id": "existing_thread",
            "body": "Original body",
            "user_id": str(self.user.id),
            "course_id": unicode(self.course.id),
        }))

    def save_and_reserialize(self, data, instance=None):
        """
        Create a serializer with the given data, ensure that it is valid, save
        the result, and return the full comment data from the serializer.
        """
        context = get_context(
            self.course,
            self.request,
            make_minimal_cs_thread({"course_id": unicode(self.course.id)})
        )
        serializer = CommentSerializer(
            instance,
            data=data,
            partial=(instance is not None),
            context=context
        )
        self.assertTrue(serializer.is_valid())
        serializer.save()
        return serializer.data

    @ddt.data(None, "test_parent")
    def test_create_success(self, parent_id):
        data = self.minimal_data.copy()
        if parent_id:
            data["parent_id"] = parent_id
            self.register_get_comment_response({"thread_id": "test_thread", "id": parent_id})
        self.register_post_comment_response(
            {"id": "test_comment"},
            thread_id="test_thread",
            parent_id=parent_id
        )
        saved = self.save_and_reserialize(data)
        expected_url = (
            "/api/v1/comments/{}".format(parent_id) if parent_id else
            "/api/v1/threads/test_thread/comments"
        )
        self.assertEqual(urlparse(httpretty.last_request().path).path, expected_url)
        self.assertEqual(
            httpretty.last_request().parsed_body,
            {
                "course_id": [unicode(self.course.id)],
                "body": ["Test body"],
                "user_id": [str(self.user.id)],
            }
        )
        self.assertEqual(saved["id"], "test_comment")
        self.assertEqual(saved["parent_id"], parent_id)

    def test_create_all_fields(self):
        data = self.minimal_data.copy()
        data["parent_id"] = "test_parent"
        data["endorsed"] = True
        self.register_get_comment_response({"thread_id": "test_thread", "id": "test_parent"})
        self.register_post_comment_response(
            {"id": "test_comment"},
            thread_id="test_thread",
            parent_id="test_parent"
        )
        self.save_and_reserialize(data)
        self.assertEqual(
            httpretty.last_request().parsed_body,
            {
                "course_id": [unicode(self.course.id)],
                "body": ["Test body"],
                "user_id": [str(self.user.id)],
                "endorsed": ["True"],
            }
        )

    def test_create_parent_id_nonexistent(self):
        self.register_get_comment_error_response("bad_parent", 404)
        data = self.minimal_data.copy()
        data["parent_id"] = "bad_parent"
        context = get_context(self.course, self.request, make_minimal_cs_thread())
        serializer = CommentSerializer(data=data, context=context)
        self.assertFalse(serializer.is_valid())
        self.assertEqual(
            serializer.errors,
            {
                "non_field_errors": [
                    "parent_id does not identify a comment in the thread identified by thread_id."
                ]
            }
        )

    def test_create_parent_id_wrong_thread(self):
        self.register_get_comment_response({"thread_id": "different_thread", "id": "test_parent"})
        data = self.minimal_data.copy()
        data["parent_id"] = "test_parent"
        context = get_context(self.course, self.request, make_minimal_cs_thread())
        serializer = CommentSerializer(data=data, context=context)
        self.assertFalse(serializer.is_valid())
        self.assertEqual(
            serializer.errors,
            {
                "non_field_errors": [
                    "parent_id does not identify a comment in the thread identified by thread_id."
                ]
            }
        )

    @ddt.data(None, -1, 0, 2, 5)
    def test_create_parent_id_too_deep(self, max_depth):
        with mock.patch("django_comment_client.utils.MAX_COMMENT_DEPTH", max_depth):
            data = self.minimal_data.copy()
            context = get_context(self.course, self.request, make_minimal_cs_thread())
            if max_depth is None or max_depth >= 0:
                if max_depth != 0:
                    self.register_get_comment_response({
                        "id": "not_too_deep",
                        "thread_id": "test_thread",
                        "depth": max_depth - 1 if max_depth else 100
                    })
                    data["parent_id"] = "not_too_deep"
                else:
                    data["parent_id"] = None
                serializer = CommentSerializer(data=data, context=context)
                self.assertTrue(serializer.is_valid(), serializer.errors)
            if max_depth is not None:
                if max_depth >= 0:
                    self.register_get_comment_response({
                        "id": "too_deep",
                        "thread_id": "test_thread",
                        "depth": max_depth
                    })
                    data["parent_id"] = "too_deep"
                else:
                    data["parent_id"] = None
                serializer = CommentSerializer(data=data, context=context)
                self.assertFalse(serializer.is_valid())
                self.assertEqual(serializer.errors, {"parent_id": ["Comment level is too deep."]})

    def test_create_missing_field(self):
        for field in self.minimal_data:
            data = self.minimal_data.copy()
            data.pop(field)
            serializer = CommentSerializer(
                data=data,
                context=get_context(self.course, self.request, make_minimal_cs_thread())
            )
            self.assertFalse(serializer.is_valid())
            self.assertEqual(
                serializer.errors,
                {field: ["This field is required."]}
            )

    def test_create_endorsed(self):
        # TODO: The comments service doesn't populate the endorsement field on
        # comment creation, so this is sadly realistic
        self.register_post_comment_response({}, thread_id="test_thread")
        data = self.minimal_data.copy()
        data["endorsed"] = True
        saved = self.save_and_reserialize(data)
        self.assertEqual(
            httpretty.last_request().parsed_body,
            {
                "course_id": [unicode(self.course.id)],
                "body": ["Test body"],
                "user_id": [str(self.user.id)],
                "endorsed": ["True"],
            }
        )
        self.assertTrue(saved["endorsed"])
        self.assertIsNone(saved["endorsed_by"])
        self.assertIsNone(saved["endorsed_by_label"])
        self.assertIsNone(saved["endorsed_at"])

    def test_update_empty(self):
        self.register_put_comment_response(self.existing_comment.attributes)
        self.save_and_reserialize({}, instance=self.existing_comment)
        self.assertEqual(
            httpretty.last_request().parsed_body,
            {
                "body": ["Original body"],
                "course_id": [unicode(self.course.id)],
                "user_id": [str(self.user.id)],
                "anonymous": ["False"],
                "anonymous_to_peers": ["False"],
                "endorsed": ["False"],
            }
        )

    def test_update_all(self):
        cs_response_data = self.existing_comment.attributes.copy()
        cs_response_data["endorsement"] = {
            "user_id": str(self.user.id),
            "time": "2015-06-05T00:00:00Z",
        }
        self.register_put_comment_response(cs_response_data)
        data = {"raw_body": "Edited body", "endorsed": True}
        saved = self.save_and_reserialize(data, instance=self.existing_comment)
        self.assertEqual(
            httpretty.last_request().parsed_body,
            {
                "body": ["Edited body"],
                "course_id": [unicode(self.course.id)],
                "user_id": [str(self.user.id)],
                "anonymous": ["False"],
                "anonymous_to_peers": ["False"],
                "endorsed": ["True"],
                "endorsement_user_id": [str(self.user.id)],
            }
        )
        for key in data:
            self.assertEqual(saved[key], data[key])
        self.assertEqual(saved["endorsed_by"], self.user.username)
        self.assertEqual(saved["endorsed_at"], "2015-06-05T00:00:00Z")

    @ddt.data("", " ")
    def test_update_empty_raw_body(self, value):
        serializer = CommentSerializer(
            self.existing_comment,
            data={"raw_body": value},
            partial=True,
            context=get_context(self.course, self.request)
        )
        self.assertEqual(
            serializer.errors,
            {"raw_body": ["This field is required."]}
        )

    @ddt.data("thread_id", "parent_id")
    def test_update_non_updatable(self, field):
        serializer = CommentSerializer(
            self.existing_comment,
            data={field: "different_value"},
            partial=True,
            context=get_context(self.course, self.request)
        )
        self.assertEqual(
            serializer.errors,
            {field: ["This field is not allowed in an update."]}
        )