# -*- 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.

from twisted.trial.unittest import TestCase
from twisted.internet import defer, reactor
from twisted.python import reflect

from elisa.core import common
from elisa.core.config import Config
from elisa.core.utils.misc import pkg_resources_copy_dir

from elisa.plugins.pigment.pigment_frontend import PigmentFrontendExtensionMixin
from elisa.plugins.pigment.pigment_controller import PigmentController

import pkg_resources
import os.path


class TestController(PigmentController):
    decorated = 0

class GenericAlbumController(TestController):
    pass

class SpecificAlbumController(TestController):
    pass

class AlternateGenericAlbumController(TestController):
    pass

class MustWaitForDecoratorsControllerStub(TestController):
    def ready(self):
        if self.decorated == 0:
            raise Exception('mustbedecorated')

def decorator(controller):
    controller.decorated += 1

    def on_clean(controller):
        controller.decorated -= 1
        return defer.succeed(None)

    controller.connect('clean', on_clean)

    return defer.succeed(None)

def failing_decorator(controller):
    raise Exception('woot')

def failing_decorator_two(controller):
    return defer.fail(Exception('woot'))

def returns_none_decorator(controller):
    controller.decorated += 1
    return None

class PluginRegistryStub(object):
    def get_enabled_plugins(self):
        return ['elisa-plugin-test-pigment-frontend']


class BusStub(object):
    def register(self, callback, message_class):
        pass


class ApplicationStub(object):
    def __init__(self):
        self.plugin_registry = PluginRegistryStub()
        self.bus = BusStub()


class TestPigmentFrontendExtensionMixin(TestCase):
    def setUp(self):
        self._patch_application()
        self.frontend = PigmentFrontendExtensionMixin()

    def tearDown(self):
        self._unpatch_application()

    def _patch_application(self):
        self._old_application = common.application
        common.application = ApplicationStub()

    def _unpatch_application(self):
        common.application = self._old_application

    def test_create(self):
        """
        Register and create two controllers. Test that two patterns don't
        interfere with each other.
        """
        self.frontend.add_controller('/elisa/album/.*',
                GenericAlbumController)
        self.frontend.add_controller('/elisa/album/Acid Food',
                SpecificAlbumController)

        def first_controller_created_callback(controller):
            self.failUnless(isinstance(controller, GenericAlbumController))
            self.failUnlessEqual(controller.path, '/elisa/album/album 1')
            
            return self.frontend.create_controller('/elisa/album/Acid Food',
                    Config())

        def second_controller_created_callback(controller):
            self.failUnless(isinstance(controller, SpecificAlbumController))
            self.failUnlessEqual(controller.path, '/elisa/album/Acid Food')

            return controller

        dfr = self.frontend.create_controller('/elisa/album/album 1',
                Config())
        dfr.addCallback(first_controller_created_callback)
        dfr.addCallback(second_controller_created_callback)

        return dfr

    def test_ready_called_after_decorators(self):
        """
        Register and create controller. 
        Test if the ready() function is properly called after finishing 
        all decorators.
        """
        self.frontend.add_controller('/elisa/mustwait', 
                MustWaitForDecoratorsControllerStub)
        self.frontend.add_decorator('/elisa/mustwait', decorator)
        
        def first_controller_created_callback(controller):
            self.failUnless(isinstance(controller, MustWaitForDecoratorsControllerStub))
            self.failUnlessEqual(controller.path, '/elisa/mustwait')

            return controller

        dfr = self.frontend.create_controller('/elisa/mustwait', Config() )
        dfr.addCallback(first_controller_created_callback)

    def test_create_overridden(self):
        """
        Register two controllers for the same path_pattern. Test that the second
        controller overrides the first.
        """
        def first_controller_created_callback(controller):
            self.failUnless(isinstance(controller, GenericAlbumController))
            self.failUnlessEqual(controller.path, '/elisa/album/album 1')

            self.frontend.add_controller('/elisa/album/.*',
                    AlternateGenericAlbumController)
            return self.frontend.create_controller('/elisa/album/album 1',
                    Config())

        def second_controller_created_callback(controller):
            self.failUnless(isinstance(controller,
                    AlternateGenericAlbumController))
            self.failUnlessEqual(controller.path, '/elisa/album/album 1')

            return controller
        
        self.frontend.add_controller('/elisa/album/.*',
                GenericAlbumController)
        dfr = self.frontend.create_controller('/elisa/album/album 1',
                Config())
        dfr.addCallback(first_controller_created_callback)

        return dfr

    def test_decorate(self):
        """
        Register a controller decorator to decorate a controller.
        """
        self.frontend.add_controller('/elisa/album/.*', GenericAlbumController)
        self.frontend.add_decorator('/elisa/album/.*', decorator)

        def create_controller_callback(controller):
            self.failUnlessEqual(controller.decorated, 1)

        dfr = self.frontend.create_controller('/elisa/album/', Config())
        dfr.addCallback(create_controller_callback)

        return dfr

    def test_decorate_path(self):
        """
        Register a controller decorator using a path string instead of a
        callable.
        """
        self.frontend.add_controller('/elisa/album/.*', GenericAlbumController)
        path = 'elisa.plugins.pigment.tests.test_pigment_frontend:decorator'
        self.frontend.add_decorator('/elisa/album/.*', path)

        def create_controller_callback(controller):
            self.failUnlessEqual(controller.decorated, 1)

        dfr = self.frontend.create_controller('/elisa/album/', Config())
        dfr.addCallback(create_controller_callback)

        return dfr
    
    def test_decorate_unexisting_path(self):
        """
        Register a controller decorator using a path string instead of a
        callable.
        """
        self.frontend.add_controller('/elisa/album/.*', GenericAlbumController)
        path = 'elisa.plugins.pigment.tests.test_pigment_frontend:i_am_not_here'
        self.frontend.add_decorator('/elisa/album/.*', path)

        def create_controller_callback(controller):
            self.failUnlessEqual(controller.decorated, 0)

        dfr = self.frontend.create_controller('/elisa/album/', Config())
        dfr.addCallback(create_controller_callback)

        return dfr

    def test_decorate_multiple(self):
        """
        Test multiple decorators.
        """
        self.frontend.add_controller('/elisa/album/.*', GenericAlbumController)
        self.frontend.add_decorator('/elisa/album/.*', decorator)
        self.frontend.add_decorator('/elisa/album/.*', decorator)
        self.frontend.add_decorator('/elisa/album/.*', decorator)

        def create_controller_callback(controller):
            self.failUnlessEqual(controller.decorated, 3)

        dfr = self.frontend.create_controller('/elisa/album/', Config())
        dfr.addCallback(create_controller_callback)

        return dfr
    
    def test_decorate_multiple_with_errors(self):
        """
        Test multiple decorators.
        """
        self.frontend.add_controller('/elisa/album/.*', GenericAlbumController)
        self.frontend.add_decorator('/elisa/album/.*', decorator)
        self.frontend.add_decorator('/elisa/album/.*', failing_decorator)
        self.frontend.add_decorator('/elisa/album/.*', decorator)
        path = 'elisa.plugins.pigment.tests.test_pigment_frontend:i_am_not_here'
        self.frontend.add_decorator('/elisa/album/.*', path)
        self.frontend.add_decorator('/elisa/album/.*', failing_decorator_two)
        self.frontend.add_decorator('/elisa/album/.*', decorator)

        def create_controller_callback(controller):
            self.failUnlessEqual(controller.decorated, 3)

        dfr = self.frontend.create_controller('/elisa/album/', Config())
        dfr.addCallback(create_controller_callback)

        return dfr

    def test_broken_decorator(self):
        """
        Test a decorator not returning a deferred doesn't break the
        controller decoration.
        """
        self.frontend.add_controller('/elisa/album/.*', GenericAlbumController)
        self.frontend.add_decorator('/elisa/album/.*', returns_none_decorator)

        def create_controller_callback(controller):
            self.failUnlessEqual(controller.decorated, 1)

        dfr = self.frontend.create_controller('/elisa/album/', Config())
        dfr.addCallback(create_controller_callback)

        return dfr

    def test_decorate_clean(self):
        """
        Test the clean signal as used by a decorator.
        """
        self.frontend.add_controller('/elisa/album/.*', GenericAlbumController)
        self.frontend.add_decorator('/elisa/album/.*', decorator)

        def create_controller_callback(controller):
            self.failUnlessEqual(controller.decorated, 1)
            return controller

        def clean_controller(controller):
            dfr = controller.clean()
            dfr.addCallback(lambda ignored: controller)
            return dfr

        def check_clean(controller):
            self.failUnlessEqual(controller.decorated, 0)
            
        dfr = self.frontend.create_controller('/elisa/album/', Config())
        dfr.addCallback(create_controller_callback)
        dfr.addCallback(clean_controller)
        dfr.addCallback(check_clean)

        return dfr

    def test_decorate_clean_multiple(self):
        """
        Test the clean signal as used by multiple decorators.
        """
        self.frontend.add_controller('/elisa/album/.*', GenericAlbumController)
        self.frontend.add_decorator('/elisa/album/.*', decorator)
        self.frontend.add_decorator('/elisa/album/.*', decorator)
        self.frontend.add_decorator('/elisa/album/.*', decorator)

        def create_controller_callback(controller):
            self.failUnlessEqual(controller.decorated, 3)
            return controller

        def clean_controller(controller):
            dfr = controller.clean()
            dfr.addCallback(lambda ignored: controller)
            return dfr

        def check_clean(controller):
            self.failUnlessEqual(controller.decorated, 0)

        dfr = self.frontend.create_controller('/elisa/album/', Config())
        dfr.addCallback(create_controller_callback)
        dfr.addCallback(clean_controller)
        dfr.addCallback(check_clean)

        return dfr

    def test_retrieve_single_controller(self):
        """
        Test retrieval of an existing instance of a controller.
        """
        path = '/elisa/album/album 1'
        self.frontend.add_controller('/elisa/album/.*', GenericAlbumController)

        def controller_created_callback(controller):
            saved_controllers = self.frontend.retrieve_controllers(path)
            self.failUnless(controller in saved_controllers)

        dfr = self.frontend.create_controller(path, Config())
        dfr.addCallback(controller_created_callback)

        return dfr

    def test_retrieve_multiple_controllers(self):
        """
        Test retrieval of multiple controller instances that correspond to
        paths with a common part.
        """
        self.frontend.add_controller('/elisa/album/.*',
                GenericAlbumController)
        self.frontend.add_controller('/elisa/album/Acid Food',
                SpecificAlbumController)

        self.first_controller = None
        self.second_controller = None

        def first_controller_created_callback(controller):
            self.first_controller = controller
            return self.frontend.create_controller('/elisa/album/Acid Food',
                    Config())

        def second_controller_created_callback(controller):
            self.second_controller = controller

            path = '/elisa/album/album 1'
            saved_controllers = self.frontend.retrieve_controllers(path)
            self.failUnless(self.first_controller in saved_controllers)
            self.failUnless(self.second_controller not in saved_controllers)

            path = '/elisa/album/Acid Food'
            saved_controllers = self.frontend.retrieve_controllers(path)
            self.failUnless(self.second_controller in saved_controllers)
            self.failUnless(self.first_controller not in saved_controllers)

        dfr = self.frontend.create_controller('/elisa/album/album 1',
                Config())
        dfr.addCallback(first_controller_created_callback)
        dfr.addCallback(second_controller_created_callback)

        return dfr

    def test_retrieve_dropped_controllers(self):
        """
        Test that the frontend releases controllers that are not used anymore.
        """
        path = '/elisa/album/album 1'
        self.frontend.add_controller('/elisa/album/.*', GenericAlbumController)

        def controller_created_callback(controller):
            controller.clean()

        def controller_dropped_callback(result):
            saved_controllers = self.frontend.retrieve_controllers(path)
            self.failUnlessEqual(len(saved_controllers), 0)

        dfr = self.frontend.create_controller(path, Config())
        dfr.addCallback(controller_created_callback)
        dfr.addCallback(controller_dropped_callback)

        return dfr


class PigmentFrontendExtensionMixinStub(PigmentFrontendExtensionMixin):

    def __init__(self):
        self.added_controllers = []
        self.added_decorators = []
        super(PigmentFrontendExtensionMixinStub, self).__init__()

    def add_controller(self, path_pattern, controller):
        self.added_controllers.append((path_pattern, controller))

    def remove_controller(self, path_pattern):
        for index, (pattern, controller) in enumerate(self.added_controllers):
            if pattern == path_pattern:
                self.added_controllers.pop(index)
                return

    def add_decorator(self, path_pattern, decorator):
        self.added_decorators.append((path_pattern, decorator))

    def remove_decorator(self, path_pattern):
        for index, (pattern, decorator) in enumerate(self.added_decorators):
            if pattern == path_pattern:
                self.added_decorators.pop(index)
                return


class TestPigmentFrontendExtensionMixinMappings(TestCase):
    def setUp(self):
        # do everything under _trial_temp/test_pigment_plugin
        self.test_dir = os.path.abspath('test_pigment_plugin')
        pkg_resources.ensure_directory(self.test_dir)

        self.copy_plugin('tests/data/plugins/test_simple')
        self.load_plugins()

        self._patch_application()
        self.frontend = PigmentFrontendExtensionMixinStub()

    def tearDown(self):
        self._unpatch_application()

    def _patch_application(self):
        self._old_application = common.application
        common.application = ApplicationStub()

    def _unpatch_application(self):
        common.application = self._old_application

    def copy_plugin(self, path):
        # copy the plugin to _trial_temp/test_pigment_plugin
        pkg_resources_copy_dir('elisa.plugins.pigment', path, self.test_dir)

    def load_plugins(self):
        # look up the plugin directory
        env = pkg_resources.Environment([self.test_dir])
        distributions, errors = pkg_resources.working_set.find_plugins(env)

        # add the distributions to the working set
        for dist in distributions:
            pkg_resources.working_set.add(dist)

    def test_loading_controller_mappings(self):
        """
        Test loading controller mappings from a fake plugin.
        """
        plugin_name = 'elisa-plugin-test-pigment-frontend'
        requirement = pkg_resources.Requirement.parse(plugin_name)
        plugin = pkg_resources.working_set.find(requirement)
        self.frontend.plugin_status_changed_cb(plugin, True)

        controller_mappings = set([("test_controller_key1", "test_controller_value1"),
                                   ("test_controller_key2", "test_controller_value2")])
        loaded = set(self.frontend.added_controllers)
        self.failUnless(controller_mappings.issubset(loaded))

    def test_loading_decorator_mappings(self):
        """
        Test loading decorator mappings from a fake plugin.
        """
        plugin_name = 'elisa-plugin-test-pigment-frontend'
        requirement = pkg_resources.Requirement.parse(plugin_name)
        plugin = pkg_resources.working_set.find(requirement)
        self.frontend.plugin_status_changed_cb(plugin, True)

        decorator_mappings = set([("test_decorator_key1", "test_decorator_value1"),
                                  ("test_decorator_key2", "test_decorator_value2")])

        loaded = set(self.frontend.added_decorators)
        self.failUnless(decorator_mappings.issubset(loaded))

    def test_disable_then_enable_plugin(self):
        """
        Test disabling a plugin, then re-enabling it.
        """
        plugin_name = 'elisa-plugin-test-pigment-frontend'
        requirement = pkg_resources.Requirement.parse(plugin_name)
        plugin = pkg_resources.working_set.find(requirement)

        # Disable the plugin.
        self.frontend.plugin_status_changed_cb(plugin, False)
        self.failUnlessEqual(len(self.frontend.added_controllers), 0)
        self.failUnlessEqual(len(self.frontend.added_decorators), 0)

        # Try to disable it again. Nothing should happen.
        self.frontend.plugin_status_changed_cb(plugin, False)

        # Re-enable the plugin.
        self.frontend.plugin_status_changed_cb(plugin, True)
        controller_mappings = \
            set([('test_controller_key1', 'test_controller_value1'),
                 ('test_controller_key2', 'test_controller_value2')])
        loaded_controllers = set(self.frontend.added_controllers)
        self.failUnless(controller_mappings.issubset(loaded_controllers))
        decorator_mappings = \
            set([('test_decorator_key1', 'test_decorator_value1'),
                 ('test_decorator_key2', 'test_decorator_value2')])
        loaded_decorators = set(self.frontend.added_decorators)
        self.failUnless(decorator_mappings.issubset(loaded_decorators))

    def test_initialize_theme(self):
        self.fail(NotImplementedError("Test not yet implemented"))

    test_initialize_theme.todo = "Test not yet implemented"

    def test_get_theme(self):
        self.fail(NotImplementedError("Test not yet implemented"))

    test_get_theme.todo = "Test not yet implemented"

    def test_set_theme(self):
        self.fail(NotImplementedError("Test not yet implemented"))

    test_set_theme.todo = "Test not yet implemented"

    def test_load_from_theme_theme(self):
        self.fail(NotImplementedError("Test not yet implemented"))

    test_load_from_theme_theme.todo = "Test not yet implemented"

