# -*- coding: utf-8 -*-
#
# Copyright (C) 2014-2016 Avencall
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>

import unittest

from hamcrest import assert_that
from hamcrest import contains_exactly
from hamcrest import contains_inanyorder
from hamcrest import equal_to
from hamcrest import none
from hamcrest import not_
from mock import Mock
from mock import patch
from mock import sentinel

from xivo_dird import make_result_class
from xivo_dird.plugins.lookup import DEFAULT_MAX_RESULTS
from xivo_dird.plugins.lookup import LookupServicePlugin
from xivo_dird.plugins.lookup import _LookupService


class TestLookupServicePlugin(unittest.TestCase):

    def test_load_no_config(self):
        plugin = LookupServicePlugin()

        self.assertRaises(ValueError, plugin.load, {'sources': sentinel.sources})

    def test_load_no_sources(self):
        plugin = LookupServicePlugin()

        self.assertRaises(ValueError, plugin.load, {'config': sentinel.sources})

    def test_that_load_returns_a_service(self):
        plugin = LookupServicePlugin()

        service = plugin.load({'sources': sentinel.sources,
                               'config': {}})

        assert_that(service, not_(none()))

    @patch('xivo_dird.plugins.lookup._LookupService')
    def test_that_load_injects_config_to_the_service(self, MockedLookupService):
        plugin = LookupServicePlugin()

        service = plugin.load({'config': sentinel.config,
                               'sources': sentinel.sources})

        MockedLookupService.assert_called_once_with(sentinel.config, sentinel.sources)
        assert_that(service, equal_to(MockedLookupService.return_value))

    def test_no_error_on_unload_not_loaded(self):
        plugin = LookupServicePlugin()

        plugin.unload()

    @patch('xivo_dird.plugins.lookup._LookupService')
    def test_that_unload_stops_the_services(self, MockedLookupService):
        plugin = LookupServicePlugin()
        plugin.load({'config': sentinel.config, 'sources': sentinel.sources})

        plugin.unload()

        MockedLookupService.return_value.stop.assert_called_once_with()


class TestLookupService(unittest.TestCase):
    result_class = make_result_class('test')

    def test_that_lookup_passes_down_token_infos(self):
        result1 = self.result_class({'f': 1})
        source = Mock(search=Mock(return_value=[result1]))
        config = {
            'services': {
                'lookup': {
                    'my_profile': {
                        'sources': ['source']
                    }
                }
            }
        }
        s = _LookupService(config, {'source': source})

        s.lookup('', 'my_profile', sentinel.uuid, {}, sentinel.token)

        source.search.assert_called_once_with(
            '',
            {'token': sentinel.token,
             'xivo_user_uuid': sentinel.uuid}
        )

        s.stop()

    def test_that_lookup_searches_only_the_configured_sources(self):
        result1 = self.result_class({'f': 1})
        result2 = self.result_class({'f': 2})
        result3 = self.result_class({'f': 3})
        sources = {
            'source_1': Mock(name='source_1', search=Mock(return_value=[result1])),
            'source_2': Mock(name='source_2', search=Mock(return_value=[result2])),
            'source_3': Mock(name='source_3', search=Mock(return_value=[result3])),
        }
        config = {
            'services': {
                'lookup': {
                    'my_profile': {
                        'sources': ['source_1', 'source_3'],
                        'timeout': 1,
                    }
                }
            }
        }

        s = _LookupService(config, sources)

        results = s.lookup('', 'my_profile', sentinel.uuid, {}, sentinel.token)

        expected_results = [result1, result3]

        assert_that(sources['source_1'].search.call_count, equal_to(1))
        assert_that(sources['source_2'].search.call_count, equal_to(0))
        assert_that(sources['source_3'].search.call_count, equal_to(1))

        assert_that(results, contains_inanyorder(*expected_results))

        s.stop()

    def test_that_lookup_does_not_fail_if_one_config_is_not_correct(self):
        result1 = self.result_class({'f': 1})
        result2 = self.result_class({'f': 2})
        result3 = self.result_class({'f': 3})
        sources = {
            'source_1': Mock(name='source_1', search=Mock(return_value=[result1])),
            'source_2': Mock(name='source_2', search=Mock(return_value=[result2])),
            # 'source_3': Mock(name='source_3', search=Mock(return_value=[result3])),
        }
        config = {
            'services': {
                'lookup': {
                    'my_profile': {
                        'sources': ['source_1', 'source_3'],
                        'timeout': 1,
                    }
                }
            }
        }

        s = _LookupService(config, sources)

        results = s.lookup('', 'my_profile', sentinel.uuid, {}, sentinel.token)

        expected_results = [result1]

        assert_that(sources['source_1'].search.call_count, equal_to(1))
        assert_that(sources['source_2'].search.call_count, equal_to(0))

        assert_that(results, contains_inanyorder(*expected_results))

        s.stop()

    def test_that_lookup_by_labels_searches_each_source_only_once(self):
        result1 = self.result_class({'f': 1})
        result2 = self.result_class({'f': 2})
        sources = {
            'source_1': Mock(name='source_1', search=Mock(return_value=[result1])),
            'source_2': Mock(name='source_2', search=Mock(return_value=[result2])),
        }
        config = {
            'services': {
                'lookup_by_labels': {
                    'labels': {
                        1: {'sources': ['source_1', 'source_2'], 'timeout': 1},
                        2: {'sources': ['source_1'], 'timeout': 2},
                    }
                }
            }
        }

        s = _LookupService(config, sources)

        results = s.lookup_by_labels('', [1, 2], sentinel.uuid, {}, sentinel.token)

        assert_that(sources['source_1'].search.call_count, equal_to(1))
        assert_that(sources['source_2'].search.call_count, equal_to(1))
        assert_that(results, contains_inanyorder(result1, result2))

        s.stop()

    def test_that_lookup_by_labels_uses_the_max_timeout(self):
        sources = {'source_1': Mock(name='source_1', search=Mock(return_value=[]))}
        config = {
            'services': {
                'lookup_by_labels': {
                    'labels': {
                        1: {'sources': ['source_1'], 'timeout': 1},
                        2: {'sources': ['source_1'], 'timeout': 3},
                    }
                }
            }
        }
        s = _LookupService(config, sources)

        with patch.object(s, '_run_sources') as run_sources:
            s.lookup_by_labels('', [1, 2], sentinel.uuid, {}, sentinel.token)

        run_sources.assert_called_once_with(
            [sources['source_1']], '', sentinel.uuid, {}, sentinel.token, 3)
        s.stop()

    def test_that_lookup_by_labels_uses_no_timeout_when_labels_have_no_timeout(self):
        sources = {'source_1': Mock(name='source_1', search=Mock(return_value=[]))}
        config = {
            'services': {
                'lookup_by_labels': {
                    'labels': {
                        1: {'sources': ['source_1']},
                        2: {'sources': ['source_1']},
                    }
                }
            }
        }
        s = _LookupService(config, sources)

        with patch.object(s, '_run_sources') as run_sources:
            s.lookup_by_labels('', [1, 2], sentinel.uuid, {}, sentinel.token)

        run_sources.assert_called_once_with(
            [sources['source_1']], '', sentinel.uuid, {}, sentinel.token, None)
        s.stop()

    def test_that_lookup_by_labels_searches_a_single_label(self):
        result1 = self.result_class({'f': 1})
        sources = {'source_1': Mock(name='source_1', search=Mock(return_value=[result1]))}
        config = {
            'services': {
                'lookup_by_labels': {
                    'labels': {1: {'sources': ['source_1'], 'timeout': 1}}
                }
            }
        }
        s = _LookupService(config, sources)

        results = s.lookup_by_labels('', [1], sentinel.uuid, {}, sentinel.token)

        assert_that(results, contains_inanyorder(result1))
        s.stop()

    def test_that_lookup_by_labels_silently_skips_unmapped_labels(self):
        result1 = self.result_class({'f': 1})
        sources = {'source_1': Mock(name='source_1', search=Mock(return_value=[result1]))}
        config = {
            'services': {
                'lookup_by_labels': {
                    'labels': {1: {'sources': ['source_1'], 'timeout': 1}}
                }
            }
        }
        s = _LookupService(config, sources)

        results = s.lookup_by_labels('', [1, 999], sentinel.uuid, {}, sentinel.token)

        assert_that(sources['source_1'].search.call_count, equal_to(1))
        assert_that(results, contains_inanyorder(result1))
        s.stop()

    def test_that_lookup_by_labels_falls_back_to_default_sources_when_no_labels(self):
        default_result = self.result_class({'f': 'default'})
        label_result = self.result_class({'f': 'label'})
        sources = {
            'default_source': Mock(name='default_source', search=Mock(return_value=[default_result])),
            'label_source': Mock(name='label_source', search=Mock(return_value=[label_result])),
        }
        config = {
            'services': {
                'lookup_by_labels': {
                    'default_sources': {'sources': ['default_source'], 'timeout': 5},
                    'labels': {1: {'sources': ['label_source'], 'timeout': 1}},
                }
            }
        }
        s = _LookupService(config, sources)

        results = s.lookup_by_labels('', [], sentinel.uuid, {}, sentinel.token)

        assert_that(sources['default_source'].search.call_count, equal_to(1))
        assert_that(sources['label_source'].search.call_count, equal_to(0))
        assert_that(results, contains_inanyorder(default_result))
        s.stop()

    def test_that_lookup_by_labels_falls_back_to_defaults_for_unmapped_label(self):
        default_result = self.result_class({'f': 'default'})
        sources = {
            'default_source': Mock(name='default_source', search=Mock(return_value=[default_result])),
        }
        config = {
            'services': {
                'lookup_by_labels': {
                    'default_sources': {'sources': ['default_source'], 'timeout': 5},
                    'labels': {},
                }
            }
        }
        s = _LookupService(config, sources)

        results = s.lookup_by_labels('', [999], sentinel.uuid, {}, sentinel.token)

        assert_that(sources['default_source'].search.call_count, equal_to(1))
        assert_that(results, contains_inanyorder(default_result))
        s.stop()

    def test_that_lookup_by_labels_does_not_add_defaults_when_a_label_matches(self):
        default_result = self.result_class({'f': 'default'})
        label_result = self.result_class({'f': 'label'})
        sources = {
            'default_source': Mock(name='default_source', search=Mock(return_value=[default_result])),
            'label_source': Mock(name='label_source', search=Mock(return_value=[label_result])),
        }
        config = {
            'services': {
                'lookup_by_labels': {
                    'default_sources': {'sources': ['default_source'], 'timeout': 5},
                    'labels': {1: {'sources': ['label_source'], 'timeout': 1}},
                }
            }
        }
        s = _LookupService(config, sources)

        results = s.lookup_by_labels('', [1], sentinel.uuid, {}, sentinel.token)

        assert_that(sources['label_source'].search.call_count, equal_to(1))
        assert_that(sources['default_source'].search.call_count, equal_to(0))
        assert_that(results, contains_inanyorder(label_result))
        s.stop()

    def test_that_lookup_by_labels_uses_default_timeout_on_fallback(self):
        sources = {'default_source': Mock(name='default_source', search=Mock(return_value=[]))}
        config = {
            'services': {
                'lookup_by_labels': {
                    'default_sources': {'sources': ['default_source'], 'timeout': 5},
                    'labels': {},
                }
            }
        }
        s = _LookupService(config, sources)

        with patch.object(s, '_run_sources') as run_sources:
            s.lookup_by_labels('', [], sentinel.uuid, {}, sentinel.token)

        run_sources.assert_called_once_with(
            [sources['default_source']], '', sentinel.uuid, {}, sentinel.token, 5)
        s.stop()

    def test_when_the_profile_is_not_configured(self):
        s = _LookupService({}, {})

        result = s.lookup(sentinel.term, 'my_profile', sentinel.uuid, {}, sentinel.token)

        assert_that(result, contains_exactly())

        s.stop()

    def test_when_the_sources_are_not_configured(self):
        s = _LookupService({'my_profile': {}}, {})

        result = s.lookup(sentinel.term, 'my_profile', sentinel.uuid, {}, sentinel.token)

        assert_that(result, contains_exactly())

        s.stop()

    @patch('xivo_dird.plugins.lookup.ThreadPoolExecutor')
    def test_that_the_service_starts_the_thread_pool(self, MockedThreadPoolExecutor):
        _LookupService({}, {})

        MockedThreadPoolExecutor.assert_called_once_with(max_workers=10)

    @patch('xivo_dird.plugins.lookup.ThreadPoolExecutor')
    def test_that_stop_shuts_down_the_thread_pool(self, MockedThreadPoolExecutor):
        s = _LookupService({}, {})

        s.stop()

        MockedThreadPoolExecutor.return_value.shutdown.assert_called_once_with()

    def test_that_max_result_is_loaded_from_the_config(self):
        s = _LookupService({'lookup_service': {'max_result': 300}}, {})

        assert_that(s.max_result, equal_to(300))

        s.stop()

    def test_that_max_result_defaults_when_the_section_is_missing(self):
        s = _LookupService({}, {})

        assert_that(s.max_result, equal_to(DEFAULT_MAX_RESULTS))

        s.stop()

    def test_that_max_result_defaults_when_the_key_is_missing(self):
        s = _LookupService({'lookup_service': {}}, {})

        assert_that(s.max_result, equal_to(DEFAULT_MAX_RESULTS))

        s.stop()
