"""
Deep behavioral tests for NewsScheduler user operations.
Tests update_user_info, unregister_user, start/stop, and singleton management.
"""

from datetime import datetime, UTC
from unittest.mock import Mock, patch

import pytest
from apscheduler.jobstores.base import JobLookupError

from local_deep_research.news.subscription_manager.scheduler import (
    DocumentSchedulerSettings,
    NewsScheduler,
)


@pytest.fixture(autouse=True)
def reset_scheduler():
    """Reset the singleton before and after each test."""
    NewsScheduler._instance = None
    yield
    NewsScheduler._instance = None


# --- update_user_info ---


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

    def test_does_nothing_when_not_running(self):
        s = NewsScheduler()
        s.is_running = False
        s.update_user_info("user1", "pass1")
        assert "user1" not in s.user_sessions

    def test_creates_new_session(self):
        s = NewsScheduler()
        s.is_running = True
        with patch.object(s, "_schedule_user_subscriptions"):
            s.update_user_info("user1", "pass1")
        assert "user1" in s.user_sessions

    def test_stores_password_in_credential_store(self):
        s = NewsScheduler()
        s.is_running = True
        with patch.object(s, "_schedule_user_subscriptions"):
            s.update_user_info("user1", "mypass")
        assert "password" not in s.user_sessions["user1"]
        assert s._credential_store.retrieve("user1") == "mypass"

    def test_sets_last_activity(self):
        s = NewsScheduler()
        s.is_running = True
        with patch.object(s, "_schedule_user_subscriptions"):
            s.update_user_info("user1", "pass1")
        assert "last_activity" in s.user_sessions["user1"]
        assert isinstance(s.user_sessions["user1"]["last_activity"], datetime)

    def test_initializes_empty_scheduled_jobs(self):
        s = NewsScheduler()
        s.is_running = True
        with patch.object(s, "_schedule_user_subscriptions"):
            s.update_user_info("user1", "pass1")
        assert s.user_sessions["user1"]["scheduled_jobs"] == set()

    def test_calls_schedule_user_subscriptions(self):
        s = NewsScheduler()
        s.is_running = True
        with patch.object(s, "_schedule_user_subscriptions") as mock_sched:
            s.update_user_info("user1", "pass1")
        mock_sched.assert_called_once_with("user1")

    def test_updates_existing_password(self):
        s = NewsScheduler()
        s.is_running = True
        with patch.object(s, "_schedule_user_subscriptions"):
            s.update_user_info("user1", "pass1")
            s.update_user_info("user1", "pass2")
        assert s._credential_store.retrieve("user1") == "pass2"

    def test_updates_last_activity_for_existing(self):
        s = NewsScheduler()
        s.is_running = True
        with patch.object(s, "_schedule_user_subscriptions"):
            s.update_user_info("user1", "pass1")
            first_activity = s.user_sessions["user1"]["last_activity"]
            s.update_user_info("user1", "pass1")
            second_activity = s.user_sessions["user1"]["last_activity"]
        assert second_activity >= first_activity

    def test_reschedules_for_existing_user(self):
        s = NewsScheduler()
        s.is_running = True
        with patch.object(s, "_schedule_user_subscriptions") as mock_sched:
            s.update_user_info("user1", "pass1")
            s.update_user_info("user1", "pass1")
        assert mock_sched.call_count == 2

    def test_multiple_users(self):
        s = NewsScheduler()
        s.is_running = True
        with patch.object(s, "_schedule_user_subscriptions"):
            s.update_user_info("user1", "pass1")
            s.update_user_info("user2", "pass2")
        assert "user1" in s.user_sessions
        assert "user2" in s.user_sessions


# --- unregister_user ---


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

    def test_removes_from_sessions(self):
        s = NewsScheduler()
        s.user_sessions["user1"] = {
            "last_activity": datetime.now(UTC),
            "scheduled_jobs": set(),
        }
        s._credential_store.store("user1", "pass")
        s.unregister_user("user1")
        assert "user1" not in s.user_sessions

    def test_removes_scheduled_jobs(self):
        s = NewsScheduler()
        mock_scheduler = Mock()
        s.scheduler = mock_scheduler
        s.user_sessions["user1"] = {
            "last_activity": datetime.now(UTC),
            "scheduled_jobs": {"job1", "job2"},
        }
        s._credential_store.store("user1", "pass")
        s.unregister_user("user1")
        assert mock_scheduler.remove_job.call_count == 2

    def test_handles_job_lookup_error(self):
        s = NewsScheduler()
        mock_scheduler = Mock()
        mock_scheduler.remove_job.side_effect = JobLookupError("job1")
        s.scheduler = mock_scheduler
        s.user_sessions["user1"] = {
            "last_activity": datetime.now(UTC),
            "scheduled_jobs": {"job1"},
        }
        s._credential_store.store("user1", "pass")
        # Should not raise
        s.unregister_user("user1")

    def test_invalidates_settings_cache(self):
        s = NewsScheduler()
        s._settings_cache["user1"] = DocumentSchedulerSettings()
        s.user_sessions["user1"] = {
            "last_activity": datetime.now(UTC),
            "scheduled_jobs": set(),
        }
        s._credential_store.store("user1", "pass")
        s.unregister_user("user1")
        assert "user1" not in s._settings_cache

    def test_handles_nonexistent_user(self):
        s = NewsScheduler()
        # Should not raise
        s.unregister_user("nonexistent")

    def test_does_not_affect_other_users(self):
        s = NewsScheduler()
        s.user_sessions["user1"] = {
            "last_activity": datetime.now(UTC),
            "scheduled_jobs": set(),
        }
        s._credential_store.store("user1", "pass1")
        s.user_sessions["user2"] = {
            "last_activity": datetime.now(UTC),
            "scheduled_jobs": set(),
        }
        s._credential_store.store("user2", "pass2")
        s.unregister_user("user1")
        assert "user2" in s.user_sessions


# --- start ---


class TestNewsSchedulerStart:
    """Tests for start method."""

    def test_disabled_config_does_not_start(self):
        s = NewsScheduler()
        s.config["enabled"] = False
        s.start()
        assert s.is_running is False

    def test_already_running_does_not_restart(self):
        s = NewsScheduler()
        s.is_running = True
        mock_scheduler = Mock()
        s.scheduler = mock_scheduler
        s.start()
        mock_scheduler.start.assert_not_called()


# --- stop ---


class TestNewsSchedulerStop:
    """Tests for stop method."""

    def test_clears_user_sessions(self):
        s = NewsScheduler()
        s.is_running = True
        s.user_sessions["user1"] = {"data": "test"}
        mock_scheduler = Mock()
        s.scheduler = mock_scheduler
        s.stop()
        assert s.user_sessions == {}

    def test_sets_is_running_false(self):
        s = NewsScheduler()
        s.is_running = True
        mock_scheduler = Mock()
        s.scheduler = mock_scheduler
        s.stop()
        assert s.is_running is False

    def test_calls_scheduler_shutdown(self):
        s = NewsScheduler()
        s.is_running = True
        mock_scheduler = Mock()
        s.scheduler = mock_scheduler
        s.stop()
        mock_scheduler.shutdown.assert_called_once_with(wait=True)

    def test_not_running_does_nothing(self):
        s = NewsScheduler()
        s.is_running = False
        mock_scheduler = Mock()
        s.scheduler = mock_scheduler
        s.stop()
        mock_scheduler.shutdown.assert_not_called()


# --- _get_document_scheduler_settings ---


class TestGetDocumentSchedulerSettings:
    """Tests for document scheduler settings retrieval with caching."""

    def test_returns_defaults_without_session(self):
        s = NewsScheduler()
        result = s._get_document_scheduler_settings("unknown_user")
        assert result == DocumentSchedulerSettings.defaults()

    def test_returns_cached_settings(self):
        s = NewsScheduler()
        cached = DocumentSchedulerSettings(enabled=False)
        s._settings_cache["user1"] = cached
        result = s._get_document_scheduler_settings("user1")
        assert result is cached

    def test_force_refresh_bypasses_cache(self):
        s = NewsScheduler()
        cached = DocumentSchedulerSettings(enabled=False)
        s._settings_cache["user1"] = cached
        # Without a session, force_refresh returns defaults (can't query DB)
        result = s._get_document_scheduler_settings("user1", force_refresh=True)
        assert result == DocumentSchedulerSettings.defaults()

    def test_returns_defaults_on_no_session(self):
        s = NewsScheduler()
        result = s._get_document_scheduler_settings("user1")
        assert result.enabled is True
        assert result.interval_seconds == 1800


# --- get_document_scheduler_status ---


class TestGetDocumentSchedulerStatus:
    """Tests for document scheduler status retrieval."""

    def test_unknown_user_returns_disabled(self):
        s = NewsScheduler()
        result = s.get_document_scheduler_status("unknown")
        assert result["enabled"] is False
        assert "not found" in result["message"].lower()

    def test_known_user_returns_settings(self):
        s = NewsScheduler()
        s.user_sessions["user1"] = {
            "last_activity": datetime.now(UTC),
            "scheduled_jobs": set(),
        }
        s._credential_store.store("user1", "pass")
        s._settings_cache["user1"] = DocumentSchedulerSettings()
        result = s.get_document_scheduler_status("user1")
        assert result["enabled"] is True
        assert result["interval_seconds"] == 1800

    def test_includes_processing_options(self):
        s = NewsScheduler()
        s.user_sessions["user1"] = {
            "last_activity": datetime.now(UTC),
            "scheduled_jobs": set(),
        }
        s._credential_store.store("user1", "pass")
        s._settings_cache["user1"] = DocumentSchedulerSettings(
            download_pdfs=True, extract_text=True, generate_rag=False
        )
        result = s.get_document_scheduler_status("user1")
        assert result["processing_options"]["download_pdfs"] is True
        assert result["processing_options"]["extract_text"] is True
        assert result["processing_options"]["generate_rag"] is False

    def test_has_scheduled_job_true(self):
        s = NewsScheduler()
        s.user_sessions["user1"] = {
            "last_activity": datetime.now(UTC),
            "scheduled_jobs": {"user1_document_processing"},
        }
        s._credential_store.store("user1", "pass")
        s._settings_cache["user1"] = DocumentSchedulerSettings()
        result = s.get_document_scheduler_status("user1")
        assert result["has_scheduled_job"] is True

    def test_has_scheduled_job_false(self):
        s = NewsScheduler()
        s.user_sessions["user1"] = {
            "last_activity": datetime.now(UTC),
            "scheduled_jobs": set(),
        }
        s._credential_store.store("user1", "pass")
        s._settings_cache["user1"] = DocumentSchedulerSettings()
        result = s.get_document_scheduler_status("user1")
        assert result["has_scheduled_job"] is False

    def test_user_active_status(self):
        s = NewsScheduler()
        s.user_sessions["user1"] = {
            "last_activity": datetime.now(UTC),
            "scheduled_jobs": set(),
        }
        s._credential_store.store("user1", "pass")
        s._settings_cache["user1"] = DocumentSchedulerSettings()
        result = s.get_document_scheduler_status("user1")
        assert result["user_active"] is True


# --- trigger_document_processing ---


class TestTriggerDocumentProcessing:
    """Tests for manual document processing trigger."""

    def test_returns_false_for_unknown_user(self):
        s = NewsScheduler()
        s.is_running = True
        result = s.trigger_document_processing("unknown")
        assert result is False

    def test_returns_false_when_not_running(self):
        s = NewsScheduler()
        s.is_running = False
        s.user_sessions["user1"] = {
            "last_activity": datetime.now(UTC),
            "scheduled_jobs": set(),
        }
        s._credential_store.store("user1", "pass")
        result = s.trigger_document_processing("user1")
        assert result is False

    def test_schedules_immediate_job(self):
        s = NewsScheduler()
        s.is_running = True
        mock_scheduler = Mock()
        mock_job = Mock()
        mock_job.next_run_time = datetime.now(UTC)
        mock_scheduler.get_job.return_value = mock_job
        s.scheduler = mock_scheduler
        s.user_sessions["user1"] = {
            "last_activity": datetime.now(UTC),
            "scheduled_jobs": set(),
        }
        s._credential_store.store("user1", "pass")
        result = s.trigger_document_processing("user1")
        assert result is True
        mock_scheduler.add_job.assert_called_once()

    def test_returns_false_on_job_verification_failure(self):
        s = NewsScheduler()
        s.is_running = True
        mock_scheduler = Mock()
        mock_scheduler.get_job.return_value = None  # Verification fails
        s.scheduler = mock_scheduler
        s.user_sessions["user1"] = {
            "last_activity": datetime.now(UTC),
            "scheduled_jobs": set(),
        }
        s._credential_store.store("user1", "pass")
        result = s.trigger_document_processing("user1")
        assert result is False

    def test_handles_exception(self):
        s = NewsScheduler()
        s.is_running = True
        mock_scheduler = Mock()
        mock_scheduler.add_job.side_effect = RuntimeError("scheduler error")
        s.scheduler = mock_scheduler
        s.user_sessions["user1"] = {
            "last_activity": datetime.now(UTC),
            "scheduled_jobs": set(),
        }
        s._credential_store.store("user1", "pass")
        result = s.trigger_document_processing("user1")
        assert result is False


# --- initialize_with_settings ---


class TestInitializeWithSettings:
    """Tests for settings initialization."""

    def test_stores_settings_manager(self):
        s = NewsScheduler()
        sm = Mock()
        sm.get_setting = Mock(return_value=True)
        s.initialize_with_settings(sm)
        assert s.settings_manager is sm

    def test_loads_config_from_settings(self):
        s = NewsScheduler()
        sm = Mock()

        def mock_get(key, default=None):
            return {
                "news.scheduler.enabled": True,
                "news.scheduler.retention_hours": 72,
                "news.scheduler.cleanup_interval_hours": 2,
                "news.scheduler.max_jitter_seconds": 600,
                "news.scheduler.max_concurrent_jobs": 20,
                "news.scheduler.batch_size": 10,
                "news.scheduler.activity_check_interval": 10,
            }.get(key, default)

        sm.get_setting = mock_get
        s.initialize_with_settings(sm)
        assert s.config["retention_hours"] == 72
        assert s.config["cleanup_interval_hours"] == 2
        assert s.config["max_jitter_seconds"] == 600

    def test_handles_settings_error(self):
        s = NewsScheduler()
        sm = Mock()
        sm.get_setting.side_effect = RuntimeError("DB error")
        # Should not raise, keeps default config
        s.initialize_with_settings(sm)
        assert s.config["enabled"] is True  # Default still intact
