"""Tests for the session cleanup background task.

Validates that :func:`~app.tasks.session_cleanup._run_cleanup_with_resources`
correctly calls the repository function to delete expired sessions and logs
the results, and that :func:`~app.tasks.session_cleanup.register` configures
the APScheduler job with the correct interval and stable job ID.
"""

from __future__ import annotations

from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from app.tasks.session_cleanup import (
    JOB_ID,
    SESSION_CLEANUP_INTERVAL,
    register,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_app() -> MagicMock:
    """Build a minimal mock ``app`` for session cleanup task tests.

    Returns:
        A :class:`unittest.mock.MagicMock` that mimics ``fastapi.FastAPI``.
    """
    app = MagicMock()
    app.state.scheduler = MagicMock()
    app.state.settings = MagicMock(database_path="/tmp/fake.db")
    app.state.runtime_settings = None
    return app


# ---------------------------------------------------------------------------
# Tests for _run_cleanup_with_resources
# ---------------------------------------------------------------------------


class TestRunCleanup:
    """Tests for :func:`~app.tasks.session_cleanup._run_cleanup_with_resources`."""

    @pytest.mark.asyncio
    async def test_run_cleanup_calls_delete_expired_sessions(self) -> None:
        """``_run_cleanup_with_resources`` must call ``delete_expired_sessions`` with db and now_iso."""
        settings = MagicMock(database_path="/tmp/fake.db")

        mock_db = MagicMock()
        with patch(
            "app.tasks.session_cleanup.task_db",
            MagicMock(
                return_value=AsyncMock(
                    __aenter__=AsyncMock(return_value=mock_db),
                    __aexit__=AsyncMock(return_value=False),
                )
            ),
        ), patch(
            "app.tasks.session_cleanup.session_repo.delete_expired_sessions",
            new_callable=AsyncMock,
            return_value=0,
        ) as mock_delete:
            from app.tasks.session_cleanup import _run_cleanup_with_resources

            await _run_cleanup_with_resources(settings)

        mock_delete.assert_awaited_once()
        # Verify db was passed as first argument
        call_args = mock_delete.await_args
        assert call_args[0][0] == mock_db

    @pytest.mark.asyncio
    async def test_run_cleanup_logs_deleted_count(self) -> None:
        """``_run_cleanup_with_resources`` must log the count of deleted sessions."""
        settings = MagicMock(database_path="/tmp/fake.db")

        with patch(
            "app.tasks.session_cleanup.task_db",
            MagicMock(
                return_value=AsyncMock(
                    __aenter__=AsyncMock(return_value=MagicMock()),
                    __aexit__=AsyncMock(return_value=False),
                )
            ),
        ), patch(
            "app.tasks.session_cleanup.session_repo.delete_expired_sessions",
            new_callable=AsyncMock,
            return_value=42,
        ), patch(
            "app.tasks.session_cleanup.log"
        ) as mock_log:
            from app.tasks.session_cleanup import _run_cleanup_with_resources

            await _run_cleanup_with_resources(settings)

        # Verify the log.info call happened with deleted_count
        info_calls = [c for c in mock_log.info.call_args_list if c[0][0] == "session_cleanup_ran"]
        assert len(info_calls) == 1
        assert info_calls[0][1]["deleted_count"] == 42

    @pytest.mark.asyncio
    async def test_run_cleanup_logs_even_with_zero_deletions(self) -> None:
        """``_run_cleanup_with_resources`` must log even when no sessions were deleted."""
        settings = MagicMock(database_path="/tmp/fake.db")

        with patch(
            "app.tasks.session_cleanup.task_db",
            MagicMock(
                return_value=AsyncMock(
                    __aenter__=AsyncMock(return_value=MagicMock()),
                    __aexit__=AsyncMock(return_value=False),
                )
            ),
        ), patch(
            "app.tasks.session_cleanup.session_repo.delete_expired_sessions",
            new_callable=AsyncMock,
            return_value=0,
        ), patch(
            "app.tasks.session_cleanup.log"
        ) as mock_log:
            from app.tasks.session_cleanup import _run_cleanup_with_resources

            await _run_cleanup_with_resources(settings)

        info_calls = [c for c in mock_log.info.call_args_list if c[0][0] == "session_cleanup_ran"]
        assert len(info_calls) == 1
        assert info_calls[0][1]["deleted_count"] == 0


# ---------------------------------------------------------------------------
# Tests for register
# ---------------------------------------------------------------------------


class TestRegister:
    """Tests for :func:`~app.tasks.session_cleanup.register`."""

    def test_register_adds_interval_job_to_scheduler(self) -> None:
        """``register`` must add a job with an ``"interval"`` trigger."""
        app = _make_app()

        register(app)

        app.state.scheduler.add_job.assert_called_once()
        _, kwargs = app.state.scheduler.add_job.call_args
        assert kwargs["trigger"] == "interval"
        assert kwargs["seconds"] == SESSION_CLEANUP_INTERVAL

    def test_register_uses_stable_job_id(self) -> None:
        """``register`` must use the module-level ``JOB_ID`` constant."""
        app = _make_app()

        register(app)

        _, kwargs = app.state.scheduler.add_job.call_args
        assert kwargs["id"] == JOB_ID

    def test_register_sets_replace_existing(self) -> None:
        """``register`` must use ``replace_existing=True`` to avoid duplicate jobs."""
        app = _make_app()

        register(app)

        _, kwargs = app.state.scheduler.add_job.call_args
        assert kwargs["replace_existing"] is True

    def test_register_passes_settings_in_kwargs(self) -> None:
        """The scheduled job must receive settings as kwargs."""
        app = _make_app()

        register(app)

        _, kwargs = app.state.scheduler.add_job.call_args
        assert "settings" in kwargs["kwargs"]

    def test_register_logs_scheduling_confirmation(self) -> None:
        """``register`` must emit an info log when scheduling the job."""
        app = _make_app()

        with patch("app.tasks.session_cleanup.log") as mock_log:
            register(app)

        info_calls = [
            c
            for c in mock_log.info.call_args_list
            if c[0][0] == "session_cleanup_scheduled"
        ]
        assert len(info_calls) == 1
        assert info_calls[0][1]["interval_seconds"] == SESSION_CLEANUP_INTERVAL
