# -*- coding: utf-8 -*-
#
# Copyright (C) 2015-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 uuid import uuid4

from hamcrest import assert_that
from hamcrest import contains_exactly
from hamcrest import contains_inanyorder
from hamcrest import equal_to
from hamcrest import has_entries
from hamcrest import has_entry
from hamcrest import has_item
from hamcrest import not_
from mock import ANY
from mock import Mock
from mock import call
from mock import patch
from requests import RequestException

from xivo_dird import make_result_class
from xivo_dird.plugins.default_json_view import DisabledFavoriteService, Classifier
from xivo_dird.plugins.default_json_view import DisplayColumn
from xivo_dird.plugins.default_json_view import FavoritesRead
from xivo_dird.plugins.default_json_view import FavoritesWrite
from xivo_dird.plugins.default_json_view import JsonViewPlugin
from xivo_dird.plugins.default_json_view import Lookup
from xivo_dird.plugins.default_json_view import LookupByLabel
from xivo_dird.plugins.default_json_view import Personal
from xivo_dird.plugins.default_json_view import _ResultFormatter
from xivo_dird.plugins.default_json_view import _parse_labels
from xivo_dird.plugins.default_json_view import make_displays
from xivo_dird.plugins.tests.base_http_view_test_case import BaseHTTPViewTestCase

UUID1 = str(uuid4())
UUID2 = str(uuid4())


class TestJsonViewPlugin(BaseHTTPViewTestCase):

    def setUp(self):
        self.plugin = JsonViewPlugin()

    def tearDown(self):
        # reset class Lookup
        Lookup.configure(displays=None, lookup_service=None, favorite_service=DisabledFavoriteService())
        FavoritesRead.configure(displays=None, favorites_service=None)
        FavoritesWrite.configure(favorites_service=None)

    @patch('xivo_dird.plugins.default_json_view.api.add_resource')
    def test_that_load_with_no_lookup_service_does_not_add_route(self, add_resource):
        self.plugin.load({'config': {},
                          'http_namespace': Mock(),
                          'rest_api': Mock(),
                          'services': {}})

        assert_that(add_resource.call_args_list, not_(has_item(call(Lookup, ANY))))

    @patch('xivo_dird.plugins.default_json_view.api.add_resource')
    def test_that_load_adds_the_lookup_route(self, add_resource):
        args = {
            'config': {'displays': {},
                       'profile_to_display': {}},
            'http_namespace': Mock(),
            'rest_api': Mock(),
            'services': {'lookup': Mock()},
        }

        self.plugin.load(args)

        add_resource.assert_any_call(Lookup, JsonViewPlugin.lookup_url)
        add_resource.assert_any_call(LookupByLabel, JsonViewPlugin.lookup_by_label_url)

    def test_that_parse_labels_returns_integer_ids_and_drops_non_numeric(self):
        result = _parse_labels(' 1, 2 ,,foo,3 ')

        assert_that(result, contains_exactly(1, 2, 3))

    @patch('xivo_dird.plugins.default_json_view.api.add_resource')
    def test_that_load_with_no_favorites_service_does_not_add_route(self, add_resource):
        JsonViewPlugin().load({'config': {},
                               'http_namespace': Mock(),
                               'rest_api': Mock(),
                               'services': {}})

        assert_that(add_resource.call_args_list, not_(has_item(call(FavoritesRead, ANY))))
        assert_that(add_resource.call_args_list, not_(has_item(call(FavoritesWrite, ANY))))

    @patch('xivo_dird.plugins.default_json_view.api.add_resource')
    def test_that_load_adds_the_favorite_route(self, add_resource):
        args = {
            'config': {'displays': {},
                       'profile_to_display': {}},
            'http_namespace': Mock(),
            'rest_api': Mock(),
            'services': {'favorites': Mock()},
        }

        JsonViewPlugin().load(args)

        add_resource.assert_any_call(FavoritesRead, JsonViewPlugin.favorites_read_url)
        add_resource.assert_any_call(FavoritesWrite, JsonViewPlugin.favorites_write_url)

    @patch('xivo_dird.plugins.default_json_view.api.add_resource')
    def test_that_load_with_no_personal_service_does_not_add_route(self, add_resource):
        JsonViewPlugin().load({'config': {},
                               'http_namespace': Mock(),
                               'rest_api': Mock(),
                               'services': {}})

        assert_that(add_resource.call_args_list, not_(has_item(call(Personal, ANY))))

    @patch('xivo_dird.plugins.default_json_view.api.add_resource')
    def test_that_load_adds_the_personal_routes(self, add_resource):
        args = {
            'config': {'displays': {},
                       'profile_to_display': {}},
            'http_namespace': Mock(),
            'rest_api': Mock(),
            'services': {'personal': Mock()},
        }

        JsonViewPlugin().load(args)

        add_resource.assert_any_call(Personal, JsonViewPlugin.personal_url)


class TestLookupByLabel(BaseHTTPViewTestCase):

    def setUp(self):
        self.lookup_service = Mock()
        self.lookup_service.lookup_by_labels.return_value = []
        self.favorite_service = Mock()
        self.favorite_service.favorite_ids_all.return_value = {}
        self.label_dao = Mock()
        LookupByLabel.configure(displays={'lookup_by_labels': []},
                                lookup_service=self.lookup_service,
                                favorite_service=self.favorite_service,
                                label_dao=self.label_dao)

    def tearDown(self):
        LookupByLabel.configure(displays=None, lookup_service=None,
                                favorite_service=DisabledFavoriteService(), label_dao=None)

    def _call_get(self, term='smith', labels=None):
        with patch('xivo_dird.plugins.default_json_view.parser_lookup_by_labels') as parser_mock:
            parser_mock.parse_args.return_value = {'term': term, 'labels': labels}
            return LookupByLabel().get()

    def _searched_labels(self):
        return self.lookup_service.lookup_by_labels.call_args.kwargs['labels']

    def test_that_the_endpoint_declares_the_coarse_gate_acl(self):
        assert_that(LookupByLabel.get.acl, equal_to('dird.directories.lookup_by_label.read'))

    def test_that_a_cti_user_only_searches_the_intersection_of_requested_and_assigned(self):
        self.label_dao.label_ids_for_user.return_value = {1, 3}
        with patch.object(LookupByLabel, '_token_and_user_uuid', return_value=('token', UUID1)):
            self._call_get(labels=[1, 2, 3])

        assert_that(self._searched_labels(), contains_exactly(1, 3))

    def test_that_a_cti_user_without_requested_labels_searches_all_assigned(self):
        self.label_dao.label_ids_for_user.return_value = {1, 3}
        with patch.object(LookupByLabel, '_token_and_user_uuid', return_value=('token', UUID1)):
            self._call_get(labels=None)

        assert_that(self._searched_labels(), contains_inanyorder(1, 3))

    def test_that_a_cti_user_with_no_matching_label_searches_an_empty_set(self):
        self.label_dao.label_ids_for_user.return_value = {5}
        with patch.object(LookupByLabel, '_token_and_user_uuid', return_value=('token', UUID1)), \
                patch('xivo_dird.plugins.default_json_view.logger') as logger:
            self._call_get(labels=[1, 2])

        assert_that(self._searched_labels(), equal_to([]))
        logger.info.assert_any_call(
            "Lookup by labels: no authorized label, falling back to default_sources")

    def test_that_a_cti_user_falls_back_to_default_sources_when_the_dao_is_unavailable(self):
        LookupByLabel.label_dao = None
        with patch.object(LookupByLabel, '_token_and_user_uuid', return_value=('token', UUID1)):
            self._call_get(labels=[1, 2])

        assert_that(self._searched_labels(), equal_to([]))

    def test_that_a_webservice_user_only_searches_acl_permitted_labels(self):
        with patch.object(LookupByLabel, '_token_and_user_uuid', return_value=('token', None)), \
                patch('xivo_dird.plugins.default_json_view.auth') as auth:
            auth.client.return_value.token.is_valid.side_effect = \
                lambda token, acl: acl == 'dird.directories.lookup_by_label.1.read'
            self._call_get(labels=[1, 2])

        assert_that(self._searched_labels(), contains_exactly(1))

    def test_that_a_webservice_user_without_requested_labels_searches_an_empty_set(self):
        with patch.object(LookupByLabel, '_token_and_user_uuid', return_value=('token', None)), \
                patch('xivo_dird.plugins.default_json_view.auth') as auth:
            self._call_get(labels=None)

        assert_that(self._searched_labels(), equal_to([]))
        assert_that(auth.client.return_value.token.is_valid.called, equal_to(False))

    def test_that_unauthorized_labels_are_logged(self):
        self.label_dao.label_ids_for_user.return_value = {1}
        with patch.object(LookupByLabel, '_token_and_user_uuid', return_value=('token', UUID1)), \
                patch('xivo_dird.plugins.default_json_view.logger') as logger:
            self._call_get(labels=[1, 2])

        logger.info.assert_any_call("Lookup by labels: dropping unauthorized labels %s", [2])

    def test_that_a_webservice_user_gets_a_503_when_auth_is_unreachable(self):
        with patch.object(LookupByLabel, '_token_and_user_uuid', return_value=('token', None)), \
                patch('xivo_dird.plugins.default_json_view.auth') as auth:
            auth.client.return_value.token.is_valid.side_effect = RequestException
            body, status_code = self._call_get(labels=[1])

        assert_that(status_code, equal_to(503))
        assert_that(self.lookup_service.lookup_by_labels.called, equal_to(False))


class TestMakeDisplays(unittest.TestCase):

    def test_that_make_displays_with_no_config_returns_empty_dict(self):
        result = make_displays({})

        assert_that(result, equal_to({}))

    def test_that_make_displays_generate_display_dict(self):
        first_display = [
            DisplayColumn('Firstname', None, 'Unknown', 'firstname'),
            DisplayColumn('Lastname', None, 'ln', 'lastname'),
        ]
        second_display = [
            DisplayColumn('fn', 'some_type', 'N/A', 'firstname'),
            DisplayColumn('ln', None, 'N/A', 'LAST'),
        ]

        config = {'displays': {'first_display': [{'title': 'Firstname',
                                                  'type': None,
                                                  'default': 'Unknown',
                                                  'field': 'firstname'},
                                                 {'title': 'Lastname',
                                                  'type': None,
                                                  'default': 'ln',
                                                  'field': 'lastname'}],
                               'second_display': [{'title': 'fn',
                                                   'type': 'some_type',
                                                   'default': 'N/A',
                                                   'field': 'firstname'},
                                                  {'title': 'ln',
                                                   'type': None,
                                                   'default': 'N/A',
                                                   'field': 'LAST'}]},
                  'profile_to_display': {'profile_1': 'first_display',
                                         'profile_2': 'second_display',
                                         'profile_3': 'first_display'}}

        display_dict = make_displays(config)

        expected = {
            'profile_1': first_display,
            'profile_2': second_display,
            'profile_3': first_display,
        }

        assert_that(display_dict, equal_to(expected))


class TestFormatResult(unittest.TestCase):

    def setUp(self):
        self.source_name = 'my_source'
        self.xivo_id = 'my_xivo_abc'
        self.SourceResult = make_result_class(self.source_name, unique_column='id')

    def test_that_format_results_adds_columns_headers(self):
        display = [
            DisplayColumn('Firstname', None, 'Unknown', 'firstname'),
            DisplayColumn('Lastname', None, '', 'lastname'),
            DisplayColumn(None, 'status', None, None),
            DisplayColumn('Number', 'office_number', None, 'telephoneNumber'),
            DisplayColumn('Country', None, 'Canada', 'country'),
        ]
        formatter = _ResultFormatter(display)

        result = formatter.format_results([], [])

        expected_headers = ['Firstname', 'Lastname', None, 'Number', 'Country']
        assert_that(result, has_entries('column_headers', expected_headers))

    def test_that_format_results_adds_columns_types(self):
        display = [
            DisplayColumn('Firstname', None, 'Unknown', 'firstname'),
            DisplayColumn('Lastname', None, '', 'lastname'),
            DisplayColumn(None, 'status', None, None),
            DisplayColumn('Number', 'office_number', None, 'telephoneNumber'),
            DisplayColumn('Country', None, 'Canada', 'country'),
            DisplayColumn(None, 'favorite', None, None),
        ]
        formatter = _ResultFormatter(display)

        result = formatter.format_results([], [])

        expected_types = [None, None, 'status', 'office_number', None, 'favorite']
        assert_that(result, has_entries('column_types', expected_types))

    def test_that_format_results_adds_results(self):
        result1 = self.SourceResult({'id': 1,
                                     'firstname': 'Alice',
                                     'lastname': 'AAA',
                                     'telephoneNumber': '5555555555'},
                                    self.xivo_id, None, None, None, None)
        result2 = self.SourceResult({'id': 'user_id',
                                     'firstname': 'Bob',
                                     'lastname': 'BBB',
                                     'telephoneNumber': '5555556666'},
                                    self.xivo_id, 'agent_id', 'user_id', UUID1, 'endpoint_id')
        display = [
            DisplayColumn('Firstname', None, 'Unknown', 'firstname'),
            DisplayColumn('Lastname', None, '', 'lastname'),
            DisplayColumn(None, 'status', None, None),
            DisplayColumn('Number', 'office_number', None, 'telephoneNumber'),
            DisplayColumn('Country', None, 'Canada', 'country')
        ]
        formatter = _ResultFormatter(display)

        result = formatter.format_results([result1, result2], [])

        assert_that(result, has_entries('results', [
            {
                'column_values': ['Alice', 'AAA', None, '5555555555', 'Canada'],
                'relations': {'xivo_id': self.xivo_id,
                              'agent_id': None,
                              'user_id': None,
                              'user_uuid': None,
                              'endpoint_id': None,
                              'source_entry_id': '1'},
                'source': self.source_name,
            },
            {
                'column_values': ['Bob', 'BBB', None, '5555556666', 'Canada'],
                'relations': {'xivo_id': self.xivo_id,
                              'agent_id': 'agent_id',
                              'user_id': 'user_id',
                              'user_uuid': UUID1,
                              'endpoint_id': 'endpoint_id',
                              'source_entry_id': 'user_id'},
                'source': self.source_name,
            },
        ]))

    def test_that_format_results_marks_favorites(self):
        result1 = self.SourceResult({'id': 1,
                                     'firstname': 'Alice',
                                     'lastname': 'AAA',
                                     'telephoneNumber': '5555555555'},
                                    self.xivo_id, None, 1, UUID1, None)
        result2 = self.SourceResult({'id': 2,
                                     'firstname': 'Bob',
                                     'lastname': 'BBB',
                                     'telephoneNumber': '5555556666'},
                                    self.xivo_id, 'agent_id', 2, UUID2, 'endpoint_id')
        display = [
            DisplayColumn('Firstname', None, 'Unknown', 'firstname'),
            DisplayColumn('Lastname', None, '', 'lastname'),
            DisplayColumn('Number', 'office_number', None, 'telephoneNumber'),
            DisplayColumn('Favorite', 'favorite', None, None),
        ]
        formatter = _ResultFormatter(display)

        result = formatter.format_results([result1, result2], {'my_source': ['2'],
                                                               'my_other_source': ['1', '2', '3']})

        assert_that(result, has_entry('results', contains_inanyorder(
            has_entry('column_values', contains_exactly('Alice', 'AAA', '5555555555', False)),
            has_entry('column_values', contains_exactly('Bob', 'BBB', '5555556666', True)))))

    def test_that_format_results_marks_personal(self):
        result1 = self.SourceResult({'id': 1,
                                     'firstname': 'Alice',
                                     'lastname': 'AAA',
                                     'telephoneNumber': '5555555555'},
                                    self.xivo_id, None, 1, UUID1, None)
        result2 = self.SourceResult({'id': 2,
                                     'firstname': 'Bob',
                                     'lastname': 'BBB',
                                     'telephoneNumber': '5555556666'},
                                    self.xivo_id, 'agent_id', 2, UUID2, 'endpoint_id')
        result3 = make_result_class(
            'personal_source',
            unique_column='id',
            is_personal=True,
            is_deletable=True)({
                'id': 'my-id',
                'firstname': 'Charlie',
                'lastname': 'CCC',
                'telephoneNumber': '5555557777'
            }, self.xivo_id, None, None, None)

        display = [
            DisplayColumn('Firstname', None, 'Unknown', 'firstname'),
            DisplayColumn('Lastname', None, '', 'lastname'),
            DisplayColumn('Number', 'office_number', None, 'telephoneNumber'),
            DisplayColumn('Personal', 'personal', None, None),
        ]
        formatter = _ResultFormatter(display)

        result = formatter.format_results([result1, result2, result3], {})

        assert_that(result, has_entry('results', contains_inanyorder(
            has_entry('column_values', contains_exactly('Alice', 'AAA', '5555555555', False)),
            has_entry('column_values', contains_exactly('Bob', 'BBB', '5555556666', False)),
            has_entry('column_values', contains_exactly('Charlie', 'CCC', '5555557777', True)))))


class TestSearchSort(unittest.TestCase):
    def test_tokenize_values(self):
        test_item = ('black cat', 'white cat', 'fluffy dog')
        expected = {'black', 'cat', 'white', 'fluffy', 'dog'}

        classifier = Classifier('')

        assert_that(
            classifier.tokenize_values(test_item),
            equal_to(expected)
        )

    def test_score_values(self):
        test_items = (
            ('black cat', 200),
            ('black caterpillar', 150),
            ('polecat blacksmith', 60),
            ('cat', -100),
            ('bat', -400)
        )

        classifier = Classifier('black cat')

        for value, expected in test_items:
            assert_that(
                classifier.score_values(classifier.tokenize_values([value])),
                equal_to(expected),
                'score test failed for {0}'.format(value)
            )

    def test_that_lookup_sorts_properly(self):
        result_class = make_result_class('test')
        result1 = result_class({'name': 'bat'})
        result2 = result_class({'name': 'room cat'})
        result3 = result_class({'name': 'rabbit', 'company': 'black cat'})
        result4 = result_class({'name': 'cute caterpillar'})
        result5 = result_class({'name': 'tabby', 'lastname': 'cat'})
        result6 = result_class({'name': 'Root cat'})
        result7 = result_class({'name': 'Řoot cat'})

        test_results = [result1, result2, result3, result4, result5, result6, result7]
        expected_results = [result3, result2, result6, result7, result5, result4, result1]

        acquired = Lookup.sort_results(test_results, 'cat', ['name'])
        assert_that(acquired, contains_exactly(*expected_results))

    def test_that_lookup_sorts_multi_word_results_by_score(self):
        result_class = make_result_class('test')
        result1 = result_class({'name': 'John Doe'})
        result2 = result_class({'name': 'John Doel'})
        result3 = result_class({'name': 'Johnny Doe'})
        result4 = result_class({'name': 'John Smith'})
        result5 = result_class({'name': 'Doe Consulting'})

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

        acquired = Lookup.sort_results(test_results, 'john doe', ['name'])
        assert_that(acquired, contains_exactly(*expected_results))

    def test_that_lookup_keeps_only_the_first_200_results(self):
        result_class = make_result_class('test')
        test_results = [result_class({'name': 'Name {0:03d}'.format(index)}) for index in range(250)]

        acquired = Lookup.sort_results(test_results, 'name', ['name'])

        assert_that(len(acquired), equal_to(200))
        assert_that(acquired[0].fields['name'], equal_to('Name 000'))
        assert_that(acquired[-1].fields['name'], equal_to('Name 199'))

    def test_that_the_kept_results_are_the_best_scoring_ones(self):
        result_class = make_result_class('test')
        good = result_class({'name': 'John Doe'})
        bad = [result_class({'name': 'Nobody {0:03d}'.format(index)}) for index in range(5)]

        acquired = Lookup.sort_results(bad + [good], 'john doe', ['name'], 1)

        assert_that(acquired, contains_exactly(good))

    def test_that_the_score_uses_every_field_not_only_the_name_fields(self):
        result_class = make_result_class('test')
        matches_on_company = result_class({'name': 'Alice Roe', 'company': 'Doe Consulting'})
        matches_on_nothing = result_class({'name': 'Alice Roe', 'company': 'Acme'})

        acquired = Lookup.sort_results([matches_on_nothing, matches_on_company], 'doe', ['name'])

        assert_that(acquired, contains_exactly(matches_on_company, matches_on_nothing))

    def test_that_missing_one_word_ranks_below_matching_every_word_weakly(self):
        result_class = make_result_class('test')
        exact_on_one_word = result_class({'name': 'John Smith'})
        substring_on_both_words = result_class({'name': 'Bejohnson Redoe'})

        acquired = Lookup.sort_results([exact_on_one_word, substring_on_both_words], 'john doe', ['name'])

        assert_that(acquired, contains_exactly(substring_on_both_words, exact_on_one_word))

    def test_that_results_matching_nothing_are_still_sorted_alphabetically(self):
        result_class = make_result_class('test')
        result1 = result_class({'name': 'zebra'})
        result2 = result_class({'name': 'apple'})
        result3 = result_class({'name': 'moose'})

        acquired = Lookup.sort_results([result1, result2, result3], 'cat', ['name'])

        assert_that(acquired, contains_exactly(result2, result3, result1))

    def test_that_lookup_keeps_only_the_configured_max_results(self):
        result_class = make_result_class('test')
        test_results = [result_class({'name': 'Name {0:03d}'.format(index)}) for index in range(10)]

        acquired = Lookup.sort_results(test_results, 'name', ['name'], 3)

        assert_that(len(acquired), equal_to(3))
        assert_that(acquired[0].fields['name'], equal_to('Name 000'))
        assert_that(acquired[-1].fields['name'], equal_to('Name 002'))

    def test_that_favorites_properly(self):
        result_class = make_result_class('test')
        result1 = result_class({'name': 'bat'})
        result2 = result_class({'name': 'room cat'})
        result3 = result_class({'name': 'rabbit', 'company': 'black cat'})
        result4 = result_class({'name': 'cute caterpillar'})
        result5 = result_class({'name': 'tabby', 'lastname': 'cat'})
        result6 = result_class({'name': 'Root cat'})
        result7 = result_class({'name': 'Řoot cat'})

        test_results = [result1, result2, result3, result4, result5, result6, result7]
        expected_results = [result1, result4, result3, result2, result6, result7, result5]

        acquired = FavoritesRead.sort_results(test_results, ['name'])
        assert_that(acquired, contains_exactly(*expected_results))
