"""
Tests for the QueueProcessorV2 class.

Tests cover:
- Initialization
- Starting and stopping
- User activity notification
- Research queuing
- Completion notifications
- Error handling
"""

import threading
from unittest.mock import Mock, MagicMock, patch


class TestQueueProcessorV2Init:
    """Tests for QueueProcessorV2 initialization."""

    def test_init_default_interval(self):
        """Initializes with default check interval."""
        from local_deep_research.web.queue.processor_v2 import (
            QueueProcessorV2,
        )

        processor = QueueProcessorV2()

        assert processor.check_interval == 10
        assert processor.running is False
        assert processor.thread is None

    def test_init_custom_interval(self):
        """Initializes with custom check interval."""
        from local_deep_research.web.queue.processor_v2 import (
            QueueProcessorV2,
        )

        processor = QueueProcessorV2(check_interval=30)

        assert processor.check_interval == 30

    def test_init_creates_empty_user_set(self):
        """Initializes with empty users to check set."""
        from local_deep_research.web.queue.processor_v2 import (
            QueueProcessorV2,
        )

        processor = QueueProcessorV2()

        assert len(processor._users_to_check) == 0

    def test_init_creates_empty_pending_operations(self):
        """Initializes with empty pending operations dict."""
        from local_deep_research.web.queue.processor_v2 import (
            QueueProcessorV2,
        )

        processor = QueueProcessorV2()

        assert processor.pending_operations == {}


class TestQueueProcessorV2StartStop:
    """Tests for start and stop methods."""

    def test_start_sets_running_flag(self):
        """start sets running flag to True."""
        from local_deep_research.web.queue.processor_v2 import (
            QueueProcessorV2,
        )

        processor = QueueProcessorV2()

        with patch.object(processor, "_process_queue_loop"):
            processor.start()

            assert processor.running is True

            # Clean up
            processor.running = False
            if processor.thread:
                processor.thread.join(timeout=1)

    def test_start_creates_thread(self):
        """start creates a daemon thread."""
        from local_deep_research.web.queue.processor_v2 import (
            QueueProcessorV2,
        )

        processor = QueueProcessorV2()

        with patch.object(processor, "_process_queue_loop"):
            processor.start()

            assert processor.thread is not None
            assert processor.thread.daemon is True

            # Clean up
            processor.running = False
            processor.thread.join(timeout=1)

    def test_start_when_already_running(self):
        """start does nothing if already running."""
        from local_deep_research.web.queue.processor_v2 import (
            QueueProcessorV2,
        )

        processor = QueueProcessorV2()
        processor.running = True

        # Should not create a new thread
        processor.start()

        assert processor.thread is None

    def test_stop_sets_running_flag_false(self):
        """stop sets running flag to False."""
        from local_deep_research.web.queue.processor_v2 import (
            QueueProcessorV2,
        )

        processor = QueueProcessorV2()
        processor.running = True
        processor.thread = Mock()
        processor.thread.join = Mock()

        processor.stop()

        assert processor.running is False
        processor.thread.join.assert_called_once_with(timeout=10)

    def test_stop_interrupts_check_interval_wait(self):
        """stop returns promptly while the loop sleeps between iterations.

        The autouse reset_singletons fixture stops the processor after
        every app test, so a stop() that blocks for check_interval adds
        that many seconds of teardown to each of those tests (observed
        as uniform 10.00s teardowns that pushed CI past its job timeout).
        """
        import time

        from local_deep_research.web.queue.processor_v2 import (
            QueueProcessorV2,
        )

        processor = QueueProcessorV2(check_interval=30)

        # Wrap the real wait so the test knows when the loop has entered
        # it — deterministic, unlike sleeping and hoping the first
        # iteration has finished.
        loop_entered_wait = threading.Event()
        real_wait = processor._stop_event.wait

        def tracking_wait(timeout=None):
            loop_entered_wait.set()
            return real_wait(timeout)

        processor._stop_event.wait = tracking_wait
        processor.start()
        try:
            assert loop_entered_wait.wait(timeout=10), (
                "loop never reached its inter-iteration wait"
            )

            started = time.monotonic()
            processor.stop()
            elapsed = time.monotonic() - started

            assert elapsed < 5, (
                f"stop() blocked for {elapsed:.1f}s; the loop's "
                "inter-iteration wait was not interrupted"
            )
            assert not processor.thread.is_alive()
        finally:
            processor.stop()

    def test_start_after_stop_rearms_loop_wait(self):
        """A restarted processor loops again instead of exiting instantly.

        stop() leaves _stop_event set; start() must clear it or the
        restarted loop's wait returns immediately every iteration
        (busy-spin) — and create_app() does restart this singleton after
        the test-suite teardown stops it.
        """
        from local_deep_research.web.queue.processor_v2 import (
            QueueProcessorV2,
        )

        processor = QueueProcessorV2()
        processor.start()
        processor.stop()

        with patch.object(processor, "_process_queue_loop"):
            processor.start()
            try:
                assert not processor._stop_event.is_set()
            finally:
                processor.stop()


class TestQueueProcessorV2NotifyUserActivity:
    """Tests for notify_user_activity method."""

    def test_notify_user_activity_adds_to_set(self):
        """notify_user_activity adds user to check set."""
        from local_deep_research.web.queue.processor_v2 import (
            QueueProcessorV2,
        )

        processor = QueueProcessorV2()

        processor.notify_user_activity("testuser", "session123")

        assert ("testuser", "session123") in processor._users_to_check

    def test_notify_user_activity_thread_safe(self):
        """notify_user_activity is thread safe."""
        from local_deep_research.web.queue.processor_v2 import (
            QueueProcessorV2,
        )

        processor = QueueProcessorV2()

        # Add from multiple threads
        threads = []
        for i in range(10):
            t = threading.Thread(
                target=processor.notify_user_activity,
                args=(f"user{i}", f"session{i}"),
            )
            threads.append(t)
            t.start()

        for t in threads:
            t.join()

        # All should be added
        assert len(processor._users_to_check) == 10


class TestQueueProcessorV2NotifyResearchQueued:
    """Tests for notify_research_queued method."""

    @patch("local_deep_research.web.queue.processor_v2.get_user_db_session")
    def test_notify_research_queued_queues_task(self, mock_get_session):
        """notify_research_queued adds task to queue."""
        from local_deep_research.web.queue.processor_v2 import (
            QueueProcessorV2,
        )

        processor = QueueProcessorV2()

        mock_session = MagicMock()
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)
        mock_get_session.return_value = mock_session

        with patch(
            "local_deep_research.web.queue.processor_v2.UserQueueService"
        ) as mock_queue_service_class:
            mock_queue_service = Mock()
            mock_queue_service_class.return_value = mock_queue_service

            processor.notify_research_queued("testuser", "research123")

            mock_queue_service.add_task_metadata.assert_called_once()


class TestQueueProcessorV2NotifyResearchCompleted:
    """Tests for notify_research_completed method."""

    def test_notify_research_completed_removes_from_active(self):
        """notify_research_completed removes user from active set."""
        from local_deep_research.web.queue.processor_v2 import (
            QueueProcessorV2,
        )

        processor = QueueProcessorV2()
        processor._users_to_check.add("testuser:session123")

        # Complete should trigger next queue check
        processor.notify_research_completed("testuser", 123)

        # Method exists and doesn't crash


class TestQueueProcessorV2QueueOperations:
    """Tests for queue operation methods."""

    def test_queue_error_update_stores_pending(self):
        """queue_error_update stores pending operation."""
        from local_deep_research.web.queue.processor_v2 import (
            QueueProcessorV2,
        )

        processor = QueueProcessorV2()

        processor.queue_error_update(
            username="testuser",
            research_id="123",
            status="failed",
            error_message="Test error",
            metadata={"key": "value"},
            completed_at="2024-01-01T00:00:00",
        )

        # Should store in pending operations
        assert len(processor.pending_operations) > 0
        # Check operation details
        operation = list(processor.pending_operations.values())[0]
        assert operation["operation_type"] == "error_update"
        assert operation["username"] == "testuser"
        assert operation["status"] == "failed"

    def test_queue_progress_update_stores_pending(self):
        """queue_progress_update stores pending operation."""
        from local_deep_research.web.queue.processor_v2 import (
            QueueProcessorV2,
        )

        processor = QueueProcessorV2()

        processor.queue_progress_update(
            username="testuser",
            research_id="123",
            progress=0.5,
        )

        # Should store in pending operations
        assert len(processor.pending_operations) > 0


class TestQueueProcessorV2PendingOperations:
    """Tests for pending operations handling."""

    def test_pending_operations_thread_safe(self):
        """Pending operations are thread safe."""
        from local_deep_research.web.queue.processor_v2 import (
            QueueProcessorV2,
        )

        processor = QueueProcessorV2()

        # Add operations from multiple threads using progress updates (simpler signature)
        threads = []
        for i in range(10):
            t = threading.Thread(
                target=processor.queue_progress_update,
                kwargs={
                    "username": f"user{i}",
                    "research_id": str(i),
                    "progress": 0.5,
                },
            )
            threads.append(t)
            t.start()

        for t in threads:
            t.join()

        # All should be stored
        assert len(processor.pending_operations) == 10


class TestQueueProcessorV2DirectExecution:
    """Tests for direct execution mode."""

    @patch("local_deep_research.web.queue.processor_v2.session_password_store")
    @patch("local_deep_research.web.queue.processor_v2.db_manager")
    @patch("local_deep_research.web.queue.processor_v2.get_user_db_session")
    def test_direct_execution_checks_queue_mode(
        self, mock_get_session, mock_db_manager, mock_password_store
    ):
        """Direct execution checks user's queue_mode setting."""
        from local_deep_research.web.queue.processor_v2 import (
            QueueProcessorV2,
        )

        processor = QueueProcessorV2()

        mock_password_store.get_session_password.return_value = "password123"
        mock_db_manager.open_user_database.return_value = Mock()

        mock_session = MagicMock()
        mock_session.__enter__ = Mock(return_value=mock_session)
        mock_session.__exit__ = Mock(return_value=False)
        mock_get_session.return_value = mock_session

        with patch(
            "local_deep_research.web.queue.processor_v2.UserQueueService"
        ) as mock_queue:
            mock_queue_instance = Mock()
            mock_queue.return_value = mock_queue_instance

            # Direct execution requires settings manager
            with patch(
                "local_deep_research.settings.manager.SettingsManager"
            ) as mock_settings:
                mock_settings_instance = Mock()
                mock_settings_instance.get_setting.side_effect = (
                    lambda key, default: (
                        "queue" if key == "app.queue_mode" else default
                    )
                )
                mock_settings.return_value = mock_settings_instance

                processor.notify_research_queued(
                    "testuser",
                    "research123",
                    session_id="session456",
                    query="test query",
                )

                # Should fall back to queue mode
                mock_queue_instance.add_task_metadata.assert_called_once()
