"""
Extended tests for news/subscription_manager/scheduler.py

Covers advanced functionality:
- User info updates with scheduling
- Document processing
- Subscription checking
- Research result storage
- Cleanup operations
- Status reporting
"""

import pytest
from unittest.mock import Mock, MagicMock, patch
from datetime import datetime, timedelta, UTC
import threading


@pytest.fixture
def mock_background_scheduler():
    """Mock BackgroundScheduler for all tests."""
    with patch(
        "local_deep_research.scheduler.background.BackgroundScheduler"
    ) as mock_cls:
        mock_instance = MagicMock()
        mock_cls.return_value = mock_instance
        yield mock_instance


@pytest.fixture
def scheduler(mock_background_scheduler):
    """Create a fresh scheduler instance with mocked dependencies."""
    from local_deep_research.scheduler.background import (
        BackgroundJobScheduler,
    )

    BackgroundJobScheduler._instance = None
    instance = BackgroundJobScheduler()
    return instance


@pytest.fixture
def running_scheduler(scheduler, mock_background_scheduler):
    """Create a scheduler that is in running state."""
    scheduler.is_running = True
    return scheduler


class TestUpdateUserInfo:
    """Tests for update_user_info method."""

    def test_update_user_info_when_not_running(self, scheduler):
        """update_user_info does nothing when scheduler not running."""
        scheduler.is_running = False

        scheduler.update_user_info("testuser", "password123")

        assert "testuser" not in scheduler.user_sessions

    def test_update_user_info_creates_new_session(self, running_scheduler):
        """update_user_info creates session for new user."""
        with patch.object(
            running_scheduler, "_schedule_user_subscriptions"
        ) as mock_schedule:
            running_scheduler.update_user_info("newuser", "password123")

            assert "newuser" in running_scheduler.user_sessions
            session = running_scheduler.user_sessions["newuser"]
            assert "password" not in session
            assert (
                running_scheduler._credential_store.retrieve("newuser")
                == "password123"
            )
            assert "last_activity" in session
            assert "scheduled_jobs" in session
            mock_schedule.assert_called_once_with("newuser")

    def test_update_user_info_updates_existing_session(self, running_scheduler):
        """update_user_info updates existing user session."""
        # Set up existing user
        old_time = datetime.now(UTC) - timedelta(hours=1)
        running_scheduler.user_sessions["existinguser"] = {
            "last_activity": old_time,
            "scheduled_jobs": set(),
        }
        running_scheduler._credential_store.store("existinguser", "oldpassword")

        with patch.object(
            running_scheduler, "_schedule_user_subscriptions"
        ) as mock_schedule:
            running_scheduler.update_user_info("existinguser", "newpassword")

            session = running_scheduler.user_sessions["existinguser"]
            assert (
                running_scheduler._credential_store.retrieve("existinguser")
                == "newpassword"
            )
            assert session["last_activity"] > old_time
            mock_schedule.assert_called_once_with("existinguser")

    def test_update_user_info_thread_safety(self, running_scheduler):
        """update_user_info is thread-safe."""
        with patch.object(running_scheduler, "_schedule_user_subscriptions"):
            # Run multiple updates concurrently
            threads = []
            for i in range(10):
                t = threading.Thread(
                    target=running_scheduler.update_user_info,
                    args=(f"user{i}", f"pass{i}"),
                )
                threads.append(t)
                t.start()

            for t in threads:
                t.join()

            # All users should be added
            for i in range(10):
                assert f"user{i}" in running_scheduler.user_sessions


class TestUnregisterUser:
    """Tests for unregister_user method."""

    def test_unregister_user_removes_session(
        self, scheduler, mock_background_scheduler
    ):
        """unregister_user removes user from sessions."""
        scheduler.user_sessions["testuser"] = {
            "scheduled_jobs": set(),
            "last_activity": datetime.now(UTC),
        }
        scheduler._credential_store.store("testuser", "test")

        scheduler.unregister_user("testuser")

        assert "testuser" not in scheduler.user_sessions

    def test_unregister_user_removes_scheduled_jobs(
        self, scheduler, mock_background_scheduler
    ):
        """unregister_user removes all scheduled jobs for user."""
        job_ids = {"testuser_1", "testuser_2", "testuser_3"}
        scheduler.user_sessions["testuser"] = {
            "scheduled_jobs": job_ids.copy(),
            "last_activity": datetime.now(UTC),
        }
        scheduler._credential_store.store("testuser", "test")

        scheduler.unregister_user("testuser")

        # Verify remove_job was called for each job
        assert mock_background_scheduler.remove_job.call_count == 3
        for job_id in job_ids:
            mock_background_scheduler.remove_job.assert_any_call(job_id)

    def test_unregister_user_handles_job_lookup_error(
        self, scheduler, mock_background_scheduler
    ):
        """unregister_user handles JobLookupError gracefully."""
        from apscheduler.jobstores.base import JobLookupError

        mock_background_scheduler.remove_job.side_effect = JobLookupError(
            "job1"
        )
        scheduler.user_sessions["testuser"] = {
            "scheduled_jobs": {"job1"},
            "last_activity": datetime.now(UTC),
        }
        scheduler._credential_store.store("testuser", "test")

        # Should not raise
        scheduler.unregister_user("testuser")
        assert "testuser" not in scheduler.user_sessions

    def test_unregister_nonexistent_user_safe(self, scheduler):
        """unregister_user is safe for non-existent users."""
        scheduler.unregister_user("nonexistent")
        # Should not raise


class TestScheduleUserSubscriptions:
    """Tests for _schedule_user_subscriptions method."""

    def test_schedule_subscriptions_no_session(self, scheduler):
        """_schedule_user_subscriptions handles missing session."""
        # Should not raise
        scheduler._schedule_user_subscriptions("nonexistent")

    def test_schedule_subscriptions_clears_old_jobs(
        self, scheduler, mock_background_scheduler
    ):
        """_schedule_user_subscriptions clears old jobs before scheduling new ones."""
        scheduler.user_sessions["testuser"] = {
            "scheduled_jobs": {"old_job_1", "old_job_2"},
            "last_activity": datetime.now(UTC),
        }
        scheduler._credential_store.store("testuser", "testpass")

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_db:
            mock_session = MagicMock()
            mock_session.__enter__ = Mock(return_value=mock_session)
            mock_session.__exit__ = Mock(return_value=False)
            mock_session.query.return_value.filter_by.return_value.all.return_value = []
            mock_db.return_value = mock_session

            scheduler._schedule_user_subscriptions("testuser")

            # Old jobs should be removed
            assert mock_background_scheduler.remove_job.call_count >= 2

    def test_schedule_subscriptions_with_interval_trigger(
        self, scheduler, mock_background_scheduler
    ):
        """_schedule_user_subscriptions uses interval trigger for frequent subscriptions."""
        scheduler.user_sessions["testuser"] = {
            "scheduled_jobs": set(),
            "last_activity": datetime.now(UTC),
        }
        scheduler._credential_store.store("testuser", "testpass")

        mock_subscription = MagicMock()
        mock_subscription.id = 1
        mock_subscription.name = "Hourly News"
        mock_subscription.query_or_topic = "test query"
        mock_subscription.refresh_interval_minutes = 60
        mock_subscription.next_refresh = None

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_db:
            mock_session = MagicMock()
            mock_session.__enter__ = Mock(return_value=mock_session)
            mock_session.__exit__ = Mock(return_value=False)
            mock_session.query.return_value.filter_by.return_value.all.return_value = [
                mock_subscription
            ]
            mock_db.return_value = mock_session

            scheduler._schedule_user_subscriptions("testuser")

            # Should add job with interval trigger
            mock_background_scheduler.add_job.assert_called()
            call_kwargs = mock_background_scheduler.add_job.call_args_list[-1][
                1
            ]
            assert call_kwargs.get("trigger") == "interval"

    def test_schedule_subscriptions_with_date_trigger_for_infrequent(
        self, scheduler, mock_background_scheduler
    ):
        """_schedule_user_subscriptions uses date trigger for infrequent subscriptions."""
        scheduler.user_sessions["testuser"] = {
            "scheduled_jobs": set(),
            "last_activity": datetime.now(UTC),
        }
        scheduler._credential_store.store("testuser", "testpass")

        mock_subscription = MagicMock()
        mock_subscription.id = 1
        mock_subscription.name = "Daily News"
        mock_subscription.query_or_topic = "test query"
        mock_subscription.refresh_interval_minutes = 1440  # Daily
        mock_subscription.next_refresh = datetime.now(UTC) + timedelta(hours=12)

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_db:
            mock_session = MagicMock()
            mock_session.__enter__ = Mock(return_value=mock_session)
            mock_session.__exit__ = Mock(return_value=False)
            mock_session.query.return_value.filter_by.return_value.all.return_value = [
                mock_subscription
            ]
            mock_db.return_value = mock_session

            scheduler._schedule_user_subscriptions("testuser")

            # Should add job with date trigger
            mock_background_scheduler.add_job.assert_called()

    def test_schedule_subscriptions_handles_overdue(
        self, scheduler, mock_background_scheduler
    ):
        """_schedule_user_subscriptions handles overdue subscriptions."""
        scheduler.user_sessions["testuser"] = {
            "scheduled_jobs": set(),
            "last_activity": datetime.now(UTC),
        }
        scheduler._credential_store.store("testuser", "testpass")

        mock_subscription = MagicMock()
        mock_subscription.id = 1
        mock_subscription.name = "Overdue News"
        mock_subscription.query_or_topic = "test query"
        mock_subscription.refresh_interval_minutes = 1440
        mock_subscription.next_refresh = datetime.now(UTC) - timedelta(hours=2)

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_db:
            mock_session = MagicMock()
            mock_session.__enter__ = Mock(return_value=mock_session)
            mock_session.__exit__ = Mock(return_value=False)
            mock_session.query.return_value.filter_by.return_value.all.return_value = [
                mock_subscription
            ]
            mock_db.return_value = mock_session

            scheduler._schedule_user_subscriptions("testuser")

            # Should add job (overdue should be scheduled immediately)
            mock_background_scheduler.add_job.assert_called()


class TestScheduleDocumentProcessing:
    """Tests for _schedule_document_processing method."""

    def test_schedule_document_processing_no_session(self, scheduler):
        """_schedule_document_processing handles missing session."""
        # Should not raise
        scheduler._schedule_document_processing("nonexistent")

    def test_schedule_document_processing_disabled(
        self, scheduler, mock_background_scheduler
    ):
        """_schedule_document_processing respects disabled setting."""
        scheduler.user_sessions["testuser"] = {
            "scheduled_jobs": set(),
            "last_activity": datetime.now(UTC),
        }
        scheduler._credential_store.store("testuser", "testpass")

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_db:
            mock_session = MagicMock()
            mock_session.__enter__ = Mock(return_value=mock_session)
            mock_session.__exit__ = Mock(return_value=False)
            mock_db.return_value = mock_session

            with patch(
                "local_deep_research.settings.manager.SettingsManager"
            ) as mock_settings:
                mock_settings_instance = MagicMock()
                mock_settings_instance.get_setting.return_value = (
                    False  # disabled
                )
                mock_settings.return_value = mock_settings_instance

                scheduler._schedule_document_processing("testuser")

                # If settings returns disabled, no document job should be added
                # (actual behavior depends on implementation)

    def test_schedule_document_processing_enabled(
        self, scheduler, mock_background_scheduler
    ):
        """_schedule_document_processing schedules job when enabled."""
        scheduler.user_sessions["testuser"] = {
            "scheduled_jobs": set(),
            "last_activity": datetime.now(UTC),
        }
        scheduler._credential_store.store("testuser", "testpass")

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_db:
            mock_session = MagicMock()
            mock_session.__enter__ = Mock(return_value=mock_session)
            mock_session.__exit__ = Mock(return_value=False)
            mock_db.return_value = mock_session

            with patch(
                "local_deep_research.settings.manager.SettingsManager"
            ) as mock_settings:
                mock_settings_instance = MagicMock()
                mock_settings_instance.get_setting.side_effect = (
                    lambda key, default=None: {
                        "document_scheduler.enabled": True,
                        "document_scheduler.interval_seconds": 1800,
                        "document_scheduler.download_pdfs": False,
                        "document_scheduler.extract_text": True,
                        "document_scheduler.generate_rag": False,
                    }.get(key, default)
                )
                mock_settings.return_value = mock_settings_instance

                # Mock get_job to return None (no existing job)
                mock_background_scheduler.get_job.return_value = MagicMock(
                    next_run_time=datetime.now(UTC)
                )

                scheduler._schedule_document_processing("testuser")

                # Should add document processing job
                mock_background_scheduler.add_job.assert_called()


class TestProcessUserDocuments:
    """Tests for _process_user_documents method."""

    def test_process_documents_no_session(self, scheduler):
        """_process_user_documents handles missing session."""
        # Should not raise
        scheduler._process_user_documents("nonexistent")

    def test_process_documents_no_options_enabled(self, scheduler):
        """_process_user_documents returns early when no options enabled."""
        scheduler.user_sessions["testuser"] = {
            "scheduled_jobs": set(),
            "last_activity": datetime.now(UTC),
        }
        scheduler._credential_store.store("testuser", "testpass")

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_db:
            mock_session = MagicMock()
            mock_session.__enter__ = Mock(return_value=mock_session)
            mock_session.__exit__ = Mock(return_value=False)
            mock_db.return_value = mock_session

            with patch(
                "local_deep_research.settings.manager.SettingsManager"
            ) as mock_settings:
                mock_settings_instance = MagicMock()
                mock_settings_instance.get_setting.side_effect = (
                    lambda key, default=None: {
                        "document_scheduler.download_pdfs": False,
                        "document_scheduler.extract_text": False,
                        "document_scheduler.generate_rag": False,
                        "document_scheduler.last_run": "",
                    }.get(key, default)
                )
                mock_settings.return_value = mock_settings_instance

                # Should return early and not query for research
                scheduler._process_user_documents("testuser")


class TestCheckSubscription:
    """Tests for _check_subscription method."""

    def test_check_subscription_no_session(
        self, scheduler, mock_background_scheduler
    ):
        """_check_subscription removes job when no session."""
        scheduler._check_subscription("nonexistent", 1)

        # Should try to remove the job
        mock_background_scheduler.remove_job.assert_called_with("nonexistent_1")

    def test_check_subscription_inactive_subscription(
        self, scheduler, mock_background_scheduler
    ):
        """_check_subscription handles inactive subscription."""
        scheduler.user_sessions["testuser"] = {
            "scheduled_jobs": set(),
            "last_activity": datetime.now(UTC),
        }
        scheduler._credential_store.store("testuser", "testpass")

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_db:
            mock_session = MagicMock()
            mock_session.__enter__ = Mock(return_value=mock_session)
            mock_session.__exit__ = Mock(return_value=False)
            mock_session.query.return_value.get.return_value = None
            mock_db.return_value = mock_session

            # Should not raise
            scheduler._check_subscription("testuser", 1)

    def test_check_subscription_replaces_date_placeholder(
        self, scheduler, mock_background_scheduler
    ):
        """_check_subscription replaces YYYY-MM-DD in query."""
        scheduler.user_sessions["testuser"] = {
            "scheduled_jobs": set(),
            "last_activity": datetime.now(UTC),
        }
        scheduler._credential_store.store("testuser", "testpass")

        mock_subscription = MagicMock()
        mock_subscription.id = 1
        mock_subscription.is_active = True
        mock_subscription.query_or_topic = "news for YYYY-MM-DD"
        mock_subscription.refresh_interval_minutes = 60
        mock_subscription.name = "Test"
        mock_subscription.model_provider = "test"
        mock_subscription.model = "test"
        mock_subscription.search_strategy = "news_aggregation"
        mock_subscription.search_engine = "searxng"

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_db:
            mock_session = MagicMock()
            mock_session.__enter__ = Mock(return_value=mock_session)
            mock_session.__exit__ = Mock(return_value=False)
            mock_session.query.return_value.get.return_value = mock_subscription
            mock_db.return_value = mock_session

            with patch("local_deep_research.settings.manager.SettingsManager"):
                with patch.object(
                    scheduler, "_trigger_subscription_research_sync"
                ) as mock_trigger:
                    with patch(
                        "local_deep_research.news.core.utils.get_local_date_string"
                    ) as mock_date:
                        mock_date.return_value = "2024-01-15"
                        mock_background_scheduler.get_job.return_value = None

                        scheduler._check_subscription("testuser", 1)

                        # Verify date was replaced
                        if mock_trigger.called:
                            call_args = mock_trigger.call_args[0]
                            subscription_data = call_args[1]
                            assert "2024-01-15" in subscription_data["query"]


class TestTriggerSubscriptionResearchSync:
    """Tests for _trigger_subscription_research_sync method."""

    def test_trigger_research_no_session(self, scheduler):
        """_trigger_subscription_research_sync handles missing session."""
        subscription = {"id": 1, "name": "Test", "query": "test"}

        # Should not raise
        scheduler._trigger_subscription_research_sync(
            "nonexistent", subscription
        )

    def test_trigger_research_calls_quick_summary(self, scheduler):
        """_trigger_subscription_research_sync calls quick_summary API."""
        scheduler.user_sessions["testuser"] = {
            "scheduled_jobs": set(),
            "last_activity": datetime.now(UTC),
        }
        scheduler._credential_store.store("testuser", "testpass")

        subscription = {
            "id": 1,
            "name": "Test Sub",
            "query": "test query",
            "original_query": "test query",
            "model_provider": "openai",
            "model": "gpt-4",
            "search_strategy": "news_aggregation",
            "search_engine": "searxng",
        }

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_db:
            mock_session = MagicMock()
            mock_session.__enter__ = Mock(return_value=mock_session)
            mock_session.__exit__ = Mock(return_value=False)
            mock_db.return_value = mock_session

            with patch(
                "local_deep_research.settings.manager.SettingsManager"
            ) as mock_settings:
                mock_settings.return_value.get_settings_snapshot.return_value = {}

                with patch(
                    "local_deep_research.api.research_functions.quick_summary"
                ) as mock_summary:
                    mock_summary.return_value = {"report": "Test report"}

                    with patch.object(scheduler, "_store_research_result"):
                        with patch(
                            "local_deep_research.config.thread_settings.set_settings_context"
                        ):
                            scheduler._trigger_subscription_research_sync(
                                "testuser", subscription
                            )

                            mock_summary.assert_called_once()


class TestStoreResearchResult:
    """Tests for _store_research_result method."""

    def test_store_result_creates_history_entry(self, scheduler):
        """_store_research_result creates ResearchHistory entry."""
        result = {"report": "Test report", "query": "test query", "sources": []}

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_db:
            mock_session = MagicMock()
            mock_session.__enter__ = Mock(return_value=mock_session)
            mock_session.__exit__ = Mock(return_value=False)
            mock_db.return_value = mock_session

            with patch(
                "local_deep_research.settings.manager.SettingsManager"
            ) as mock_settings:
                mock_settings.return_value.get_settings_snapshot.return_value = {}

                with patch(
                    "local_deep_research.news.utils.headline_generator.generate_headline"
                ) as mock_headline:
                    mock_headline.return_value = "Test Headline"

                    with patch(
                        "local_deep_research.news.utils.topic_generator.generate_topics"
                    ) as mock_topics:
                        mock_topics.return_value = ["topic1", "topic2"]

                        with patch(
                            "local_deep_research.storage.get_report_storage"
                        ) as mock_storage:
                            mock_storage.return_value.save_report.return_value = None

                            scheduler._store_research_result(
                                "testuser",
                                "testpass",
                                "research-123",
                                1,
                                result,
                                {"name": "Test Sub", "query": "test"},
                            )

                            mock_session.add.assert_called_once()
                            mock_session.commit.assert_called_once()


class TestCheckUserOverdueSubscriptions:
    """Tests for _check_user_overdue_subscriptions method."""

    def test_overdue_no_session(self, scheduler):
        """_check_user_overdue_subscriptions handles missing session."""
        # Should not raise
        scheduler._check_user_overdue_subscriptions("nonexistent")

    def test_overdue_schedules_immediate_jobs(
        self, scheduler, mock_background_scheduler
    ):
        """_check_user_overdue_subscriptions schedules overdue subscriptions."""
        scheduler.user_sessions["testuser"] = {
            "scheduled_jobs": set(),
            "last_activity": datetime.now(UTC),
        }
        scheduler._credential_store.store("testuser", "testpass")

        mock_subscription = MagicMock()
        mock_subscription.id = 1
        mock_subscription.name = "Overdue"
        mock_subscription.query_or_topic = "test"

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_db:
            mock_session = MagicMock()
            mock_session.__enter__ = Mock(return_value=mock_session)
            mock_session.__exit__ = Mock(return_value=False)
            mock_session.query.return_value.filter.return_value.all.return_value = [
                mock_subscription
            ]
            mock_db.return_value = mock_session

            scheduler._check_user_overdue_subscriptions("testuser")

            # Should schedule immediate job for overdue subscription
            mock_background_scheduler.add_job.assert_called()


class TestCleanupInactiveUsers:
    """Tests for _cleanup_inactive_users method."""

    def test_cleanup_removes_inactive_users(
        self, scheduler, mock_background_scheduler
    ):
        """_cleanup_inactive_users removes users past retention."""
        old_time = datetime.now(UTC) - timedelta(hours=100)
        scheduler.user_sessions["inactive_user"] = {
            "scheduled_jobs": {"job1", "job2"},
            "last_activity": old_time,
        }
        scheduler._credential_store.store("inactive_user", "test")

        cleaned = scheduler._cleanup_inactive_users()

        assert cleaned == 1
        assert "inactive_user" not in scheduler.user_sessions

    def test_cleanup_keeps_active_users(
        self, scheduler, mock_background_scheduler
    ):
        """_cleanup_inactive_users keeps active users."""
        recent_time = datetime.now(UTC) - timedelta(hours=1)
        scheduler.user_sessions["active_user"] = {
            "scheduled_jobs": set(),
            "last_activity": recent_time,
        }
        scheduler._credential_store.store("active_user", "test")

        cleaned = scheduler._cleanup_inactive_users()

        assert cleaned == 0
        assert "active_user" in scheduler.user_sessions

    def test_cleanup_removes_jobs_for_inactive(
        self, scheduler, mock_background_scheduler
    ):
        """_cleanup_inactive_users removes jobs for inactive users."""
        old_time = datetime.now(UTC) - timedelta(hours=100)
        scheduler.user_sessions["inactive_user"] = {
            "scheduled_jobs": {"job1", "job2"},
            "last_activity": old_time,
        }
        scheduler._credential_store.store("inactive_user", "test")

        scheduler._cleanup_inactive_users()

        # Jobs should be removed
        assert mock_background_scheduler.remove_job.call_count == 2


class TestReloadConfig:
    """Tests for _reload_config method."""

    def test_reload_config_no_settings_manager(self, scheduler):
        """_reload_config does nothing without settings manager."""
        # Should not raise
        scheduler._reload_config()

    def test_reload_config_updates_settings(
        self, scheduler, mock_background_scheduler
    ):
        """_reload_config updates config from settings manager."""
        mock_settings = MagicMock()
        mock_settings.get_setting.return_value = 72  # New retention hours

        scheduler.settings_manager = mock_settings

        scheduler._reload_config()

        # Config should be updated
        mock_settings.get_setting.assert_called()

    def test_reload_config_triggers_cleanup_on_retention_change(
        self, scheduler, mock_background_scheduler
    ):
        """_reload_config triggers cleanup when retention changes."""
        mock_settings = MagicMock()
        mock_settings.get_setting.side_effect = lambda key, default: (
            24
        )  # Changed retention

        scheduler.settings_manager = mock_settings
        scheduler.config["retention_hours"] = 48  # Old value

        scheduler._reload_config()

        # Should schedule immediate cleanup
        mock_background_scheduler.add_job.assert_called()


class TestGetStatus:
    """Tests for get_status method."""

    def test_get_status_returns_complete_info(
        self, scheduler, mock_background_scheduler
    ):
        """get_status returns complete status information."""
        scheduler.user_sessions["user1"] = {
            "scheduled_jobs": {"job1", "job2"},
            "last_activity": datetime.now(UTC),
        }
        scheduler._credential_store.store("user1", "test")
        scheduler.is_running = True

        mock_job = MagicMock()
        mock_job.next_run_time = datetime.now(UTC)
        mock_background_scheduler.get_job.return_value = mock_job

        status = scheduler.get_status()

        assert "is_running" in status
        assert status["is_running"] is True
        assert "config" in status
        assert "active_users" in status
        assert status["active_users"] == 1
        assert "total_scheduled_jobs" in status
        assert status["total_scheduled_jobs"] == 2
        assert "next_cleanup" in status
        assert "memory_usage" in status

    def test_get_status_when_not_running(
        self, scheduler, mock_background_scheduler
    ):
        """get_status returns correct status when not running."""
        scheduler.is_running = False

        status = scheduler.get_status()

        assert status["is_running"] is False
        assert status["next_cleanup"] is None


class TestGetUserSessionsSummary:
    """Tests for get_user_sessions_summary method."""

    def test_summary_returns_all_users(self, scheduler):
        """get_user_sessions_summary returns info for all users."""
        now = datetime.now(UTC)
        scheduler.user_sessions = {
            "user1": {
                "scheduled_jobs": {"job1"},
                "last_activity": now,
            },
            "user2": {
                "scheduled_jobs": {"job2", "job3"},
                "last_activity": now - timedelta(hours=1),
            },
        }
        scheduler._credential_store.store("user1", "pass1")
        scheduler._credential_store.store("user2", "pass2")

        summary = scheduler.get_user_sessions_summary()

        assert len(summary) == 2
        user_ids = [s["user_id"] for s in summary]
        assert "user1" in user_ids
        assert "user2" in user_ids

    def test_summary_does_not_include_passwords(self, scheduler):
        """get_user_sessions_summary does not expose passwords."""
        scheduler.user_sessions["user1"] = {
            "scheduled_jobs": set(),
            "last_activity": datetime.now(UTC),
        }
        scheduler._credential_store.store("user1", "secret123")

        summary = scheduler.get_user_sessions_summary()

        assert len(summary) == 1
        assert "password" not in summary[0]

    def test_summary_includes_activity_info(self, scheduler):
        """get_user_sessions_summary includes activity information."""
        now = datetime.now(UTC)
        scheduler.user_sessions["user1"] = {
            "scheduled_jobs": {"job1", "job2"},
            "last_activity": now,
        }
        scheduler._credential_store.store("user1", "pass")

        summary = scheduler.get_user_sessions_summary()

        assert "last_activity" in summary[0]
        assert "scheduled_jobs" in summary[0]
        assert summary[0]["scheduled_jobs"] == 2
        assert "time_since_activity" in summary[0]


class TestGetDocumentSchedulerStatus:
    """Tests for get_document_scheduler_status method."""

    def test_doc_status_no_session(self, scheduler):
        """get_document_scheduler_status handles missing session."""
        status = scheduler.get_document_scheduler_status("nonexistent")

        assert status["enabled"] is False
        assert "message" in status

    def test_doc_status_returns_config(self, scheduler):
        """get_document_scheduler_status returns configuration."""
        scheduler.user_sessions["testuser"] = {
            "scheduled_jobs": {"testuser_document_processing"},
            "last_activity": datetime.now(UTC),
        }
        scheduler._credential_store.store("testuser", "testpass")

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_db:
            mock_session = MagicMock()
            mock_session.__enter__ = Mock(return_value=mock_session)
            mock_session.__exit__ = Mock(return_value=False)
            mock_db.return_value = mock_session

            with patch(
                "local_deep_research.settings.manager.SettingsManager"
            ) as mock_settings:
                mock_settings_instance = MagicMock()
                mock_settings_instance.get_setting.side_effect = (
                    lambda key, default=None: {
                        "document_scheduler.enabled": True,
                        "document_scheduler.interval_seconds": 1800,
                        "document_scheduler.download_pdfs": True,
                        "document_scheduler.extract_text": True,
                        "document_scheduler.generate_rag": False,
                        "document_scheduler.last_run": "2024-01-15T10:00:00",
                    }.get(key, default)
                )
                mock_settings.return_value = mock_settings_instance

                status = scheduler.get_document_scheduler_status("testuser")

                assert status["enabled"] is True
                assert status["interval_seconds"] == 1800
                assert "processing_options" in status
                assert status["processing_options"]["download_pdfs"] is True
                assert status["has_scheduled_job"] is True


class TestTriggerDocumentProcessing:
    """Tests for trigger_document_processing method."""

    def test_trigger_no_session(self, scheduler):
        """trigger_document_processing returns False for missing session."""
        result = scheduler.trigger_document_processing("nonexistent")

        assert result is False

    def test_trigger_when_not_running(self, scheduler):
        """trigger_document_processing returns False when scheduler not running."""
        scheduler.user_sessions["testuser"] = {
            "scheduled_jobs": set(),
            "last_activity": datetime.now(UTC),
        }
        scheduler._credential_store.store("testuser", "testpass")
        scheduler.is_running = False

        result = scheduler.trigger_document_processing("testuser")

        assert result is False

    def test_trigger_schedules_immediate_job(
        self, running_scheduler, mock_background_scheduler
    ):
        """trigger_document_processing schedules immediate processing job."""
        running_scheduler.user_sessions["testuser"] = {
            "scheduled_jobs": set(),
            "last_activity": datetime.now(UTC),
        }
        running_scheduler._credential_store.store("testuser", "testpass")

        mock_job = MagicMock()
        mock_job.next_run_time = datetime.now(UTC)
        mock_background_scheduler.get_job.return_value = mock_job

        result = running_scheduler.trigger_document_processing("testuser")

        assert result is True
        mock_background_scheduler.add_job.assert_called()


class TestRunCleanupWithTracking:
    """Tests for _run_cleanup_with_tracking method."""

    def test_cleanup_with_tracking_calls_cleanup(self, scheduler):
        """_run_cleanup_with_tracking calls _cleanup_inactive_users."""
        with patch.object(
            scheduler, "_cleanup_inactive_users", return_value=3
        ) as mock_cleanup:
            scheduler._run_cleanup_with_tracking()

            mock_cleanup.assert_called_once()

    def test_cleanup_with_tracking_handles_errors(self, scheduler):
        """_run_cleanup_with_tracking handles exceptions."""
        with patch.object(
            scheduler,
            "_cleanup_inactive_users",
            side_effect=Exception("Test error"),
        ):
            # Should not raise
            scheduler._run_cleanup_with_tracking()


class TestEstimateMemoryUsage:
    """Tests for _estimate_memory_usage method."""

    def test_memory_usage_empty(self, scheduler):
        """_estimate_memory_usage returns 0 for no users."""
        usage = scheduler._estimate_memory_usage()

        assert usage == 0

    def test_memory_usage_scales_with_users(self, scheduler):
        """_estimate_memory_usage scales with number of users."""
        for i in range(5):
            scheduler.user_sessions[f"user{i}"] = {
                "scheduled_jobs": set(),
                "last_activity": datetime.now(UTC),
            }
            scheduler._credential_store.store(f"user{i}", "pass")

        usage = scheduler._estimate_memory_usage()

        assert usage > 0
        assert usage == 5 * 350  # 350 bytes per user estimate


class TestGetNewsScheduler:
    """Tests for get_background_job_scheduler function."""

    def test_get_news_scheduler_returns_singleton(
        self, mock_background_scheduler
    ):
        """get_background_job_scheduler returns singleton instance."""
        from local_deep_research.scheduler.background import (
            get_background_job_scheduler,
            BackgroundJobScheduler,
        )
        import local_deep_research.scheduler.background as scheduler_module

        # Reset singletons
        BackgroundJobScheduler._instance = None
        scheduler_module._scheduler_instance = None

        scheduler1 = get_background_job_scheduler()
        scheduler2 = get_background_job_scheduler()

        assert scheduler1 is scheduler2

    def test_get_news_scheduler_creates_instance(
        self, mock_background_scheduler
    ):
        """get_background_job_scheduler creates instance if none exists."""
        from local_deep_research.scheduler.background import (
            get_background_job_scheduler,
            BackgroundJobScheduler,
        )
        import local_deep_research.scheduler.background as scheduler_module

        # Reset singletons
        BackgroundJobScheduler._instance = None
        scheduler_module._scheduler_instance = None

        scheduler = get_background_job_scheduler()

        assert scheduler is not None
        assert isinstance(scheduler, BackgroundJobScheduler)


class TestCompletedAtDatetimeParsing:
    """Tests for completed_at datetime parsing (PR #2029).

    PR #2029 changed bare `except:` to `except ValueError:` in
    _process_user_documents when parsing completed_at with fromisoformat.
    """

    def test_valid_iso_datetime_parsed(self):
        """Valid ISO datetime string is parsed correctly."""
        completed_at_str = "2024-01-15T10:30:00"
        completed_at_obj = datetime.fromisoformat(completed_at_str)
        assert completed_at_obj.year == 2024
        assert completed_at_obj.month == 1

    def test_iso_datetime_with_z_suffix(self):
        """ISO datetime with Z suffix is parsed after replacement."""
        completed_at_str = "2024-01-15T10:30:00Z"
        completed_at_obj = datetime.fromisoformat(
            completed_at_str.replace("Z", "+00:00")
        )
        assert completed_at_obj is not None

    def test_invalid_datetime_raises_value_error(self):
        """Invalid datetime string raises ValueError (not caught by bare except)."""
        with pytest.raises(ValueError):
            datetime.fromisoformat("not-a-date")

    def test_empty_string_raises_value_error(self):
        """Empty string raises ValueError."""
        with pytest.raises(ValueError):
            datetime.fromisoformat("")
