from __future__ import absolute_import

from django.test.client import RequestFactory
from django.utils import timezone
from rest_framework import serializers
from rest_framework.fields import SkipField
from rest_framework.reverse import reverse

from rest_framework_json_api.exceptions import Conflict
from rest_framework_json_api.relations import (
    HyperlinkedRelatedField,
    ResourceRelatedField,
    SerializerMethodHyperlinkedRelatedField
)
from rest_framework_json_api.utils import format_resource_type

from . import TestBase
from example.models import Author, Blog, Comment, Entry
from example.serializers import CommentSerializer
from example.views import EntryViewSet


class TestResourceRelatedField(TestBase):

    def setUp(self):
        super(TestResourceRelatedField, self).setUp()
        self.blog = Blog.objects.create(name='Some Blog', tagline="It's a blog")
        self.entry = Entry.objects.create(
            blog=self.blog,
            headline='headline',
            body_text='body_text',
            pub_date=timezone.now(),
            mod_date=timezone.now(),
            n_comments=0,
            n_pingbacks=0,
            rating=3
        )
        for i in range(1, 6):
            name = 'some_author{}'.format(i)
            self.entry.authors.add(
                Author.objects.create(name=name, email='{}@example.org'.format(name))
            )

        self.comment = Comment.objects.create(
            entry=self.entry,
            body='testing one two three',
            author=Author.objects.first()
        )

    def test_data_in_correct_format_when_instantiated_with_blog_object(self):
        serializer = BlogFKSerializer(instance={'blog': self.blog})

        expected_data = {
            'type': format_resource_type('Blog'),
            'id': str(self.blog.id)
        }

        actual_data = serializer.data['blog']

        self.assertEqual(actual_data, expected_data)

    def test_data_in_correct_format_when_instantiated_with_entry_object(self):
        serializer = EntryFKSerializer(instance={'entry': self.entry})

        expected_data = {
            'type': format_resource_type('Entry'),
            'id': str(self.entry.id)
        }

        actual_data = serializer.data['entry']

        self.assertEqual(actual_data, expected_data)

    def test_deserialize_primitive_data_blog(self):
        serializer = BlogFKSerializer(data={
            'blog': {
                'type': format_resource_type('Blog'),
                'id': str(self.blog.id)
            }
        }
        )

        self.assertTrue(serializer.is_valid())
        self.assertEqual(serializer.validated_data['blog'], self.blog)

    def test_validation_fails_for_wrong_type(self):
        with self.assertRaises(Conflict) as cm:
            serializer = BlogFKSerializer(data={
                'blog': {
                    'type': 'Entries',
                    'id': str(self.blog.id)
                }
            }
            )
            serializer.is_valid()
        the_exception = cm.exception
        self.assertEqual(the_exception.status_code, 409)

    def test_serialize_many_to_many_relation(self):
        serializer = EntryModelSerializer(instance=self.entry)

        type_string = format_resource_type('Author')
        author_pks = Author.objects.values_list('pk', flat=True)
        expected_data = [{'type': type_string, 'id': str(pk)} for pk in author_pks]

        self.assertEqual(
            serializer.data['authors'],
            expected_data
        )

    def test_deserialize_many_to_many_relation(self):
        type_string = format_resource_type('Author')
        author_pks = Author.objects.values_list('pk', flat=True)
        authors = [{'type': type_string, 'id': pk} for pk in author_pks]

        serializer = EntryModelSerializer(data={'authors': authors, 'comments': []})

        self.assertTrue(serializer.is_valid())
        self.assertEqual(len(serializer.validated_data['authors']), Author.objects.count())
        for author in serializer.validated_data['authors']:
            self.assertIsInstance(author, Author)

    def test_read_only(self):
        serializer = EntryModelSerializer(
            data={'authors': [], 'comments': [{'type': 'Comments', 'id': 2}]}
        )
        serializer.is_valid(raise_exception=True)
        self.assertNotIn('comments', serializer.validated_data)

    def test_invalid_resource_id_object(self):
        comment = {'body': 'testing 123', 'entry': {'type': 'entry'}, 'author': {'id': '5'}}
        serializer = CommentSerializer(data=comment)
        assert not serializer.is_valid()
        assert serializer.errors == {
            'author': ["Invalid resource identifier object: missing 'type' attribute"],
            'entry': ["Invalid resource identifier object: missing 'id' attribute"]
        }


class TestHyperlinkedFieldBase(TestBase):

    def setUp(self):
        super(TestHyperlinkedFieldBase, self).setUp()
        self.blog = Blog.objects.create(name='Some Blog', tagline="It's a blog")
        self.entry = Entry.objects.create(
            blog=self.blog,
            headline='headline',
            body_text='body_text',
            pub_date=timezone.now(),
            mod_date=timezone.now(),
            n_comments=0,
            n_pingbacks=0,
            rating=3
        )
        self.comment = Comment.objects.create(
            entry=self.entry,
            body='testing one two three',
        )

        self.request = RequestFactory().get(reverse('entry-detail', kwargs={'pk': self.entry.pk}))
        self.view = EntryViewSet(request=self.request, kwargs={'entry_pk': self.entry.id})


class TestHyperlinkedRelatedField(TestHyperlinkedFieldBase):

    def test_single_hyperlinked_related_field(self):
        field = HyperlinkedRelatedField(
            related_link_view_name='entry-blog',
            related_link_url_kwarg='entry_pk',
            self_link_view_name='entry-relationships',
            read_only=True,
        )
        field._context = {'request': self.request, 'view': self.view}
        field.field_name = 'blog'

        self.assertRaises(NotImplementedError, field.to_representation, self.entry)
        self.assertRaises(SkipField, field.get_attribute, self.entry)

        links_expected = {
            'self': 'http://testserver/entries/{}/relationships/blog'.format(self.entry.pk),
            'related': 'http://testserver/entries/{}/blog'.format(self.entry.pk)
        }
        got = field.get_links(self.entry)
        self.assertEqual(got, links_expected)

    def test_many_hyperlinked_related_field(self):
        field = HyperlinkedRelatedField(
            related_link_view_name='entry-comments',
            related_link_url_kwarg='entry_pk',
            self_link_view_name='entry-relationships',
            read_only=True,
            many=True
        )
        field._context = {'request': self.request, 'view': self.view}
        field.field_name = 'comments'

        self.assertRaises(NotImplementedError, field.to_representation, self.entry.comments.all())
        self.assertRaises(SkipField, field.get_attribute, self.entry)

        links_expected = {
            'self': 'http://testserver/entries/{}/relationships/comments'.format(self.entry.pk),
            'related': 'http://testserver/entries/{}/comments'.format(self.entry.pk)
        }
        got = field.child_relation.get_links(self.entry)
        self.assertEqual(got, links_expected)


class TestSerializerMethodHyperlinkedRelatedField(TestHyperlinkedFieldBase):

    def test_single_serializer_method_hyperlinked_related_field(self):
        serializer = EntryModelSerializerWithHyperLinks(
            instance=self.entry,
            context={
                'request': self.request,
                'view': self.view
            }
        )
        field = serializer.fields['blog']

        self.assertRaises(NotImplementedError, field.to_representation, self.entry)
        self.assertRaises(SkipField, field.get_attribute, self.entry)

        expected = {
            'self': 'http://testserver/entries/{}/relationships/blog'.format(self.entry.pk),
            'related': 'http://testserver/entries/{}/blog'.format(self.entry.pk)
        }
        got = field.get_links(self.entry)
        self.assertEqual(got, expected)

    def test_many_serializer_method_hyperlinked_related_field(self):
        serializer = EntryModelSerializerWithHyperLinks(
            instance=self.entry,
            context={
                'request': self.request,
                'view': self.view
            }
        )
        field = serializer.fields['comments']

        self.assertRaises(NotImplementedError, field.to_representation, self.entry)
        self.assertRaises(SkipField, field.get_attribute, self.entry)

        expected = {
            'self': 'http://testserver/entries/{}/relationships/comments'.format(self.entry.pk),
            'related': 'http://testserver/entries/{}/comments'.format(self.entry.pk)
        }
        got = field.get_links(self.entry)
        self.assertEqual(got, expected)

    def test_get_blog(self):
        serializer = EntryModelSerializerWithHyperLinks(instance=self.entry)
        got = serializer.get_blog(self.entry)
        expected = self.entry.blog

        self.assertEqual(got, expected)

    def test_get_comments(self):
        serializer = EntryModelSerializerWithHyperLinks(instance=self.entry)
        got = serializer.get_comments(self.entry)
        expected = self.entry.comments.all()

        self.assertListEqual(list(got), list(expected))


class BlogResourceRelatedField(ResourceRelatedField):
    def get_queryset(self):
        return Blog.objects


class BlogFKSerializer(serializers.Serializer):
    blog = BlogResourceRelatedField()


class EntryFKSerializer(serializers.Serializer):
    entry = ResourceRelatedField(queryset=Entry.objects)


class EntryModelSerializer(serializers.ModelSerializer):
    authors = ResourceRelatedField(many=True, queryset=Author.objects)
    comments = ResourceRelatedField(many=True, read_only=True)

    class Meta:
        model = Entry
        fields = ('authors', 'comments')


class EntryModelSerializerWithHyperLinks(serializers.ModelSerializer):
    blog = SerializerMethodHyperlinkedRelatedField(
        related_link_view_name='entry-blog',
        related_link_url_kwarg='entry_pk',
        self_link_view_name='entry-relationships',
        many=True,
    )
    comments = SerializerMethodHyperlinkedRelatedField(
        related_link_view_name='entry-comments',
        related_link_url_kwarg='entry_pk',
        self_link_view_name='entry-relationships',
        many=True,
    )

    class Meta:
        model = Entry
        fields = ('blog', 'comments',)

    def get_blog(self, obj):
        return obj.blog

    def get_comments(self, obj):
        return obj.comments.all()
