import json
import os
import unittest
from unittest import mock


class TestC4Cache(unittest.TestCase):

	def setUp(self):
		# Reset module state between tests
		import cc_py_commons.services.c4.c4_cache as c4_cache
		c4_cache._redis_client = None
		c4_cache._cache_available = True

	def tearDown(self):
		# Clean up environment variables
		for key in ['C4_SDK_CACHE_ENABLED', 'C4_SDK_CACHE_REDIS_HOST', 'C4_SDK_CACHE_REDIS_PORT']:
			if key in os.environ:
				del os.environ[key]

	def test_caching_disabled_by_default(self):
		"""Test that caching is disabled when env vars not set"""
		# Reload module to pick up env changes
		import importlib
		import cc_py_commons.services.c4.c4_cache as c4_cache
		importlib.reload(c4_cache)

		self.assertFalse(c4_cache.is_caching_enabled())

	def test_get_integration_cache_key(self):
		"""Test cache key generation for integrations"""
		from cc_py_commons.services.c4.c4_cache import get_integration_cache_key

		key = get_integration_cache_key('1234', 'TAI')
		self.assertEqual(key, 'integration:1234:TAI')

	def test_build_cache_key(self):
		"""Test full Redis key format matches Java SDK"""
		from cc_py_commons.services.c4.c4_cache import _build_cache_key, CACHE_INTEGRATION_SETTINGS

		full_key = _build_cache_key(CACHE_INTEGRATION_SETTINGS, 'integration:1234:TAI')
		self.assertEqual(full_key, 'c4-sdk:c4-sdk-integration-settings:integration:1234:TAI')

	@mock.patch.dict(os.environ, {
		'C4_SDK_CACHE_ENABLED': 'true',
		'C4_SDK_CACHE_REDIS_HOST': 'localhost',
		'C4_SDK_CACHE_REDIS_PORT': '6379'
	})
	def test_get_cached_integration_hit(self):
		"""Test cache hit returns cached data"""
		import importlib
		import cc_py_commons.services.c4.c4_cache as c4_cache
		importlib.reload(c4_cache)

		# Setup mock Redis module and client after reload
		mock_redis_module = mock.MagicMock()
		mock_client = mock.MagicMock()
		mock_redis_module.Redis.return_value = mock_client
		c4_cache.redis = mock_redis_module

		cached_data = {'code': 200, 'data': {'activeStatusField': ['BOOKED']}}
		mock_client.get.return_value = json.dumps(cached_data)

		result = c4_cache.get_cached_integration('1234', 'TAI')

		self.assertEqual(result, cached_data)
		mock_client.get.assert_called_once_with('c4-sdk:c4-sdk-integration-settings:integration:1234:TAI')

	@mock.patch.dict(os.environ, {
		'C4_SDK_CACHE_ENABLED': 'true',
		'C4_SDK_CACHE_REDIS_HOST': 'localhost',
		'C4_SDK_CACHE_REDIS_PORT': '6379'
	})
	def test_get_cached_integration_miss(self):
		"""Test cache miss returns None"""
		import importlib
		import cc_py_commons.services.c4.c4_cache as c4_cache
		importlib.reload(c4_cache)

		# Setup mock Redis module and client after reload
		mock_redis_module = mock.MagicMock()
		mock_client = mock.MagicMock()
		mock_redis_module.Redis.return_value = mock_client
		c4_cache.redis = mock_redis_module
		mock_client.get.return_value = None

		result = c4_cache.get_cached_integration('1234', 'TAI')

		self.assertIsNone(result)

	@mock.patch.dict(os.environ, {
		'C4_SDK_CACHE_ENABLED': 'true',
		'C4_SDK_CACHE_REDIS_HOST': 'localhost',
		'C4_SDK_CACHE_REDIS_PORT': '6379'
	})
	def test_cache_integration(self):
		"""Test caching an integration response"""
		import importlib
		import cc_py_commons.services.c4.c4_cache as c4_cache
		importlib.reload(c4_cache)

		# Setup mock Redis module and client after reload
		mock_redis_module = mock.MagicMock()
		mock_client = mock.MagicMock()
		mock_redis_module.Redis.return_value = mock_client
		c4_cache.redis = mock_redis_module

		data = {'code': 200, 'data': {'activeStatusField': ['BOOKED']}}
		c4_cache.cache_integration('1234', 'TAI', data)

		mock_client.setex.assert_called_once()
		call_args = mock_client.setex.call_args
		self.assertEqual(call_args[0][0], 'c4-sdk:c4-sdk-integration-settings:integration:1234:TAI')
		self.assertEqual(call_args[0][1], 24 * 3600)  # 24 hours in seconds
		self.assertEqual(json.loads(call_args[0][2]), data)

	@mock.patch.dict(os.environ, {
		'C4_SDK_CACHE_ENABLED': 'true',
		'C4_SDK_CACHE_REDIS_HOST': 'localhost',
		'C4_SDK_CACHE_REDIS_PORT': '6379'
	})
	def test_evict_integration(self):
		"""Test evicting a cached integration"""
		import importlib
		import cc_py_commons.services.c4.c4_cache as c4_cache
		importlib.reload(c4_cache)

		# Setup mock Redis module and client after reload
		mock_redis_module = mock.MagicMock()
		mock_client = mock.MagicMock()
		mock_redis_module.Redis.return_value = mock_client
		c4_cache.redis = mock_redis_module

		c4_cache.evict_integration('1234', 'TAI')

		mock_client.delete.assert_called_once_with('c4-sdk:c4-sdk-integration-settings:integration:1234:TAI')

	def test_caching_disabled_returns_none(self):
		"""Test that cache functions return None when caching is disabled"""
		import importlib
		import cc_py_commons.services.c4.c4_cache as c4_cache
		importlib.reload(c4_cache)

		result = c4_cache.get_cached_integration('1234', 'TAI')
		self.assertIsNone(result)

	@mock.patch.dict(os.environ, {
		'C4_SDK_CACHE_ENABLED': 'true',
		'C4_SDK_CACHE_REDIS_HOST': 'localhost'
	})
	def test_redis_connection_failure_marks_cache_unavailable(self):
		"""Test that Redis connection failure marks cache as unavailable"""
		import importlib
		import cc_py_commons.services.c4.c4_cache as c4_cache
		importlib.reload(c4_cache)

		# Setup mock Redis module and client after reload
		mock_redis_module = mock.MagicMock()
		mock_client = mock.MagicMock()
		mock_redis_module.Redis.return_value = mock_client
		mock_client.ping.side_effect = Exception("Connection refused")
		c4_cache.redis = mock_redis_module

		result = c4_cache.get_cached_integration('1234', 'TAI')

		self.assertIsNone(result)
		self.assertFalse(c4_cache._cache_available)

	def test_safe_int_parsing(self):
		"""Test that _safe_int handles invalid values gracefully"""
		from cc_py_commons.services.c4.c4_cache import _safe_int

		# Valid values
		self.assertEqual(_safe_int('6379', 0), 6379)
		self.assertEqual(_safe_int('24', 0), 24)

		# Invalid values fall back to default
		self.assertEqual(_safe_int('invalid', 6379), 6379)
		self.assertEqual(_safe_int('', 24), 24)
		self.assertEqual(_safe_int(None, 6379), 6379)

	@mock.patch.dict(os.environ, {
		'C4_SDK_CACHE_ENABLED': 'true',
		'C4_SDK_CACHE_REDIS_HOST': 'localhost',
		'C4_SDK_CACHE_REDIS_PORT': 'invalid_port',
		'C4_SDK_CACHE_TTL_HOURS': 'invalid_ttl'
	})
	def test_invalid_env_vars_use_defaults(self):
		"""Test that invalid env var values use defaults"""
		import importlib
		import cc_py_commons.services.c4.c4_cache as c4_cache
		importlib.reload(c4_cache)

		self.assertEqual(c4_cache.CACHE_REDIS_PORT, 6379)
		self.assertEqual(c4_cache.CACHE_TTL_HOURS, 24)


class TestGetIntegrationWithCache(unittest.TestCase):

	def setUp(self):
		# Reset module state between tests
		import cc_py_commons.services.c4.c4_cache as c4_cache
		c4_cache._redis_client = None
		c4_cache._cache_available = True

	@mock.patch('cc_py_commons.services.c4.get_integration.c4_cache')
	@mock.patch('cc_py_commons.services.c4.get_integration.requests')
	def test_cache_hit_skips_api_call(self, mock_requests, mock_cache):
		"""Test that cache hit returns cached data without API call"""
		from cc_py_commons.services.c4.get_integration import execute

		cached_data = {'code': 200, 'data': {'activeStatusField': ['BOOKED']}}
		mock_cache.get_cached_integration.return_value = cached_data

		result = execute('1234', 'TAI')

		self.assertEqual(result, cached_data)
		mock_requests.get.assert_not_called()
		mock_cache.cache_integration.assert_not_called()

	@mock.patch('cc_py_commons.services.c4.get_integration.c4_cache')
	@mock.patch('cc_py_commons.services.c4.get_integration.requests')
	@mock.patch('cc_py_commons.services.c4.get_integration.C4_API_URL', 'https://api.test.com')
	@mock.patch('cc_py_commons.services.c4.get_integration.C4_API_AUTH_TOKEN', 'test-token')
	def test_cache_miss_fetches_and_caches(self, mock_requests, mock_cache):
		"""Test that cache miss fetches from API and caches result"""
		from cc_py_commons.services.c4.get_integration import execute

		mock_cache.get_cached_integration.return_value = None

		api_response = {'code': 200, 'data': {'activeStatusField': ['BOOKED']}}
		mock_response = mock.MagicMock()
		mock_response.status_code = 200
		mock_response.json.return_value = api_response
		mock_requests.get.return_value = mock_response

		result = execute('1234', 'TAI')

		self.assertEqual(result, api_response)
		mock_requests.get.assert_called_once()
		mock_cache.cache_integration.assert_called_once_with('1234', 'TAI', api_response)

	@mock.patch('cc_py_commons.services.c4.get_integration.c4_cache')
	@mock.patch('cc_py_commons.services.c4.get_integration.requests')
	@mock.patch('cc_py_commons.services.c4.get_integration.C4_API_URL', 'https://api.test.com')
	@mock.patch('cc_py_commons.services.c4.get_integration.C4_API_AUTH_TOKEN', 'test-token')
	def test_api_error_not_cached(self, mock_requests, mock_cache):
		"""Test that API errors are not cached"""
		from cc_py_commons.services.c4.get_integration import execute

		mock_cache.get_cached_integration.return_value = None

		mock_response = mock.MagicMock()
		mock_response.status_code = 500
		mock_requests.get.return_value = mock_response

		result = execute('1234', 'TAI')

		self.assertIsNone(result)
		mock_cache.cache_integration.assert_not_called()

	@mock.patch('cc_py_commons.services.c4.get_integration.c4_cache')
	@mock.patch('cc_py_commons.services.c4.get_integration.requests')
	@mock.patch('cc_py_commons.services.c4.get_integration.C4_API_URL', 'https://api.test.com')
	@mock.patch('cc_py_commons.services.c4.get_integration.C4_API_AUTH_TOKEN', 'test-token')
	def test_cache_error_falls_back_to_api(self, mock_requests, mock_cache):
		"""Test that cache errors fall back gracefully to API calls"""
		from cc_py_commons.services.c4.get_integration import execute

		# Simulate cache read exception
		mock_cache.get_cached_integration.side_effect = Exception("Redis connection failed")

		api_response = {'code': 200, 'data': {'activeStatusField': ['BOOKED']}}
		mock_response = mock.MagicMock()
		mock_response.status_code = 200
		mock_response.json.return_value = api_response
		mock_requests.get.return_value = mock_response

		result = execute('1234', 'TAI')

		# Should still get the API response despite cache error
		self.assertEqual(result, api_response)
		mock_requests.get.assert_called_once()

	@mock.patch('cc_py_commons.services.c4.get_integration.c4_cache')
	@mock.patch('cc_py_commons.services.c4.get_integration.requests')
	@mock.patch('cc_py_commons.services.c4.get_integration.C4_API_URL', 'https://api.test.com')
	@mock.patch('cc_py_commons.services.c4.get_integration.C4_API_AUTH_TOKEN', 'test-token')
	def test_cache_write_error_still_returns_result(self, mock_requests, mock_cache):
		"""Test that cache write errors don't prevent returning API result"""
		from cc_py_commons.services.c4.get_integration import execute

		mock_cache.get_cached_integration.return_value = None
		mock_cache.cache_integration.side_effect = Exception("Redis write failed")

		api_response = {'code': 200, 'data': {'activeStatusField': ['BOOKED']}}
		mock_response = mock.MagicMock()
		mock_response.status_code = 200
		mock_response.json.return_value = api_response
		mock_requests.get.return_value = mock_response

		result = execute('1234', 'TAI')

		# Should still get the API response even if cache write fails
		self.assertEqual(result, api_response)

	@mock.patch('cc_py_commons.services.c4.get_integration.c4_cache')
	@mock.patch('cc_py_commons.services.c4.get_integration.requests')
	def test_cached_false_returns_false(self, mock_requests, mock_cache):
		"""Test that cached 'not found' (False) responses are returned correctly"""
		from cc_py_commons.services.c4.get_integration import execute

		# False means "integration not found" - this should be cached
		mock_cache.get_cached_integration.return_value = False

		result = execute('1234', 'TAI')

		# Should return False without calling API
		self.assertFalse(result)
		mock_requests.get.assert_not_called()


class TestCacheFallbackBehavior(unittest.TestCase):
	"""Tests to verify graceful fallback when cache is unavailable"""

	@mock.patch.dict(os.environ, {
		'C4_SDK_CACHE_ENABLED': 'true',
		'C4_SDK_CACHE_REDIS_HOST': 'localhost'
	})
	def test_redis_read_exception_returns_none(self):
		"""Test that Redis read exceptions return None (allowing API fallback)"""
		import importlib
		import cc_py_commons.services.c4.c4_cache as c4_cache
		importlib.reload(c4_cache)

		# Setup mock Redis module and client after reload
		mock_redis_module = mock.MagicMock()
		mock_client = mock.MagicMock()
		mock_redis_module.Redis.return_value = mock_client
		mock_client.get.side_effect = Exception("Connection lost")
		c4_cache.redis = mock_redis_module

		result = c4_cache.get_cached_integration('1234', 'TAI')

		self.assertIsNone(result)

	@mock.patch.dict(os.environ, {
		'C4_SDK_CACHE_ENABLED': 'true',
		'C4_SDK_CACHE_REDIS_HOST': 'localhost'
	})
	def test_redis_write_exception_does_not_throw(self):
		"""Test that Redis write exceptions are handled gracefully (no throw)"""
		import importlib
		import cc_py_commons.services.c4.c4_cache as c4_cache
		importlib.reload(c4_cache)

		# Setup mock Redis module and client after reload
		mock_redis_module = mock.MagicMock()
		mock_client = mock.MagicMock()
		mock_redis_module.Redis.return_value = mock_client
		mock_client.setex.side_effect = Exception("Connection lost")
		c4_cache.redis = mock_redis_module

		# Should not raise exception
		try:
			c4_cache.cache_integration('1234', 'TAI', {'code': 200})

		except Exception as e:
			self.fail(f"cache_integration raised exception: {e}")

	@mock.patch.dict(os.environ, {
		'C4_SDK_CACHE_ENABLED': 'true',
		'C4_SDK_CACHE_REDIS_HOST': 'localhost'
	})
	def test_missing_redis_package_falls_back_gracefully(self):
		"""Test graceful fallback when redis package is not installed"""
		import importlib
		import cc_py_commons.services.c4.c4_cache as c4_cache
		importlib.reload(c4_cache)

		# Reset state
		c4_cache._redis_client = None
		c4_cache._cache_available = True

		# Simulate redis not being installed by setting module-level redis to None
		original_redis = c4_cache.redis
		c4_cache.redis = None

		try:
			result = c4_cache.get_cached_integration('1234', 'TAI')

			self.assertIsNone(result)
			self.assertFalse(c4_cache._cache_available)

		finally:
			# Restore original redis module
			c4_cache.redis = original_redis


if __name__ == '__main__':
	unittest.main()
