# -*- coding: utf-8 -*-
# Moovida - Home multimedia server
# Copyright (C) 2009 Fluendo Embedded S.L. (www.fluendo.com).
# All rights reserved.
#
# This file is available under one of two license agreements.
#
# This file is licensed under the GPL version 3.
# See "LICENSE.GPL" in the root of this distribution including a special
# exception to use Moovida with Fluendo's plugins.
#
# The GPL part of Moovida is also available under a commercial licensing
# agreement from Fluendo.
# See "LICENSE.Moovida" in the root directory of this distribution package
# for details on that license.
#
# Author: Olivier Tilloy <olivier@tilloy.net>

from twisted.trial.unittest import TestCase
from twisted.internet import task

from elisa.extern.storm_wrapper import store
from storm.locals import create_database

from elisa.plugins.database.database_updater import SCHEMA

from elisa.plugins.database import models as dbmodels


class DBMixin(object):
    def setUp(self):
        self.db = create_database('sqlite:')
        self.store = store.DeferredStore(self.db)
        dfr = self.store.start()
        dfr.addCallback(lambda x: self._create_schema())
        return dfr
    
    def tearDown(self):
        return self.store.stop()

    def _create_schema(self):
        dfr = self._execute_many(SCHEMA)
        dfr.addCallback(lambda x: self.store.commit())
        return dfr

    def _execute_many(self, queries):
        def go_through(queries):
            for query in queries:
                yield self._execute(query)

        return task.coiterate(go_through(queries))

    def _execute(self, query):
        return self.store.execute(query)

    def add_items(self, items):
        def iterator(items):
            for item in items:
                yield self.store.add(item)

        return task.coiterate(iterator(items))

    def check_db_contents(self, result, expected):
        # Expected is a sequence of (class, count) tuples.
        # For each class, check that the total number of objects in the DB is
        # the expected total.
        def check_count(count, cls, expected_count):
            msg = '%s has an unexpected number of entries (%d != %d)' % \
                (cls, count, expected_count)
            self.assertEquals(count, expected_count, msg)

        def iterate_expected(expected):
            for cls, count in expected:
                dfr = self.store.find(cls)
                dfr.addCallback(lambda result_set: result_set.count())
                dfr.addCallback(check_count, cls, count)
                yield dfr

        return task.coiterate(iterate_expected(expected))


class TestConsistentRemovals(DBMixin, TestCase):

    def test_delete_file_no_references(self):
        obj = dbmodels.File()
        obj.path = u'/tmp/file'
        expected = [(dbmodels.File, 0)]
        dfr = self.add_items([obj])
        dfr.addCallback(lambda result: dbmodels.delete_file(obj, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_file_musictrack(self):
        obj = dbmodels.File()
        obj.path = u'/tmp/file'
        track = dbmodels.MusicTrack()
        track.file_path = u'/tmp/file'
        expected = [(dbmodels.File, 0), (dbmodels.MusicTrack, 0)]
        dfr = self.add_items([obj, track])
        dfr.addCallback(lambda result: dbmodels.delete_file(obj, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_file_image(self):
        obj = dbmodels.File()
        obj.path = u'/tmp/file'
        image = dbmodels.Image()
        image.file_path = u'/tmp/file'
        expected = [(dbmodels.File, 0), (dbmodels.Image, 0)]
        dfr = self.add_items([obj, image])
        dfr.addCallback(lambda result: dbmodels.delete_file(obj, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_file_video(self):
        obj = dbmodels.File()
        obj.path = u'/tmp/file'
        video = dbmodels.Video()
        video.file_path = u'/tmp/file'
        expected = [(dbmodels.File, 0), (dbmodels.Video, 0)]
        dfr = self.add_items([obj, video])
        dfr.addCallback(lambda result: dbmodels.delete_file(obj, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_music_track_no_album(self):
        track = dbmodels.MusicTrack()
        track.file_path = u'/tmp/file'
        expected = [(dbmodels.MusicTrack, 0)]
        dfr = self.add_items([track])
        dfr.addCallback(lambda result: dbmodels.delete_music_track(track, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_music_track_last_in_album(self):
        album = dbmodels.MusicAlbum()
        album.name = u'greatest hits'
        track = dbmodels.MusicTrack()
        track.file_path = u'/tmp/file'
        track.album_name = u'greatest hits'
        expected = [(dbmodels.MusicTrack, 0), (dbmodels.MusicAlbum, 0)]
        dfr = self.add_items([album, track])
        dfr.addCallback(lambda result: dbmodels.delete_music_track(track, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_music_track_not_last_in_album(self):
        album = dbmodels.MusicAlbum()
        album.name = u'greatest hits'
        track = dbmodels.MusicTrack()
        track.file_path = u'/tmp/file'
        track.album_name = u'greatest hits'
        other_track = dbmodels.MusicTrack()
        other_track.file_path = u'/tmp/other_file'
        other_track.album_name = u'greatest hits'
        expected = [(dbmodels.MusicTrack, 1), (dbmodels.MusicAlbum, 1)]
        dfr = self.add_items([album, track, other_track])
        dfr.addCallback(lambda result: dbmodels.delete_music_track(track, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_music_track_one_artist(self):
        track = dbmodels.MusicTrack()
        track.file_path = u'/tmp/file'
        artist = dbmodels.Artist()
        artist.name = u'queen'
        track_artist = dbmodels.TrackArtist()
        track_artist.artist_name = u'queen'
        track_artist.track_path = u'/tmp/file'
        expected = [(dbmodels.MusicTrack, 0), (dbmodels.TrackArtist, 0)]
        dfr = self.add_items([track, artist, track_artist])
        dfr.addCallback(lambda result: dbmodels.delete_music_track(track, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_music_track_severals_artists(self):
        track = dbmodels.MusicTrack()
        track.file_path = u'/tmp/file'
        artist = dbmodels.Artist()
        artist.name = u'queen'
        other_artist = dbmodels.Artist()
        other_artist.name = u'the beatles'
        track_artist = dbmodels.TrackArtist()
        track_artist.artist_name = u'queen'
        track_artist.track_path = u'/tmp/file'
        other_track_artist = dbmodels.TrackArtist()
        other_track_artist.artist_name = u'the beatles'
        other_track_artist.track_path = u'/tmp/file'
        expected = [(dbmodels.MusicTrack, 0), (dbmodels.TrackArtist, 0)]
        dfr = self.add_items([track, artist, other_artist, track_artist, other_track_artist])
        dfr.addCallback(lambda result: dbmodels.delete_music_track(track, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_music_album_no_tracks(self):
        album = dbmodels.MusicAlbum()
        album.name = u'greatest hits'
        expected = [(dbmodels.MusicAlbum, 0)]
        dfr = self.add_items([album])
        dfr.addCallback(lambda result: dbmodels.delete_music_album(album, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_music_album_with_tracks(self):
        album = dbmodels.MusicAlbum()
        album.name = u'greatest hits'
        track = dbmodels.MusicTrack()
        track.file_path = u'/tmp/file'
        track.album_name = u'greatest hits'
        other_track = dbmodels.MusicTrack()
        other_track.file_path = u'/tmp/other_file'
        other_track.album_name = u'greatest hits'
        expected = [(dbmodels.MusicAlbum, 0), (dbmodels.MusicTrack, 0)]
        dfr = self.add_items([album, track, other_track])
        dfr.addCallback(lambda result: dbmodels.delete_music_album(album, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_track_artist_last_for_artist(self):
        artist = dbmodels.Artist()
        artist.name = u'queen'
        track_artist = dbmodels.TrackArtist()
        track_artist.artist_name = u'queen'
        track_artist.track_path = u'/tmp/file'
        expected = [(dbmodels.Artist, 0), (dbmodels.TrackArtist, 0)]
        dfr = self.add_items([artist, track_artist])
        dfr.addCallback(lambda result: dbmodels.delete_track_artist(track_artist, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_track_artist_not_last_for_artist(self):
        artist = dbmodels.Artist()
        artist.name = u'queen'
        track_artist = dbmodels.TrackArtist()
        track_artist.artist_name = u'queen'
        track_artist.track_path = u'/tmp/file'
        other_track_artist = dbmodels.TrackArtist()
        other_track_artist.artist_name = u'queen'
        other_track_artist.track_path = u'/tmp/other_file'
        expected = [(dbmodels.Artist, 1), (dbmodels.TrackArtist, 1)]
        dfr = self.add_items([artist, track_artist, other_track_artist])
        dfr.addCallback(lambda result: dbmodels.delete_track_artist(track_artist, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_artist_no_track_artists(self):
        artist = dbmodels.Artist()
        artist.name = u'queen'
        expected = [(dbmodels.Artist, 0)]
        dfr = self.add_items([artist])
        dfr.addCallback(lambda result: dbmodels.delete_artist(artist, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_artist_several_track_artists(self):
        artist = dbmodels.Artist()
        artist.name = u'queen'
        track = dbmodels.MusicTrack()
        track.file_path = u'/tmp/file'
        track_artist = dbmodels.TrackArtist()
        track_artist.artist_name = u'queen'
        track_artist.track_path = u'/tmp/file'
        other_track = dbmodels.MusicTrack()
        other_track.file_path = u'/tmp/other_file'
        other_track_artist = dbmodels.TrackArtist()
        other_track_artist.artist_name = u'queen'
        other_track_artist.track_path = u'/tmp/other_file'
        expected = [(dbmodels.Artist, 0), (dbmodels.MusicTrack, 2), (dbmodels.TrackArtist, 0)]
        dfr = self.add_items([artist, track, other_track, track_artist, other_track_artist])
        dfr.addCallback(lambda result: dbmodels.delete_artist(artist, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_image_no_album(self):
        image = dbmodels.Image()
        image.file_path = u'/tmp/file'
        expected = [(dbmodels.Image, 0)]
        dfr = self.add_items([image])
        dfr.addCallback(lambda result: dbmodels.delete_image(image, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_image_last_in_album(self):
        album = dbmodels.PhotoAlbum()
        album.name = u'album'
        image = dbmodels.Image()
        image.file_path = u'/tmp/file'
        image.album_name = u'album'
        expected = [(dbmodels.Image, 0), (dbmodels.PhotoAlbum, 0)]
        dfr = self.add_items([album, image])
        dfr.addCallback(lambda result: dbmodels.delete_image(image, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_image_not_last_in_album(self):
        album = dbmodels.PhotoAlbum()
        album.name = u'album'
        image = dbmodels.Image()
        image.file_path = u'/tmp/file'
        image.album_name = u'album'
        other_image = dbmodels.Image()
        other_image.file_path = u'/tmp/other_file'
        other_image.album_name = u'album'
        expected = [(dbmodels.Image, 1), (dbmodels.PhotoAlbum, 1)]
        dfr = self.add_items([album, image, other_image])
        dfr.addCallback(lambda result: dbmodels.delete_image(image, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_photo_album_no_images(self):
        album = dbmodels.PhotoAlbum()
        album.name = u'album'
        expected = [(dbmodels.PhotoAlbum, 0)]
        dfr = self.add_items([album])
        dfr.addCallback(lambda result: dbmodels.delete_photo_album(album, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_photo_album_several_images(self):
        album = dbmodels.PhotoAlbum()
        album.name = u'album'
        image = dbmodels.Image()
        image.file_path = u'/tmp/file'
        image.album_name = u'album'
        other_image = dbmodels.Image()
        other_image.file_path = u'/tmp/other_file'
        other_image.album_name = u'album'
        expected = [(dbmodels.Image, 0), (dbmodels.PhotoAlbum, 0)]
        dfr = self.add_items([album, image, other_image])
        dfr.addCallback(lambda result: dbmodels.delete_photo_album(album, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_video_no_references(self):
        video = dbmodels.Video()
        video.file_path = u'/tmp/file'
        expected = [(dbmodels.Video, 0)]
        dfr = self.add_items([video])
        dfr.addCallback(lambda result: dbmodels.delete_video(video, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_video_movie(self):
        video = dbmodels.Video()
        video.file_path = u'/tmp/file'
        movie = dbmodels.Movie()
        movie.file_path = u'/tmp/file'
        expected = [(dbmodels.Video, 0), (dbmodels.Movie, 0)]
        dfr = self.add_items([video, movie])
        dfr.addCallback(lambda result: dbmodels.delete_video(video, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_video_tvepisode(self):
        video = dbmodels.Video()
        video.file_path = u'/tmp/file'
        tvshow = dbmodels.TVShow()
        tvshow.id = 64
        tvseason = dbmodels.TVSeason()
        tvseason.id = 37
        tvseason.tvshow_id = 64
        tvepisode = dbmodels.TVEpisode()
        tvepisode.file_path = u'/tmp/file'
        tvepisode.season_id = 37
        expected = [(dbmodels.Video, 0), (dbmodels.TVEpisode, 0)]
        dfr = self.add_items([video, tvshow, tvseason, tvepisode])
        dfr.addCallback(lambda result: dbmodels.delete_video(video, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_movie(self):
        movie = dbmodels.Movie()
        movie.file_path = u'/tmp/file'
        expected = [(dbmodels.Movie, 0)]
        dfr = self.add_items([movie])
        dfr.addCallback(lambda result: dbmodels.delete_movie(movie, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_tvepisode_last_in_season(self):
        tvshow = dbmodels.TVShow()
        tvshow.id = 64
        tvseason = dbmodels.TVSeason()
        tvseason.id = 37
        tvseason.tvshow_id = 64
        tvepisode = dbmodels.TVEpisode()
        tvepisode.file_path = u'/tmp/file'
        tvepisode.season_id = 37
        expected = [(dbmodels.TVEpisode, 0), (dbmodels.TVSeason, 0)]
        dfr = self.add_items([tvshow, tvseason, tvepisode])
        dfr.addCallback(lambda result: dbmodels.delete_tvepisode(tvepisode, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_tvepisode_not_last_in_season(self):
        tvshow = dbmodels.TVShow()
        tvshow.id = 64
        tvseason = dbmodels.TVSeason()
        tvseason.id = 37
        tvseason.tvshow_id = 64
        tvepisode = dbmodels.TVEpisode()
        tvepisode.file_path = u'/tmp/file'
        tvepisode.season_id = 37
        other_tvepisode = dbmodels.TVEpisode()
        other_tvepisode.file_path = u'/tmp/other_file'
        other_tvepisode.season_id = 37
        expected = [(dbmodels.TVEpisode, 1), (dbmodels.TVSeason, 1)]
        dfr = self.add_items([tvshow, tvseason, tvepisode, other_tvepisode])
        dfr.addCallback(lambda result: dbmodels.delete_tvepisode(tvepisode, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_tvseason_no_episodes_last_in_show(self):
        tvshow = dbmodels.TVShow()
        tvshow.id = 64
        tvseason = dbmodels.TVSeason()
        tvseason.id = 37
        tvseason.tvshow_id = 64
        expected = [(dbmodels.TVSeason, 0), (dbmodels.TVShow, 0)]
        dfr = self.add_items([tvshow, tvseason])
        dfr.addCallback(lambda result: dbmodels.delete_tvseason(tvseason, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_tvseason_no_episodes_not_last_in_show(self):
        tvshow = dbmodels.TVShow()
        tvshow.id = 64
        tvseason = dbmodels.TVSeason()
        tvseason.id = 37
        tvseason.tvshow_id = 64
        other_tvseason = dbmodels.TVSeason()
        other_tvseason.id = 39
        other_tvseason.tvshow_id = 64
        expected = [(dbmodels.TVSeason, 1), (dbmodels.TVShow, 1)]
        dfr = self.add_items([tvshow, tvseason, other_tvseason])
        dfr.addCallback(lambda result: dbmodels.delete_tvseason(tvseason, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_tvseason_several_episodes_last_in_show(self):
        tvshow = dbmodels.TVShow()
        tvshow.id = 64
        tvseason = dbmodels.TVSeason()
        tvseason.id = 37
        tvseason.tvshow_id = 64
        tvepisode = dbmodels.TVEpisode()
        tvepisode.file_path = u'/tmp/file'
        tvepisode.season_id = 37
        other_tvepisode = dbmodels.TVEpisode()
        other_tvepisode.file_path = u'/tmp/other_file'
        other_tvepisode.season_id = 37
        expected = [(dbmodels.TVEpisode, 0), (dbmodels.TVSeason, 0), (dbmodels.TVShow, 0)]
        dfr = self.add_items([tvshow, tvseason, tvepisode, other_tvepisode])
        dfr.addCallback(lambda result: dbmodels.delete_tvseason(tvseason, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_tvseason_several_episodes_not_last_in_show(self):
        tvshow = dbmodels.TVShow()
        tvshow.id = 64
        tvseason = dbmodels.TVSeason()
        tvseason.id = 37
        tvseason.tvshow_id = 64
        tvepisode = dbmodels.TVEpisode()
        tvepisode.file_path = u'/tmp/file'
        tvepisode.season_id = 37
        other_tvepisode = dbmodels.TVEpisode()
        other_tvepisode.file_path = u'/tmp/other_file'
        other_tvepisode.season_id = 37
        other_tvseason = dbmodels.TVSeason()
        other_tvseason.id = 39
        other_tvseason.tvshow_id = 64
        expected = [(dbmodels.TVEpisode, 0), (dbmodels.TVSeason, 1), (dbmodels.TVShow, 1)]
        dfr = self.add_items([tvshow, tvseason, tvepisode, other_tvepisode, other_tvseason])
        dfr.addCallback(lambda result: dbmodels.delete_tvseason(tvseason, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_tvshow_no_seasons(self):
        tvshow = dbmodels.TVShow()
        tvshow.id = 64
        expected = [(dbmodels.TVShow, 0)]
        dfr = self.add_items([tvshow])
        dfr.addCallback(lambda result: dbmodels.delete_tvshow(tvshow, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr

    def test_delete_tvshow_several_seasons(self):
        tvshow = dbmodels.TVShow()
        tvshow.id = 64
        tvseason = dbmodels.TVSeason()
        tvseason.id = 37
        tvseason.tvshow_id = 64
        other_tvseason = dbmodels.TVSeason()
        other_tvseason.id = 39
        other_tvseason.tvshow_id = 64
        expected = [(dbmodels.TVShow, 0), (dbmodels.TVSeason, 0)]
        dfr = self.add_items([tvshow, tvseason, other_tvseason])
        dfr.addCallback(lambda result: dbmodels.delete_tvshow(tvshow, self.store))
        dfr.addCallback(self.check_db_contents, expected)
        return dfr
