"""Tests for KeyboardInterrupt handling in exit cleanup paths.

``except Exception`` does not catch ``KeyboardInterrupt`` (which inherits
from ``BaseException``).  A second Ctrl+C during exit cleanup must not
abort remaining cleanup steps.  These tests exercise the actual production
code paths — not a copy of the try/except pattern.
"""

import atexit
import weakref
from unittest.mock import MagicMock, patch, call

import pytest


class TestHonchoAtexitFlush:
    """run_agent.py — _register_honcho_exit_hook atexit handler."""

    def test_keyboard_interrupt_during_flush_does_not_propagate(self):
        """The atexit handler must swallow KeyboardInterrupt from flush_all()."""
        mock_manager = MagicMock()
        mock_manager.flush_all.side_effect = KeyboardInterrupt

        # Capture functions passed to atexit.register
        registered_fns = []
        original_register = atexit.register

        def capturing_register(fn, *args, **kwargs):
            registered_fns.append(fn)
            # Don't actually register — we don't want side effects

        with patch("atexit.register", side_effect=capturing_register):
            from run_agent import AIAgent
            agent = object.__new__(AIAgent)
            agent._honcho = mock_manager
            agent._honcho_exit_hook_registered = False
            agent._register_honcho_exit_hook()

        # Our handler is the last one registered
        assert len(registered_fns) >= 1, "atexit handler was not registered"
        flush_handler = registered_fns[-1]

        # Invoke the registered handler — must not raise
        flush_handler()
        mock_manager.flush_all.assert_called_once()


class TestCronJobCleanup:
    """cron/scheduler.py — end_session + close in the finally block."""

    def test_keyboard_interrupt_in_end_session_does_not_skip_close(self):
        """If end_session raises KeyboardInterrupt, close() must still run."""
        mock_db = MagicMock()
        mock_db.end_session.side_effect = KeyboardInterrupt

        from cron import scheduler

        job = {
            "id": "test-job-1",
            "name": "test cleanup",
            "prompt": "hello",
            "schedule": "0 9 * * *",
            "model": "test/model",
        }

        with patch("hermes_state.SessionDB", return_value=mock_db), \
             patch.object(scheduler, "_build_job_prompt", return_value="hello"), \
             patch.object(scheduler, "_resolve_origin", return_value=None), \
             patch.object(scheduler, "_resolve_delivery_target", return_value=None), \
             patch("dotenv.load_dotenv", return_value=None), \
             patch("run_agent.AIAgent") as MockAgent:
            # Make the agent raise immediately so we hit the finally block
            MockAgent.return_value.run_conversation.side_effect = RuntimeError("boom")
            scheduler.run_job(job)

        mock_db.end_session.assert_called_once()
        mock_db.close.assert_called_once()

    def test_keyboard_interrupt_in_close_does_not_propagate(self):
        """If close() raises KeyboardInterrupt, it must not escape run_job."""
        mock_db = MagicMock()
        mock_db.close.side_effect = KeyboardInterrupt

        from cron import scheduler

        job = {
            "id": "test-job-2",
            "name": "test close interrupt",
            "prompt": "hello",
            "schedule": "0 9 * * *",
            "model": "test/model",
        }

        with patch("hermes_state.SessionDB", return_value=mock_db), \
             patch.object(scheduler, "_build_job_prompt", return_value="hello"), \
             patch.object(scheduler, "_resolve_origin", return_value=None), \
             patch.object(scheduler, "_resolve_delivery_target", return_value=None), \
             patch("dotenv.load_dotenv", return_value=None), \
             patch("run_agent.AIAgent") as MockAgent:
            MockAgent.return_value.run_conversation.side_effect = RuntimeError("boom")
            # Must not raise
            scheduler.run_job(job)

        mock_db.end_session.assert_called_once()
        mock_db.close.assert_called_once()
