# -*- 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 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 Personal
from xivo_dird.plugins.default_json_view import _ResultFormatter
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)

    @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 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_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)
        )

        classifier = Classifier('black cat')

        for value, expected in test_items:
            assert_that(
                classifier.check_fuzzines(classifier.tokenize_values([value])),
                equal_to(expected),
                f'fuzzines test failed for {value}'
            )

    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)
        )

        classifier = Classifier('black cat')

        for value, expected in test_items:
            assert_that(
                classifier.check_word_match(classifier.tokenize_values([value])),
                equal_to(expected),
                f'match word test failed for {value}'
            )

    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)
        )

        classifier = Classifier('black cat')

        for value, expected in test_items:
            assert_that(
                classifier.check_start_match(classifier.tokenize_values([value])),
                equal_to(expected),
                f'start match test failed for {value}'
            )

    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')
        )

        classifier = Classifier('black cat')

        for value, expected in test_items:
            result = Mock()
            result.fields.values.return_value = (value,)
            assert_that(classifier.classify(result), equal_to(expected), f'classify failed for {expected}')

    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'})

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

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