# -*- coding: utf-8 -*-
# Moovida - Home multimedia server
# Copyright (C) 2007-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: Benjamin Kampmann <benjamin@fluendo.com>

from elisa.core import common, log
from elisa.core.media_uri import MediaUri
from elisa.core.config import Config
from elisa.core.plugin_registry import PluginRegistry
from elisa.core.bus import Bus

from elisa.core.utils.misc import pkg_resources_copy_dir as copy_dir
from elisa.core.utils.misc import run_functional_tests_check
from elisa.core.utils import defer

from elisa.plugins.base.tests.resource_providers import UriRegExpTester, \
     GenericSetup
from elisa.plugins.search.search_metaresource_provider import SearchMetaresourceProvider
from elisa.plugins.search.searcher import Searcher

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

import pkg_resources
import os


class FakeApplication(object):
    def __init__(self, config, plugin_registry, bus):
        self.config = config
        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)

    def register(self, callback, message_type):
        pass

class GenericSetupWithApplication(GenericSetup):
    plugin_registry_enabled = False
    old_application = None

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

        if self.plugin_registry_enabled:
            self.plugins_dir = self.test_dir

            # copy the test plugins
            self.copy_plugins()

        # setup common.application
        self.patch_application()
        return super(GenericSetupWithApplication, self).setUp()

    def tearDown(self):
        self.unpatch_application()
        return super(GenericSetupWithApplication, self).tearDown()

    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_generic_setup.conf')
        if self.plugin_registry_enabled:
            self.bus = Bus()
            config = Config(config_file)
            config.set_section('general')
            self.plugin_registry = PluginRegistry(config,
                                                  [self.plugins_dir,])
        else:
            self.plugin_registry = None
            self.bus = FakeBus()
            config = None

        common.set_application(FakeApplication(config,
                                               self.plugin_registry, self.bus))

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

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

    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 TestUris(GenericSetupWithApplication, UriRegExpTester, TestCase):
    resource_class = SearchMetaresourceProvider
    working_uris = [ 'elisa://search/music', 'elisa://search/pictures',
                     'elisa://search/music/madonna?filter=artist',
                     'elisa://search/music/music?filter=albums',
                     'elisa://search/videos/water?filter=title',
                     'elisa://search/music/water?filter=albums,artists,tracks']
    failing_uris = ['elisa:///', 'elisa://', 'elisa://search_something/',
                    'elisas://search/',
                    'elisa://search/someone']

class BitterSearcher(Searcher):
    """
    This dummy searcher gets angry when he is unreferenced without being cleaned
    up
    """
    def __init__(self):
        super(BitterSearcher, self).__init__()
        self._cleaned = False

    def clean(self):
        self._cleaned = True
        return super(BitterSearcher, self).clean()

    def __del__(self):
        assert self._cleaned

class DummySearcher(Searcher):
    def __init__(self):
        self.searches = []
    def search(self, *args):
        self.searches.append(args)
        return defer.succeed(self)

class HangingSearcher(Searcher):
    def __init__(self):
        self.cancelled = False
    def cancel(self, dfr):
        self.cancelled = True
    def search(self, *args):
        return defer.Deferred(self.cancel)

class UnloadMixin(object):

    def _unload_searchers(self, old_result):
        searchers = self.resource._searchers_by_path.values()
        self.resource._searchers_by_path = {}

        full_list = []
        for searcher_list in searchers:
            full_list.extend(searcher_list)

        dfrs = []
        # filter them: each only once
        for searcher in set(full_list):
            dfrs.append(searcher.clean())

        return defer.DeferredList(dfrs)

    def _patch_default(self, old_result):
        # we overwrite the default_searcher function to speed up the process
        self.resource._default_searcher = None


class TestSearchers(GenericSetupWithApplication, UnloadMixin, TestCase):
    resource_class = SearchMetaresourceProvider
    config = {'default_searcher' : ''}

    def setUp(self):
        # don't load the pkg_resources
        SearchMetaresourceProvider._load_searchers = lambda x,y: x
        dfr = super(TestSearchers, self).setUp()
        # Whatever is automatically loaded, unload it to have a
        # clean component.
        dfr.addCallback(self._unload_searchers)
        dfr.addCallback(self._patch_default)
        return dfr

    def test_searcher_cleaned(self):
        """
        Adds the L{BitterSearcher} into the searchers list and tears down to
        see if the resource provider is cleaning it up properly.
        """
        def created(searcher):
            self.resource._searchers_by_path['test'] = [searcher]

        dfr = BitterSearcher.create({})
        dfr.addCallback(created)
        return dfr

    def test_searchers_called(self):

        uri = MediaUri('elisa://search/music/madonna')
        self.resource._searchers_by_path['music'] = []
        searchers = []

        def created(searcher):
            self.resource._searchers_by_path['music'].append(searcher)
            searchers.append(searcher)

        def iterate():
            for i in xrange(3):
                dfr = DummySearcher.create({})
                dfr.addCallback(created)
                yield dfr

        def run_test(result):
            model, dfr = self.resource.get(uri)
            return dfr

        def check(result_model):
            for searcher in searchers:
                self.assertEquals(len(searcher.searches), 1)
                req_uri, model = searcher.searches[0]
                self.assertTrue(uri is req_uri)
                self.assertTrue(model is result_model)

        dfr = task.coiterate(iterate())
        dfr.addCallback(run_test)
        dfr.addCallback(check)
        return dfr

    def test_only_default_searchers_called(self):

        uri = MediaUri('elisa://search/music/madonna?only_default=True')
        self.resource._searchers_by_path['music'] = []
        searchers = []

        def created(searcher):
            self.resource._searchers_by_path['music'].append(searcher)
            searchers.append(searcher)

        def iterate():
            for i in xrange(3):
                dfr = DummySearcher.create({})
                dfr.addCallback(created)
                yield dfr

        def run_test(result):
            model, dfr = self.resource.get(uri)
            return dfr

        def set_default_searcher(result):
            def set_def(default):
                self.resource._default_searcher = default

            dfr = DummySearcher.create({})
            dfr.addCallback(set_def)
            return dfr

        def check(result_model):
            # only the default searcher was asked
            default = self.resource._default_searcher
            self.assertEquals(len(default.searches), 1)

            # and no one else
            for searcher in searchers:
                self.assertEquals(len(searcher.searches), 0)

        dfr = task.coiterate(iterate())
        dfr.addCallback(set_default_searcher)
        dfr.addCallback(run_test)
        dfr.addCallback(check)
        return dfr

    def test_only_selected_searchers(self):

        uri = MediaUri('elisa://search/music/madonna?' \
                'only_searchers=searcher0,searcher2')
        self.resource._searchers_by_path['music'] = []
        searchers = []

        def created(searcher, name):
            self.resource._searchers_by_path['music'].append(searcher)
            searcher.name = name
            searchers.append(searcher)

        def iterate():
            for i in xrange(3):
                dfr = DummySearcher.create({})
                dfr.addCallback(created, "searcher%s" % i)
                yield dfr

        def run_test(result):
            model, dfr = self.resource.get(uri)
            return dfr

        def set_default_searcher(result):
            def set_def(default):
                default.name = "searcher-1"
                self.resource._default_searcher = default

            dfr = DummySearcher.create({})
            dfr.addCallback(set_def)
            return dfr

        def check(result_model):
            # the default searcher was not asked
            default = self.resource._default_searcher
            self.assertEquals(len(default.searches), 0)

            # because only searcher0 and searcher2 are asked
            s0, s1, s2 = searchers
            self.assertEquals(len(s0.searches), 1)
            self.assertEquals(len(s1.searches), 0)
            self.assertEquals(len(s2.searches), 1)

        dfr = task.coiterate(iterate())
        dfr.addCallback(set_default_searcher)
        dfr.addCallback(run_test)
        dfr.addCallback(check)
        return dfr

    def test_only_selected_searchers_with_default(self):

        uri = MediaUri('elisa://search/music/madonna?' \
                'only_searchers=searcher0,searcher2,searcher-1')
        self.resource._searchers_by_path['music'] = []
        searchers = []

        def created(searcher, name):
            self.resource._searchers_by_path['music'].append(searcher)
            searcher.name = name
            searchers.append(searcher)

        def iterate():
            for i in xrange(3):
                dfr = DummySearcher.create({})
                dfr.addCallback(created, "searcher%s" % i)
                yield dfr

        def run_test(result):
            model, dfr = self.resource.get(uri)
            return dfr

        def set_default_searcher(result):
            def set_def(default):
                default.name = "searcher-1"
                self.resource._default_searcher = default

            dfr = DummySearcher.create({})
            dfr.addCallback(set_def)
            return dfr

        def check(result_model):
            # the default searcher was asked, because we requested it
            default = self.resource._default_searcher
            self.assertEquals(len(default.searches), 1)

            # because only searcher0 and searcher2 are asked
            s0, s1, s2 = searchers
            self.assertEquals(len(s0.searches), 1)
            self.assertEquals(len(s1.searches), 0)
            self.assertEquals(len(s2.searches), 1)

        dfr = task.coiterate(iterate())
        dfr.addCallback(set_default_searcher)
        dfr.addCallback(run_test)
        dfr.addCallback(check)
        return dfr

    def test_only_selected_and_only_default(self):

        uri = MediaUri('elisa://search/music/madonna?' \
                'only_searchers=a,b,c&only_default=True')
        model, dfr = self.resource.get(uri)
        self.assertFailure(dfr, TypeError)
        return dfr

    def test_cancellable(self):

        uri = MediaUri('elisa://search/music/coldplay')
        self.resource._searchers_by_path['music'] = []
        searchers = []

        def created(searcher):
            self.resource._searchers_by_path['music'].append(searcher)
            searchers.append(searcher)

        def iterate():
            for i in xrange(3):
                dfr = HangingSearcher.create({})
                dfr.addCallback(created)
                yield dfr

        def run_test(result):
            model, dfr = self.resource.get(uri)
            # wait some time before cancelling the deferred
            reactor.callLater(1.0, dfr.cancel)
            return dfr

        def check(failure):
            for searcher in searchers:
                self.assertTrue(searcher.cancelled)
            return None

        dfr = task.coiterate(iterate())
        dfr.addCallback(run_test)
        dfr.addErrback(check)
        return dfr
    test_cancellable.timeout = 5


class SimpleSearcher(object):
    def clean(self):
        return defer.succeed(None)

class TestSearchersAdding(GenericSetupWithApplication, UnloadMixin, TestCase):
    resource_class = SearchMetaresourceProvider
    config = {'default_searcher' : 'default'}

    def setUp(self):
        dfr = super(TestSearchersAdding, self).setUp()
        # what ever there is automatically loaded, we want a clear component, so
        # we unload it
        dfr.addCallback(self._unload_searchers)
        dfr.addCallback(self._patch_default)
        return dfr

    def test_add_searcher(self):
        """
        Test if the usual adding of a normal searcher works as expected
        """
        this_searcher = SimpleSearcher()
        this_searcher.paths = ['test', 42]

        self.resource._add_searcher(this_searcher, 'cool_name')

        searchers = self.resource._searchers_by_path
        self.assertEquals(set(searchers.keys()), set(['test', 42]))
        self.assertEquals(len(searchers['test']), 1)
        self.assertEquals(len(searchers[42]), 1)
        self.assertTrue(searchers['test'][0] is this_searcher)
        self.assertTrue(searchers[42][0] is this_searcher)

    def test_add_searcher_with_uppercase_name(self):
        """
        As the searcher filtering is using lowercase only, the name has to be
        lowercased
        """
        this_searcher = SimpleSearcher()
        this_searcher.paths = ['test', 42]

        self.resource._add_searcher(this_searcher, 'UpperName')
        self.assertEquals(this_searcher.name, 'uppername')

    def test_add_another_searcher(self):

        first_searcher = SimpleSearcher()
        another_searcher = SimpleSearcher()
        self.assertEquals(self.resource._searchers_by_path, {})
        self.resource._searchers_by_path['33'] = [first_searcher]
        self.resource._searchers_by_path['88'] =  \
                    [another_searcher, first_searcher]

        new_searcher = SimpleSearcher()
        new_searcher.paths = ['33', 'test']

        self.resource._add_searcher(new_searcher, 'another_name')

        searchers = self.resource._searchers_by_path
        self.assertEquals(set(searchers.keys()), set(['33','88','test']))
        self.assertEquals(len(searchers['test']), 1)
        self.assertEquals(len(searchers['88']), 2)
        self.assertEquals(len(searchers['33']), 2)
        self.assertTrue(searchers['test'][0] is new_searcher)

        self.assertEquals(searchers['33'], [first_searcher, new_searcher])
        self.assertEquals(searchers['88'],
                [another_searcher, first_searcher])

    def test_adding_default_searcher(self):
        """
        Test if adding the default searcher works as expected
        """
        default = SimpleSearcher()
        default.paths = ['test', 42]

        self.resource._add_searcher(default, 'default')

        searchers = self.resource._searchers_by_path
        self.assertEqual(searchers, {})
        self.assertTrue(self.resource._default_searcher is default)


class TestFunctionalSearch(GenericSetupWithApplication, TestCase):
    resource_class = SearchMetaresourceProvider
    plugin_registry_enabled = True

    def setUp(self):
        run_functional_tests_check()


        def parent_setup(result):
            self.install_plugin('elisa-plugin-test-simple-0.1', '0.1/test_simple')
            # 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()

        dfr = super(TestFunctionalSearch, self).setUp()
        dfr.addCallback(parent_setup)
        return dfr

    def test_dynamic_new_searcher(self):
        """ Enable a plugin containing a searcher and check it is
        correctly registered in the resource_provider.
        """
        self.assertEqual(self.resource._searchers_by_path, {})

        def dispatched(result):
            self.assertEqual(self.resource._searchers_by_path.keys(),
                             ['videos',])
            self.assertEqual(len(self.resource._searchers_by_path['videos']),
                             1)

        def enabled(result):
            dfr = self.bus.deferred_dispatch()
            dfr.addCallback(dispatched)
            return dfr

        dfr = self.plugin_registry.enable_plugin('elisa-plugin-test-simple')
        dfr.addCallback(enabled)
        return dfr

    def test_dynamic_searcher_remove(self):
        """ Disable a plugin containing a searcher and check it is
        correctly unregistered from the resource_provider.
        """

        def enabled(result):
            dfr = self.bus.deferred_dispatch()
            dfr.addCallback(dispatched, True)
            return dfr

        def dispatched(result, enabled):
            if enabled:
                self.assertEqual(len(self.resource._searchers_by_path['videos']),
                             1)
                dfr = self.plugin_registry.disable_plugin('elisa-plugin-test-simple')
                dfr.addCallback(plugin_disabled)
                return dfr
            else:
                self.assertEqual(self.resource._searchers_by_path, {})

        def plugin_disabled(result):
            dfr = self.bus.deferred_dispatch()
            dfr.addCallback(dispatched, False)
            return dfr

        dfr = self.plugin_registry.enable_plugin('elisa-plugin-test-simple')
        dfr.addCallback(enabled)
        return dfr
