#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

from unittest import mock

from oslo_config import cfg
from oslo_config import fixture as config_fixture

from neutron.common import cache_utils as cache
from neutron.tests import base


class CacheConfFixture(config_fixture.Config):
    def setUp(self):
        super().setUp()
        cache.register_oslo_configs(self.conf)
        self.config(enabled=True, group='cache')


class TestOsloCache(base.BaseTestCase):
    def setUp(self):
        super().setUp()
        self.memory_conf = cfg.ConfigOpts()
        memory_conf_fixture = CacheConfFixture(self.memory_conf)
        self.useFixture(memory_conf_fixture)

        self.dict_conf = cfg.ConfigOpts()
        dict_conf_fixture = CacheConfFixture(self.dict_conf)
        self.useFixture(dict_conf_fixture)
        dict_conf_fixture.config(expiration_time=60,
                                 backend='oslo_cache.dict', group='cache')

        self.null_cache_conf = cfg.ConfigOpts()
        null_conf_fixture = CacheConfFixture(self.null_cache_conf)
        self.useFixture(null_conf_fixture)
        null_conf_fixture.config(expiration_time=600,
                                 backend='dogpile.cache.null', group='cache')

    def _test_get_cache_region_helper(self, conf):
        region = cache._get_cache_region(conf)
        self.assertIsNotNone(region)

    def test_get_cache_region(self):
        self._test_get_cache_region_helper(self.dict_conf)
        self._test_get_cache_region_helper(self.null_cache_conf)

    @mock.patch('neutron.common.cache_utils._get_cache_region')
    def test_get_cache(self, mock_get_cache_region):
        self.assertIsNotNone(cache.get_cache(self.memory_conf))
        self.assertIsNotNone(cache.get_cache(self.dict_conf))
        self.assertIsNotNone(cache.get_cache(self.null_cache_conf))
        mock_get_cache_region.assert_has_calls(
            [mock.call(self.dict_conf),
             mock.call(self.null_cache_conf)]
        )


class _CachingDecorator:
    def __init__(self):
        self.func_retval = 'bar'
        self._cache = mock.Mock()

    @cache.cache_method_results
    def func(self, *args, **kwargs):
        return self.func_retval


class TestCachingDecorator(base.BaseTestCase):
    def setUp(self):
        super().setUp()
        self.decor = _CachingDecorator()
        self.func_name = '{module}._CachingDecorator.func'.format(
            module=self.__module__
        )
        self.not_cached = self.decor.func.func.__self__._not_cached

    def test_cache_miss(self):
        expected_key = (self.func_name, 1, 2, ('foo', 'bar'))
        args = (1, 2)
        kwargs = {'foo': 'bar'}
        self.decor._cache.get.return_value = self.not_cached
        retval = self.decor.func(*args, **kwargs)
        self.decor._cache.set.assert_called_once_with(
            str(expected_key), self.decor.func_retval)
        self.assertEqual(self.decor.func_retval, retval)

    def test_cache_hit(self):
        expected_key = (self.func_name, 1, 2, ('foo', 'bar'))
        args = (1, 2)
        kwargs = {'foo': 'bar'}
        retval = self.decor.func(*args, **kwargs)
        self.assertFalse(self.decor._cache.set.called)
        self.assertEqual(self.decor._cache.get.return_value, retval)
        self.decor._cache.get.assert_called_once_with(str(expected_key))

    def test_get_unhashable(self):
        expected_key = (self.func_name, [1], 2)
        self.decor._cache.get.side_effect = TypeError
        retval = self.decor.func([1], 2)
        self.assertFalse(self.decor._cache.set.called)
        self.assertEqual(self.decor.func_retval, retval)
        self.decor._cache.get.assert_called_once_with(str(expected_key))

    def test_missing_cache(self):
        delattr(self.decor, '_cache')
        self.assertRaises(NotImplementedError, self.decor.func, (1, 2))

    def test_no_cache(self):
        self.decor._cache = False
        retval = self.decor.func((1, 2))
        self.assertEqual(self.decor.func_retval, retval)

    def test_skip_cache(self):
        self.decor.func(1, 2, skip_cache=True)
        expected_key = (self.func_name, 1, 2)
        self.decor._cache.get.assert_not_called()
        self.decor._cache.set.assert_called_once_with(str(expected_key),
                                                      self.decor.func_retval)
