# #region TestCoreScheduler [C:3] [TYPE Module] [SEMANTICS test, scheduler, apscheduler, load, reload]
# @BRIEF Verify SchedulerService load_schedules and translation job add/remove contracts.
# @RELATION BINDS_TO -> [SchedulerService]
# @TEST_EDGE: load_schedules_reloads — jobs survive remove_all_jobs when re-registered from DB
# @TEST_EDGE: add_remove_translation_job — job correctly registered and removed from APScheduler
# @TEST_INVARIANT: translation_jobs_persist_across_reload -> VERIFIED_BY: [test_load_schedules_reloads_translation_schedules]

import asyncio
from datetime import time
from pathlib import Path
import sys

sys.path.insert(0, str(Path(__file__).parent.parent / "src"))

from unittest.mock import MagicMock, patch

from apscheduler.schedulers.background import BackgroundScheduler

# ---------------------------------------------------------------------------
# Helpers — fresh scheduler & mock config
# ---------------------------------------------------------------------------

def _make_mock_config_manager(backup_env_id="env1", backup_cron="0 3 * * *"):
    """Build a mock ConfigManager returning a single backup-enabled environment."""
    mgr = MagicMock()
    mock_env = MagicMock()
    mock_env.id = backup_env_id
    mock_env.backup_schedule = MagicMock()
    mock_env.backup_schedule.enabled = True
    mock_env.backup_schedule.cron_expression = backup_cron
    mock_config = MagicMock()
    mock_config.environments = [mock_env]
    mgr.get_config.return_value = mock_config
    return mgr


def _make_mock_task_manager():
    """Build a mock TaskManager returning no active tasks."""
    tm = MagicMock()
    tm.get_tasks.return_value = []
    return tm


# ---------------------------------------------------------------------------
# Fixture helpers — start/stop a real BackgroundScheduler
# ---------------------------------------------------------------------------

def _start_scheduler():
    """Create and start a BackgroundScheduler. Returns the scheduler instance."""
    sched = BackgroundScheduler()
    sched.start()
    return sched


def _stop_scheduler(sched):
    """Shutdown a BackgroundScheduler if running."""
    if sched.running:
        sched.shutdown(wait=False)


# #region test_load_schedules_reloads_translation_schedules [C:1] [TYPE Function]
def test_load_schedules_reloads_translation_schedules():
    """Verify load_schedules re-registers translation jobs after remove_all_jobs."""
    from src.core.scheduler import SchedulerService

    sched = _start_scheduler()
    try:
        cfg = _make_mock_config_manager()
        tm = _make_mock_task_manager()

        svc = SchedulerService.__new__(SchedulerService)
        svc.task_manager = tm
        svc.config_manager = cfg
        svc.scheduler = sched

        # Pre-register a translation job (simulates what happens before reload)
        svc.add_translation_job(
            schedule_id="sched-1",
            job_id="job-1",
            cron_expression="0 2 * * *",
        )

        # Verify it exists BEFORE reload
        job_before = sched.get_job("translate_sched-1")
        assert job_before is not None, "Translation job should exist before reload"

        # Mock the DB query that load_schedules uses internally
        mock_schedule = MagicMock()
        mock_schedule.id = "sched-1"
        mock_schedule.job_id = "job-1"
        mock_schedule.cron_expression = "0 2 * * *"
        mock_schedule.timezone = "UTC"
        mock_schedule.execution_mode = "full"

        mock_db = MagicMock()
        mock_db.query.return_value.filter.return_value.all.return_value = [mock_schedule]

        with patch("src.core.scheduler.SessionLocal", return_value=mock_db):
            svc.load_schedules()

        # After reload, the translation job should STILL exist
        job_after = sched.get_job("translate_sched-1")
        assert job_after is not None, (
            "Translation job should survive load_schedules (re-registered from DB)"
        )

        # Backup job should also exist (added from config)
        backup_job = sched.get_job("backup_env1")
        assert backup_job is not None, "Backup job should be registered by load_schedules"
    finally:
        _stop_scheduler(sched)
# #endregion test_load_schedules_reloads_translation_schedules


# #region test_add_and_remove_translation_job [C:1] [TYPE Function]
def test_add_and_remove_translation_job():
    """Verify add_translation_job registers and remove_translation_job removes."""
    from src.core.scheduler import SchedulerService

    sched = _start_scheduler()
    try:
        cfg = _make_mock_config_manager()
        tm = _make_mock_task_manager()

        svc = SchedulerService.__new__(SchedulerService)
        svc.task_manager = tm
        svc.config_manager = cfg
        svc.scheduler = sched

        # Should NOT exist before add
        assert sched.get_job("translate_sched-x") is None

        svc.add_translation_job(
            schedule_id="sched-x",
            job_id="job-x",
            cron_expression="*/5 * * * *",
        )

        # Should exist after add
        job = sched.get_job("translate_sched-x")
        assert job is not None, "Translation job should be registered after add"
        assert job.id == "translate_sched-x"

        svc.remove_translation_job("sched-x")

        # Should NOT exist after remove
        assert sched.get_job("translate_sched-x") is None, (
            "Translation job should be removed after remove_translation_job"
        )
    finally:
        _stop_scheduler(sched)
# #endregion test_add_and_remove_translation_job


# #region test_remove_nonexistent_translation_job_no_error [C:1] [TYPE Function]
def test_remove_nonexistent_translation_job_no_error():
    """Verify remove_translation_job handles non-existent job silently."""
    from src.core.scheduler import SchedulerService

    sched = _start_scheduler()
    try:
        cfg = _make_mock_config_manager()
        tm = _make_mock_task_manager()

        svc = SchedulerService.__new__(SchedulerService)
        svc.task_manager = tm
        svc.config_manager = cfg
        svc.scheduler = sched

        # Should not raise — silent ignore
        svc.remove_translation_job("nonexistent-sched")
    finally:
        _stop_scheduler(sched)
# #endregion test_remove_nonexistent_translation_job_no_error


# #region test_add_validation_job [C:1] [TYPE Function]
def test_add_validation_job():
    """Verify add_validation_job registers an APScheduler job."""
    from src.core.scheduler import SchedulerService

    sched = _start_scheduler()
    try:
        cfg = _make_mock_config_manager()
        tm = _make_mock_task_manager()

        svc = SchedulerService.__new__(SchedulerService)
        svc.task_manager = tm
        svc.config_manager = cfg
        svc.scheduler = sched

        assert sched.get_job("validation_pol-1") is None

        svc.add_validation_job(
            policy_id="pol-1",
            cron_expression="30 10 * * 0,2,4",  # 10:30 Mon, Wed, Fri
            app_timezone="Europe/Moscow",
        )

        job = sched.get_job("validation_pol-1")
        assert job is not None, "Validation job should be registered after add"
        assert job.id == "validation_pol-1"
    finally:
        _stop_scheduler(sched)
# #endregion test_add_validation_job


# #region test_load_schedules_loads_validation_policies [C:1] [TYPE Function]
def test_load_schedules_loads_validation_policies():
    """Verify load_schedules re-registers validation jobs from DB."""
    from src.core.scheduler import SchedulerService

    sched = _start_scheduler()
    try:
        cfg = _make_mock_config_manager()
        # Add app_timezone to config so cron gets a timezone
        cfg.get_config.return_value.settings.app_timezone = "Europe/Moscow"
        tm = _make_mock_task_manager()

        svc = SchedulerService.__new__(SchedulerService)
        svc.task_manager = tm
        svc.config_manager = cfg
        svc.scheduler = sched

        # Pre-register a validation job (to test it survives reload)
        svc.add_validation_job(
            policy_id="pol-1",
            cron_expression="0 10 * * 1,3,5",
        )
        assert sched.get_job("validation_pol-1") is not None

        # Mock DB to return an active ValidationPolicy with schedule_days
        mock_policy = MagicMock()
        mock_policy.id = "pol-1"
        mock_policy.schedule_days = [0, 2, 4]  # Mon, Wed, Fri
        mock_policy.window_start = time(10, 30)
        mock_policy.window_end = time(11, 0)

        mock_db = MagicMock()
        # First query is for TranslationSchedule (empty), second is ValidationPolicy
        mock_db.query.side_effect = [
            MagicMock(filter=MagicMock(return_value=MagicMock(all=MagicMock(return_value=[])))),
            MagicMock(filter=MagicMock(return_value=MagicMock(all=MagicMock(return_value=[mock_policy])))),
        ]

        with patch("src.core.scheduler.SessionLocal", return_value=mock_db):
            svc.load_schedules()

        # Validation job should still exist after reload
        job = sched.get_job("validation_pol-1")
        assert job is not None, "Validation job should survive load_schedules"
    finally:
        _stop_scheduler(sched)
# #endregion test_load_schedules_loads_validation_policies


# #region test_trigger_validation_creates_tasks [C:1] [TYPE Function]
def test_trigger_validation_creates_tasks():
    """Verify _trigger_validation creates TaskManager tasks for each dashboard."""
    from src.core.scheduler import SchedulerService

    sched = _start_scheduler()
    try:
        cfg = _make_mock_config_manager()
        tm = _make_mock_task_manager()

        svc = SchedulerService.__new__(SchedulerService)
        svc.task_manager = tm
        svc.config_manager = cfg
        svc.scheduler = sched
        svc.loop = asyncio.new_event_loop()

        # Mock the ValidationPolicy DB query
        mock_policy = MagicMock()
        mock_policy.id = "pol-1"
        mock_policy.is_active = True
        mock_policy.dashboard_ids = ["dash-1", "dash-2"]
        mock_policy.environment_id = "ss-dev"
        mock_policy.provider_id = "prov-1"
        mock_policy.window_start = time(10, 0)
        mock_policy.window_end = time(10, 30)

        mock_db = MagicMock()
        mock_db.query.return_value.filter.return_value.first.return_value = mock_policy

        # Patch asyncio.run_coroutine_threadsafe to call the function synchronously
        # so we can verify the mock was called
        captured_calls = []

        def fake_run_coro(_coro_or_fn, _loop):
            captured_calls.append(1)
            return MagicMock()

        with patch("src.core.scheduler.SessionLocal", return_value=mock_db), \
             patch("src.core.scheduler.asyncio.run_coroutine_threadsafe", side_effect=fake_run_coro):
            svc._trigger_validation("pol-1")

        # Should have submitted 2 coroutines via run_coroutine_threadsafe (one per dashboard)
        assert len(captured_calls) == 2, (
            f"Expected 2 task submissions via run_coroutine_threadsafe, got {len(captured_calls)}"
        )
    finally:
        _stop_scheduler(sched)
# #endregion test_trigger_validation_creates_tasks


# #region test_trigger_validation_skips_missing_policy [C:1] [TYPE Function]
def test_trigger_validation_skips_missing_policy():
    """Verify _trigger_validation does not fail when policy is missing."""
    from src.core.scheduler import SchedulerService

    sched = _start_scheduler()
    try:
        cfg = _make_mock_config_manager()
        tm = _make_mock_task_manager()

        svc = SchedulerService.__new__(SchedulerService)
        svc.task_manager = tm
        svc.config_manager = cfg
        svc.scheduler = sched
        svc.loop = asyncio.new_event_loop()

        # Mock DB to return None (policy not found)
        mock_db = MagicMock()
        mock_db.query.return_value.filter.return_value.first.return_value = None

        with patch("src.core.scheduler.SessionLocal", return_value=mock_db):
            # Should not raise
            svc._trigger_validation("nonexistent-policy")

        # create_task should not have been called
        assert tm.create_task.call_count == 0
    finally:
        _stop_scheduler(sched)
# #endregion test_trigger_validation_skips_missing_policy


# #region test_trigger_validation_skips_inactive_policy [C:1] [TYPE Function]
def test_trigger_validation_skips_inactive_policy():
    """Verify _trigger_validation skips an inactive policy."""
    from src.core.scheduler import SchedulerService

    sched = _start_scheduler()
    try:
        cfg = _make_mock_config_manager()
        tm = _make_mock_task_manager()

        svc = SchedulerService.__new__(SchedulerService)
        svc.task_manager = tm
        svc.config_manager = cfg
        svc.scheduler = sched
        svc.loop = asyncio.new_event_loop()

        mock_policy = MagicMock()
        mock_policy.id = "pol-inactive"
        mock_policy.is_active = False
        mock_policy.dashboard_ids = ["dash-1"]

        mock_db = MagicMock()
        mock_db.query.return_value.filter.return_value.first.return_value = mock_policy

        with patch("src.core.scheduler.SessionLocal", return_value=mock_db):
            svc._trigger_validation("pol-inactive")

        # create_task should not have been called for inactive policy
        assert tm.create_task.call_count == 0
    finally:
        _stop_scheduler(sched)
# #endregion test_trigger_validation_skips_inactive_policy


# #region test_trigger_validation_skips_no_dashboards [C:1] [TYPE Function]
def test_trigger_validation_skips_no_dashboards():
    """Verify _trigger_validation skips a policy with no dashboards."""
    from src.core.scheduler import SchedulerService

    sched = _start_scheduler()
    try:
        cfg = _make_mock_config_manager()
        tm = _make_mock_task_manager()

        svc = SchedulerService.__new__(SchedulerService)
        svc.task_manager = tm
        svc.config_manager = cfg
        svc.scheduler = sched
        svc.loop = asyncio.new_event_loop()

        mock_policy = MagicMock()
        mock_policy.id = "pol-empty"
        mock_policy.is_active = True
        mock_policy.dashboard_ids = []

        mock_db = MagicMock()
        mock_db.query.return_value.filter.return_value.first.return_value = mock_policy

        with patch("src.core.scheduler.SessionLocal", return_value=mock_db):
            svc._trigger_validation("pol-empty")

        assert tm.create_task.call_count == 0
    finally:
        _stop_scheduler(sched)
# #endregion test_trigger_validation_skips_no_dashboards


# #region test_remove_validation_job [C:1] [TYPE Function]
def test_remove_validation_job():
    """Verify remove_validation_job removes an existing job."""
    from src.core.scheduler import SchedulerService

    sched = _start_scheduler()
    try:
        cfg = _make_mock_config_manager()
        tm = _make_mock_task_manager()

        svc = SchedulerService.__new__(SchedulerService)
        svc.task_manager = tm
        svc.config_manager = cfg
        svc.scheduler = sched

        svc.add_validation_job("pol-remove", cron_expression="0 10 * * 1")
        assert sched.get_job("validation_pol-remove") is not None

        svc.remove_validation_job("pol-remove")
        assert sched.get_job("validation_pol-remove") is None
    finally:
        _stop_scheduler(sched)
# #endregion test_remove_validation_job


# #region test_remove_nonexistent_validation_job [C:1] [TYPE Function]
def test_remove_nonexistent_validation_job():
    """Verify remove_validation_job handles non-existent job silently."""
    from src.core.scheduler import SchedulerService

    sched = _start_scheduler()
    try:
        cfg = _make_mock_config_manager()
        tm = _make_mock_task_manager()

        svc = SchedulerService.__new__(SchedulerService)
        svc.task_manager = tm
        svc.config_manager = cfg
        svc.scheduler = sched

        # Should not raise
        svc.remove_validation_job("nonexistent-pol")
    finally:
        _stop_scheduler(sched)
# #endregion test_remove_nonexistent_validation_job


# #region test_reload_validation_policy_adds_job [C:1] [TYPE Function]
def test_reload_validation_policy_adds_job():
    """Verify reload_validation_policy registers a job for an active policy with schedule."""
    from src.core.scheduler import SchedulerService

    sched = _start_scheduler()
    try:
        cfg = _make_mock_config_manager()
        tm = _make_mock_task_manager()

        svc = SchedulerService.__new__(SchedulerService)
        svc.task_manager = tm
        svc.config_manager = cfg
        svc.scheduler = sched

        # Mock DB to return an active policy with schedule_days
        mock_policy = MagicMock()
        mock_policy.id = "pol-reload"
        mock_policy.is_active = True
        mock_policy.schedule_days = [0, 2, 4]
        mock_policy.window_start = time(10, 30)

        mock_db = MagicMock()
        mock_db.query.return_value.filter.return_value.first.return_value = mock_policy

        with patch("src.core.scheduler.SessionLocal", return_value=mock_db):
            svc.reload_validation_policy("pol-reload")

        job = sched.get_job("validation_pol-reload")
        assert job is not None, "Job should be registered after reload"
    finally:
        _stop_scheduler(sched)
# #endregion test_reload_validation_policy_adds_job


# #region test_reload_validation_policy_removes_job_when_policy_gone [C:1] [TYPE Function]
def test_reload_validation_policy_removes_job_when_policy_gone():
    """Verify reload_validation_policy removes job when policy is missing or inactive."""
    from src.core.scheduler import SchedulerService

    sched = _start_scheduler()
    try:
        cfg = _make_mock_config_manager()
        tm = _make_mock_task_manager()

        svc = SchedulerService.__new__(SchedulerService)
        svc.task_manager = tm
        svc.config_manager = cfg
        svc.scheduler = sched

        # Pre-register a job
        svc.add_validation_job("pol-gone", cron_expression="0 10 * * 1")
        assert sched.get_job("validation_pol-gone") is not None

        # Mock DB to return None (policy not found)
        mock_db = MagicMock()
        mock_db.query.return_value.filter.return_value.first.return_value = None

        with patch("src.core.scheduler.SessionLocal", return_value=mock_db):
            svc.reload_validation_policy("pol-gone")

        # Job should have been removed
        assert sched.get_job("validation_pol-gone") is None
    finally:
        _stop_scheduler(sched)
# #endregion test_reload_validation_policy_removes_job_when_policy_gone
# #endregion TestCoreScheduler
