"""
Tests for research_service lifecycle management.

Tests cover:
- Resource cleanup
- Progress callbacks
"""

from unittest.mock import Mock, patch
import threading


class TestResourceCleanup:
    """Tests for resource cleanup."""

    @patch("local_deep_research.settings.env_registry.is_test_mode")
    @patch("local_deep_research.web.queue.processor_v2.queue_processor")
    @patch("local_deep_research.web.routes.globals.cleanup_research")
    @patch("local_deep_research.web.services.socket_service.SocketIOService")
    def test_cleanup_removes_from_active_dict(
        self,
        mock_socket,
        mock_cleanup,
        mock_queue,
        mock_test_mode,
    ):
        """Cleanup calls cleanup_research to remove from active dicts."""
        from local_deep_research.web.services.research_service import (
            cleanup_research_resources,
        )

        mock_test_mode.return_value = False

        cleanup_research_resources(123, "testuser")

        mock_cleanup.assert_called_once_with(123)

    @patch("local_deep_research.settings.env_registry.is_test_mode")
    @patch("local_deep_research.web.queue.processor_v2.queue_processor")
    @patch("local_deep_research.web.routes.globals.cleanup_research")
    @patch("local_deep_research.web.services.socket_service.SocketIOService")
    def test_cleanup_notifies_queue_processor(
        self,
        mock_socket,
        mock_cleanup,
        mock_queue,
        mock_test_mode,
    ):
        """Cleanup notifies queue processor of completion."""
        from local_deep_research.web.services.research_service import (
            cleanup_research_resources,
        )

        mock_test_mode.return_value = False

        cleanup_research_resources(123, "testuser")

        mock_queue.notify_research_completed.assert_called_once_with(
            "testuser", 123, user_password=None
        )

    def test_cleanup_socket_emit_success(self):
        """Cleanup emits socket event on success."""
        # Simulate socket emit
        mock_socket = Mock()

        # Emit to subscribers
        mock_socket.emit_to_subscribers(
            "research_complete",
            123,
            {"status": "completed"},
        )

        mock_socket.emit_to_subscribers.assert_called_once()

    @patch("local_deep_research.settings.env_registry.is_test_mode")
    @patch("local_deep_research.web.queue.processor_v2.queue_processor")
    @patch("local_deep_research.web.routes.globals.cleanup_research")
    @patch("local_deep_research.web.services.socket_service.SocketIOService")
    def test_cleanup_socket_emit_failure_handling(
        self,
        mock_socket_class,
        mock_cleanup,
        mock_queue,
        mock_test_mode,
    ):
        """Cleanup handles socket emit failure gracefully."""
        from local_deep_research.web.services.research_service import (
            cleanup_research_resources,
        )

        mock_test_mode.return_value = False
        mock_socket = Mock()
        mock_socket.emit_to_subscribers.side_effect = Exception("Socket error")
        mock_socket_class.return_value = mock_socket

        # Should not raise even with socket error
        cleanup_research_resources(123, "testuser")

    @patch("local_deep_research.settings.env_registry.is_test_mode")
    @patch("local_deep_research.web.queue.processor_v2.queue_processor")
    @patch("local_deep_research.web.routes.globals.cleanup_research")
    @patch("local_deep_research.web.services.socket_service.SocketIOService")
    def test_cleanup_database_session_handling(
        self,
        mock_socket,
        mock_cleanup,
        mock_queue,
        mock_test_mode,
    ):
        """Cleanup handles database session properly."""
        from local_deep_research.web.services.research_service import (
            cleanup_research_resources,
        )

        mock_test_mode.return_value = False

        # Should complete without database errors
        cleanup_research_resources(123, "testuser")

        mock_queue.notify_research_completed.assert_called()

    def test_cleanup_file_handle_closure(self):
        """Cleanup closes file handles."""
        # Simulate file handle management
        file_handles = []

        class MockFileHandle:
            def __init__(self):
                self.closed = False
                file_handles.append(self)

            def close(self):
                self.closed = True

        # Create handles
        MockFileHandle()
        MockFileHandle()

        # Cleanup closes handles
        for handle in file_handles:
            handle.close()

        assert all(h.closed for h in file_handles)

    def test_cleanup_memory_release(self):
        """Cleanup releases memory references."""
        # Simulate memory management
        large_data = {"data": "x" * 10000}
        active_research = {123: {"results": large_data}}

        # Cleanup removes references
        del active_research[123]

        assert 123 not in active_research

    def test_cleanup_concurrent_access_safety(self):
        """Cleanup is thread-safe."""
        active_research = {}
        lock = threading.Lock()
        cleanup_count = [0]

        def cleanup(research_id):
            with lock:
                if research_id in active_research:
                    del active_research[research_id]
                cleanup_count[0] += 1

        # Add some research
        for i in range(10):
            active_research[i] = {"data": i}

        # Concurrent cleanup
        threads = []
        for i in range(10):
            t = threading.Thread(target=cleanup, args=(i,))
            threads.append(t)
            t.start()

        for t in threads:
            t.join()

        assert len(active_research) == 0
        assert cleanup_count[0] == 10


class TestProgressCallbacks:
    """Tests for progress callback functionality."""

    def test_progress_callback_sequencing(self):
        """Progress callbacks are called in sequence."""
        calls = []

        def progress_callback(message, progress, metadata):
            calls.append((message, progress, metadata.get("phase")))

        # Simulate progress sequence
        progress_callback("Starting", 0, {"phase": "init"})
        progress_callback("Searching", 20, {"phase": "search"})
        progress_callback("Analyzing", 50, {"phase": "analysis"})
        progress_callback("Synthesizing", 80, {"phase": "synthesis"})
        progress_callback("Complete", 100, {"phase": "complete"})

        assert len(calls) == 5
        assert calls[0][1] == 0
        assert calls[-1][1] == 100
        assert calls[-1][2] == "complete"

    def test_progress_callback_percentage_accuracy(self):
        """Progress callbacks report accurate percentages."""
        progress_values = []

        def progress_callback(message, progress, metadata):
            if progress is not None:
                progress_values.append(progress)

        # Simulate progress
        for p in [0, 10, 25, 50, 75, 90, 100]:
            progress_callback("Progress", p, {})

        # All values should be in valid range
        assert all(0 <= p <= 100 for p in progress_values)
        # Values should be non-decreasing (progress doesn't go backwards)
        for i in range(1, len(progress_values)):
            assert progress_values[i] >= progress_values[i - 1]

    def test_progress_callback_status_message_updates(self):
        """Progress callbacks include meaningful status messages."""
        messages = []

        def progress_callback(message, progress, metadata):
            messages.append(message)

        progress_callback("Starting research process", 5, {"phase": "init"})
        progress_callback("Searching for information", 30, {"phase": "search"})
        progress_callback(
            "Generating summary", 90, {"phase": "output_generation"}
        )

        assert "Starting" in messages[0]
        assert "Searching" in messages[1]
        assert "summary" in messages[2].lower()

    @patch("local_deep_research.web.services.socket_service.SocketIOService")
    def test_progress_callback_socket_integration(self, mock_socket_class):
        """Progress callbacks integrate with socket service."""
        mock_socket = Mock()
        mock_socket_class.return_value = mock_socket

        def progress_callback(research_id, progress, metadata):
            mock_socket.emit_to_subscribers(
                "progress",
                research_id,
                {"progress": progress, **metadata},
            )

        progress_callback(123, 50, {"phase": "search"})

        mock_socket.emit_to_subscribers.assert_called_once_with(
            "progress",
            123,
            {"progress": 50, "phase": "search"},
        )

    @patch("local_deep_research.web.queue.processor_v2.queue_processor")
    def test_progress_callback_database_update(self, mock_queue):
        """Progress callbacks queue database updates."""

        def progress_callback(username, research_id, progress):
            mock_queue.queue_progress_update(username, research_id, progress)

        progress_callback("testuser", 123, 75)

        mock_queue.queue_progress_update.assert_called_once_with(
            "testuser", 123, 75
        )

    def test_progress_callback_rate_limiting(self):
        """Progress callbacks respect rate limiting."""
        import time

        last_call_time = [0]
        min_interval = 0.1  # 100ms
        calls_made = []

        def rate_limited_callback(progress):
            current_time = time.time()
            if current_time - last_call_time[0] >= min_interval:
                calls_made.append(progress)
                last_call_time[0] = current_time

        # Rapid calls
        for i in range(10):
            rate_limited_callback(i * 10)
            time.sleep(0.05)  # 50ms between calls

        # Only about half should go through due to rate limiting
        assert len(calls_made) < 10

    def test_progress_callback_error_handling(self):
        """Progress callbacks handle errors gracefully."""
        errors = []

        def progress_callback(progress, metadata):
            try:
                if metadata.get("fail"):
                    raise ValueError("Callback error")
            except Exception as e:
                errors.append(str(e))

        # Normal call
        progress_callback(50, {})

        # Error call
        progress_callback(60, {"fail": True})

        assert len(errors) == 1
        assert "Callback error" in errors[0]


class TestResearchPhaseTracking:
    """Tests for research phase tracking."""

    def test_phase_transition_init_to_search(self):
        """Phase transitions from init to search."""
        phases = []

        def track_phase(phase):
            phases.append(phase)

        track_phase("init")
        track_phase("search")

        assert phases == ["init", "search"]

    def test_phase_transition_search_to_analysis(self):
        """Phase transitions from search to analysis."""
        current_phase = "search"
        next_phase = "analysis"

        assert current_phase != next_phase

    def test_phase_transition_analysis_to_synthesis(self):
        """Phase transitions from analysis to synthesis."""
        phases = ["init", "search", "analysis", "synthesis"]

        # Synthesis follows analysis
        analysis_idx = phases.index("analysis")
        synthesis_idx = phases.index("synthesis")

        assert synthesis_idx == analysis_idx + 1

    def test_phase_transition_synthesis_to_complete(self):
        """Phase transitions from synthesis to complete."""
        all_phases = [
            "init",
            "search",
            "analysis",
            "synthesis",
            "output_generation",
            "complete",
        ]

        # Complete is the final phase
        assert all_phases[-1] == "complete"

    def test_phase_metadata_preservation(self):
        """Phase metadata is preserved across transitions."""
        phase_metadata = {}

        def update_phase(phase, **kwargs):
            phase_metadata[phase] = kwargs

        update_phase("search", engine="google", results_count=10)
        update_phase("analysis", findings_count=5)
        update_phase("synthesis", tokens_used=1000)

        assert phase_metadata["search"]["results_count"] == 10
        assert phase_metadata["analysis"]["findings_count"] == 5
        assert phase_metadata["synthesis"]["tokens_used"] == 1000


class TestActiveResearchManagement:
    """Tests for active research dictionary management."""

    def test_active_research_creation(self):
        """Active research entry is created correctly."""
        active_research = {}

        active_research[123] = {
            "thread": Mock(),
            "progress": 0,
            "status": "in_progress",
            "log": [],
            "settings": {"model": "gpt-4"},
        }

        assert 123 in active_research
        assert active_research[123]["status"] == "in_progress"

    def test_active_research_progress_update(self):
        """Active research progress is updated correctly."""
        active_research = {123: {"progress": 0, "status": "in_progress"}}

        active_research[123]["progress"] = 50

        assert active_research[123]["progress"] == 50

    def test_active_research_status_transition(self):
        """Active research status transitions correctly."""
        active_research = {123: {"status": "in_progress"}}

        # Transition to completed
        active_research[123]["status"] = "completed"

        assert active_research[123]["status"] == "completed"

    def test_active_research_removal(self):
        """Active research entry is removed on cleanup."""
        active_research = {123: {"status": "completed"}}

        del active_research[123]

        assert 123 not in active_research

    def test_active_research_thread_safety(self):
        """Active research is thread-safe with locking."""
        active_research = {}
        lock = threading.Lock()

        def add_research(research_id):
            with lock:
                active_research[research_id] = {"status": "in_progress"}

        def remove_research(research_id):
            with lock:
                if research_id in active_research:
                    del active_research[research_id]

        # Add and remove concurrently
        threads = []
        for i in range(10):
            t1 = threading.Thread(target=add_research, args=(i,))
            threads.append(t1)
            t1.start()

        for t in threads:
            t.join()

        # All should be added
        assert len(active_research) == 10

        # Remove all
        threads = []
        for i in range(10):
            t = threading.Thread(target=remove_research, args=(i,))
            threads.append(t)
            t.start()

        for t in threads:
            t.join()

        assert len(active_research) == 0
