"""
Deep behavioral tests for scheduler.py pure logic.
Tests default config, settings dataclass, singleton pattern,
user session tracking, jitter calculation, and job ID patterns.
"""

import random
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone


# --- DocumentSchedulerSettings defaults ---


@dataclass(frozen=True)
class TestDocumentSchedulerSettings:
    """Test copy of DocumentSchedulerSettings."""

    enabled: bool = True
    interval_seconds: int = 1800
    download_pdfs: bool = False
    extract_text: bool = True
    generate_rag: bool = False
    last_run: str = ""

    @classmethod
    def defaults(cls) -> "TestDocumentSchedulerSettings":
        return cls()


class TestDocumentSchedulerSettingsDefaults:
    """Tests for DocumentSchedulerSettings default values."""

    def test_enabled_default_true(self):
        settings = TestDocumentSchedulerSettings.defaults()
        assert settings.enabled is True

    def test_interval_seconds_default(self):
        settings = TestDocumentSchedulerSettings.defaults()
        assert settings.interval_seconds == 1800  # 30 minutes

    def test_download_pdfs_default_false(self):
        settings = TestDocumentSchedulerSettings.defaults()
        assert settings.download_pdfs is False

    def test_extract_text_default_true(self):
        settings = TestDocumentSchedulerSettings.defaults()
        assert settings.extract_text is True

    def test_generate_rag_default_false(self):
        settings = TestDocumentSchedulerSettings.defaults()
        assert settings.generate_rag is False

    def test_last_run_default_empty(self):
        settings = TestDocumentSchedulerSettings.defaults()
        assert settings.last_run == ""

    def test_frozen_dataclass(self):
        settings = TestDocumentSchedulerSettings.defaults()
        try:
            settings.enabled = False  # type: ignore
            assert False, "Should have raised FrozenInstanceError"
        except Exception:
            pass  # Expected - frozen dataclass


class TestDocumentSchedulerSettingsCustom:
    """Tests for custom DocumentSchedulerSettings values."""

    def test_custom_enabled(self):
        settings = TestDocumentSchedulerSettings(enabled=False)
        assert settings.enabled is False

    def test_custom_interval(self):
        settings = TestDocumentSchedulerSettings(interval_seconds=3600)
        assert settings.interval_seconds == 3600

    def test_custom_download_pdfs(self):
        settings = TestDocumentSchedulerSettings(download_pdfs=True)
        assert settings.download_pdfs is True

    def test_custom_last_run(self):
        settings = TestDocumentSchedulerSettings(
            last_run="2025-06-15T10:00:00Z"
        )
        assert settings.last_run == "2025-06-15T10:00:00Z"


# --- Default config ---


class TestDefaultConfig:
    """Tests for _load_default_config values."""

    def _load_defaults(self):
        return {
            "enabled": True,
            "retention_hours": 48,
            "cleanup_interval_hours": 1,
            "max_jitter_seconds": 300,
            "max_concurrent_jobs": 10,
            "subscription_batch_size": 5,
            "activity_check_interval_minutes": 5,
        }

    def test_enabled_default(self):
        config = self._load_defaults()
        assert config["enabled"] is True

    def test_retention_hours_default(self):
        config = self._load_defaults()
        assert config["retention_hours"] == 48

    def test_cleanup_interval_default(self):
        config = self._load_defaults()
        assert config["cleanup_interval_hours"] == 1

    def test_max_jitter_default(self):
        config = self._load_defaults()
        assert config["max_jitter_seconds"] == 300

    def test_max_concurrent_jobs_default(self):
        config = self._load_defaults()
        assert config["max_concurrent_jobs"] == 10

    def test_batch_size_default(self):
        config = self._load_defaults()
        assert config["subscription_batch_size"] == 5

    def test_activity_check_interval_default(self):
        config = self._load_defaults()
        assert config["activity_check_interval_minutes"] == 5


# --- Singleton pattern ---


class TestSingletonPattern:
    """Tests for singleton pattern logic."""

    def test_instance_initially_none(self):
        _instance = None
        assert _instance is None

    def test_instance_created(self):
        _instance = None
        if _instance is None:
            _instance = {"initialized": True}
        assert _instance is not None

    def test_instance_reused(self):
        _instance = {"count": 1}
        if _instance is None:
            _instance = {"count": 2}
        assert _instance["count"] == 1

    def test_double_check_locking(self):
        _instance = None
        if _instance is None:
            # Simulating double-check
            if _instance is None:
                _instance = {"value": "created"}
        assert _instance is not None


# --- User session structure ---


class TestUserSessionStructure:
    """Tests for user session data structure.

    Note: passwords are now stored in SchedulerCredentialStore, not in
    the session dict. Session dict only has last_activity and scheduled_jobs.
    """

    def _create_session(self):
        now = datetime.now(timezone.utc)
        return {
            "last_activity": now,
            "scheduled_jobs": set(),
        }

    def test_has_last_activity(self):
        session = self._create_session()
        assert "last_activity" in session
        assert isinstance(session["last_activity"], datetime)

    def test_has_empty_scheduled_jobs(self):
        session = self._create_session()
        assert session["scheduled_jobs"] == set()

    def test_scheduled_jobs_is_set(self):
        session = self._create_session()
        assert isinstance(session["scheduled_jobs"], set)

    def test_no_password_in_session(self):
        session = self._create_session()
        assert "password" not in session


class TestUserSessionUpdate:
    """Tests for user session update logic."""

    def test_new_user_creates_session(self):
        sessions = {}
        username = "user1"
        if username not in sessions:
            sessions[username] = {
                "last_activity": None,
                "scheduled_jobs": set(),
            }
        assert "user1" in sessions

    def test_activity_time_updated(self):
        old_time = datetime(2025, 1, 1, tzinfo=timezone.utc)
        new_time = datetime(2025, 6, 15, tzinfo=timezone.utc)
        sessions = {"user1": {"last_activity": old_time}}
        sessions["user1"]["last_activity"] = new_time
        assert sessions["user1"]["last_activity"] == new_time


# --- Jitter calculation ---


class TestJitterCalculation:
    """Tests for jitter calculation in scheduling."""

    def _calc_jitter(self, max_jitter):
        return random.randint(0, max_jitter)

    def test_jitter_within_range(self):
        for _ in range(100):
            jitter = self._calc_jitter(300)
            assert 0 <= jitter <= 300

    def test_jitter_with_zero_max(self):
        jitter = self._calc_jitter(0)
        assert jitter == 0

    def test_jitter_with_small_max(self):
        for _ in range(50):
            jitter = self._calc_jitter(10)
            assert 0 <= jitter <= 10


class TestJitterFromConfig:
    """Tests for getting jitter from config."""

    def _get_max_jitter(self, config):
        return int(config.get("max_jitter_seconds", 300))

    def test_from_config(self):
        config = {"max_jitter_seconds": 600}
        assert self._get_max_jitter(config) == 600

    def test_default_when_missing(self):
        config = {}
        assert self._get_max_jitter(config) == 300

    def test_converts_to_int(self):
        config = {"max_jitter_seconds": "500"}
        result = self._get_max_jitter(config)
        assert result == 500
        assert isinstance(result, int)


# --- Job ID patterns ---


class TestJobIdPatterns:
    """Tests for job ID construction patterns."""

    def _make_job_id(self, username, subscription_id):
        return f"{username}_{subscription_id}"

    def test_basic_job_id(self):
        result = self._make_job_id("user1", "sub123")
        assert result == "user1_sub123"

    def test_job_id_with_uuid(self):
        result = self._make_job_id(
            "alice", "550e8400-e29b-41d4-a716-446655440000"
        )
        assert "alice_550e8400" in result

    def test_job_id_unique_per_user(self):
        id1 = self._make_job_id("user1", "sub1")
        id2 = self._make_job_id("user2", "sub1")
        assert id1 != id2

    def test_job_id_unique_per_sub(self):
        id1 = self._make_job_id("user1", "sub1")
        id2 = self._make_job_id("user1", "sub2")
        assert id1 != id2


class TestSystemJobIds:
    """Tests for system job ID patterns."""

    def test_cleanup_job_id(self):
        job_id = "cleanup_inactive_users"
        assert "cleanup" in job_id

    def test_reload_config_job_id(self):
        job_id = "reload_config"
        assert "config" in job_id

    def test_initial_cleanup_job_id(self):
        job_id = "initial_cleanup"
        assert "initial" in job_id

    def test_document_processing_job_id(self):
        job_id = "document_processing_user1"
        assert "document" in job_id
        assert "user1" in job_id


# --- Retention and cleanup ---


class TestRetentionCalculation:
    """Tests for user session retention logic."""

    def _is_expired(self, last_activity, retention_hours):
        cutoff = datetime.now(timezone.utc) - timedelta(hours=retention_hours)
        return last_activity < cutoff

    def test_expired_session(self):
        old_time = datetime.now(timezone.utc) - timedelta(hours=50)
        assert self._is_expired(old_time, 48) is True

    def test_active_session(self):
        recent_time = datetime.now(timezone.utc) - timedelta(hours=1)
        assert self._is_expired(recent_time, 48) is False

    def test_boundary_just_under(self):
        # Just under the retention window - not expired
        just_under = datetime.now(timezone.utc) - timedelta(
            hours=47, minutes=59
        )
        assert self._is_expired(just_under, 48) is False

    def test_just_over_retention(self):
        just_over = datetime.now(timezone.utc) - timedelta(hours=48, seconds=1)
        assert self._is_expired(just_over, 48) is True


# --- Scheduler state ---


class TestSchedulerState:
    """Tests for scheduler state management."""

    def test_initial_not_running(self):
        is_running = False
        assert is_running is False

    def test_start_sets_running(self):
        is_running = False
        is_running = True
        assert is_running is True

    def test_stop_clears_running(self):
        is_running = True
        is_running = False
        assert is_running is False

    def test_double_start_prevented(self):
        is_running = True
        should_start = not is_running
        assert should_start is False


class TestSchedulerEnabledCheck:
    """Tests for scheduler enabled/disabled logic."""

    def _should_start(self, config):
        return config.get("enabled", True)

    def test_enabled_true(self):
        assert self._should_start({"enabled": True}) is True

    def test_enabled_false(self):
        assert self._should_start({"enabled": False}) is False

    def test_default_enabled(self):
        assert self._should_start({}) is True


# --- Settings cache patterns ---


class TestSettingsCachePatterns:
    """Tests for settings cache logic."""

    def test_cache_hit(self):
        cache = {"user1": "settings"}
        result = cache.get("user1")
        assert result is not None

    def test_cache_miss(self):
        cache = {}
        result = cache.get("user1")
        assert result is None

    def test_force_refresh_bypasses_cache(self):
        cache = {"user1": "old_settings"}
        force_refresh = True
        if force_refresh:
            result = None  # Force fetch
        else:
            result = cache.get("user1")
        assert result is None


class TestCacheInvalidation:
    """Tests for cache invalidation patterns."""

    def test_invalidate_user(self):
        cache = {"user1": "s1", "user2": "s2"}
        if "user1" in cache:
            del cache["user1"]
        assert "user1" not in cache
        assert "user2" in cache

    def test_invalidate_returns_true_if_existed(self):
        cache = {"user1": "s"}
        existed = "user1" in cache
        if existed:
            del cache["user1"]
        assert existed is True

    def test_invalidate_returns_false_if_not_existed(self):
        cache = {}
        existed = "user1" in cache
        assert existed is False

    def test_invalidate_all(self):
        cache = {"user1": "s1", "user2": "s2"}
        count = len(cache)
        cache.clear()
        assert len(cache) == 0
        assert count == 2


# --- Scheduled jobs set management ---


class TestScheduledJobsSetManagement:
    """Tests for scheduled jobs set operations."""

    def test_add_job_to_set(self):
        jobs = set()
        jobs.add("job1")
        assert "job1" in jobs

    def test_remove_job_from_set(self):
        jobs = {"job1", "job2"}
        jobs.remove("job1")
        assert "job1" not in jobs

    def test_remove_with_discard(self):
        jobs = {"job1"}
        jobs.discard("job2")  # No error if not present
        assert "job1" in jobs

    def test_copy_before_iteration(self):
        jobs = {"job1", "job2"}
        for job_id in jobs.copy():
            jobs.remove(job_id)
        assert len(jobs) == 0


# --- Config setting retrieval ---


class TestConfigSettingRetrieval:
    """Tests for _get_setting pattern."""

    def _get_setting(self, settings_manager, key, default):
        if settings_manager:
            return settings_manager.get(key, default)
        return default

    def test_with_manager(self):
        manager = {"news.scheduler.enabled": True}
        result = self._get_setting(manager, "news.scheduler.enabled", False)
        assert result is True

    def test_without_manager(self):
        result = self._get_setting(None, "news.scheduler.enabled", True)
        assert result is True

    def test_missing_key_uses_default(self):
        manager = {}
        result = self._get_setting(manager, "missing.key", "default_value")
        assert result == "default_value"


# --- Activity delta calculation ---


class TestActivityDeltaCalculation:
    """Tests for activity time delta calculation."""

    def _calc_delta(self, old_activity, now):
        return now - old_activity

    def test_one_hour_delta(self):
        old = datetime(2025, 6, 15, 10, 0, tzinfo=timezone.utc)
        now = datetime(2025, 6, 15, 11, 0, tzinfo=timezone.utc)
        delta = self._calc_delta(old, now)
        assert delta.total_seconds() == 3600

    def test_zero_delta(self):
        time = datetime(2025, 6, 15, 10, 0, tzinfo=timezone.utc)
        delta = self._calc_delta(time, time)
        assert delta.total_seconds() == 0

    def test_multi_day_delta(self):
        old = datetime(2025, 6, 10, 10, 0, tzinfo=timezone.utc)
        now = datetime(2025, 6, 15, 10, 0, tzinfo=timezone.utc)
        delta = self._calc_delta(old, now)
        assert delta.days == 5


# --- User unregistration ---


class TestUserUnregistration:
    """Tests for user unregistration logic."""

    def test_removes_from_sessions(self):
        sessions = {"user1": {}, "user2": {}}
        if "user1" in sessions:
            del sessions["user1"]
        assert "user1" not in sessions
        assert "user2" in sessions

    def test_clears_scheduled_jobs(self):
        session = {"scheduled_jobs": {"job1", "job2"}}
        jobs_to_remove = session["scheduled_jobs"].copy()
        assert len(jobs_to_remove) == 2

    def test_invalidates_cache_after_unregister(self):
        cache = {"user1": "settings"}
        # After removing user, invalidate cache
        if "user1" in cache:
            del cache["user1"]
        assert "user1" not in cache


# --- Initial run date calculation ---


class TestInitialRunDateCalculation:
    """Tests for initial job run date calculation."""

    def _calc_initial_run(self, delay_seconds):
        return datetime.now(timezone.utc) + timedelta(seconds=delay_seconds)

    def test_30_second_delay(self):
        before = datetime.now(timezone.utc)
        run_date = self._calc_initial_run(30)
        assert run_date > before
        assert (run_date - before).total_seconds() >= 30
        assert (run_date - before).total_seconds() < 32

    def test_zero_delay(self):
        run_date = self._calc_initial_run(0)
        now = datetime.now(timezone.utc)
        diff = abs((run_date - now).total_seconds())
        assert diff < 1  # Within 1 second


# --- Stop scheduler cleanup ---


class TestStopSchedulerCleanup:
    """Tests for cleanup during stop."""

    def test_clears_user_sessions(self):
        sessions = {"user1": {}, "user2": {}}
        sessions.clear()
        assert len(sessions) == 0

    def test_sets_not_running(self):
        is_running = True
        is_running = False
        assert is_running is False
