import time
from unittest.mock import patch

import frappe
from frappe.tests import IntegrationTestCase
from frappe.utils.telemetry.pulse.client import EventQueue, capture, is_enabled
from frappe.utils.telemetry.pulse.utils import anonymize_user, parse_interval


class TestPulseClient(IntegrationTestCase):
	def setUp(self):
		super().setUp()
		# Clear any existing events from queue
		eq = EventQueue()
		while eq.length > 0:
			eq.collect(batch_size=1000)
		frappe.cache.delete_keys("pulse-client:")

	def tearDown(self):
		# Clean up after tests
		eq = EventQueue()
		while eq.length > 0:
			eq.collect(batch_size=1000)
		frappe.cache.delete_keys("pulse-client:")
		super().tearDown()


class TestEventQueue(TestPulseClient):
	def test_queue_operations(self):
		"""Test queue add, collect, and FIFO behavior"""
		eq = EventQueue()

		# Add events
		for i in range(10):
			event = {
				"event_name": f"test_event_{i}",
				"captured_at": "2026-01-01T00:00:00",
				"app": "frappe",
				"user": "test@example.com",
				"site": "test.localhost",
				"properties": {},
			}
			eq.add(event)

		self.assertEqual(eq.length, 10)

		# Collect events (FIFO order)
		events = eq.collect(batch_size=5)
		self.assertEqual(len(events), 5)
		self.assertEqual(eq.length, 5)
		self.assertEqual(events[0]["event_name"], "test_event_0")

	def test_queue_size_limit(self):
		"""Test that queue respects size limit"""
		eq = EventQueue()
		queue_size = eq.queue_size

		# Add more events than the queue size
		for i in range(queue_size + 100):
			event = {
				"event_name": f"test_event_{i}",
				"captured_at": "2026-01-01T00:00:00",
				"app": "frappe",
				"user": "test@example.com",
				"site": "test.localhost",
				"properties": {},
			}
			eq.add(event)

		# Queue should not exceed max size
		self.assertEqual(eq.length, queue_size)

	def test_requeue_events(self):
		"""Test requeueing events preserves order"""
		eq = EventQueue()

		# Add events
		event_names = ["event_1", "event_2", "event_3"]
		for name in event_names:
			event = {
				"event_name": name,
				"captured_at": "2026-01-01T00:00:00",
				"app": "frappe",
				"user": "test@example.com",
				"site": "test.localhost",
				"properties": {},
			}
			eq.add(event)

		# Collect and requeue
		events = eq.collect(batch_size=3)
		eq._requeue_events(events)

		# Check order is preserved
		requeued = eq.collect(batch_size=3)
		for i, event in enumerate(requeued):
			self.assertEqual(event["event_name"], event_names[i])


class TestRateLimiting(TestPulseClient):
	def test_ratelimit_basic(self):
		"""Test basic rate limiting functionality"""
		eq = EventQueue()

		event = {
			"event_name": "test_event",
			"captured_at": "2026-01-01T00:00:00",
			"app": "frappe",
			"user": "test@example.com",
			"site": "test.localhost",
			"properties": {},
		}

		# First event should be added
		eq.add(event, interval="5s")
		self.assertEqual(eq.length, 1)

		# Second event should be rate-limited
		eq.add(event, interval="5s")
		self.assertEqual(eq.length, 1)

	def test_ratelimit_different_events(self):
		"""Test that rate limiting is per-event"""
		eq = EventQueue()

		event1 = {
			"event_name": "event_1",
			"captured_at": "2026-01-01T00:00:00",
			"app": "frappe",
			"user": "test@example.com",
			"site": "test.localhost",
			"properties": {},
		}

		event2 = {
			"event_name": "event_2",
			"captured_at": "2026-01-01T00:00:00",
			"app": "frappe",
			"user": "test@example.com",
			"site": "test.localhost",
			"properties": {},
		}

		# Both events should be added as they are different
		eq.add(event1, interval="5s")
		eq.add(event2, interval="5s")
		self.assertEqual(eq.length, 2)

	def test_ratelimit_expiry(self):
		"""Test that rate limit expires after interval"""
		eq = EventQueue()

		event = {
			"event_name": "test_event",
			"captured_at": "2026-01-01T00:00:00",
			"app": "frappe",
			"user": "test@example.com",
			"site": "test.localhost",
			"properties": {},
		}

		# Add event with short interval
		eq.add(event, interval="1s")
		self.assertEqual(eq.length, 1)

		# Wait for interval to expire
		time.sleep(1.1)

		# Event should be added again
		eq.add(event, interval="1s")
		self.assertEqual(eq.length, 2)


class TestBatchProcessing(TestPulseClient):
	def test_batch_process_success(self):
		"""Test successful batch processing"""
		eq = EventQueue()
		processed = []

		def process_fn(events):
			processed.extend(events)

		# Add events
		for i in range(15):
			event = {
				"event_name": f"test_event_{i}",
				"captured_at": "2026-01-01T00:00:00",
				"app": "frappe",
				"user": "test@example.com",
				"site": "test.localhost",
				"properties": {},
			}
			eq.add(event)

		# Process in batches
		eq.batch_process(process_fn, batch_size=10, max_batches=2)

		# All events should be processed
		self.assertEqual(len(processed), 15)
		self.assertEqual(eq.length, 0)

	def test_batch_process_with_failure_and_retry(self):
		"""Test batch processing with failure and retry"""
		eq = EventQueue()
		call_count = 0

		def failing_fn(events):
			nonlocal call_count
			call_count += 1
			if call_count < 3:
				raise Exception("Temporary failure")
			return True

		# Add events
		for i in range(5):
			event = {
				"event_name": f"test_event_{i}",
				"captured_at": "2026-01-01T00:00:00",
				"app": "frappe",
				"user": "test@example.com",
				"site": "test.localhost",
				"properties": {},
			}
			eq.add(event)

		# Process with retries
		eq.batch_process(failing_fn, batch_size=10, max_retries=5, backoff_seconds=0.1)

		# Should succeed after retries
		self.assertGreaterEqual(call_count, 3)
		self.assertEqual(eq.length, 0)

	def test_batch_process_max_retries_exceeded(self):
		"""Test batch processing when max retries is exceeded"""
		eq = EventQueue()

		def always_failing_fn(events):
			raise Exception("Always fails")

		# Add events
		for i in range(5):
			event = {
				"event_name": f"test_event_{i}",
				"captured_at": "2026-01-01T00:00:00",
				"app": "frappe",
				"user": "test@example.com",
				"site": "test.localhost",
				"properties": {},
			}
			eq.add(event)

		# Process with limited retries
		eq.batch_process(always_failing_fn, batch_size=10, max_retries=2, backoff_seconds=0.1)

		# Events should be requeued
		self.assertEqual(eq.length, 5)


class TestCapture(TestPulseClient):
	@patch("frappe.utils.telemetry.pulse.client.is_enabled")
	def test_capture_when_disabled(self, mock_enabled):
		"""Test that capture does nothing when disabled"""
		is_enabled.clear_cache()
		mock_enabled.return_value = False
		eq = EventQueue()

		capture("test_event", site="test.localhost")

		self.assertEqual(eq.length, 0)

	@patch("frappe.utils.telemetry.pulse.client.is_enabled")
	def test_capture_basic(self, mock_enabled):
		"""Test basic event capture"""
		is_enabled.clear_cache()
		mock_enabled.return_value = True
		eq = EventQueue()

		capture(
			"test_event",
			site="test.localhost",
			app="frappe",
			user="test@example.com",
			properties={"key": "value"},
		)

		self.assertEqual(eq.length, 1)
		events = eq.collect(batch_size=1)
		self.assertEqual(events[0]["event_name"], "test_event")
		self.assertEqual(events[0]["properties"]["key"], "value")

	@patch("frappe.utils.telemetry.pulse.client.is_enabled")
	def test_capture_anonymizes_user(self, mock_enabled):
		"""Test that user is anonymized"""
		is_enabled.clear_cache()
		mock_enabled.return_value = True
		eq = EventQueue()

		test_user = "test@example.com"
		capture("test_event", site="test.localhost", user=test_user)

		events = eq.collect(batch_size=1)
		# User should be anonymized
		self.assertNotEqual(events[0]["user"], test_user)
		self.assertTrue(events[0]["user"].startswith("anon_"))


class TestUtils(TestPulseClient):
	def test_parse_interval(self):
		"""Test parsing various interval formats"""
		# Seconds
		self.assertEqual(parse_interval(60), 60)
		self.assertEqual(parse_interval("60"), 60)

		# Minutes, hours, days, weeks
		self.assertEqual(parse_interval("1m"), 60)
		self.assertEqual(parse_interval("1h"), 3600)
		self.assertEqual(parse_interval("1d"), 86400)
		self.assertEqual(parse_interval("1w"), 604800)

		# Invalid formats
		with self.assertRaises(ValueError):
			parse_interval("1x")

	def test_anonymize_user(self):
		"""Test user anonymization"""
		user = "test@example.com"
		anon_user = anonymize_user(user)

		# Should be anonymized and consistent
		self.assertNotEqual(anon_user, user)
		self.assertTrue(anon_user.startswith("anon_"))
		self.assertEqual(anonymize_user(user), anon_user)

		# Standard users not anonymized
		for standard_user in frappe.STANDARD_USERS:
			self.assertEqual(anonymize_user(standard_user), standard_user)


class TestEventQueueDecoding(TestPulseClient):
	def test_decode_valid_event(self):
		"""Test decoding valid event JSON"""
		eq = EventQueue()

		event = {
			"event_name": "test_event",
			"captured_at": "2026-01-01T00:00:00",
			"app": "frappe",
			"user": "test@example.com",
			"site": "test.localhost",
			"properties": {},
		}

		# Add and retrieve
		eq.add(event)
		event_json = frappe.cache.rpop(eq.queue)

		decoded = eq._decode_event(event_json)
		self.assertIsNotNone(decoded)
		self.assertEqual(decoded["event_name"], "test_event")

	def test_decode_invalid_json(self):
		"""Test decoding invalid JSON"""
		eq = EventQueue()

		# Invalid JSON should return None
		decoded = eq._decode_event(b"invalid json{")
		self.assertIsNone(decoded)


class TestEventKey(TestPulseClient):
	def test_event_key_generation_and_uniqueness(self):
		"""Test event key generation and uniqueness for rate limiting"""
		eq = EventQueue()

		event1 = {
			"event_name": "event_1",
			"app": "frappe",
			"user": "user1@example.com",
			"site": "test.localhost",
		}

		event2 = {
			"event_name": "event_2",
			"app": "frappe",
			"user": "user1@example.com",
			"site": "test.localhost",
		}

		# Test key composition
		key1 = eq._get_event_key(event1)
		self.assertIn("event_1", key1)
		self.assertIn("test.localhost", key1)
		self.assertIn("frappe", key1)

		# Test uniqueness
		key2 = eq._get_event_key(event2)
		self.assertNotEqual(key1, key2)
