from __future__ import unicode_literals

import json
import datetime

from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase
from django.utils import timezone

from tests.models import Band, BandMember, Album, Restaurant, Dish, MenuItem, Chef, Wine, \
    Review, Log, Document, Article, Author, Category


class SerializeTest(TestCase):
    def test_serialize(self):
        beatles = Band(name='The Beatles', members=[
            BandMember(name='John Lennon'),
            BandMember(name='Paul McCartney'),
        ])

        expected = {'pk': None, 'albums': [], 'name': 'The Beatles', 'members': [
            {'pk': None, 'name': 'John Lennon', 'band': None, 'favourite_restaurant': None},
            {'pk': None, 'name': 'Paul McCartney', 'band': None, 'favourite_restaurant': None}
        ]}
        self.assertEqual(expected, beatles.serializable_data())

    def test_serialize_m2m(self):
        george_orwell = Author.objects.create(name='George Orwell')
        charles_dickens = Author.objects.create(name='Charles Dickens')

        article = Article(
            title='Down and Out in Paris and London',
            authors=[george_orwell, charles_dickens],
        )

        article_serialised = article.serializable_data()
        self.assertEqual(article_serialised['title'], 'Down and Out in Paris and London')
        self.assertIn(george_orwell.pk, article_serialised['authors'])
        self.assertEqual(article_serialised['categories'], [])

    def test_serialize_json_with_dates(self):
        beatles = Band(name='The Beatles', members=[
            BandMember(name='John Lennon'),
            BandMember(name='Paul McCartney'),
        ], albums=[
            Album(name='Rubber Soul', release_date=datetime.date(1965, 12, 3))
        ])

        beatles_json = beatles.to_json()
        self.assertTrue("John Lennon" in beatles_json)
        self.assertTrue("1965-12-03" in beatles_json)
        unpacked_beatles = Band.from_json(beatles_json)
        self.assertEqual(datetime.date(1965, 12, 3), unpacked_beatles.albums.all()[0].release_date)

    def test_deserialize(self):
        beatles = Band.from_serializable_data({
            'pk': 9,
            'albums': [],
            'name': 'The Beatles',
            'members': [
                {'pk': None, 'name': 'John Lennon', 'band': None},
                {'pk': None, 'name': 'Paul McCartney', 'band': None},
            ]
        })
        self.assertEqual(9, beatles.id)
        self.assertEqual('The Beatles', beatles.name)
        self.assertEqual(2, beatles.members.count())
        self.assertEqual(BandMember, beatles.members.all()[0].__class__)

    def test_deserialize_m2m(self):
        authors = {}
        categories = {}
        for i in range(1, 6):
            authors[i] = Author.objects.create(name="Author %d" % i)
            categories[i] = Category.objects.create(name="Category %d" % i)

        article = Article.from_serializable_data({
            'pk': 1,
            'title': 'Article Title 1',
            'authors': [authors[1].pk, authors[2].pk],
            'categories': [categories[2].pk, categories[3].pk, categories[4].pk]
        })
        self.assertEqual(article.id, 1)
        self.assertEqual(article.title, 'Article Title 1')
        self.assertEqual(article.authors.count(), 2)
        self.assertEqual(
            [author.name for author in article.authors.order_by('name')],
            ['Author 1', 'Author 2']
        )
        self.assertEqual(article.categories.count(), 3)

    def test_deserialize_json(self):
        beatles = Band.from_json('{"pk": 9, "albums": [], "name": "The Beatles", "members": [{"pk": null, "name": "John Lennon", "band": null}, {"pk": null, "name": "Paul McCartney", "band": null}]}')
        self.assertEqual(9, beatles.id)
        self.assertEqual('The Beatles', beatles.name)
        self.assertEqual(2, beatles.members.count())
        self.assertEqual(BandMember, beatles.members.all()[0].__class__)

    def test_serialize_with_multi_table_inheritance(self):
        fat_duck = Restaurant(name='The Fat Duck', serves_hot_dogs=False, reviews=[
            Review(author='Michael Winner', body='Rubbish.')
        ])
        data = json.loads(fat_duck.to_json())
        self.assertEqual(data['name'], 'The Fat Duck')
        self.assertEqual(data['serves_hot_dogs'], False)
        self.assertEqual(data['reviews'][0]['author'], 'Michael Winner')

    def test_deserialize_with_multi_table_inheritance(self):
        fat_duck = Restaurant.from_json('{"pk": 42, "name": "The Fat Duck", "serves_hot_dogs": false, "reviews": [{"pk": null, "author": "Michael Winner", "body": "Rubbish."}]}')
        self.assertEqual(fat_duck.id, 42)
        self.assertEqual(fat_duck.name, "The Fat Duck")
        self.assertEqual(fat_duck.serves_hot_dogs, False)
        self.assertEqual(fat_duck.reviews.all()[0].author, "Michael Winner")

    def test_dangling_foreign_keys(self):
        heston_blumenthal = Chef.objects.create(name="Heston Blumenthal")
        snail_ice_cream = Dish.objects.create(name="Snail ice cream")
        chateauneuf = Wine.objects.create(name="Chateauneuf-du-Pape 1979")
        fat_duck = Restaurant(name="The Fat Duck", proprietor=heston_blumenthal, serves_hot_dogs=False, menu_items=[
            MenuItem(dish=snail_ice_cream, price='20.00', recommended_wine=chateauneuf)
        ])
        fat_duck_json = fat_duck.to_json()

        fat_duck = Restaurant.from_json(fat_duck_json)
        self.assertEqual("Heston Blumenthal", fat_duck.proprietor.name)
        self.assertEqual("Chateauneuf-du-Pape 1979", fat_duck.menu_items.all()[0].recommended_wine.name)

        heston_blumenthal.delete()
        fat_duck = Restaurant.from_json(fat_duck_json)
        # the deserialised record should recognise that the heston_blumenthal record is now missing
        self.assertEqual(None, fat_duck.proprietor)
        self.assertEqual("Chateauneuf-du-Pape 1979", fat_duck.menu_items.all()[0].recommended_wine.name)

        chateauneuf.delete()  # oh dear, looks like we just drank the last bottle
        fat_duck = Restaurant.from_json(fat_duck_json)
        # the deserialised record should now have a null recommended_wine field
        self.assertEqual(None, fat_duck.menu_items.all()[0].recommended_wine)

        snail_ice_cream.delete()  # NOM NOM NOM
        fat_duck = Restaurant.from_json(fat_duck_json)
        # the menu item should now be dropped entirely (because the foreign key to Dish has on_delete=CASCADE)
        self.assertEqual(0, fat_duck.menu_items.count())

    def test_deserialize_with_sort_order(self):
        beatles = Band.from_json('{"pk": null, "albums": [{"pk": null, "name": "With The Beatles", "sort_order": 2}, {"pk": null, "name": "Please Please Me", "sort_order": 1}], "name": "The Beatles", "members": []}')
        self.assertEqual(2, beatles.albums.count())

        # Make sure the albums were ordered correctly
        self.assertEqual("Please Please Me", beatles.albums.all()[0].name)
        self.assertEqual("With The Beatles", beatles.albums.all()[1].name)

    def test_deserialize_with_reversed_sort_order(self):
        Album._meta.ordering = ['-sort_order']
        beatles = Band.from_json('{"pk": null, "albums": [{"pk": null, "name": "Please Please Me", "sort_order": 1}, {"pk": null, "name": "With The Beatles", "sort_order": 2}], "name": "The Beatles", "members": []}')
        Album._meta.ordering = ['sort_order']
        self.assertEqual(2, beatles.albums.count())

        # Make sure the albums were ordered correctly
        self.assertEqual("With The Beatles", beatles.albums.all()[0].name)
        self.assertEqual("Please Please Me", beatles.albums.all()[1].name)

    def test_deserialize_with_multiple_sort_order(self):
        Album._meta.ordering = ['sort_order', 'name']
        beatles = Band.from_json('{"pk": null, "albums": [{"pk": 1, "name": "With The Beatles", "sort_order": 1}, {"pk": 2, "name": "Please Please Me", "sort_order": 1}, {"pk": 3, "name": "Please Please Me", "sort_order": 2}], "name": "The Beatles", "members": []}')
        Album._meta.ordering = ['sort_order']
        self.assertEqual(3, beatles.albums.count())

        # Make sure the albums were ordered correctly
        self.assertEqual(2, beatles.albums.all()[0].pk)
        self.assertEqual(1, beatles.albums.all()[1].pk)
        self.assertEqual(3, beatles.albums.all()[2].pk)

    WAGTAIL_05_RELEASE_DATETIME = datetime.datetime(2014, 8, 1, 11, 1, 42)

    def test_serialise_with_naive_datetime(self):
        """
        This tests that naive datetimes are saved as UTC
        """
        # Time is in America/Chicago time
        log = Log(time=self.WAGTAIL_05_RELEASE_DATETIME, data="Wagtail 0.5 released")
        log_json = json.loads(log.to_json())

        # Now check that the time is stored correctly with the timezone information at the end
        self.assertEqual(log_json['time'], '2014-08-01T16:01:42Z')

    def test_serialise_with_aware_datetime(self):
        """
        This tests that aware datetimes are converted to as UTC
        """
        # make an aware datetime, consisting of WAGTAIL_05_RELEASE_DATETIME
        # in a timezone 1hr west of UTC
        one_hour_west = timezone.get_fixed_timezone(-60)

        local_time = timezone.make_aware(self.WAGTAIL_05_RELEASE_DATETIME, one_hour_west)
        log = Log(time=local_time, data="Wagtail 0.5 released")
        log_json = json.loads(log.to_json())

        # Now check that the time is stored correctly with the timezone information at the end
        self.assertEqual(log_json['time'], '2014-08-01T12:01:42Z')

    def test_deserialise_with_utc_datetime(self):
        """
        This tests that a datetimes saved as UTC are converted back correctly
        """
        # Time is in UTC
        log = Log.from_json('{"data": "Wagtail 0.5 released", "time": "2014-08-01T16:01:42Z", "pk": null}')

        # Naive and aware timezones cannot be compared so make the release date timezone-aware before comparison
        expected_time = timezone.make_aware(self.WAGTAIL_05_RELEASE_DATETIME, timezone.get_default_timezone())

        # Check that the datetime is correct and was converted back into the correct timezone
        self.assertEqual(log.time, expected_time)
        self.assertEqual(log.time.tzinfo, expected_time.tzinfo)

    def test_deserialise_with_local_datetime(self):
        """
        This tests that a datetime without timezone information is interpreted as a local time
        """
        log = Log.from_json('{"data": "Wagtail 0.5 released", "time": "2014-08-01T11:01:42", "pk": null}')

        expected_time = timezone.make_aware(self.WAGTAIL_05_RELEASE_DATETIME, timezone.get_default_timezone())
        self.assertEqual(log.time, expected_time)
        self.assertEqual(log.time.tzinfo, expected_time.tzinfo)

    def test_serialise_with_null_datetime(self):
        log = Log(time=None, data="Someone scanned a QR code")
        log_json = json.loads(log.to_json())
        self.assertEqual(log_json['time'], None)

    def test_deserialise_with_null_datetime(self):
        log = Log.from_json('{"data": "Someone scanned a QR code", "time": null, "pk": null}')
        self.assertEqual(log.time, None)

    def test_serialise_saves_file_fields(self):
        doc = Document(title='Hello')
        doc.file = SimpleUploadedFile('hello.txt', b'Hello world')

        doc_json = doc.to_json()
        new_doc = Document.from_json(doc_json)

        self.assertEqual(new_doc.file.read(), b'Hello world')

    def test_ignored_relations(self):
        george_orwell = Author.objects.create(name='George Orwell')
        charles_dickens = Author.objects.create(name='Charles Dickens')

        rel_article = Article(
            title='Round and round wherever',
            authors=[george_orwell],
        )
        article = Article(
            title='Down and Out in Paris and London',
            authors=[george_orwell, charles_dickens],
            related_articles=[rel_article],
            view_count=123
        )

        article_serialised = article.serializable_data()
        # check that related_articles and view_count are not serialized (marked with serialize=False)
        self.assertNotIn('related_articles', article_serialised)
        self.assertNotIn('view_count', article_serialised)

        rel_article.save()
        article.save()

        article_json = article.to_json()
        restored_article = Article.from_json(article_json)
        restored_article.save()
        restored_article = Article.objects.get(pk=restored_article.pk)
        # check that related_articles and view_count hasn't been touched
        self.assertIn(rel_article, restored_article.related_articles.all())