from unittest.mock import MagicMock, patch, mock_open

import json
from asynqp import IncomingMessage
from coverage.backunittest import TestCase
from tornado.concurrent import Future
from tornado.testing import AsyncTestCase, gen_test

from utils import Config
from utils.config import ToucanConsumer
from utils.exceptions import ToucanException


class ConfigTest(TestCase):
    """
    Test config
    """

    CONFIG = {
        'alice': {
            'has': {
                'a': 'cat',
                'not': [
                    'cat'
                ]
            },
        },
        'config_filename': 'test',
        'empty_dict': {}
    }

    YAML = '''alice:
    has:
        a: dog'''

    def setUp(self):
        super(ConfigTest, self).setUp()
        self.CONFIG = {
            'alice': {
                'has': {
                    'a': 'cat',
                    'not': [
                        'cat'
                    ]
                },
            },
            'config_filename': 'test',
            'empty_dict': {}
        }
        self.config = Config(cfg=self.CONFIG)

    def test_len(self):
        self.assertEqual(len(self.config), 3)

    def test_empty_dict(self):
        self.assertEqual(self.config._cfg['empty_dict'], {})

    def test_empty_config_len(self):
        config = Config()
        self.assertEqual(len(config), 0)

    def test_simplify_defaults_callable(self):
        callable_dafaults = MagicMock()
        self.config._simplify_defaults(callable_dafaults)

        callable_dafaults.assert_called_once_with()

    def test_simplify_dict(self):
        dict_defaults = {'test': {'test2': 'nothing'}}
        self.assertDictEqual(self.config._simplify_defaults(dict_defaults), dict_defaults)

    def test_simplify_list(self):
        list_defaults = ['test', 'test2']
        self.assertListEqual(self.config._simplify_defaults(list_defaults), list_defaults)

    def test_recursive_merge_neither_dict_nor_list(self):
        data = 'config'
        result = self.config._recursive_merge(data, self.CONFIG)

        self.assertEqual(result, data)

    def test_recursive_merge_lists(self):
        defaults = [{'test1': 'test'}, {'test2':'test2'}]
        data = [{'test1': 'test3'}]
        expected = [{'test1': 'test3'}, {'test2':'test2'}]

        result = self.config._recursive_merge(data, defaults)
        self.assertListEqual(result, expected)

    def test_recursive_merge_dicts(self):
        defaults = {'test1': 'test1', 'test2': 'test2'}
        data = {'test1': 'test3', 'test4': 'test'}
        expected = {'test1': 'test3', 'test2': 'test2', 'test4': 'test'}

        result = self.config._recursive_merge(data, defaults)
        self.assertDictEqual(result, expected)

    def test_recursive_merge_dicts_with_adding_value(self):
        defaults = {'test1': [{'test1': 'test'}, {'test2':'test2'}], 'test2': 'test2'}
        data = {'test1': [{'test1': 'test3', 'test4': 'test'}]}
        expected = {'test1': [{'test1': 'test3', 'test4': 'test'}, {'test2':'test2'}], 'test2': 'test2'}

        result = self.config._recursive_merge(data, defaults)
        self.assertDictEqual(result, expected)

    @patch('builtins.open', mock_open(read_data=YAML))
    def test_load_yaml(self):
        expected = self.CONFIG.copy()
        expected['alice']['has']['a'] = 'dog'

        self.config.load('test', self.CONFIG)

        result = self.config.cfg

        self.assertDictEqual(result, expected)

    @patch('builtins.open', mock_open(read_data=YAML))
    def test_load_yaml_without_defaults(self):
        expected = {'alice': {'has': {'not': ['cat'], 'a': 'dog'}}, 'config_filename': 'test', 'empty_dict': {}}

        self.config.load('test')

        result = self.config.cfg

        self.assertDictEqual(result, expected)

    def test_get(self):
        expected = 'cat'
        result = self.config.get('alice.has.a')

        self.assertEqual(result, expected)

        self.assertEqual(self.config.get('alice.has.a'), 'cat')
        self.assertEqual(self.config.get('alice.has.not.0'), 'cat')
        self.assertDictEqual(self.config.get('alice.has')._cfg, self.CONFIG['alice']['has'])
        self.assertRaises(KeyError, self.config.get, 'alice.has.a.cat.named.kitty')

    def test_get_toucan_exception(self):
        self.config.toucan = MagicMock()
        self.config.toucan.async_get = MagicMock(side_effect=ToucanException)
        self.assertRaises(KeyError, self.config.get, 'alice.has.a.cat.named.kitty')

    def test_get_non_exist(self):
        self.assertRaises(KeyError, self.config.get, 'this.not.exist')

    def test_magic_get(self):
        expected = 'cat'
        result = self.config['alice.has.a']

        self.assertEqual(result, expected)

    def test_reload(self):
        self.config.load = MagicMock()
        filename = 'test_filename'
        self.config.reload(filename)
        self.config.load.assert_called_once_with(filename, self.CONFIG)

    def test_reload_after_change_cfg(self):
        self.config.load = MagicMock()
        self.config._cfg['alice'] = None
        filename = 'test_filename'
        self.config.reload(filename)
        cfg =  {
            'alice': {
                'has': {
                    'a': 'cat',
                    'not': [
                        'cat'
                    ]
                },
            },
            'config_filename': 'test',
            'empty_dict': {}
        }
        self.config.load.assert_called_once_with(filename, cfg)

    def test_contains(self):
        self.assertIn('cat', self.config['alice.has.not'])

    def test_not_contains(self):
        self.assertNotIn('dog', self.config['alice.has.not'])

    def test_not_list(self):
        self.assertNotIn('dog', self.config['alice.has'])

    def test_set(self):
        expected = MagicMock()
        self.config['test.adding.key'] = expected
        result = self.config._cfg.get('test', {}).get('adding', {}).get('key', None)

        self.assertEqual(result, expected)

    def test_set_exist_key(self):
        self.config._cfg = {
            'test': {
                'adding': {
                    'key': 'exist_key'
                }
            }
        }
        expected = MagicMock()
        self.config['test.adding.key'] = expected
        result = self.config._cfg.get('test', {}).get('adding', {}).get('key', None)

        self.assertEqual(result, expected)

    @patch('utils.config.time.time', MagicMock(return_value=20))
    def test_get_non_exist_key_with_toucan(self):
        self.config.toucan = MagicMock()
        self.config.toucan.is_special.return_value = False
        self.cache_time = 1
        expected = 'test_value'
        self.config.toucan.get.return_value = expected

        result = self.config['non.exisists.key']

        self.assertEqual(result, expected)
        self.config.toucan.get.assert_called_once_with('non.exisists.key')

    @patch('utils.config.time.time', MagicMock(return_value=20))
    def test_get_cached_config_with_toucan(self):
        self.config.toucan = MagicMock()
        self.config.timestamps = {
            'alice.has.a': 15,
        }
        self.config._immutable = {}
        self.config.cache_time = 10
        expected = 'cat'
        self.assertFalse(self.config.toucan.get.called)

        result = self.config['alice.has.a']
        self.assertEqual(result, expected)

    @patch('utils.config.time.time')
    def test_get_strict_config_with_toucan(self, mock_time):
        self.config.toucan = MagicMock()
        self.config.timestamps = {
            'alice.has.a': 15,
        }
        self.config._immutable = {'alice.has.a'}
        self.config.cache_time = 10
        expected = 'cat'
        result = self.config['alice.has.a']

        self.assertFalse(self.config.toucan.get.called)

        self.assertEqual(result, expected)

    @patch('utils.config.time.time', MagicMock(return_value=50))
    def test_get_special_config_with_toucan(self):
        self.config.toucan = MagicMock()
        self.config.toucan.is_special.return_value = True
        self.config.toucan.get.return_value = {'alice.has.a': 'cat', 'test.key': 'test_value'}

        self.config.timestamps = {
            'alice.has.a': 15,
        }
        self.config.cache_time = 10
        expected = 'cat'

        result = self.config['alice.has.a']
        self.assertEqual(result, expected)
        self.assertEqual(self.config['test.key'], 'test_value')
        self.assertEqual(self.config.timestamps['alice.has.a'], 50)
        self.assertEqual(self.config.timestamps['test.key'], 50)

    def test_multtiple_key(self):
        result = self.config._get('alice.*')
        expected = {
            'has': {
                'a': 'cat',
                'not': [
                    'cat'
                ]
            }
        }

        self.assertEqual(result, expected)


class ToucanConsumerTest(AsyncTestCase):
    def setUp(self):
        super(ToucanConsumerTest, self).setUp()
        self.cfg = Config()
        self.consumer = ToucanConsumer(self.cfg)

    @gen_test
    async def test_process_message(self):
        message_body = {
            'status': 'OK',
            'value': 'test_value',
            'key': '/aucote/example/key'
        }
        message = IncomingMessage(sender=MagicMock(), delivery_tag=1, exchange_name='toucan',
                                  routing_key='toucan.config.aucote.example.key',
                                  body=json.dumps(message_body).encode('utf-8'))

        await self.consumer.process_message(message)
        result = self.cfg['example.key']
        expected = 'test_value'

        self.assertEqual(result, expected)