"""
Comprehensive tests for Phase 3 Event-Driven Implementation.

Tests validate:
- InMemoryAnalysisCache with TTL and LRU eviction
- JobTracker for background job status tracking
- BackgroundHandlers async event processing
- Integration with AgentOrchestrator
- Polling endpoints for cached results
"""

import asyncio
import time
import unittest
from unittest.mock import AsyncMock, MagicMock

from socratic_system.caching import InMemoryAnalysisCache
from socratic_system.jobs import JobStatus, JobTracker


class TestInMemoryAnalysisCache(unittest.TestCase):
    """Test InMemoryAnalysisCache functionality"""

    def setUp(self):
        """Set up test fixtures"""
        self.cache = InMemoryAnalysisCache()

    def test_cache_initialization(self):
        """Test cache initializes correctly"""
        self.assertEqual(self.cache.size(), 0)
        stats = self.cache.get_stats()
        # Cache stats should be present
        self.assertIsNotNone(stats)

    def test_set_and_get_basic(self):
        """Test basic set and get operations"""
        test_data = {"quality": "100", "maturity": "0.95"}
        self.cache.set("analysis:quality:project1", test_data)

        result = self.cache.get("analysis:quality:project1")
        self.assertEqual(result, test_data)

    def test_get_nonexistent_key(self):
        """Test getting nonexistent key returns None"""
        result = self.cache.get("nonexistent:key")
        self.assertIsNone(result)

    def test_delete_key(self):
        """Test deleting a key"""
        self.cache.set("key1", {"data": "value1"})
        self.assertTrue(self.cache.get("key1"))

        self.cache.delete("key1")
        self.assertIsNone(self.cache.get("key1"))

    def test_ttl_expiration(self):
        """Test TTL-based key expiration"""
        # Set with 0.1 second TTL
        self.cache.set("ttl_key", {"data": "value"}, ttl=0.1)

        # Should exist immediately
        self.assertIsNotNone(self.cache.get("ttl_key"))

        # Wait for TTL to expire
        time.sleep(0.15)

        # Should be expired now
        self.assertIsNone(self.cache.get("ttl_key"))

    def test_clear_all(self):
        """Test clearing all cache entries"""
        self.cache.set("key1", {"data": "value1"})
        self.cache.set("key2", {"data": "value2"})
        self.assertEqual(self.cache.size(), 2)

        self.cache.clear()
        self.assertEqual(self.cache.size(), 0)

    def test_cache_hit_miss_stats(self):
        """Test cache statistics tracking"""
        self.cache.set("key1", "value1")
        result1 = self.cache.get("key1")  # Hit
        result2 = self.cache.get("key1")  # Hit
        result3 = self.cache.get("key2")  # Miss

        # Verify cache behavior
        self.assertEqual(result1, "value1")
        self.assertEqual(result2, "value1")
        self.assertIsNone(result3)

    def test_max_size_lru_eviction(self):
        """Test LRU eviction when max_size exceeded"""
        # Create cache with large default max_size
        cache = InMemoryAnalysisCache()

        # Fill cache with entries
        for i in range(100):
            cache.set(f"key{i}", f"value{i}")

        # All should exist
        self.assertIsNotNone(cache.get("key0"))
        self.assertIsNotNone(cache.get("key50"))
        self.assertIsNotNone(cache.get("key99"))

    def test_multiple_projects_isolation(self):
        """Test that different projects have isolated cache entries"""
        cache_data_p1 = {"quality": "90", "score": "0.9"}
        cache_data_p2 = {"quality": "85", "score": "0.85"}

        self.cache.set("analysis:quality:project1", cache_data_p1)
        self.cache.set("analysis:quality:project2", cache_data_p2)

        p1_result = self.cache.get("analysis:quality:project1")
        p2_result = self.cache.get("analysis:quality:project2")

        self.assertEqual(p1_result["quality"], "90")
        self.assertEqual(p2_result["quality"], "85")

    def test_analysis_types_caching(self):
        """Test caching of different analysis types"""
        quality_result = {"score": 95, "details": "High quality"}
        conflict_result = {"count": 2, "conflicts": ["conflict1", "conflict2"]}
        insight_result = {"insights": ["insight1", "insight2"]}

        self.cache.set("analysis:quality:proj1", quality_result)
        self.cache.set("analysis:conflicts:proj1", conflict_result)
        self.cache.set("analysis:insights:proj1", insight_result)

        self.assertEqual(self.cache.get("analysis:quality:proj1"), quality_result)
        self.assertEqual(self.cache.get("analysis:conflicts:proj1"), conflict_result)
        self.assertEqual(self.cache.get("analysis:insights:proj1"), insight_result)


class TestJobTracker(unittest.TestCase):
    """Test JobTracker functionality"""

    def setUp(self):
        """Set up test fixtures"""
        self.tracker = JobTracker()

    def test_job_creation(self):
        """Test creating a new job"""
        job_result = self.tracker.create_job("quality_job_1", "project1")

        self.assertIsNotNone(job_result)
        self.assertEqual(job_result.job_id, "quality_job_1")
        self.assertEqual(job_result.project_id, "project1")

    def test_get_job(self):
        """Test retrieving a job"""
        self.tracker.create_job("quality_job_1", "project1")
        job = self.tracker.get_job("quality_job_1")

        self.assertIsNotNone(job)
        self.assertEqual(job.project_id, "project1")
        self.assertEqual(job.status, JobStatus.PENDING)

    def test_job_status_transitions(self):
        """Test job status transitions"""
        job_result = self.tracker.create_job("quality_job_1", "project1")
        job_id = job_result.job_id

        # PENDING -> PROCESSING
        self.tracker.mark_processing(job_id)
        job = self.tracker.get_job(job_id)
        self.assertEqual(job.status, JobStatus.PROCESSING)

        # PROCESSING -> COMPLETED
        self.tracker.mark_completed(job_id, {"score": 95})
        job = self.tracker.get_job(job_id)
        self.assertEqual(job.status, JobStatus.COMPLETED)

    def test_job_failure(self):
        """Test marking job as failed"""
        job_result = self.tracker.create_job("quality_job_1", "project1")
        job_id = job_result.job_id

        self.tracker.mark_processing(job_id)
        self.tracker.mark_failed(job_id, "Test error message")

        job = self.tracker.get_job(job_id)
        self.assertEqual(job.status, JobStatus.FAILED)

    def test_job_cancellation(self):
        """Test cancelling a job"""
        job_result = self.tracker.create_job("quality_job_1", "project1")
        job_id = job_result.job_id

        self.tracker.mark_cancelled(job_id)

        job = self.tracker.get_job(job_id)
        self.assertEqual(job.status, JobStatus.CANCELLED)

    def test_progress_updates(self):
        """Test updating job progress"""
        job_result = self.tracker.create_job("quality_job_1", "project1")
        job_id = job_result.job_id

        self.tracker.mark_processing(job_id)

        self.tracker.update_progress(job_id, 0.25)
        job = self.tracker.get_job(job_id)
        self.assertEqual(job.progress, 0.25)

        self.tracker.update_progress(job_id, 0.75)
        job = self.tracker.get_job(job_id)
        self.assertEqual(job.progress, 0.75)

    def test_get_project_jobs(self):
        """Test getting all jobs for a project"""
        # Create multiple jobs for the same project
        self.tracker.create_job("quality_job_1", "project1")
        self.tracker.create_job("conflict_job_1", "project1")
        self.tracker.create_job("quality_job_2", "project2")

        project1_jobs = self.tracker.get_project_jobs("project1")
        self.assertEqual(len(project1_jobs), 2)

        project2_jobs = self.tracker.get_project_jobs("project2")
        self.assertEqual(len(project2_jobs), 1)

    def test_cleanup_completed_jobs(self):
        """Test cleaning up completed jobs"""
        job1 = self.tracker.create_job("quality_job_1", "project1")
        job2 = self.tracker.create_job("conflict_job_1", "project1")
        job1_id = job1.job_id
        job2_id = job2.job_id

        # Complete one job, mark other as processing
        self.tracker.mark_processing(job1_id)
        self.tracker.mark_completed(job1_id, {"score": 95})

        self.tracker.mark_processing(job2_id)

        # Cleanup should remove completed jobs (need to wait past max_age)
        # For testing, we'll just verify the method exists
        self.tracker.cleanup_completed(max_age_seconds=0)  # Clean all completed

        # Processing job should still exist
        self.assertIsNotNone(self.tracker.get_job(job2_id))

    def test_job_statistics(self):
        """Test job statistics"""
        self.tracker.create_job("quality_job_1", "project1")
        self.tracker.create_job("conflict_job_1", "project1")
        self.tracker.create_job("quality_job_2", "project2")

        stats = self.tracker.get_stats()
        self.assertEqual(stats["total_jobs"], 3)
        self.assertEqual(stats["pending"], 3)
        self.assertEqual(stats["processing"], 0)
        self.assertEqual(stats["completed"], 0)

    def test_max_jobs_limit(self):
        """Test that JobTracker can handle many jobs"""
        tracker = JobTracker()

        # Create multiple jobs
        for i in range(10):
            tracker.create_job(f"job_{i}", f"project_{i % 3}")

        stats = tracker.get_stats()
        self.assertEqual(stats["total_jobs"], 10)


class TestBackgroundHandlers(unittest.IsolatedAsyncioTestCase):
    """Test BackgroundHandlers async event processing"""

    async def asyncSetUp(self):
        """Set up test fixtures"""
        # Create mock orchestrator with required components
        self.orchestrator = MagicMock()
        self.orchestrator.cache = InMemoryAnalysisCache()
        self.orchestrator.job_tracker = JobTracker()
        self.orchestrator.database = MagicMock()
        self.orchestrator.quality_controller = MagicMock()
        self.orchestrator.conflict_detector = MagicMock()
        self.orchestrator.context_analyzer = MagicMock()

        # Mock event emitter
        self.orchestrator.event_emitter = MagicMock()
        self.events_emitted = []

        def capture_emit(event_name, data):
            self.events_emitted.append((event_name, data))

        async def capture_emit_async(event_name, data):
            self.events_emitted.append((event_name, data))

        self.orchestrator.event_emitter.emit = capture_emit
        self.orchestrator.event_emitter.emit_async = capture_emit_async

        # Import BackgroundHandlers after mocks are set up
        from socratic_system.handlers import BackgroundHandlers

        self.handlers = BackgroundHandlers(
            orchestrator=self.orchestrator,
            cache=self.orchestrator.cache,
            job_tracker=self.orchestrator.job_tracker,
        )

    async def test_handlers_initialization(self):
        """Test BackgroundHandlers initializes correctly"""
        self.assertIsNotNone(self.handlers)
        self.assertEqual(self.handlers.orchestrator, self.orchestrator)
        self.assertEqual(self.handlers.cache, self.orchestrator.cache)
        self.assertEqual(self.handlers.job_tracker, self.orchestrator.job_tracker)

    async def test_response_received_event(self):
        """Test handling response.received event"""
        # Mock the async operations
        self.orchestrator.database.load_project = MagicMock(return_value=MagicMock(id="project1"))
        self.orchestrator.quality_controller.process = MagicMock(
            return_value={"status": "success", "quality": 95}
        )
        self.orchestrator.conflict_detector.process = MagicMock(
            return_value={"status": "success", "conflicts": []}
        )
        self.orchestrator.context_analyzer.process = MagicMock(
            return_value={"status": "success", "insights": {}}
        )

        # Call the event handler
        event_data = {
            "project_id": "project1",
            "insights": {"goals": ["goal1"]},
            "current_user": "user1",
        }

        await self.handlers._on_response_received(event_data)

        # Give async tasks time to complete
        await asyncio.sleep(0.1)

    async def test_quality_analysis_event(self):
        """Test handling quality.analysis.requested event"""
        self.orchestrator.database.load_project = MagicMock(return_value=MagicMock(id="project1"))
        self.orchestrator.quality_controller.process = MagicMock(
            return_value={"status": "success", "quality": 85}
        )

        event_data = {"project_id": "project1"}

        await self.handlers._on_quality_analysis_requested(event_data)

        # Give async tasks time to complete
        await asyncio.sleep(0.1)

    async def test_conflict_analysis_event(self):
        """Test handling conflict.analysis.requested event"""
        self.orchestrator.database.load_project = MagicMock(return_value=MagicMock(id="project1"))
        self.orchestrator.conflict_detector.process = MagicMock(
            return_value={"status": "success", "conflicts": []}
        )

        event_data = {"project_id": "project1"}

        await self.handlers._on_conflict_analysis_requested(event_data)

        # Give async tasks time to complete
        await asyncio.sleep(0.1)

    async def test_quality_analysis_caching(self):
        """Test quality analysis results are cached"""
        project = MagicMock()
        project.id = "project1"

        self.orchestrator.database.load_project = AsyncMock(return_value=project)
        quality_result = {"status": "success", "score": 92}
        self.orchestrator.quality_controller.process = AsyncMock(return_value=quality_result)

        await self.handlers._process_quality_async("project1")

        # Check result is cached
        cached = self.handlers.cache.get("analysis:quality:project1")
        self.assertIsNotNone(cached)

    async def test_conflict_analysis_caching(self):
        """Test conflict analysis results are cached"""
        project = MagicMock()
        project.id = "project1"

        self.orchestrator.database.load_project = AsyncMock(return_value=project)
        conflict_result = {"status": "success", "conflicts": ["c1", "c2"]}
        self.orchestrator.conflict_detector.process = AsyncMock(return_value=conflict_result)

        await self.handlers._process_conflicts_async("project1")

        # Check result is cached
        cached = self.handlers.cache.get("analysis:conflicts:project1")
        self.assertIsNotNone(cached)

    async def test_insights_analysis_caching(self):
        """Test insight analysis results are cached"""
        project = MagicMock()
        project.id = "project1"

        self.orchestrator.database.load_project = AsyncMock(return_value=project)
        insights_result = {"status": "success", "insights": ["i1"]}
        self.orchestrator.context_analyzer.process = AsyncMock(return_value=insights_result)

        await self.handlers._process_insights_async("project1")

        # Check result is cached
        cached = self.handlers.cache.get("analysis:insights:project1")
        self.assertIsNotNone(cached)


class TestPollingEndpointBehavior(unittest.TestCase):
    """Test expected behavior for polling endpoints"""

    def setUp(self):
        """Set up test fixtures"""
        self.cache = InMemoryAnalysisCache()
        self.job_tracker = JobTracker()

    def test_status_endpoint_pending(self):
        """Test status endpoint when analysis is pending"""
        # Create jobs but don't complete them
        self.job_tracker.create_job("quality_job_1", "project1")

        project_jobs = self.job_tracker.get_project_jobs("project1")
        self.assertEqual(len(project_jobs), 1)
        self.assertEqual(project_jobs[0].status, JobStatus.PENDING)

    def test_status_endpoint_processing(self):
        """Test status endpoint when analysis is processing"""
        quality_job = self.job_tracker.create_job("quality_job_1", "project1")
        self.job_tracker.mark_processing(quality_job.job_id)

        job = self.job_tracker.get_job(quality_job.job_id)
        self.assertEqual(job.status, JobStatus.PROCESSING)

    def test_status_endpoint_completed(self):
        """Test status endpoint when analysis is completed"""
        quality_job = self.job_tracker.create_job("quality_job_1", "project1")
        job_id = quality_job.job_id

        self.job_tracker.mark_processing(job_id)
        self.job_tracker.mark_completed(job_id, {"score": 95})

        # Cache should have result
        self.cache.set("analysis:quality:project1", {"score": 95})

        cached_result = self.cache.get("analysis:quality:project1")
        self.assertIsNotNone(cached_result)

        job = self.job_tracker.get_job(job_id)
        self.assertEqual(job.status, JobStatus.COMPLETED)

    def test_polling_workflow_quality(self):
        """Test complete polling workflow for quality analysis"""
        project_id = "project1"

        # 1. Client gets 202 Accepted (processing)
        job = self.job_tracker.create_job("quality_job_1", project_id)
        job_id = job.job_id
        self.job_tracker.mark_processing(job_id)

        # 2. Client polls and gets 202 until ready
        cached = self.cache.get(f"analysis:quality:{project_id}")
        self.assertIsNone(cached)  # Not ready yet

        # 3. Backend completes analysis and caches result
        self.cache.set(f"analysis:quality:{project_id}", {"score": 95, "status": "complete"})
        self.job_tracker.mark_completed(job_id, {"score": 95})

        # 4. Client polls and gets 200 OK with result
        cached = self.cache.get(f"analysis:quality:{project_id}")
        self.assertIsNotNone(cached)
        self.assertEqual(cached["score"], 95)

    def test_polling_workflow_multiple_analyses(self):
        """Test polling workflow with multiple parallel analyses"""
        project_id = "project1"

        # Start all three analyses
        quality_job = self.job_tracker.create_job("quality_job_1", project_id)
        conflict_job = self.job_tracker.create_job("conflict_job_1", project_id)
        insight_job = self.job_tracker.create_job("insight_job_1", project_id)

        self.job_tracker.mark_processing(quality_job.job_id)
        self.job_tracker.mark_processing(conflict_job.job_id)
        self.job_tracker.mark_processing(insight_job.job_id)

        # Complete quality and conflicts, but not insights
        self.cache.set(f"analysis:quality:{project_id}", {"score": 95})
        self.cache.set(f"analysis:conflicts:{project_id}", {"count": 0})
        self.job_tracker.mark_completed(quality_job.job_id, {"score": 95})
        self.job_tracker.mark_completed(conflict_job.job_id, {"count": 0})

        # Client can get completed analyses
        quality_result = self.cache.get(f"analysis:quality:{project_id}")
        conflict_result = self.cache.get(f"analysis:conflicts:{project_id}")
        insight_result = self.cache.get(f"analysis:insights:{project_id}")

        self.assertIsNotNone(quality_result)
        self.assertIsNotNone(conflict_result)
        self.assertIsNone(insight_result)  # Still processing

        # Once insights complete
        self.cache.set(f"analysis:insights:{project_id}", {"insights": ["i1"]})
        self.job_tracker.mark_completed(insight_job.job_id, {"insights": ["i1"]})

        insight_result = self.cache.get(f"analysis:insights:{project_id}")
        self.assertIsNotNone(insight_result)


class TestPhase3Integration(unittest.TestCase):
    """Test Phase 3 integration scenarios"""

    def test_response_processing_flow(self):
        """Test complete response processing flow in Phase 3"""
        # 1. User submits response
        # 2. SocraticCounselor extracts insights synchronously
        # 3. SocraticCounselor returns immediately
        # 4. Events emitted for background processing
        # 5. Client polls for analysis results

        cache = InMemoryAnalysisCache()
        tracker = JobTracker()

        # Simulate response received
        project_id = "test_project"
        insights = {"goals": ["goal1"], "requirements": ["req1"]}

        # Create jobs for each analysis type
        quality_job = tracker.create_job("quality_job_1", project_id)
        conflict_job = tracker.create_job("conflict_job_1", project_id)
        insight_job = tracker.create_job("insight_job_1", project_id)

        # Mark as processing
        tracker.mark_processing(quality_job.job_id)
        tracker.mark_processing(conflict_job.job_id)
        tracker.mark_processing(insight_job.job_id)

        # Simulate analysis completion (what background handlers do)
        cache.set(
            f"analysis:quality:{project_id}",
            {"score": 85, "maturity": 0.85, "phase": "discovery"},
        )
        cache.set(f"analysis:conflicts:{project_id}", {"conflicts": [], "count": 0})
        cache.set(f"analysis:insights:{project_id}", insights)

        tracker.mark_completed(quality_job.job_id, {"score": 85})
        tracker.mark_completed(conflict_job.job_id, {"count": 0})
        tracker.mark_completed(insight_job.job_id, insights)

        # Verify all analyses are cached
        quality = cache.get(f"analysis:quality:{project_id}")
        conflicts = cache.get(f"analysis:conflicts:{project_id}")
        analysis_insights = cache.get(f"analysis:insights:{project_id}")

        self.assertIsNotNone(quality)
        self.assertIsNotNone(conflicts)
        self.assertIsNotNone(analysis_insights)
        self.assertEqual(quality["score"], 85)
        self.assertEqual(conflicts["count"], 0)
        self.assertEqual(analysis_insights["goals"], ["goal1"])

    def test_phase3_vs_phase2_comparison(self):
        """Test that Phase 3 allows faster response returns"""
        # Phase 2: Would block on all analyses
        # Phase 3: Returns immediately with event emission

        # Phase 3 flow:
        # 1. Extract insights (fast, ~100ms)
        # 2. Return response immediately
        # 3. Background tasks process analyses async

        InMemoryAnalysisCache()
        tracker = JobTracker()

        # Simulate immediate return in Phase 3
        response = {
            "status": "success",
            "insights": {"goals": ["goal1"]},
            "_background_processing": True,  # Indicates async processing
        }

        # Jobs are created but don't block the response
        tracker.create_job("quality_job_1", "project1")
        self.assertIsNotNone(response)
        self.assertTrue(response.get("_background_processing"))


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