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

import os

from elisa.core import common
from elisa.core import plugin_registry as plugin_registry_module
from elisa.core.plugin_registry import PluginRegistry, \
        PluginAlreadyEnabled, PluginAlreadyDisabled, InvalidComponentPath, \
        PluginNotFound, DeserializationError
from elisa.core.utils.misc import pkg_resources_copy_dir as copy_dir
from elisa.core.config import Config
from twisted.trial.unittest import TestCase
from twisted.python import reflect
from twisted.internet import defer

import pkg_resources

class FakeApplication(object):
    def __init__(self, config_file, plugin_registry, bus):
        self.config = Config(config_file)
        self.config.set_section('general')
        self.plugin_registry = plugin_registry
        self.bus = bus

class FakeBus(object):
    def __init__(self):
        self.messages = []

    def send_message(self, message):
        self.messages.append(message)


class DownloadError(Exception):
    pass


class PluginRegistryMock(PluginRegistry):
    def __init__(self, *args, **kw):
        super(PluginRegistryMock, self).__init__(*args, **kw)
        self.gst_paths = []
        self._plugin_cache = plugin_registry_module._default_plugin_cache

    def _add_gstreamer_path(self, path):
        self.gst_paths.append(path)

    def _get_plugin_cache(self):
        return self._plugin_cache

    def _set_plugin_cache(self, cache):
        self._plugin_cache = cache

    plugin_cache = property(_get_plugin_cache, _set_plugin_cache)

    def _get(self, uri):
        if uri == 'http://fake-plugin-uri':
            return defer.succeed('fake-plugin-data')
        elif uri == 'http://broken-plugin-uri':
            return defer.fail(DownloadError())
        elif uri.startswith('http://default-repository'):
            return defer.succeed('[]')
        elif uri.startswith('http://broken-json'):
            return defer.succeed('broken_json')
        elif uri.startswith('http://broken-repository'):
            return defer.fail(DownloadError())


class TestPluginRegistryMixin(object):
    plugin_registry = None
    old_application = None

    def setUp(self): 
        # do everything under _trial_temp/test_plugin_registry
        self.test_dir = os.path.abspath('test_plugin_registry')
        pkg_resources.ensure_directory(self.test_dir)
        self.plugins_dir = self.test_dir

        # copy the test plugins
        self.copy_plugins()

        self.install_plugin('test_simple-0.1', '0.1/test_simple')

        # setup common.application
        self.patch_application()

    def tearDown(self):
        self.uninstall_plugin('test_simple-0.1')
        self.unpatch_application()

    def patch_application(self):
        """
        Setup common.application, saving the old application object.

        Make common.application a generic object so we can set
        common.application.config and common.application.plugin_registry
        """
        assert self.old_application is None

        self.old_application = common.application
        config_file = os.path.join(self.test_dir,
                'test_plugin_registry.conf')

        self.bus = FakeBus()
        common.application = FakeApplication(config_file,
                self.plugin_registry, self.bus)

    def unpatch_application(self):
        """
        Restore the application object saved in patch_application().
        """
        common.application = self.old_application
        self.old_application = None

    def copy_plugins(self):
        dest_dir = os.path.join(self.test_dir, '0.1', 'test_simple')
        copy_dir('elisa.core', 'tests/data/plugins/test_simple-0.1', dest_dir) 
        dest_dir = os.path.join(self.test_dir, '0.2', 'test_simple')
        copy_dir('elisa.core', 'tests/data/plugins/test_simple-0.2', dest_dir) 

    def install_plugin(self, link_name, path):
        egg_link = os.path.join(self.test_dir, link_name + '.egg-link')
        link = file(egg_link, 'w')
        link.write(path + '\n')
        link.close()

    def uninstall_plugin(self, path):
        egg_link = os.path.join(self.test_dir, path + '.egg-link')
        assert os.path.exists(egg_link)
        os.unlink(egg_link)


class TestPluginRegistry(TestPluginRegistryMixin, TestCase):
    plugin_registry = None

    def setUp(self):
        TestPluginRegistryMixin.setUp(self)

        # create the PluginRegistry instance used by the tests
        self.plugin_registry = PluginRegistryMock(Config(), [self.plugins_dir])
        self.plugin_registry.load_plugins()

    def test_import(self):
        """
        Import a component from a plugin.

        Check that the normal import statement works correctly.
        """
        from elisa.plugins.test_simple.test_module import TestComponent

    def test_gstreamer_plugin(self):
        """
        Import a component from a plugin.

        Check that the normal import statement works correctly.
        """
        from elisa.plugins.test_simple.test_module import TestComponent
        self.failUnless(self.plugin_registry.gst_paths)

    def test_create(self):
        """
        Create a component calling ComponentClass.create().

        Check that a component can be created given its class.
        """
        from elisa.plugins.test_simple.test_module import TestComponent

        path = 'elisa.plugins.test_simple.test_module:TestComponent'
        self.assertEqual(TestComponent.path, path)

        def clean(component):
            self.assertTrue(component.initialize_called)
            return component.clean()

        d = TestComponent.create()
        d.addCallback(clean)

        return d

    def test_create_component_factory(self):
        """
        Create a component with PluginRegistry.create_component.

        Check that a plugin can be created given its path.
        """
        def clean(component):
            klass = reflect.getClass(component)
            path = 'elisa.plugins.test_simple.test_module:TestComponent'
            self.assertEqual(klass.path, path)
            
            # this seems silly but was actually broken before
            from elisa.plugins.test_simple.test_module import TestComponent
            self.failUnless(isinstance(component, TestComponent))

            return component.clean()

        path = 'test_simple.test_module:TestComponent'
        d = self.plugin_registry.create_component(path)
        d.addCallback(clean)

        return d

    def test_create_component_factory_invalid_path(self):
        path = 'i_should_contain_a_colon'
        d = self.plugin_registry.create_component(path)
        self.failUnlessFailure(d, InvalidComponentPath)

    def test_get_plugin_names(self):
        """
        Get the list of available plugins.
        """
        plugins = list(self.plugin_registry.get_plugin_names())
        self.failUnlessIn('elisa-plugin-test-simple', plugins)

    def test_default_config(self):
        """
        Create a component and check that the default configuration is loaded.
        """
        from elisa.plugins.test_simple.test_module import TestComponent

        def check_config(component):
            # Component.create() should load the configuration
            self.assertEqual(component.config['sample_config_key'], 'default value')

            return component.clean()

        d = TestComponent.create()
        d.addCallback(check_config)

        return d

    def test_application_config(self):
        """
        Create a component and check that the configuration is loaded from the
        configuration file.
        """
        from elisa.plugins.test_simple.test_module import TestComponent

        config = common.application.config

        path = 'test_simple.test_module:TestComponent'
        user_value = 'user value'
        config.set_section(path, {})
        config.set_option('sample_config_key', user_value, section=path)

        def check_config(component):
            self.assertEqual(component.config['sample_config_key'], user_value)

            return component.clean()

        d = TestComponent.create()
        d.addCallback(check_config)

        return d

    def test_hot_upgrade(self):
        """
        Load a component from a plugin, then upgrade the plugin and check that
        the component is reloaded.
        """
        from elisa.plugins.test_simple.test_module import TestComponent
        from elisa.plugins.test_simple import test_module

        self.failUnlessEqual(test_module.version, '0.1')

        def upgrade_plugin(component):
            res = component.do_something()
            self.failUnlessEqual(res, 'done in 0.1')

            self.install_plugin('test_simple-0.2', '0.2/test_simple')
            self.plugin_registry.load_plugins()

            self.failUnlessEqual(test_module.version, '0.2')
            res = component.do_something()
            self.failUnlessEqual(res, 'done in 0.2')

            return component.clean()

        d = TestComponent.create()
        d.addCallback(upgrade_plugin)

        return d

    test_hot_upgrade.todo = "Reimplement Hot Upgrade"

    def test_reload_cache(self):
        self.plugin_registry.plugin_cache = 'non-existant-cache'
        result = self.plugin_registry.reload_cache()
        self.failUnlessEquals(result, False)

        self.plugin_registry.plugin_cache = \
            pkg_resources.resource_filename('elisa.core',
                                            'tests/data/plugins/plugins.cache')
        result = self.plugin_registry.reload_cache()
        self.failUnlessEquals(result, True)

    def test_update_cache(self):
        self.cache_content = None
        self.plugin_registry.plugin_cache = os.path.join(self.test_dir,
                                                         'plugin.cache')

        def test_result(result, expected):
            self.failUnlessEquals(result, expected)
            return result

        def remember_cache_content(result):
            self.cache_content = open(self.plugin_registry.plugin_cache).read()

        def check_cache_content(result):
            content = open(self.plugin_registry.plugin_cache).read()
            self.failUnlessEquals(content, self.cache_content)

        def test_good_repository():
            self.plugin_registry.config['repository'] = 'http://default-repository'
            dfr = self.plugin_registry.update_cache()
            dfr.addCallback(test_result, self.plugin_registry.plugin_cache)
            dfr.addCallback(remember_cache_content)
            return dfr

        def test_broken_json(result):
            self.plugin_registry.config['repository'] = 'http://broken-json'
            dfr = self.plugin_registry.update_cache()
            def failed(failure):
                failure.trap(DeserializationError)
            dfr.addErrback(failed)
            dfr.addCallback(check_cache_content)
            return dfr

        def test_broken_repository(result):
            self.plugin_registry.config['repository'] = 'http://broken-repository'
            dfr = self.plugin_registry.update_cache()
            def failed(failure):
                failure.trap(DownloadError)
            dfr.addErrback(failed)
            dfr.addCallback(check_cache_content)
            return dfr

        dfr = test_good_repository()
        dfr.addCallback(test_broken_json)
        dfr.addCallback(test_broken_repository)
        return dfr

    def test_get_downloadable_plugins(self):
        # Load a sample cache with describing one plugin
        self.plugin_registry.plugin_cache = \
            pkg_resources.resource_filename('elisa.core',
                                            'tests/data/plugins/plugins.cache')
        plugins = self.plugin_registry.get_downloadable_plugins(reload_cache=True)
        self.failUnlessEquals(len(plugins), 1)

        # Load a broken cache
        self.plugin_registry.plugin_cache = \
            pkg_resources.resource_filename('elisa.core',
                                            'tests/data/plugins/plugins.broken_cache')
        plugins = self.plugin_registry.get_downloadable_plugins(reload_cache=True)
        self.failUnlessEquals(len(plugins), 0)

    def test_download_plugin(self):
        plugin_dir = os.path.join(self.test_dir, 'plugins')
        if not os.path.exists(plugin_dir):
            os.mkdir(plugin_dir)
        plugin_registry_module.default_plugin_dirs = [plugin_dir]

        plugin = {'egg_name': 'test.egg',
                  'uri': 'http://fake-plugin-uri'}
        dfr = self.plugin_registry.download_plugin(plugin)

        def test_result(result):
            egg = os.path.join(plugin_dir, plugin['egg_name'])
            self.failUnlessEquals(result, egg)
            self.failUnlessEquals(open(egg).read(), 'fake-plugin-data')

        return dfr.addCallback(test_result)

    test_download_plugin.todo = "Add MD5 sum check support to the test"


class TestPluginActivation(TestPluginRegistryMixin, TestCase):
    def setUp(self):
        TestPluginRegistryMixin.setUp(self)

        # Create the PluginRegistry instance used by the tests
        self.plugin_registry = PluginRegistry(Config(), [self.plugins_dir])

        # Disable all the plugins
        plugins = [plugin.key for plugin in pkg_resources.working_set \
                   if plugin.key.startswith('elisa')]
        plugins.append('elisa-plugin-test-simple')
        self.plugin_registry.load_plugins(plugins)
        config = common.application.config
        config.set_option('disabled_plugins', ['elisa-plugin-test-simple'],
                          section='general')
        # No plugin should be enabled
        return self.plugin_registry.enable_plugins()

    def check_disabled_plugins(self, expected):
        config = common.application.config
        disabled_plugins = config.get_option('disabled_plugins',
                                             section='general', default=[])
        self.failUnlessEqual(disabled_plugins, expected)

    def test_get_plugins(self):
        plugins = [plugin_name for plugin_name, status in \
                   self.plugin_registry.get_plugins()]
        self.failUnlessIn('elisa-plugin-test-simple', plugins)

    def test_enable_plugin(self):
        def enable_plugin(result):
            plugins = list(self.plugin_registry.get_enabled_plugins())
            self.failUnlessEqual(len(plugins), 0)
            self.check_disabled_plugins(['elisa-plugin-test-simple'])
            return self.plugin_registry.enable_plugin('elisa-plugin-test-simple')

        def check_plugins(result):
            plugins = list(self.plugin_registry.get_enabled_plugins())
            self.failUnlessEqual(len(plugins), 1)
            # we should get a plugin enabled message
            self.failUnlessEqual(len(self.bus.messages), 1)
            message = self.bus.messages[0]
            self.failUnlessEqual(message.plugin_name, 'elisa-plugin-test-simple')
            self.failUnlessEqual(message.action, message.ActionType.ENABLED)
            self.check_disabled_plugins([])

        def enable_again(result):
            enable_dfr = self.plugin_registry.enable_plugin('elisa-plugin-test-simple')
            dfr = self.failUnlessFailure(enable_dfr, PluginAlreadyEnabled)
            return dfr

        def check_again_plugins(result):
            plugins = list(self.plugin_registry.get_enabled_plugins())
            self.failUnlessEqual(len(plugins), 1)
            # we shouldn't get a message here
            self.failUnlessEqual(len(self.bus.messages), 1)
            self.check_disabled_plugins([])

        dfr = defer.succeed(None)
        dfr.addCallback(enable_plugin)
        dfr.addCallback(check_plugins)
        dfr.addCallback(enable_again)
        dfr.addCallback(check_again_plugins)
        return dfr

    def test_enable_not_found(self):
        def check_plugins(result):
            plugins = list(self.plugin_registry.get_enabled_plugins())
            self.failUnlessEqual(len(plugins), 0)
            # we shouldn't get a message here
            self.failUnlessEqual(len(self.bus.messages), 0)        
            self.check_disabled_plugins(['elisa-plugin-test-simple'])

        plugins = list(self.plugin_registry.get_enabled_plugins())
        self.failUnlessEqual(len(plugins), 0)
        self.check_disabled_plugins(['elisa-plugin-test-simple'])

        enable_dfr = self.plugin_registry.enable_plugin('icannotpossiblyexist')
        dfr = self.failUnlessFailure(enable_dfr, PluginNotFound)
        dfr.addCallbacks(check_plugins)
        return dfr

    def test_disable_plugin(self):
        def disable_not_enabled(result):
            # start empty
            plugins = list(self.plugin_registry.get_enabled_plugins())
            self.failUnlessEqual(len(plugins), 0)
            self.check_disabled_plugins(['elisa-plugin-test-simple'])

            # disable a plugin that is not enabled
            disable_dfr = self.plugin_registry.disable_plugin('elisa-plugin-test-simple')
            dfr = self.failUnlessFailure(disable_dfr, PluginAlreadyDisabled)
            return dfr

        def check_no_plugins_no_messages(result):
            plugins = list(self.plugin_registry.get_enabled_plugins())
            self.failUnlessEqual(len(plugins), 0)
            # we shouldn't get a message here
            self.failUnlessEqual(len(self.bus.messages), 0)
            self.check_disabled_plugins(['elisa-plugin-test-simple'])

        def enable_plugin(result):
            # enable a plugin
            return self.plugin_registry.enable_plugin('elisa-plugin-test-simple')

        def check_one_enabled(result):
            plugins = list(self.plugin_registry.get_enabled_plugins())
            self.failUnlessEqual(len(plugins), 1)
            # we should get a plugin enabled message
            self.failUnlessEqual(len(self.bus.messages), 1)
            message = self.bus.messages[0]
            self.failUnlessEqual(message.plugin_name, 'elisa-plugin-test-simple')
            self.failUnlessEqual(message.action, message.ActionType.ENABLED)
            self.check_disabled_plugins([])

        def disable_enabled(result):
            # disable the plugin
            return self.plugin_registry.disable_plugin('elisa-plugin-test-simple',
                                                       permanent=True)

        def check_no_plugins_one_disabled_message(result):
            plugins = list(self.plugin_registry.get_enabled_plugins())
            self.failUnlessEqual(len(plugins), 0)
            message = self.bus.messages[1]
            self.failUnlessEqual(message.plugin_name, 'elisa-plugin-test-simple')
            self.failUnlessEqual(message.action, message.ActionType.DISABLED)
            self.check_disabled_plugins(['elisa-plugin-test-simple'])

        dfr = defer.succeed(None)
        dfr.addCallback(disable_not_enabled)
        dfr.addCallback(check_no_plugins_no_messages)
        dfr.addCallback(enable_plugin)
        dfr.addCallback(check_one_enabled)
        dfr.addCallback(disable_enabled)
        dfr.addCallback(check_no_plugins_one_disabled_message)
        return dfr

    def test_disable_not_found(self):
        def check_plugins(result):
            # check that it's still empty
            plugins = list(self.plugin_registry.get_enabled_plugins())
            self.failUnlessEqual(len(plugins), 0)
            # we shouldn't get a message here
            self.failUnlessEqual(len(self.bus.messages), 0)
            self.check_disabled_plugins(['elisa-plugin-test-simple'])

        # start empty
        plugins = list(self.plugin_registry.get_enabled_plugins())
        self.failUnlessEqual(len(plugins), 0)

        # disable something that is not enabled
        disable_dfr = self.plugin_registry.disable_plugin('icannotpossiblyexist')
        dfr = self.failUnlessFailure(disable_dfr, PluginNotFound)
        dfr.addCallback(check_plugins)
        return dfr

class TestPluginLoadActivation(TestPluginRegistryMixin, TestCase):
    def test_load_plugins(self):
        self.plugin_registry = PluginRegistry(Config(), [self.plugins_dir])

        # Disable all plugins but our test one
        plugins = [plugin.key for plugin in pkg_resources.working_set \
                   if plugin.key.startswith('elisa') and \
                   plugin.key != 'elisa-plugin-test-simple']
        self.plugin_registry.load_plugins(plugins)

        def check_plugin_activation(result):
            plugins = list(self.plugin_registry.get_enabled_plugins())
            self.failUnlessEqual(len(plugins), 1)
            return result

        dfr = self.plugin_registry.enable_plugins()
        dfr.addCallback(check_plugin_activation)
        return dfr
