# Copyright 2015 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import mock
import pickle
import unittest
import zlib

from libs import cache
from libs import cache_decorator


class _DummyCache(cache.Cache):

  def __init__(self, cached_data):
    self.cached_data = cached_data

  def Get(self, key):
    return self.cached_data.get(key)

  def Set(self, key, data, expire_time=0):
    self.cached_data[key] = data


def _DummyKeyGenerator(func, _args, _kwargs, namespace=None):
  namespace = namespace or '%s.%s' % (func.__module__, func.__name__)
  return namespace + '-' + func.__name__


class CachedTest(unittest.TestCase):
  """Tests ``Cached`` decorator."""

  def testDefaultKeyGenerator(self):
    namespace = 'n'
    expected_params = {
        'id1': 'fi',
        'id2': 'pi',
        'url': 'http://url',
    }
    # Hexadecimal digits of MD5 digest of "pickled_params".
    expected_key = namespace + '-f5f173c811f7c537a80d44511903a3e0'

    def MockPickleDumps(params):
      self.assertEqual(expected_params, params)
      return 'pickled_params'

    def func(id1, id2, url=None):  # Unused parameters-pylint: disable=W0613
      return 1  # pragma: no cover.

    class CallableIdentifier(object):

      def identifier(self):
        return 'fi'

    class PropertyIdentifier(object):

      @property
      def identifier(self):
        return 'pi'

    with mock.patch('pickle.dumps', MockPickleDumps):
      args = (CallableIdentifier(), PropertyIdentifier())
      kwargs = {'url': 'http://url'}
      key = cache_decorator._DefaultKeyGenerator(
          func, args, kwargs, namespace=namespace)
      self.assertEqual(expected_key, key)

  def testCachedDecoratorWhenResultIsAlreadyCached(self):
    dummy_cache = _DummyCache({'n-func': 1})

    @cache_decorator.Cached(
        namespace='n', key_generator=_DummyKeyGenerator, cache=dummy_cache)
    def func():
      return 2  # pragma: no cover.

    self.assertEqual(1, func())
    self.assertEqual({'n-func': 1}, dummy_cache.cached_data)

  def testCachedDecoratorWhenResultIsNotCachedYet(self):
    dummy_cache = _DummyCache({})

    @cache_decorator.Cached(
        namespace='n', key_generator=_DummyKeyGenerator, cache=dummy_cache)
    def func():
      return 2

    self.assertEqual(2, func())
    self.assertEqual({'n-func': 2}, dummy_cache.cached_data)

  def testCachedDecoratorWhenResultShouldNotBeCached(self):
    dummy_cache = _DummyCache({})

    results = ['invalid', None, 0, [], {}, '']

    def ResultValidator(result):
      return result != 'invalid'

    @cache_decorator.Cached(
        namespace='n',
        key_generator=_DummyKeyGenerator,
        cache=dummy_cache,
        result_validator=ResultValidator)
    def func():
      return results.pop()

    self.assertEqual('', func())
    self.assertEqual({}, dummy_cache.cached_data)
    self.assertEqual({}, func())
    self.assertEqual({}, dummy_cache.cached_data)
    self.assertEqual([], func())
    self.assertEqual({}, dummy_cache.cached_data)
    self.assertEqual(0, func())
    self.assertEqual({}, dummy_cache.cached_data)
    self.assertIsNone(func())
    self.assertEqual({}, dummy_cache.cached_data)
    self.assertEqual('invalid', func())
    self.assertEqual({}, dummy_cache.cached_data)

  def testCachedDecoratorWithMethodInAClass(self):

    class A(object):

      def __init__(self, url, retries):
        self.url = url
        self.retries = retries
        self.runs = 0

      @property
      def identifier(self):
        return self.url

      @cache_decorator.Cached(cache=_DummyCache({}))
      def func(self, path):
        self.runs += 1
        return self.url + '/' + path

    a1 = A('http://test', 3)
    self.assertEqual('http://test/p1', a1.func('p1'))
    self.assertEqual('http://test/p1', a1.func('p1'))
    self.assertEqual(1, a1.runs)

    a2 = A('http://test', 5)
    self.assertEqual('http://test/p1', a2.func('p1'))
    self.assertEqual(0, a2.runs)


class GeneratorCachedTest(unittest.TestCase):
  """Tests ``GeneratorCached`` decorator."""

  def testWhenResultIsAlreadyCached(self):
    value_list = [0, 1, 2]
    key = 'n-func'
    # Zero value won't be cached.
    cached_keys = ['%s-%d' % (key, i) for i in xrange(len(value_list))]
    cached_data = {key: value for key, value in zip(cached_keys, value_list)}
    cached_data[key] = cached_keys

    dummy_cache = _DummyCache(cached_data)

    @cache_decorator.GeneratorCached(
        dummy_cache, namespace='n', key_generator=_DummyKeyGenerator)
    def func():  # pragma: no cover
      for value in value_list:
        yield value

    for value, expected_value in zip(func(), value_list):
      self.assertEqual(value, expected_value)

  def testWhenResultIsNotCachedYet(self):
    value_list = [0, 1, 2, 3, 4]
    dummy_cache = _DummyCache({})

    @cache_decorator.GeneratorCached(
        dummy_cache, namespace='n', key_generator=_DummyKeyGenerator)
    def func():  # pragma: no cover
      for value in value_list:
        yield value

    key = _DummyKeyGenerator(func, [], {}, namespace='n')
    cached_keys = ['%s-%d' % (key, i) for i in xrange(len(value_list))]
    cached_data = {key: value for key, value in zip(cached_keys, value_list)}
    cached_data[key] = cached_keys
    for value, expected_value in zip(func(), value_list):
      self.assertEqual(value, expected_value)

    self.assertDictEqual(dummy_cache.cached_data, cached_data)

  @mock.patch('libs.cache_decorator.GeneratorCached.SetCache', lambda *_: False)
  def testFailedToSetValue(self):
    value_list = [0, 1, 2, 3, 4]
    dummy_cache = _DummyCache({})

    @cache_decorator.GeneratorCached(
        dummy_cache, namespace='n', key_generator=_DummyKeyGenerator)
    def func():  # pragma: no cover
      for value in value_list:
        yield value

    key = _DummyKeyGenerator(func, [], {}, namespace='n')
    cached_keys = ['%s-%d' % (key, i) for i in xrange(len(value_list))]
    cached_data = {key: value for key, value in zip(cached_keys, value_list)}
    cached_data[key] = cached_keys
    for value, expected_value in zip(func(), value_list):
      self.assertEqual(value, expected_value)

    self.assertDictEqual(dummy_cache.cached_data, {})
