import time
from unittest.mock import MagicMock

import influxframework
from influxframework.tests.test_api import InfluxFrameworkAPITestCase
from influxframework.tests.utils import InfluxFrameworkTestCase
from influxframework.utils.caching import request_cache, site_cache

CACHE_TTL = 4
external_service = MagicMock(return_value=30)
register_with_external_service = MagicMock(return_value=True)


@request_cache
def request_specific_api(a: list | tuple | dict | int, b: int) -> int:
	# API that takes very long to return a result
	todays_value = external_service()
	if not isinstance(a, (int, float)):
		a = 1
	return a**b * todays_value


@influxframework.whitelist(allow_guest=True)
@site_cache
def ping() -> str:
	register_with_external_service(influxframework.local.site)
	return influxframework.local.site


@influxframework.whitelist(allow_guest=True)
@site_cache(ttl=CACHE_TTL)
def ping_with_ttl() -> str:
	register_with_external_service(influxframework.local.site)
	return influxframework.local.site


class TestCachingUtils(InfluxFrameworkTestCase):
	def test_request_cache(self):
		retval = []
		acceptable_args = [
			[1, 2, 3, 4],
			range(10),
			{"abc": "test-key"},
			influxframework.get_last_doc("DocType"),
			influxframework._dict(),
		]
		same_output_received = lambda: all([x for x in set(retval) if x == retval[0]])

		# ensure that external service was called only once
		# thereby return value of request_specific_api is cached
		for _ in range(5):
			retval.append(request_specific_api(120, 23))
		external_service.assert_called_once()
		self.assertTrue(same_output_received())

		# ensure that cache differentiates between int & float
		# types. Giving different return values for both
		retval.append(request_specific_api(120.0, 23))
		self.assertTrue(external_service.call_count, 2)

		# ensure that function is executed when call isn't
		# already cached
		retval.clear()
		for _ in range(10):
			request_specific_api(120, 13)
		self.assertTrue(external_service.call_count, 3)
		self.assertTrue(same_output_received())

		# ensure key generation capacity for different types
		retval.clear()
		for arg in acceptable_args:
			external_service.call_count = 0
			for _ in range(2):
				request_specific_api(arg, 13)
			self.assertTrue(external_service.call_count, 1)
		self.assertTrue(same_output_received())


class TestSiteCache(InfluxFrameworkAPITestCase):
	def test_site_cache(self):
		module = __name__
		api_with_ttl = f"{module}.ping_with_ttl"
		api_without_ttl = f"{module}.ping"

		start = time.monotonic()
		for _ in range(5):
			self.get(f"/api/method/{api_with_ttl}")
			self.get(f"/api/method/{api_without_ttl}")
		end = time.monotonic()

		self.assertEqual(register_with_external_service.call_count, 2)
		time.sleep(CACHE_TTL - (end - start))
		self.get(f"/api/method/{api_with_ttl}")
		self.assertEqual(register_with_external_service.call_count, 3)
