import textwrap

from django.core import serializers
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase

from markupmirror.fields import Markup
from markupmirror.fields import MarkupMirrorField
from markupmirror.markup.base import markup_pool

from tests.models import Concrete, Post, PostForm

try:
    import json
except ImportError:
    import simplejson as json


class MarkupMirrorFieldTests(TestCase):
    """Tests the ``markupmirror.fields.MarkupMirrorField`` implementation."""

    def setUp(self):
        """Creates three ``Post`` objects with different field settings."""

        # Post with Markdown field
        self.mp = Post(title="example Markdown post",
                       body="**markdown**", body_markup_type='markdown')
        self.mp.save()

        # Post with reStructuredText field
        self.rp = Post(title="example reStructuredText post",
                       body="*reST*", body_markup_type='restructuredtext')
        self.rp.save()

        # Post being attacked
        self.xss_str = "<script>alert('xss');</script>"
        self.xss_post = Post(title="example XSS post",
                             body=self.xss_str, body_markup_type='markdown',
                             comment=self.xss_str)
        self.xss_post.save()

    def test_verbose_name(self):
        """Tests that the standard field property ``verbose_name`` works."""
        self.assertEqual(self.mp._meta.get_field('body').verbose_name,
                         "post body")

    def test_markup_type_exclusive(self):
        """``MarkupMirrorField`` fields in models may only define one of
        ``markup_type`` or ``default_markup_type`` parameters.

        """
        self.assertRaises(ImproperlyConfigured,
                          MarkupMirrorField,
                          verbose_name='broken',
                          markup_type='markdown',
                          default_markup_type='markdown')

    def test_markup_body(self):
        """Tests the three accessors ``raw``, ``rendered`` and ``markup_type``
        of the ``Markup`` content wrapper class.

        """
        self.assertEquals(self.mp.body.raw, "**markdown**")
        self.assertEquals(self.mp.body.rendered,
                          "<p><strong>markdown</strong></p>")
        self.assertEquals(self.mp.body.markup_type, "markdown")

    def test_markup_unicode(self):
        """Converting ``Markup`` to unicode uses the ``Markup.rendered``
        accessor internally.

        """
        self.assertEqual(
            unicode(self.rp.body.rendered), textwrap.dedent(u"""\
            <div class="document">
            <p><em>reST</em></p>
            </div>
            """))

    def test_from_database(self):
        """Test that data loads back from the database correctly and 'post'
        has the right type.

        """
        post1 = Post.objects.get(pk=self.mp.pk)
        self.assertIsInstance(post1.body, Markup)
        self.assertEqual(unicode(post1.body),
                         u"<p><strong>markdown</strong></p>")

    def test_pre_save(self):
        """Test that saving markup values for markup_types which are not
        registered fails.

        """
        post1 = Post.objects.get(pk=self.mp.pk)
        self.assertEqual(post1.body.markup_type, 'markdown')

        # unregister markdown
        del markup_pool['markdown']
        self.assertFalse('markdown' in markup_pool)

        # trying to save the Post now should fail
        self.assertRaises(ValueError, post1.save)

        # restore markup pool
        from markupmirror.markup import markdown_
        markup_pool.register_markup(markdown_.MarkdownMarkup)
        self.assertTrue('markdown' in markup_pool)

    def test_prepare_for_database(self):
        """Tests that ``Markup`` can be used to compare with
        ``MarkupMirrorField`` values at query time.

        """
        post1 = Post.objects.get(pk=self.mp.pk)
        body = post1.body
        self.assertIsInstance(body, Markup)
        self.assertEqual(post1, Post.objects.get(body=body))

    def test_accessor_classmethod(self):
        """``MarkupMirrorField`` attributes on model classes can only be
        accessed via an instance.

        """
        def access_body():
            return Post.body

        self.assertRaises(AttributeError, access_body)

    def test_accessor_none(self):
        """If the field is ``None`` that's what should be returned."""
        self.assertIsInstance(self.mp.body, Markup)
        self.mp.body = None
        self.assertEqual(self.mp.body, None)

    def test_body_assignment(self):
        """Setting the field's value works through the descriptor's setter
        (``MarkupMirrorFieldDescriptor.__set__``) and accepts both strings
        and ``Markup`` instances.

        """
        self.rp.body = "**reST**"
        self.rp.save()
        self.assertEquals(unicode(self.rp.body),
            unicode(self.rp.body.rendered), textwrap.dedent(u"""\
            <div class="document">
            <p><strong>reST</strong></p>
            </div>
            """))

        rest_markup = Markup(self.rp, 'body',
                             'body_rendered', 'body_markup_type')
        rest_markup.raw = "*reST*"
        self.rp.body = rest_markup
        self.assertEquals(unicode(self.rp.body),
            unicode(self.rp.body.rendered), textwrap.dedent(u"""\
            <div class="document">
            <p><em>reST</em></p>
            </div>
            """))

    def test_raw_assignment(self):
        """Setting the ``Markup.raw`` property modifies the field's value."""

        self.rp.body.raw = '*more reST*'
        self.rp.save()
        self.assertEquals(unicode(self.rp.body),
            unicode(self.rp.body.rendered), textwrap.dedent(u"""\
            <div class="document">
            <p><em>more reST</em></p>
            </div>
            """))

    def test_rendered_assignment(self):
        """The ``Markup.rendered`` property dos not have a setter."""

        def set_rendered(text):
            self.rp.body.rendered = text

        self.assertRaises(AttributeError, set_rendered, "fail!")

    def test_body_type_assignment(self):
        """The markup type can be changed using the ``Markup.markup_type``
        property.

        """
        self.rp.body.markup_type = 'markdown'
        self.rp.save()
        self.assertEquals(self.rp.body.markup_type, 'markdown')
        self.assertEquals(unicode(self.rp.body),
                          "<p><em>reST</em></p>")

    def test_serialize_to_json(self):
        """Serializing a ``Post`` with a ``MarkupMirrorField`` works."""
        stream = serializers.serialize('json', Post.objects.all())
        json_data = json.loads(stream)
        self.assertEqual(json_data, [
            {
                u'fields': {
                    u'body': u'**markdown**',
                    u'body_markup_type': u'markdown',
                    u'body_rendered': u'<p><strong>markdown</strong></p>',
                    u'comment': '',
                    u'comment_markup_type': u'markdown',
                    u'comment_rendered': '',
                    u'markdown_field': '',
                    u'markdown_field_markup_type': u'markdown',
                    u'markdown_field_rendered': '',
                    u'title': u'example Markdown post',
                },
                u'model': u'tests.post',
                u'pk': 1,
            },
            {
                u'fields': {
                    u'body': u'*reST*',
                    u'body_markup_type': u'restructuredtext',
                    u'body_rendered':
                        u'<div class="document">'
                        u'\n<p><em>reST</em></p>\n</div>\n',
                    u'comment': '',
                    u'comment_markup_type': u'markdown',
                    u'comment_rendered': '',
                    u'markdown_field': '',
                    u'markdown_field_markup_type': u'markdown',
                    u'markdown_field_rendered': '',
                    u'title': u'example reStructuredText post',
                },
                u'model': u'tests.post',
                u'pk': 2,
            },
            {
                u'fields': {
                    u'body': u"<script>alert('xss');</script>",
                    u'body_markup_type': u'markdown',
                    u'body_rendered': u"<script>alert('xss');</script>",
                    u'comment': u"<script>alert('xss');</script>",
                    u'comment_markup_type': u'markdown',
                    u'comment_rendered':
                        u'<p>&lt;script&gt;'
                        u'alert(&#39;xss&#39;);&lt;/script&gt;</p>',
                    u'markdown_field': '',
                    u'markdown_field_markup_type': u'markdown',
                    u'markdown_field_rendered': '',
                    u'title': u'example XSS post',
                },
                u'model': u'tests.post',
                u'pk': 3,
            },
        ])

    def test_deserialize_json(self):
        """Tests that objects with ``MarkupMirrorFields`` can be deserialized
        correctly.

        """
        stream = serializers.serialize('json', Post.objects.all())
        obj = list(serializers.deserialize('json', stream))[0]
        self.assertEquals(obj.object, self.mp)

    def test_escape_html(self):
        """Rendered content should be escaped to prevent XSS attacks."""
        self.assertEquals(self.xss_post.comment.raw, self.xss_str)
        self.assertEquals(
            unicode(self.xss_post.comment.rendered),
            u'<p>&lt;script&gt;alert(&#39;xss&#39;);&lt;/script&gt;</p>')

    def test_escape_html_false(self):
        """The ``MarkupMirrorField.escape_html`` prevents this escaping."""
        self.assertEquals(self.xss_post.body.raw, self.xss_str)
        self.assertEquals(unicode(self.xss_post.body.rendered), self.xss_str)

    def test_inheritance(self):
        """Abstract base models inherit the ``MarkupMirrorField`` to the
        concrete subclasses.

        """
        concrete_fields = [f.name for f in Concrete._meta.fields]
        self.assertEquals(
            concrete_fields,
            ['id', 'content', 'content_markup_type', 'content_rendered'])

    def test_markup_type_validation(self):
        """Invalid markup types are rejected."""
        self.assertRaises(ImproperlyConfigured, MarkupMirrorField,
            'verbose name', 'markup_field', 'bad_markup_type')

    def test_default_markup_types(self):
        """Per default the markup types plaintext, html are available.
        Depending on available third-party products, markdown,
        restructuredtext and textile are also in the markup pool.

        """
        markups = markup_pool.markups
        for markup_type, markup in markups.items():
            rendered = markup(u"test")
            self.assertTrue(hasattr(rendered, '__str__'))

    def test_formfield(self):
        """Form fields for ``MarkupMirrorFields`` always have two additional
        attributes:

        * ``class=markupmirror-editor``.
        * ``data-mm-settings``: a JSON dictionary containing the init settings
          for CodeMirror and the URLs and parameters required for the preview
          view.

        """
        form = PostForm()
        comment = form.fields['comment']
        self.assertEqual(
            sorted(comment.widget.attrs.keys()),
            ['class', 'cols', 'data-mm-settings', 'rows'])
        self.assertTrue('markupmirror-editor' in comment.widget.attrs['class'])
        self.assertEqual(
            comment.widget.attrs['data-mm-settings'],
            '{{"markup_type": "{0}", "mode": "{1}"}}'.format(
                self.mp.comment.markup_type,
                markup_pool[self.mp.comment.markup_type].codemirror_mode)
            )


__all__ = ('MarkupMirrorFieldTests',)
