# #region Test.Scheduler [C:3] [TYPE Module] [SEMANTICS test,scheduler,core]
# @BRIEF Unit tests for SchedulerModule — SchedulerService lifecycle, job management, and ThrottledSchedulerConfigurator.
# @RELATION BINDS_TO -> [SchedulerModule]
# @TEST_EDGE: empty_queue -> _trigger_backup with no active tasks creates a new task
# @TEST_EDGE: duplicate_backup -> _trigger_backup skips when backup already running
# @TEST_EDGE: invalid_cron -> add_backup_job handles invalid cron expression gracefully
# @TEST_EDGE: translation_load_failure -> load_schedules continues when translation DB query fails
# @TEST_EDGE: validation_load_failure -> load_schedules continues when validation DB query fails
# @TEST_EDGE: policy_not_found -> reload_validation_policy removes job when policy missing
# @TEST_EDGE: policy_inactive -> reload_validation_policy removes job when policy inactive
# @TEST_EDGE: policy_no_schedule -> reload_validation_policy skips when no schedule_days/window
# @TEST_EDGE: zero_window -> calculate_schedule returns start_dt repeated for zero-length window
# @TEST_EDGE: midnight_crossing -> calculate_schedule handles window crossing midnight
# @TEST_EDGE: single_dashboard -> calculate_schedule returns single start_dt for one dashboard
import asyncio
from datetime import date, datetime, time, timedelta
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch, PropertyMock

import pytest


# ── Helpers (C1 — anchor pair only) ──

# #region _build_config_manager [C:1] [TYPE Function]
def _build_config_manager(environments=None, app_timezone="Europe/Moscow"):
    """Build a mock ConfigManager with configurable environments."""
    cm = MagicMock()
    config = MagicMock()
    settings = MagicMock()
    settings.app_timezone = app_timezone
    config.settings = settings
    config.environments = environments or []
    cm.get_config.return_value = config
    return cm
# #endregion _build_config_manager

# #region _build_env [C:1] [TYPE Function]
def _build_env(env_id="env-1", cron="0 2 * * *", enabled=True):
    """Build a mock Environment with backup_schedule."""
    env = MagicMock()
    env.id = env_id
    sched = MagicMock()
    sched.enabled = enabled
    sched.cron_expression = cron
    env.backup_schedule = sched
    return env
# #endregion _build_env

# #region _build_task [C:1] [TYPE Function]
def _build_task(plugin_id="superset-backup", status="RUNNING", environment_id="env-1"):
    """Build a mock TaskRecord."""
    task = MagicMock()
    task.plugin_id = plugin_id
    task.status = status
    task.params = {"environment_id": environment_id}
    return task
# #endregion _build_task

# #region _build_policy [C:1] [TYPE Function]
def _build_policy(policy_id="pol-1", is_active=True, schedule_days=None, window_start=None, window_end=None, dashboard_ids=None, environment_id="env-1", provider_id="prov-1"):
    """Build a mock ValidationPolicy."""
    policy = MagicMock()
    policy.id = policy_id
    policy.is_active = is_active
    policy.schedule_days = schedule_days
    policy.window_start = window_start
    policy.window_end = window_end
    policy.dashboard_ids = dashboard_ids
    policy.environment_id = environment_id
    policy.provider_id = provider_id
    return policy
# #endregion _build_policy


# ── Fixtures ──

@pytest.fixture
def mock_task_manager():
    tm = MagicMock()
    tm.get_tasks.return_value = []
    tm.create_task = AsyncMock(return_value=MagicMock())
    return tm


@pytest.fixture
def mock_config_manager():
    return _build_config_manager()


@pytest.fixture
def mock_scheduler():
    """Patch BackgroundScheduler so no real threads are started."""
    with patch("src.core.scheduler.BackgroundScheduler") as cls:
        sched_instance = MagicMock()
        sched_instance.running = False
        cls.return_value = sched_instance
        yield sched_instance


@pytest.fixture
def service(mock_task_manager, mock_config_manager, mock_scheduler):
    """Construct a SchedulerService with all external deps mocked."""
    # Create a fresh event loop to avoid "no current event loop" errors
    # when the scheduler test runs after async tests in the same session.
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    with patch("src.core.scheduler.belief_scope") as mock_bs:
        mock_bs.return_value.__enter__ = MagicMock()
        mock_bs.return_value.__exit__ = MagicMock(return_value=False)
        with patch("src.core.scheduler.asyncio.get_event_loop", return_value=loop):
            from src.core.scheduler import SchedulerService
            svc = SchedulerService(mock_task_manager, mock_config_manager)
    yield svc
    loop.close()


# ══════════════════════════════════════════════════════════════
# SchedulerService.__init__ (lines 28-32)
# ══════════════════════════════════════════════════════════════

# #region test_init_stores_deps [C:2] [TYPE Function]
# @BRIEF __init__ stores task_manager, config_manager, creates scheduler and loop.
def test_init_stores_deps(service, mock_task_manager, mock_config_manager, mock_scheduler):
    assert service.task_manager is mock_task_manager
    assert service.config_manager is mock_config_manager
    assert service.scheduler is mock_scheduler
    assert service.loop is not None
# #endregion test_init_stores_deps


# ══════════════════════════════════════════════════════════════
# SchedulerService.start (lines 40-44)
# ══════════════════════════════════════════════════════════════

# #region test_start_when_not_running [C:2] [TYPE Function]
# @BRIEF start() starts scheduler and loads schedules when not already running.
def test_start_when_not_running(service, mock_scheduler):
    mock_scheduler.running = False
    with patch("src.core.scheduler.belief_scope") as mock_bs:
        mock_bs.return_value.__enter__ = MagicMock()
        mock_bs.return_value.__exit__ = MagicMock(return_value=False)
        with patch.object(service, "load_schedules") as mock_load:
            service.start()
    mock_scheduler.start.assert_called_once()
    mock_load.assert_called_once()
# #endregion test_start_when_not_running

# #region test_start_when_already_running [C:2] [TYPE Function]
# @BRIEF start() is a no-op when scheduler is already running.
def test_start_when_already_running(service, mock_scheduler):
    mock_scheduler.running = True
    with patch("src.core.scheduler.belief_scope") as mock_bs:
        mock_bs.return_value.__enter__ = MagicMock()
        mock_bs.return_value.__exit__ = MagicMock(return_value=False)
        with patch.object(service, "load_schedules") as mock_load:
            service.start()
    mock_scheduler.start.assert_not_called()
    mock_load.assert_not_called()
# #endregion test_start_when_already_running


# ══════════════════════════════════════════════════════════════
# SchedulerService.stop (lines 52-55)
# ══════════════════════════════════════════════════════════════

# #region test_stop_when_running [C:2] [TYPE Function]
# @BRIEF stop() shuts down scheduler when it is running.
def test_stop_when_running(service, mock_scheduler):
    mock_scheduler.running = True
    with patch("src.core.scheduler.belief_scope") as mock_bs:
        mock_bs.return_value.__enter__ = MagicMock()
        mock_bs.return_value.__exit__ = MagicMock(return_value=False)
        service.stop()
    mock_scheduler.shutdown.assert_called_once()
# #endregion test_stop_when_running

# #region test_stop_when_not_running [C:2] [TYPE Function]
# @BRIEF stop() is a no-op when scheduler is not running.
def test_stop_when_not_running(service, mock_scheduler):
    mock_scheduler.running = False
    with patch("src.core.scheduler.belief_scope") as mock_bs:
        mock_bs.return_value.__enter__ = MagicMock()
        mock_bs.return_value.__exit__ = MagicMock(return_value=False)
        service.stop()
    mock_scheduler.shutdown.assert_not_called()
# #endregion test_stop_when_not_running


# ══════════════════════════════════════════════════════════════
# SchedulerService.load_schedules (lines 63-130)
# ══════════════════════════════════════════════════════════════

# #region test_load_schedules_backup_jobs [C:2] [TYPE Function]
# @BRIEF load_schedules registers backup jobs for enabled environments.
def test_load_schedules_backup_jobs(service, mock_scheduler, mock_config_manager):
    env1 = _build_env("env-1", "0 2 * * *", enabled=True)
    env2 = _build_env("env-2", "0 3 * * *", enabled=False)
    cm = _build_config_manager(environments=[env1, env2])
    service.config_manager = cm

    with patch("src.core.scheduler.belief_scope") as mock_bs:
        mock_bs.return_value.__enter__ = MagicMock()
        mock_bs.return_value.__exit__ = MagicMock(return_value=False)
        with patch("src.core.scheduler.SessionLocal") as mock_session:
            db_mock = MagicMock()
            mock_session.return_value = db_mock
            # No translation schedules, no validation policies
            db_mock.query.return_value.filter.return_value.all.return_value = []
            with patch.object(service, "add_backup_job") as mock_add:
                service.load_schedules()
    # Only env-1 has enabled schedule
    mock_add.assert_called_once_with("env-1", "0 2 * * *")
# #endregion test_load_schedules_backup_jobs

# #region test_load_schedules_translation_exception [C:2] [TYPE Function]
# @BRIEF load_schedules catches exception when loading translation schedules (lines 93-94).
def test_load_schedules_translation_exception(service, mock_config_manager):
    cm = _build_config_manager(environments=[])
    service.config_manager = cm

    with patch("src.core.scheduler.belief_scope") as mock_bs:
        mock_bs.return_value.__enter__ = MagicMock()
        mock_bs.return_value.__exit__ = MagicMock(return_value=False)
        with patch("src.core.scheduler.SessionLocal", side_effect=RuntimeError("DB connection lost")):
            # Should not raise — exception is caught at line 93
            service.load_schedules()
# #endregion test_load_schedules_translation_exception

# #region test_load_schedules_validation_exception [C:2] [TYPE Function]
# @BRIEF load_schedules catches exception when loading validation schedules (lines 128-129).
def test_load_schedules_validation_exception(service, mock_config_manager):
    cm = _build_config_manager(environments=[])
    service.config_manager = cm

    call_count = 0
    original_session_local = None

    def session_side_effect():
        nonlocal call_count
        call_count += 1
        if call_count == 1:
            # First call (translation) — return empty mock
            db = MagicMock()
            db.query.return_value.filter.return_value.all.return_value = []
            return db
        else:
            # Second call (validation) — raise
            raise RuntimeError("Validation DB error")

    with patch("src.core.scheduler.belief_scope") as mock_bs:
        mock_bs.return_value.__enter__ = MagicMock()
        mock_bs.return_value.__exit__ = MagicMock(return_value=False)
        with patch("src.core.scheduler.SessionLocal", side_effect=session_side_effect):
            # Should not raise — exception caught at line 128
            service.load_schedules()
# #endregion test_load_schedules_validation_exception

# #region test_load_schedules_translation_schedules_success [C:2] [TYPE Function]
# @BRIEF load_schedules registers active translation schedules from DB (lines 82, 90).
def test_load_schedules_translation_schedules_success(service, mock_config_manager):
    cm = _build_config_manager(environments=[], app_timezone="UTC")
    service.config_manager = cm

    # Build a mock TranslationSchedule row
    ts = MagicMock()
    ts.id = "ts-1"
    ts.job_id = "job-ts-1"
    ts.cron_expression = "*/10 * * * *"
    ts.timezone = "US/Pacific"
    ts.execution_mode = "incremental"

    call_count = 0

    def session_factory():
        nonlocal call_count
        call_count += 1
        db = MagicMock()
        if call_count == 1:
            # Translation query — return one active schedule
            db.query.return_value.filter.return_value.all.return_value = [ts]
        else:
            # Validation query — empty
            db.query.return_value.filter.return_value.all.return_value = []
        return db

    with patch("src.core.scheduler.belief_scope") as mock_bs:
        mock_bs.return_value.__enter__ = MagicMock()
        mock_bs.return_value.__exit__ = MagicMock(return_value=False)
        with patch("src.core.scheduler.SessionLocal", side_effect=session_factory):
            with patch.object(service, "add_translation_job") as mock_add_t:
                with patch.object(service, "add_validation_job") as mock_add_v:
                    service.load_schedules()
    mock_add_t.assert_called_once_with(
        schedule_id="ts-1",
        job_id="job-ts-1",
        cron_expression="*/10 * * * *",
        timezone="US/Pacific",
        execution_mode="incremental",
    )
    mock_add_v.assert_not_called()
# #endregion test_load_schedules_translation_schedules_success

# #region test_load_schedules_validation_policy_skip_empty_days [C:2] [TYPE Function]
# @BRIEF load_schedules skips validation policies with empty schedule_days (line 112).
def test_load_schedules_validation_policy_skip_empty_days(service, mock_config_manager):
    cm = _build_config_manager(environments=[], app_timezone="UTC")
    service.config_manager = cm

    policy_no_days = _build_policy("pol-1", is_active=True, schedule_days=[], window_start=time(10, 0))
    policy_none_days = _build_policy("pol-2", is_active=True, schedule_days=None, window_start=time(10, 0))

    call_count = 0

    def session_factory():
        nonlocal call_count
        call_count += 1
        db = MagicMock()
        if call_count == 1:
            # Translation query — empty
            db.query.return_value.filter.return_value.all.return_value = []
        else:
            # Validation query — policies with empty/None schedule_days
            db.query.return_value.filter.return_value.all.return_value = [policy_no_days, policy_none_days]
        return db

    with patch("src.core.scheduler.belief_scope") as mock_bs:
        mock_bs.return_value.__enter__ = MagicMock()
        mock_bs.return_value.__exit__ = MagicMock(return_value=False)
        with patch("src.core.scheduler.SessionLocal", side_effect=session_factory):
            with patch.object(service, "add_validation_job") as mock_add:
                service.load_schedules()
    # Neither policy should be registered (both have empty/None schedule_days)
    mock_add.assert_not_called()
# #endregion test_load_schedules_validation_policy_skip_empty_days

# #region test_load_schedules_validation_policy_skip_no_window [C:2] [TYPE Function]
# @BRIEF load_schedules skips validation policies with None window_start (line 115).
def test_load_schedules_validation_policy_skip_no_window(service, mock_config_manager):
    cm = _build_config_manager(environments=[], app_timezone="UTC")
    service.config_manager = cm

    policy_no_ws = _build_policy("pol-3", is_active=True, schedule_days=[1, 3], window_start=None)

    call_count = 0

    def session_factory():
        nonlocal call_count
        call_count += 1
        db = MagicMock()
        if call_count == 1:
            db.query.return_value.filter.return_value.all.return_value = []
        else:
            db.query.return_value.filter.return_value.all.return_value = [policy_no_ws]
        return db

    with patch("src.core.scheduler.belief_scope") as mock_bs:
        mock_bs.return_value.__enter__ = MagicMock()
        mock_bs.return_value.__exit__ = MagicMock(return_value=False)
        with patch("src.core.scheduler.SessionLocal", side_effect=session_factory):
            with patch.object(service, "add_validation_job") as mock_add:
                service.load_schedules()
    mock_add.assert_not_called()
# #endregion test_load_schedules_validation_policy_skip_no_window

# #region test_load_schedules_validation_policy_success [C:2] [TYPE Function]
# @BRIEF load_schedules registers valid validation policies with cron expression.
def test_load_schedules_validation_policy_success(service, mock_config_manager):
    cm = _build_config_manager(environments=[], app_timezone="UTC")
    service.config_manager = cm

    ws = time(10, 30)
    policy = _build_policy("pol-ok", is_active=True, schedule_days=[1, 3, 5], window_start=ws)

    call_count = 0

    def session_factory():
        nonlocal call_count
        call_count += 1
        db = MagicMock()
        if call_count == 1:
            db.query.return_value.filter.return_value.all.return_value = []
        else:
            db.query.return_value.filter.return_value.all.return_value = [policy]
        return db

    with patch("src.core.scheduler.belief_scope") as mock_bs:
        mock_bs.return_value.__enter__ = MagicMock()
        mock_bs.return_value.__exit__ = MagicMock(return_value=False)
        with patch("src.core.scheduler.SessionLocal", side_effect=session_factory):
            with patch.object(service, "add_validation_job") as mock_add:
                service.load_schedules()
    # cron: minute=30 hour=10 * * 1,3,5
    mock_add.assert_called_once_with(policy_id="pol-ok", cron_expression="30 10 * * 1,3,5", app_timezone="UTC")
# #endregion test_load_schedules_validation_policy_success


# ══════════════════════════════════════════════════════════════
# SchedulerService.add_backup_job (lines 139-157)
# ══════════════════════════════════════════════════════════════

# #region test_add_backup_job_success [C:2] [TYPE Function]
# @BRIEF add_backup_job registers a job with APScheduler.
def test_add_backup_job_success(service, mock_scheduler):
    with patch("src.core.scheduler.belief_scope") as mock_bs:
        mock_bs.return_value.__enter__ = MagicMock()
        mock_bs.return_value.__exit__ = MagicMock(return_value=False)
        with patch("src.core.scheduler.CronTrigger") as mock_trigger:
            mock_trigger.from_crontab.return_value = "trigger_obj"
            service.add_backup_job("env-1", "0 2 * * *")
    mock_scheduler.add_job.assert_called_once()
    call_kwargs = mock_scheduler.add_job.call_args
    assert call_kwargs.kwargs["id"] == "backup_env-1"
    assert call_kwargs.kwargs["replace_existing"] is True
# #endregion test_add_backup_job_success

# #region test_add_backup_job_invalid_cron [C:2] [TYPE Function]
# @BRIEF add_backup_job handles invalid cron expression without raising (lines 156-157).
def test_add_backup_job_invalid_cron(service, mock_scheduler):
    with patch("src.core.scheduler.belief_scope") as mock_bs:
        mock_bs.return_value.__enter__ = MagicMock()
        mock_bs.return_value.__exit__ = MagicMock(return_value=False)
        with patch("src.core.scheduler.CronTrigger") as mock_trigger:
            mock_trigger.from_crontab.side_effect = ValueError("Invalid cron")
            # Should not raise
            service.add_backup_job("env-bad", "invalid_cron")
    mock_scheduler.add_job.assert_not_called()
# #endregion test_add_backup_job_invalid_cron


# ══════════════════════════════════════════════════════════════
# SchedulerService.add_translation_job (lines 167-193)
# ══════════════════════════════════════════════════════════════

# #region test_add_translation_job_success [C:2] [TYPE Function]
# @BRIEF add_translation_job registers a translation job with APScheduler.
def test_add_translation_job_success(service, mock_scheduler):
    with patch("src.core.scheduler.belief_scope") as mock_bs:
        mock_bs.return_value.__enter__ = MagicMock()
        mock_bs.return_value.__exit__ = MagicMock(return_value=False)
        with patch("src.core.scheduler.CronTrigger") as mock_trigger:
            mock_trigger.from_crontab.return_value = "trigger_obj"
            service.add_translation_job("sched-1", "job-1", "*/5 * * * *", "US/Eastern", "incremental")
    mock_scheduler.add_job.assert_called_once()
    call_kwargs = mock_scheduler.add_job.call_args
    assert call_kwargs.kwargs["id"] == "translate_sched-1"
# #endregion test_add_translation_job_success

# #region test_add_translation_job_exception [C:2] [TYPE Function]
# @BRIEF add_translation_job handles registration failure gracefully (lines 189-190).
def test_add_translation_job_exception(service, mock_scheduler):
    mock_scheduler.add_job.side_effect = RuntimeError("APScheduler failure")
    with patch("src.core.scheduler.belief_scope") as mock_bs:
        mock_bs.return_value.__enter__ = MagicMock()
        mock_bs.return_value.__exit__ = MagicMock(return_value=False)
        with patch("src.core.scheduler.CronTrigger") as mock_trigger:
            mock_trigger.from_crontab.return_value = "trigger_obj"
            # Should not raise
            service.add_translation_job("sched-2", "job-2", "0 0 * * *")
# #endregion test_add_translation_job_exception


# ══════════════════════════════════════════════════════════════
# SchedulerService.remove_translation_job
# ══════════════════════════════════════════════════════════════

# #region test_remove_translation_job_exists [C:2] [TYPE Function]
# @BRIEF remove_translation_job removes an existing job.
def test_remove_translation_job_exists(service, mock_scheduler):
    with patch("src.core.scheduler.belief_scope") as mock_bs:
        mock_bs.return_value.__enter__ = MagicMock()
        mock_bs.return_value.__exit__ = MagicMock(return_value=False)
        service.remove_translation_job("sched-1")
    mock_scheduler.remove_job.assert_called_once_with("translate_sched-1")
# #endregion test_remove_translation_job_exists

# #region test_remove_translation_job_not_found [C:2] [TYPE Function]
# @BRIEF remove_translation_job silently handles missing job.
def test_remove_translation_job_not_found(service, mock_scheduler):
    mock_scheduler.remove_job.side_effect = Exception("Job not found")
    with patch("src.core.scheduler.belief_scope") as mock_bs:
        mock_bs.return_value.__enter__ = MagicMock()
        mock_bs.return_value.__exit__ = MagicMock(return_value=False)
        # Should not raise
        service.remove_translation_job("sched-missing")
# #endregion test_remove_translation_job_not_found


# ══════════════════════════════════════════════════════════════
# SchedulerService._trigger_backup (lines 218-241)
# ══════════════════════════════════════════════════════════════

# #region test_trigger_backup_no_active_tasks [C:2] [TYPE Function]
# @BRIEF _trigger_backup creates a task when no backup is running (lines 219-236).
def test_trigger_backup_no_active_tasks(service, mock_task_manager):
    mock_task_manager.get_tasks.return_value = []
    with patch("src.core.scheduler.seed_trace_id"):
        with patch("src.core.scheduler.belief_scope") as mock_bs:
            mock_bs.return_value.__enter__ = MagicMock()
            mock_bs.return_value.__exit__ = MagicMock(return_value=False)
            with patch("src.core.scheduler.asyncio.run_coroutine_threadsafe") as mock_run:
                service._trigger_backup("env-1")
    # create_task is called and its coroutine passed to run_coroutine_threadsafe
    mock_run.assert_called_once()
    mock_task_manager.create_task.assert_called_once_with(
        "superset-backup", {"environment_id": "env-1"}
    )
# #endregion test_trigger_backup_no_active_tasks

# #region test_trigger_backup_already_running [C:2] [TYPE Function]
# @BRIEF _trigger_backup skips when a backup is already running for the environment.
def test_trigger_backup_already_running(service, mock_task_manager):
    running_task = _build_task(plugin_id="superset-backup", status="RUNNING", environment_id="env-1")
    mock_task_manager.get_tasks.return_value = [running_task]
    with patch("src.core.scheduler.seed_trace_id"):
        with patch("src.core.scheduler.belief_scope") as mock_bs:
            mock_bs.return_value.__enter__ = MagicMock()
            mock_bs.return_value.__exit__ = MagicMock(return_value=False)
            with patch("src.core.scheduler.asyncio.run_coroutine_threadsafe") as mock_run:
                service._trigger_backup("env-1")
    mock_run.assert_not_called()
# #endregion test_trigger_backup_already_running

# #region test_trigger_backup_pending_status [C:2] [TYPE Function]
# @BRIEF _trigger_backup skips when a backup is in PENDING status for the environment.
def test_trigger_backup_pending_status(service, mock_task_manager):
    pending_task = _build_task(plugin_id="superset-backup", status="PENDING", environment_id="env-1")
    mock_task_manager.get_tasks.return_value = [pending_task]
    with patch("src.core.scheduler.seed_trace_id"):
        with patch("src.core.scheduler.belief_scope") as mock_bs:
            mock_bs.return_value.__enter__ = MagicMock()
            mock_bs.return_value.__exit__ = MagicMock(return_value=False)
            with patch("src.core.scheduler.asyncio.run_coroutine_threadsafe") as mock_run:
                service._trigger_backup("env-1")
    mock_run.assert_not_called()
# #endregion test_trigger_backup_pending_status

# #region test_trigger_backup_different_env [C:2] [TYPE Function]
# @BRIEF _trigger_backup proceeds when running backup is for a different environment.
def test_trigger_backup_different_env(service, mock_task_manager):
    other_task = _build_task(plugin_id="superset-backup", status="RUNNING", environment_id="env-2")
    mock_task_manager.get_tasks.return_value = [other_task]
    with patch("src.core.scheduler.seed_trace_id"):
        with patch("src.core.scheduler.belief_scope") as mock_bs:
            mock_bs.return_value.__enter__ = MagicMock()
            mock_bs.return_value.__exit__ = MagicMock(return_value=False)
            with patch("src.core.scheduler.asyncio.run_coroutine_threadsafe") as mock_run:
                service._trigger_backup("env-1")
    mock_run.assert_called_once()
# #endregion test_trigger_backup_different_env

# #region test_trigger_backup_other_plugin [C:2] [TYPE Function]
# @BRIEF _trigger_backup proceeds when running task is a different plugin.
def test_trigger_backup_other_plugin(service, mock_task_manager):
    other_plugin_task = _build_task(plugin_id="other-plugin", status="RUNNING", environment_id="env-1")
    mock_task_manager.get_tasks.return_value = [other_plugin_task]
    with patch("src.core.scheduler.seed_trace_id"):
        with patch("src.core.scheduler.belief_scope") as mock_bs:
            mock_bs.return_value.__enter__ = MagicMock()
            mock_bs.return_value.__exit__ = MagicMock(return_value=False)
            with patch("src.core.scheduler.asyncio.run_coroutine_threadsafe") as mock_run:
                service._trigger_backup("env-1")
    mock_run.assert_called_once()
# #endregion test_trigger_backup_other_plugin


# ══════════════════════════════════════════════════════════════
# SchedulerService.add_validation_job
# ══════════════════════════════════════════════════════════════

# #region test_add_validation_job_success [C:2] [TYPE Function]
# @BRIEF add_validation_job registers a validation job with APScheduler.
def test_add_validation_job_success(service, mock_scheduler):
    with patch("src.core.scheduler.belief_scope") as mock_bs:
        mock_bs.return_value.__enter__ = MagicMock()
        mock_bs.return_value.__exit__ = MagicMock(return_value=False)
        with patch("src.core.scheduler.CronTrigger") as mock_trigger:
            mock_trigger.from_crontab.return_value = "trigger_obj"
            service.add_validation_job("pol-1", "30 10 * * 1,3", app_timezone="UTC")
    mock_scheduler.add_job.assert_called_once()
    call_kwargs = mock_scheduler.add_job.call_args
    assert call_kwargs.kwargs["id"] == "validation_pol-1"
# #endregion test_add_validation_job_success

# #region test_add_validation_job_exception [C:2] [TYPE Function]
# @BRIEF add_validation_job handles registration failure gracefully.
def test_add_validation_job_exception(service, mock_scheduler):
    mock_scheduler.add_job.side_effect = RuntimeError("Scheduler error")
    with patch("src.core.scheduler.belief_scope") as mock_bs:
        mock_bs.return_value.__enter__ = MagicMock()
        mock_bs.return_value.__exit__ = MagicMock(return_value=False)
        with patch("src.core.scheduler.CronTrigger") as mock_trigger:
            mock_trigger.from_crontab.return_value = "trigger_obj"
            # Should not raise
            service.add_validation_job("pol-bad", "30 10 * * 1")
# #endregion test_add_validation_job_exception


# ══════════════════════════════════════════════════════════════
# SchedulerService.remove_validation_job
# ══════════════════════════════════════════════════════════════

# #region test_remove_validation_job_exists [C:2] [TYPE Function]
# @BRIEF remove_validation_job removes an existing job.
def test_remove_validation_job_exists(service, mock_scheduler):
    with patch("src.core.scheduler.belief_scope") as mock_bs:
        mock_bs.return_value.__enter__ = MagicMock()
        mock_bs.return_value.__exit__ = MagicMock(return_value=False)
        service.remove_validation_job("pol-1")
    mock_scheduler.remove_job.assert_called_once_with("validation_pol-1")
# #endregion test_remove_validation_job_exists

# #region test_remove_validation_job_not_found [C:2] [TYPE Function]
# @BRIEF remove_validation_job silently handles missing job.
def test_remove_validation_job_not_found(service, mock_scheduler):
    mock_scheduler.remove_job.side_effect = Exception("Not found")
    with patch("src.core.scheduler.belief_scope") as mock_bs:
        mock_bs.return_value.__enter__ = MagicMock()
        mock_bs.return_value.__exit__ = MagicMock(return_value=False)
        # Should not raise
        service.remove_validation_job("pol-missing")
# #endregion test_remove_validation_job_not_found


# ══════════════════════════════════════════════════════════════
# SchedulerService.reload_validation_policy (lines 300-331)
# ══════════════════════════════════════════════════════════════

# #region test_reload_validation_policy_not_found [C:2] [TYPE Function]
# @BRIEF reload_validation_policy removes job when policy not found in DB (lines 315-316).
def test_reload_validation_policy_not_found(service, mock_scheduler):
    with patch("src.core.scheduler.belief_scope") as mock_bs:
        mock_bs.return_value.__enter__ = MagicMock()
        mock_bs.return_value.__exit__ = MagicMock(return_value=False)
        with patch("src.core.scheduler.SessionLocal") as mock_session:
            db_mock = MagicMock()
            mock_session.return_value = db_mock
            db_mock.query.return_value.filter.return_value.first.return_value = None
            with patch.object(service, "remove_validation_job") as mock_remove:
                with patch.object(service, "add_validation_job") as mock_add:
                    service.reload_validation_policy("pol-missing")
    mock_remove.assert_called_once_with("pol-missing")
    mock_add.assert_not_called()
# #endregion test_reload_validation_policy_not_found

# #region test_reload_validation_policy_inactive [C:2] [TYPE Function]
# @BRIEF reload_validation_policy removes job when policy is inactive (lines 319-320).
def test_reload_validation_policy_inactive(service, mock_scheduler):
    policy = _build_policy("pol-inactive", is_active=False)
    with patch("src.core.scheduler.belief_scope") as mock_bs:
        mock_bs.return_value.__enter__ = MagicMock()
        mock_bs.return_value.__exit__ = MagicMock(return_value=False)
        with patch("src.core.scheduler.SessionLocal") as mock_session:
            db_mock = MagicMock()
            mock_session.return_value = db_mock
            db_mock.query.return_value.filter.return_value.first.return_value = policy
            with patch.object(service, "remove_validation_job") as mock_remove:
                with patch.object(service, "add_validation_job") as mock_add:
                    service.reload_validation_policy("pol-inactive")
    mock_remove.assert_called_once_with("pol-inactive")
    mock_add.assert_not_called()
# #endregion test_reload_validation_policy_inactive

# #region test_reload_validation_policy_no_schedule [C:2] [TYPE Function]
# @BRIEF reload_validation_policy skips registration when no schedule_days or window_start (lines 327-328 area).
def test_reload_validation_policy_no_schedule(service, mock_scheduler):
    policy = _build_policy("pol-nosched", is_active=True, schedule_days=[], window_start=None)
    with patch("src.core.scheduler.belief_scope") as mock_bs:
        mock_bs.return_value.__enter__ = MagicMock()
        mock_bs.return_value.__exit__ = MagicMock(return_value=False)
        with patch("src.core.scheduler.SessionLocal") as mock_session:
            db_mock = MagicMock()
            mock_session.return_value = db_mock
            db_mock.query.return_value.filter.return_value.first.return_value = policy
            with patch.object(service, "remove_validation_job") as mock_remove:
                with patch.object(service, "add_validation_job") as mock_add:
                    service.reload_validation_policy("pol-nosched")
    mock_remove.assert_called_once_with("pol-nosched")
    mock_add.assert_not_called()
# #endregion test_reload_validation_policy_no_schedule

# #region test_reload_validation_policy_success [C:2] [TYPE Function]
# @BRIEF reload_validation_policy removes old and registers new job for active policy with schedule.
def test_reload_validation_policy_success(service, mock_scheduler):
    ws = time(14, 0)
    policy = _build_policy("pol-ok", is_active=True, schedule_days=[0, 2, 4], window_start=ws)
    with patch("src.core.scheduler.belief_scope") as mock_bs:
        mock_bs.return_value.__enter__ = MagicMock()
        mock_bs.return_value.__exit__ = MagicMock(return_value=False)
        with patch("src.core.scheduler.SessionLocal") as mock_session:
            db_mock = MagicMock()
            mock_session.return_value = db_mock
            db_mock.query.return_value.filter.return_value.first.return_value = policy
            with patch.object(service, "remove_validation_job") as mock_remove:
                with patch.object(service, "add_validation_job") as mock_add:
                    service.reload_validation_policy("pol-ok", app_timezone="UTC")
    mock_remove.assert_called_once_with("pol-ok")
    # cron: minute=0 hour=14 * * 0,2,4
    mock_add.assert_called_once_with("pol-ok", "0 14 * * 0,2,4", app_timezone="UTC")
# #endregion test_reload_validation_policy_success

# #region test_reload_validation_policy_exception [C:2] [TYPE Function]
# @BRIEF reload_validation_policy handles DB exception gracefully (lines 327-328).
def test_reload_validation_policy_exception(service, mock_scheduler):
    with patch("src.core.scheduler.belief_scope") as mock_bs:
        mock_bs.return_value.__enter__ = MagicMock()
        mock_bs.return_value.__exit__ = MagicMock(return_value=False)
        with patch("src.core.scheduler.SessionLocal", side_effect=RuntimeError("DB down")):
            with patch.object(service, "remove_validation_job"):
                # Should not raise
                service.reload_validation_policy("pol-err")
# #endregion test_reload_validation_policy_exception


# ══════════════════════════════════════════════════════════════
# SchedulerService._trigger_validation (lines 338-391)
# ══════════════════════════════════════════════════════════════

# #region test_trigger_validation_policy_not_found [C:2] [TYPE Function]
# @BRIEF _trigger_validation returns early when policy not found in DB.
def test_trigger_validation_policy_not_found(service, mock_task_manager):
    with patch("src.core.scheduler.seed_trace_id"):
        with patch("src.core.scheduler.belief_scope") as mock_bs:
            mock_bs.return_value.__enter__ = MagicMock()
            mock_bs.return_value.__exit__ = MagicMock(return_value=False)
            with patch("src.core.scheduler.SessionLocal") as mock_session:
                db_mock = MagicMock()
                mock_session.return_value = db_mock
                db_mock.query.return_value.filter.return_value.first.return_value = None
                # Should not raise
                service._trigger_validation("pol-missing")
    db_mock.close.assert_called_once()
# #endregion test_trigger_validation_policy_not_found

# #region test_trigger_validation_policy_inactive [C:2] [TYPE Function]
# @BRIEF _trigger_validation returns early when policy is inactive.
def test_trigger_validation_policy_inactive(service, mock_task_manager):
    policy = _build_policy("pol-inactive", is_active=False)
    with patch("src.core.scheduler.seed_trace_id"):
        with patch("src.core.scheduler.belief_scope") as mock_bs:
            mock_bs.return_value.__enter__ = MagicMock()
            mock_bs.return_value.__exit__ = MagicMock(return_value=False)
            with patch("src.core.scheduler.SessionLocal") as mock_session:
                db_mock = MagicMock()
                mock_session.return_value = db_mock
                db_mock.query.return_value.filter.return_value.first.return_value = policy
                service._trigger_validation("pol-inactive")
    mock_task_manager.create_task.assert_not_called()
# #endregion test_trigger_validation_policy_inactive

# #region test_trigger_validation_no_dashboards [C:2] [TYPE Function]
# @BRIEF _trigger_validation returns early when policy has no dashboard_ids.
def test_trigger_validation_no_dashboards(service, mock_task_manager):
    policy = _build_policy("pol-nodash", is_active=True, dashboard_ids=[], window_start=time(10, 0), window_end=time(11, 0))
    with patch("src.core.scheduler.seed_trace_id"):
        with patch("src.core.scheduler.belief_scope") as mock_bs:
            mock_bs.return_value.__enter__ = MagicMock()
            mock_bs.return_value.__exit__ = MagicMock(return_value=False)
            with patch("src.core.scheduler.SessionLocal") as mock_session:
                db_mock = MagicMock()
                mock_session.return_value = db_mock
                db_mock.query.return_value.filter.return_value.first.return_value = policy
                service._trigger_validation("pol-nodash")
    mock_task_manager.create_task.assert_not_called()
# #endregion test_trigger_validation_no_dashboards

# #region test_trigger_validation_success [C:2] [TYPE Function]
# @BRIEF _trigger_validation spawns tasks for each dashboard in the policy.
def test_trigger_validation_success(service, mock_task_manager):
    dash_ids = ["dash-1", "dash-2"]
    policy = _build_policy(
        "pol-ok", is_active=True, dashboard_ids=dash_ids,
        window_start=time(10, 0), window_end=time(11, 0),
        environment_id="env-1", provider_id="prov-1",
    )
    with patch("src.core.scheduler.seed_trace_id"):
        with patch("src.core.scheduler.belief_scope") as mock_bs:
            mock_bs.return_value.__enter__ = MagicMock()
            mock_bs.return_value.__exit__ = MagicMock(return_value=False)
            with patch("src.core.scheduler.SessionLocal") as mock_session:
                db_mock = MagicMock()
                mock_session.return_value = db_mock
                db_mock.query.return_value.filter.return_value.first.return_value = policy
                with patch("src.core.scheduler.ThrottledSchedulerConfigurator") as mock_cfg:
                    mock_cfg.calculate_schedule.return_value = [
                        datetime(2026, 6, 11, 10, 0),
                        datetime(2026, 6, 11, 11, 0),
                    ]
                    with patch("src.core.scheduler.asyncio.run_coroutine_threadsafe") as mock_run:
                        service._trigger_validation("pol-ok")
    # Two dashboards → two run_coroutine_threadsafe calls
    assert mock_run.call_count == 2
# #endregion test_trigger_validation_success

# #region test_trigger_validation_exception [C:2] [TYPE Function]
# @BRIEF _trigger_validation handles exception gracefully (lines 385-386).
def test_trigger_validation_exception(service, mock_task_manager):
    with patch("src.core.scheduler.seed_trace_id"):
        with patch("src.core.scheduler.belief_scope") as mock_bs:
            mock_bs.return_value.__enter__ = MagicMock()
            mock_bs.return_value.__exit__ = MagicMock(return_value=False)
            with patch("src.core.scheduler.SessionLocal") as mock_session:
                db_mock = MagicMock()
                mock_session.return_value = db_mock
                # Raise inside the try block (query, not SessionLocal itself)
                db_mock.query.side_effect = RuntimeError("DB crash")
                # Should not raise
                service._trigger_validation("pol-err")
    db_mock.close.assert_called_once()
# #endregion test_trigger_validation_exception


# ══════════════════════════════════════════════════════════════
# ThrottledSchedulerConfigurator.calculate_schedule (lines 410-445)
# ══════════════════════════════════════════════════════════════

# #region test_calculate_schedule_empty [C:2] [TYPE Function]
# @BRIEF calculate_schedule returns empty list for empty dashboard_ids (line 417).
def test_calculate_schedule_empty():
    from src.core.scheduler import ThrottledSchedulerConfigurator
    result = ThrottledSchedulerConfigurator.calculate_schedule(
        window_start=time(10, 0),
        window_end=time(11, 0),
        dashboard_ids=[],
        current_date=date(2026, 6, 11),
    )
    assert result == []
# #endregion test_calculate_schedule_empty

# #region test_calculate_schedule_single_dashboard [C:2] [TYPE Function]
# @BRIEF calculate_schedule returns single start_dt for one dashboard (line 434).
def test_calculate_schedule_single_dashboard():
    from src.core.scheduler import ThrottledSchedulerConfigurator
    result = ThrottledSchedulerConfigurator.calculate_schedule(
        window_start=time(10, 0),
        window_end=time(11, 0),
        dashboard_ids=["dash-1"],
        current_date=date(2026, 6, 11),
    )
    assert len(result) == 1
    assert result[0] == datetime(2026, 6, 11, 10, 0)
# #endregion test_calculate_schedule_single_dashboard

# #region test_calculate_schedule_multiple [C:2] [TYPE Function]
# @BRIEF calculate_schedule distributes tasks evenly across the window.
def test_calculate_schedule_multiple():
    from src.core.scheduler import ThrottledSchedulerConfigurator
    result = ThrottledSchedulerConfigurator.calculate_schedule(
        window_start=time(10, 0),
        window_end=time(11, 0),
        dashboard_ids=["d1", "d2", "d3"],
        current_date=date(2026, 6, 11),
    )
    assert len(result) == 3
    # First at 10:00, last at 11:00, middle at 10:30
    assert result[0] == datetime(2026, 6, 11, 10, 0)
    assert result[1] == datetime(2026, 6, 11, 10, 30)
    assert result[2] == datetime(2026, 6, 11, 11, 0)
# #endregion test_calculate_schedule_multiple

# #region test_calculate_schedule_midnight_crossing [C:2] [TYPE Function]
# @BRIEF calculate_schedule handles window crossing midnight (line 422).
def test_calculate_schedule_midnight_crossing():
    from src.core.scheduler import ThrottledSchedulerConfigurator
    result = ThrottledSchedulerConfigurator.calculate_schedule(
        window_start=time(23, 0),
        window_end=time(1, 0),
        dashboard_ids=["d1", "d2", "d3"],
        current_date=date(2026, 6, 11),
    )
    assert len(result) == 3
    # Window: 23:00 → 01:00 next day = 2 hours = 7200s
    # Interval: 7200 / 2 = 3600s = 1h
    assert result[0] == datetime(2026, 6, 11, 23, 0)
    assert result[1] == datetime(2026, 6, 12, 0, 0)
    assert result[2] == datetime(2026, 6, 12, 1, 0)
# #endregion test_calculate_schedule_midnight_crossing

# #region test_calculate_schedule_zero_window [C:2] [TYPE Function]
# @BRIEF calculate_schedule returns start_dt repeated for zero-length window (lines 426-429).
def test_calculate_schedule_zero_window():
    from src.core.scheduler import ThrottledSchedulerConfigurator
    result = ThrottledSchedulerConfigurator.calculate_schedule(
        window_start=time(10, 0),
        window_end=time(10, 0),
        dashboard_ids=["d1", "d2", "d3"],
        current_date=date(2026, 6, 11),
    )
    assert len(result) == 3
    expected = datetime(2026, 6, 11, 10, 0)
    assert all(t == expected for t in result)
# #endregion test_calculate_schedule_zero_window

# #region test_calculate_schedule_small_window_warning [C:2] [TYPE Function]
# @BRIEF calculate_schedule warns when interval < 1s (lines 439+).
def test_calculate_schedule_small_window_warning():
    from src.core.scheduler import ThrottledSchedulerConfigurator
    # 10 dashboards in 5 seconds → interval = 5/9 ≈ 0.56s < 1s
    result = ThrottledSchedulerConfigurator.calculate_schedule(
        window_start=time(10, 0, 0),
        window_end=time(10, 0, 5),
        dashboard_ids=[f"d{i}" for i in range(10)],
        current_date=date(2026, 6, 11),
    )
    assert len(result) == 10
    # First should be at start, last should be within the window
    assert result[0] == datetime(2026, 6, 11, 10, 0, 0)
# #endregion test_calculate_schedule_small_window_warning

# #endregion Test.Scheduler
