# -*- 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.util import tokenize
from xivo_dird import make_result_class
from xivo_dird.plugins.lookup import LookupServicePlugin, ResultForSort
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': sentinel.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_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_lookup_sorts_properly(self):
        result1 = self.result_class({'display_name': 'bat'})
        result2 = self.result_class({'display_name': 'room cat'})
        result3 = self.result_class({'display_name': 'rabbit', 'company': 'black cat'})
        result4 = self.result_class({'firstname': 'cute caterpillar'})
        result5 = self.result_class({'firstname': 'tabby', 'lastname': 'cat'})
        sources = {
            'source_1': Mock(name='source_1', search=Mock(return_value=[result5, result1])),
            'source_2': Mock(name='source_2', search=Mock(return_value=[result4, result2])),
            'source_3': Mock(name='source_3', search=Mock(return_value=[result3])),
        }
        config = {
            'services': {
                'lookup': {
                    'my_profile': {
                        'sources': ['source_1', 'source_2', 'source_3'],
                        'timeout': 1,
                    }
                }
            }
        }

        s = _LookupService(config, sources)

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

        expected_results = [result3, result2, result5, result4, result1]

        assert_that(results, contains_exactly(*expected_results))

        s.stop()

    def test_check_fuzzines(self):
        test_items = (
            ('cat', False),
            ('Cat', False),
            ('CAT', False),
            ('bat', True),
            ('Bat', True),
            ('BAT', True),
            ('polecat', False),
            ('caterpillar', False),
            ('i dont know what to try yet', True),
            ('I believe cat does meow', False)
        )
        term = 'black cat'
        tokenized_term = tokenize(term)

        for name, result in test_items:
            test_result = ResultForSort(self.result_class({'display_name': name}), term)
            assert_that(
                test_result.check_fuzzines(tokenized_term), equal_to(result), f'fuzzines test failed for {name}')

    def test_check_word_match(self):
        test_items = (
            ('cat', True),
            ('Cat', True),
            ('CAT', True),
            ('bat', False),
            ('Bat', False),
            ('BAT', False),
            ('polecat', False),
            ('caterpillar', False),
            ('i dont know what to try yet', False),
            ('I believe cat does meow', True)
        )
        term = 'black cat'
        tokenized_term = tokenize(term)

        for name, result in test_items:
            test_result = ResultForSort(self.result_class({'display_name': name}), term)
            assert_that(
                test_result.check_word_match(tokenized_term), equal_to(result), f'match word test failed for {name}')

    def test_check_start_match(self):
        test_items = (
            ('cat', True),
            ('Cat', True),
            ('CAT', True),
            ('bat', False),
            ('Bat', False),
            ('BAT', False),
            ('polecat', False),
            ('caterpillar', True),
            ('i dont know what to try yet', False),
            ('I believe cat does meow', True)
        )
        term = 'black cat'
        tokenized_term = tokenize(term)

        for name, result in test_items:
            test_result = ResultForSort(self.result_class({'display_name': name}), term)
            assert_that(
                test_result.check_start_match(tokenized_term), equal_to(result), f'start match test failed for {name}')

    def test_find_sort_string(self):
        test_items = (
            {'display_name': 'cat'},
            {'name': 'cat'},
            {'firstname': 'cat'},
            {'lastname': 'cat'},
            {'firstname': '', 'lastname': 'cat'},
        )

        for item in test_items:
            test_result = ResultForSort(self.result_class(item), '')
            assert_that(test_result.sort_string, equal_to('cat'), f'sort string for {item} is wrong')

    def test_classify(self):
        test_items = (
            ('cat', 'word'),
            ('bat', 'fuzzy'),
            ('polecat', 'other'),
            ('caterpillar', 'start'),
            ('i dont know what to try yet', 'fuzzy'),
            ('I believe cat does meow', 'word')
        )
        term = 'black cat'

        for name, result in test_items:
            test_result = ResultForSort(self.result_class({'display_name': name}), term)
            assert_that(test_result.classify(), equal_to(result), f'classify failed for {name}')
