# -*- coding: utf-8 -*-
# Moovida - Home multimedia server
# Copyright (C) 2006-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: Alessandro Decina <alessandro@fluendo.com>

from twisted.trial.unittest import TestCase, SkipTest
import platform

import exceptions
from twisted.internet import defer, task, reactor
from twisted.python.reflect import qual, namedAny

from elisa.core import common
from elisa.core.media_uri import MediaUri
from elisa.core.utils import caching
from elisa.extern.storm_wrapper import store
from storm.locals import create_database

from elisa.plugins.database.media_scanner import SCHEMA
from elisa.plugins.database.models import Artist, MusicAlbum, MusicTrack, \
     PhotoAlbum, Image, File, PlayableModel

try:
    import dbus
except ImportError:
    dbus = None
    DatabaseDBusServiceProvider = object
    MusicImpl = object
    Music = object
    PhotoImpl = object
    Photo = object
else:
    from dbus.exceptions import DBusException
    from elisa.plugins.database.dbus_service import DatabaseDBusServiceProvider, \
         ArtistNotFoundError, MusicAlbumNotFoundError, MusicTrackNotFoundError, \
         PhotoAlbumNotFoundError, PictureNotFoundError, Music, MusicImpl, \
         Photo, PhotoImpl

def translateDBusFailure(failure):
    failure.trap(DBusException)

    skip_len = len('org.freedesktop.DBus.Python.')
    exception_name = failure.value.get_dbus_name()[skip_len:]
    
    exception_class = getattr(exceptions, exception_name, None)
    if exception_class is None:
        exception_class = namedAny(exception_name)

    raise exception_class()

def method(*args, **kw):
    dfr = defer.Deferred()
    dfr.addErrback(translateDBusFailure)

    def callback(*args):
        dfr.callback(*args)
    
    def errback(*args):
        dfr.errback(*args)

    kw['reply_handler'] = callback
    kw['error_handler'] = errback

    method(*args, **kw)

    return dfr

class ArtistName(unicode):
    def __new__(cls, name):
        return unicode.__new__(cls, 'Artist %s' % name)

class MusicAlbumName(unicode):
    def __new__(cls, artist_name, name):
        return unicode.__new__(cls, '%s - MusicAlbum %s' % (artist_name, name))

class MusicAlbumCoverUri(unicode):
    def __new__(cls, album_name):
        return unicode.__new__(cls, '%s - Cover.png' % album_name)

class MusicTrackTitle(unicode):
    def __new__(cls, album_name, name):
        return unicode.__new__(cls, '%s - Track %s' % (album_name, name))

class MusicTrackFilePath(unicode):
    def __new__(cls, music_track_name):
        return unicode.__new__(cls, '/' + music_track_name.replace(' ', '_') + '.ogg')

class PhotoAlbumName(unicode):
    def __new__(cls, name):
        return unicode.__new__(cls, 'PhotoAlbum %s' % name)

class ImageFilePath(unicode):
    def __new__(cls, album_name, name):
        return unicode.__new__(cls, '/%s - %s.png' % (album_name, name))

class TestDatabaseDBusServiceProviderBase(DatabaseDBusServiceProvider):
    _last_played_track_tuple = None
    _last_displayed_picture_tuple = None

    def _register_player_status_changes(self):
        pass

    def _emit_last_played_track(self, last_played_track_tuple):
        self._last_played_track_tuple = last_played_track_tuple
    
    def _emit_last_displayed_picture(self, last_displayed_picture_tuple):
        self._last_displayed_picture_tuple = last_displayed_picture_tuple

class DeferredDBusMethod(object):
    def __init__(self, method_name):
        self.method_name = method_name
    
    def __get__(self, instance, klass):
        method = getattr(instance.dbus_proxy, self.method_name)
        return self._decorate(method)

    def _decorate(self, method):
        def wrapper(*args, **kw):
            dfr = defer.Deferred()
            dfr.addErrback(translateDBusFailure)

            def callback(*args):
                args = args or (None,)
                dfr.callback(*args)
            
            def errback(*args):
                dfr.errback(*args)

            kw['reply_handler'] = callback
            kw['error_handler'] = errback

            method(*args, **kw)

            return dfr

        return wrapper

class DeferredDBusProxyMeta(type):
    method_names = []

    def __new__(cls, name, bases, dic):
        newcls = type.__new__(cls, name, bases, dic)

        for method_name in newcls.method_names:
            setattr(newcls, method_name, DeferredDBusMethod(method_name))

        return newcls

class DeferredDBusPhotoProxy(object):
    __metaclass__ = DeferredDBusProxyMeta

    method_names = ['get_albums', 'get_album_pictures',
            'get_last_displayed_picture']

    def __init__(self, dbus_proxy):
        self.dbus_proxy = dbus_proxy

class DeferredDBusMusicProxy(object):
    __metaclass__ = DeferredDBusProxyMeta

    method_names = ['get_albums', 'get_album_artwork',
            'get_album_tracks', 'get_last_played_track']
    
    def __init__(self, dbus_proxy):
        self.dbus_proxy = dbus_proxy

class TestMusicImpl(MusicImpl):
    download = None

    def _download_cover_thumbnail(self, uri, filename):
        self.download = uri
        return defer.succeed(filename)

class TestMusic(Music):
    implFactory = TestMusicImpl

class TestPhotoImpl(PhotoImpl):
    pass

class TestPhoto(Photo):
    pass

class TestDatabaseDBusServiceProvider(TestDatabaseDBusServiceProviderBase):
    def _start_dbus(self):
        bus = dbus.SessionBus()
        self.bus_name = \
                dbus.service.BusName('com.fluendo.Elisa', bus)
        self.photo = TestPhoto(bus, '/com/fluendo/Elisa/Plugins/Database/Photo',
                self.bus_name)
        self.music = TestMusic(bus, '/com/fluendo/Elisa/Plugins/Database/Music',
                self.bus_name)

class TestDatabaseDBusServiceProviderNoDBus(TestDatabaseDBusServiceProviderBase):
    """
    A DBus service provider that doesn't use DBus. Yeah, really.

    And bonus points for winning the longest-class-name-ever award.
    """
    def _start_dbus(self):
        self.photo = TestPhotoImpl()
        self.music = TestMusicImpl()

    def _stop_dbus(self):
        pass

class TestMixin(object):
    test_dbus = True

    def setUp(self):
        if not dbus:
            raise SkipTest("dbus not supported on %s platform" % platform.system())
        
        # create and start the service 
        def service_created_cb(service):
            self.service = service
            return service.start()

        if self.test_dbus:
            service_provider_class = TestDatabaseDBusServiceProvider
        else:
            service_provider_class = TestDatabaseDBusServiceProviderNoDBus

        dfr1 = service_provider_class.create({})
        dfr1.addCallback(service_created_cb)
        dfr1.addCallback(self._get_proxy)
        dfr1.addCallback(lambda proxy: setattr(self, 'proxy', proxy))

        # create and start the database
        self.db = create_database('sqlite:')
        self.store = store.DeferredStore(self.db, False)
        self.patch_application()
        dfr2 = self.store.start()
        dfr2.addCallback(self._populate_database)

        return defer.DeferredList([dfr1, dfr2])

    def tearDown(self):
        self.unpatch_application()
        dfr1 = self.store.stop()
        dfr2 = self.service.stop()
        del self.proxy

        return defer.DeferredList([dfr1, dfr2])

    def patch_application(self):
        self.application = common.application

        class Dummy(object):
            pass

        common.application = Dummy()
        common.application.store = self.store

    def unpatch_application(self):
        common.application = self.application

class TestMusicMixin(TestMixin):
    number_artists = 3
    number_albums = 5
    number_tracks = 16

    connection = 'com.fluendo.Elisa'
    object_path = '/com/fluendo/Elisa/Plugins/Database/Music'

    def _populate_database(self, result):
        store = self.store

        def iterator():
            for statement in SCHEMA:
                yield store.execute(statement)
            yield store.commit()

            for a in xrange(self.number_artists):
                artist = Artist()
                artist.name = ArtistName(a)
                yield store.add(artist)

                for i in xrange(self.number_albums):
                    album = MusicAlbum()
                    album.name = MusicAlbumName(artist.name, i)
                    album.cover_uri = MusicAlbumCoverUri(album.name)
                    yield store.add(album)

                    for x in xrange(self.number_tracks):
                        track = MusicTrack()
                        track.title = MusicTrackTitle(album.name, x)
                        track.file_path = MusicTrackFilePath(track.title)
                        track.album_name = album.name
                        yield store.add(track)
                        yield track.artists.add(artist)

                        dbfile = File()
                        dbfile.path = track.file_path
                        dbfile.last_played = (a * 100) + (i * 10) + x
                        yield store.add(dbfile)

            yield store.commit()

            self._last_played_track_path = dbfile.path

        dfr = task.coiterate(iterator())
        return dfr

    def test_get_albums_wrong_arguments(self):
        deferreds = []

        dfr = self.proxy.get_albums(0, 0)
        self.failUnlessFailure(dfr, IndexError)
        deferreds.append(dfr)

        dfr = self.proxy.get_albums(-1, 10)
        self.failUnlessFailure(dfr, IndexError)
        deferreds.append(dfr)
        
        dfr = self.proxy.get_albums(1, -1)
        self.failUnlessFailure(dfr, IndexError)
        deferreds.append(dfr)

        return defer.DeferredList(deferreds)

    def test_get_albums(self):
        total_albums = self.number_artists * self.number_albums

        def get_full_range(result):
            def get_albums_cb(albums):
                self.failUnlessEqual(len(albums), 10)

            dfr = self.proxy.get_albums(0, 10)
            dfr.addCallback(get_albums_cb)
        
            return dfr

        def get_non_full_range(result):
            def get_albums_cb(albums):
                self.failUnlessEqual(len(albums), total_albums)

            dfr = self.proxy.get_albums(0, total_albums + 3)
            dfr.addCallback(get_albums_cb)
            
            return dfr

        def get_non_full_range_offset(result):
            def get_albums_cb(albums):
                self.failUnlessEqual(len(albums), 7)

            dfr = self.proxy.get_albums(total_albums - 7, 18)
            dfr.addCallback(get_albums_cb)
            
            return dfr

        dfr = defer.Deferred()
        dfr.addCallback(get_full_range)
        dfr.addCallback(get_non_full_range)
        dfr.addCallback(get_non_full_range_offset)

        dfr.callback(None)

        return dfr

    def test_get_album_artwork_wrong_arguments(self):
        artist_name = ArtistName(0)
        album_name = MusicAlbumName(artist_name, 0)
        cover_uri = MusicAlbumCoverUri(album_name) 

        def test_wrong_artist_name(result):
            dfr = self.proxy.get_album_artwork(
                    u'not in the library', album_name)
            self.failUnlessFailure(dfr, ArtistNotFoundError)
            return dfr

        def test_wrong_album_name(result):
            dfr = self.proxy.get_album_artwork(
                    artist_name, u'not in the library')
            self.failUnlessFailure(dfr, MusicAlbumNotFoundError)
            return dfr

        dfr = defer.Deferred()
        dfr.addCallback(test_wrong_artist_name)
        dfr.addCallback(test_wrong_album_name)
        dfr.callback(None)

        return dfr

    def test_get_album_artwork(self):
        def get_album_artwork_cb(result, album_name):
            cover_uri = MusicAlbumCoverUri(album_name)
            filename = caching.get_cached_image_path(MediaUri(cover_uri))
            self.failUnlessEqual(result, filename)
        
        def iterator():
            for n_artist in xrange(self.number_artists):
                artist_name = ArtistName(n_artist)

                for n_album in xrange(self.number_albums):
                    album_name = MusicAlbumName(artist_name, n_album)

                    dfr = self.proxy.get_album_artwork(
                            artist_name, album_name)
                    dfr.addCallback(get_album_artwork_cb, album_name)

                    yield dfr

        dfr = task.coiterate(iterator())
        return dfr
    
    def test_get_album_tracks_wrong_arguments(self):
        artist_name = ArtistName(0)
        album_name = MusicAlbumName(artist_name, 0)
        cover_uri = MusicAlbumCoverUri(album_name) 

        def test_wrong_artist_name(result):
            dfr = self.proxy.get_album_tracks(
                    u'not in the library', album_name)
            self.failUnlessFailure(dfr, ArtistNotFoundError)
            return dfr

        def test_wrong_album_name(result):
            dfr = self.proxy.get_album_tracks(
                    artist_name, u'not in the library')
            self.failUnlessFailure(dfr, MusicAlbumNotFoundError)
            return dfr

        dfr = defer.Deferred()
        dfr.addCallback(test_wrong_artist_name)
        dfr.addCallback(test_wrong_album_name)
        dfr.callback(None)

        return dfr

    def test_get_album_tracks(self):
        def get_album_tracks_cb(result, artist_name, album_name):
            self.failUnlessEqual(len(result), self.number_tracks)
            
            res = []
            for title, path in result:
                res.append((unicode(title), unicode(path)))
            
            for i in xrange(self.number_tracks):
                expected_title = MusicTrackTitle(album_name, i)
                expected_path = MusicTrackFilePath(expected_title)
                self.failUnlessIn((expected_title, expected_path), res)

        def iterator():
            for n_artist in xrange(self.number_artists):
                artist_name = ArtistName(n_artist)

                for n_album in xrange(self.number_albums):
                    album_name = MusicAlbumName(artist_name, n_album)

                    dfr = self.proxy.get_album_tracks(
                            artist_name, album_name)
                    dfr.addCallback(get_album_tracks_cb,
                            artist_name, album_name)

                    yield dfr

        dfr = task.coiterate(iterator())
        return dfr

    def test_get_last_played_track(self):
        def check_last_played(last_played_tuple):
            self.failUnlessEqual(len(last_played_tuple), 4)
            
            artist_name, album_name, title, path = last_played_tuple
            self.failUnlessEqual(self._last_played_track_path, path)

        dfr = self.proxy.get_last_played_track()
        dfr.addCallback(check_last_played)
        return dfr

    def test_last_played_track_signal(self):
        # create a fake PlayableModel
        class Model(PlayableModel):
            def __init__(self, filename):
                PlayableModel.__init__(self)
                self.uri = MediaUri('file://' + filename)

        # create a fake Player
        class FakePlayer(object):
            STOPPED = 1
            PLAY = 2
            BUFFERING = 3
            PLAYING = 4
            PAUSED = 5
            
            def __init__(self):
                self.playlist = []
                self.current_index = 0

            def get_current_model(self):
                return self.playlist[self.current_index]
            
        player = FakePlayer()
        tuples = []
        for i in xrange(2):
            artist_name = ArtistName(i)
            album_name = MusicAlbumName(artist_name, i+1)
            track_title = MusicTrackTitle(album_name, i+2)
            track_path = MusicTrackFilePath(track_title)
            tuples.append((artist_name, album_name, track_title, track_path))
            player.playlist.append(Model(track_path))

        def simulate_playing(result):
            return self.service._music_player_status_cb(player, FakePlayer.PLAYING)

        def pop_and_check_tuple(result, expected):
            tup, self.service._last_played_track_tuple = \
                    self.service._last_played_track_tuple, None
            self.failUnlessEqual(tup, expected)

            return result

        def set_current_index(result, index):
            player.current_index = index

            return result

        dfr = defer.Deferred()
        # simulate the first playing event, last == None, emit
        dfr.addCallback(simulate_playing)
        dfr.addCallback(pop_and_check_tuple, tuples[0])
        # simulate another playing event, last == current, don't emit
        dfr.addCallback(simulate_playing)
        dfr.addCallback(pop_and_check_tuple, None)
        # change track, emit event, last != current, emit
        dfr.addCallback(set_current_index, 1)
        dfr.addCallback(simulate_playing)
        dfr.addCallback(pop_and_check_tuple, tuples[1])
        # who-hoo
        dfr.callback(None)

        return dfr

class TestPhotoMixin(TestMixin):
    number_albums = 12
    number_images = 17

    connection = 'com.fluendo.Elisa'
    object_path = '/com/fluendo/Elisa/Plugins/Database/Photo'

    def _populate_database(self, result):
        store = self.store

        def iterator():
            for statement in SCHEMA:
                yield store.execute(statement)
            yield store.commit()

            for i in xrange(self.number_albums):
                album = PhotoAlbum()
                album.name = PhotoAlbumName(i)
                yield store.add(album)

                for x in xrange(self.number_images):
                    image = Image()
                    image.file_path = ImageFilePath(album.name, x)
                    image.album_name = album.name
                    yield store.add(image)
                    yield album.photos.add(image)
                    
                    dbfile = File()
                    dbfile.path = image.file_path
                    dbfile.last_played = (i * 10) + x
                    yield store.add(dbfile)

            yield store.commit()
            
            self._last_displayed_picture_path = dbfile.path
        

        dfr = task.coiterate(iterator())
        return dfr
    
    def test_get_albums_wrong_arguments(self):
        deferreds = []

        dfr = self.proxy.get_albums(0, 0)
        self.failUnlessFailure(dfr, IndexError)
        deferreds.append(dfr)

        dfr = self.proxy.get_albums(-1, 10)
        self.failUnlessFailure(dfr, IndexError)
        deferreds.append(dfr)
        
        dfr = self.proxy.get_albums(1, -1)
        self.failUnlessFailure(dfr, IndexError)
        deferreds.append(dfr)

        return defer.DeferredList(deferreds)

    def test_get_albums(self):
        total_albums = self.number_albums

        def get_full_range(result):
            def get_albums_cb(albums):
                self.failUnlessEqual(len(albums), 10)

            dfr = self.proxy.get_albums(0, 10)
            dfr.addCallback(get_albums_cb)
        
            return dfr

        def get_non_full_range(result):
            def get_albums_cb(albums):
                self.failUnlessEqual(len(albums), total_albums)

            dfr = self.proxy.get_albums(0, total_albums + 3)
            dfr.addCallback(get_albums_cb)
            
            return dfr

        def get_non_full_range_offset(result):
            def get_albums_cb(albums):
                self.failUnlessEqual(len(albums), 7)

            dfr = self.proxy.get_albums(total_albums - 7, 18)
            dfr.addCallback(get_albums_cb)
            
            return dfr

        dfr = defer.Deferred()
        dfr.addCallback(get_full_range)
        dfr.addCallback(get_non_full_range)
        dfr.addCallback(get_non_full_range_offset)

        dfr.callback(None)

        return dfr
    
    def test_get_album_pictures_wrong_arguments(self):
        def test_wrong_album(result):
            dfr = self.proxy.get_album_pictures(u'not in the library')
            self.failUnlessFailure(dfr, PhotoAlbumNotFoundError)
            return dfr

        dfr = defer.Deferred()
        dfr.addCallback(test_wrong_album)
        dfr.callback(None)

        return dfr

    def test_get_album_pictures(self):
        def iterator():
            for n_album in xrange(self.number_albums):
                album_name = PhotoAlbumName(n_album)

                def get_album_pictures_cb(result):
                    self.failUnlessEqual(len(result), self.number_images)

                dfr = self.proxy.get_album_pictures(album_name)
                dfr.addCallback(get_album_pictures_cb)

                yield dfr

        dfr = task.coiterate(iterator())
        return dfr
    
    def test_get_last_displayed_picture(self):
        def check_last_played(last_played_tuple):
            self.failUnlessEqual(len(last_played_tuple), 2)

            album_name, path = last_played_tuple
            self.failUnlessEqual(self._last_displayed_picture_path, path)

        dfr = self.proxy.get_last_displayed_picture()
        dfr.addCallback(check_last_played)
        return dfr

    def test_last_displayed_picture_signal(self):
        class ImageModel(object):
            def __init__(self, filename):
                self.references = [MediaUri('file://' + filename)]

        # create a fake Player
        class FakePlayer(object):
            pass

        player = FakePlayer()
        tuples = []
        playlist = []

        # inserting database like ImageModels
        for i in xrange(2):
            album_name = PhotoAlbumName(i)
            picture_path = ImageFilePath(album_name, i+1)
            tuples.append((album_name, picture_path))
            playlist.append(ImageModel(picture_path))

        # inserting a non database like ImageModel
        picture_path = ImageFilePath("a_name", 42)
        playlist.append(ImageModel(picture_path))

        def simulate_playing(result, picture, index):
            return self.service._slideshow_player_current_picture_changed_cb(player,
                    picture, index)

        def pop_and_check_tuple(result, expected):
            tup, self.service._last_displayed_picture_tuple = \
                    self.service._last_displayed_picture_tuple, None
            self.failUnlessEqual(tup, expected)

            return result

        dfr = defer.Deferred()
        # simulate the first playing event, last == None, emit
        dfr.addCallback(simulate_playing, playlist[0], 0)
        dfr.addCallback(pop_and_check_tuple, tuples[0])
        # simulate another playing event, last == current, don't emit
        dfr.addCallback(simulate_playing, playlist[0], 0)
        dfr.addCallback(pop_and_check_tuple, None)
        # change picture, emit event, last != current, emit
        dfr.addCallback(simulate_playing, playlist[1], 1)
        dfr.addCallback(pop_and_check_tuple, tuples[1])
        # change picture to a non database one, last != current but last is not
        # from the database, don't emit
        dfr.addCallback(simulate_playing, playlist[2], 2)
        dfr.addCallback(pop_and_check_tuple, None)
        # who-hoo
        dfr.callback(None)

        return dfr

class TestMusicDBus(TestMusicMixin, TestCase):
    def _get_proxy(self, service):
        bus = dbus.SessionBus()
        proxy = bus.get_object(self.connection, 
                '/com/fluendo/Elisa/Plugins/Database/Music')
        return DeferredDBusMusicProxy(proxy)

class TestPhotoDBus(TestPhotoMixin, TestCase):
    def _get_proxy(self, service):
        bus = dbus.SessionBus()
        proxy = bus.get_object(self.connection, 
                '/com/fluendo/Elisa/Plugins/Database/Photo')
        return DeferredDBusPhotoProxy(proxy)

class TestMusicNoDBus(TestMusicMixin, TestCase):
    test_dbus = False

    def _get_proxy(self, service):
        return service.music

class TestPhotoNoDBus(TestPhotoMixin, TestCase):
    test_dbus = False
    
    def _get_proxy(self, service):
        return service.photo
