"""Tests for graceful shutdown handling."""

from __future__ import annotations

import asyncio
import signal
import time
from pathlib import Path
from unittest.mock import Mock, patch

import pytest

from oclawma.shutdown import (
    JobInfo,
    QueueState,
    ShutdownManager,
    ShutdownState,
    get_shutdown_manager,
    reset_shutdown_manager,
    set_shutdown_manager,
)


class TestShutdownState:
    """Test ShutdownState enum."""

    def test_shutdown_state_values(self) -> None:
        """Test shutdown state has expected values."""
        assert ShutdownState.RUNNING.value == "running"
        assert ShutdownState.SHUTDOWN_REQUESTED.value == "shutdown_requested"
        assert ShutdownState.SHUTTING_DOWN.value == "shutting_down"
        assert ShutdownState.SHUTDOWN_COMPLETE.value == "shutdown_complete"


class TestJobInfo:
    """Test JobInfo dataclass."""

    def test_job_info_creation(self) -> None:
        """Test JobInfo can be created."""
        job = JobInfo(
            job_id="test-123",
            job_type="subagent",
            start_time=time.time(),
            metadata={"key": "value"},
        )
        assert job.job_id == "test-123"
        assert job.job_type == "subagent"
        assert job.metadata == {"key": "value"}

    def test_job_info_default_metadata(self) -> None:
        """Test JobInfo has default empty metadata."""
        job = JobInfo(
            job_id="test-123",
            job_type="task",
            start_time=time.time(),
        )
        assert job.metadata == {}


class TestQueueState:
    """Test QueueState dataclass."""

    def test_queue_state_creation(self) -> None:
        """Test QueueState can be created."""
        state = QueueState(
            version=1,
            timestamp=1234567890.0,
            pending_jobs=[{"id": "job1"}],
            active_jobs=[{"id": "job2"}],
            completed_jobs=[{"id": "job3"}],
        )
        assert state.version == 1
        assert state.timestamp == 1234567890.0
        assert state.pending_jobs == [{"id": "job1"}]
        assert state.active_jobs == [{"id": "job2"}]
        assert state.completed_jobs == [{"id": "job3"}]

    def test_queue_state_defaults(self) -> None:
        """Test QueueState has sensible defaults."""
        state = QueueState()
        assert state.version == 1
        assert state.timestamp > 0
        assert state.pending_jobs == []
        assert state.active_jobs == []
        assert state.completed_jobs == []

    def test_queue_state_to_dict(self) -> None:
        """Test QueueState serialization to dict."""
        state = QueueState(
            version=1,
            timestamp=1234567890.0,
            pending_jobs=[{"id": "job1"}],
        )
        data = state.to_dict()
        assert data["version"] == 1
        assert data["timestamp"] == 1234567890.0
        assert data["pending_jobs"] == [{"id": "job1"}]

    def test_queue_state_from_dict(self) -> None:
        """Test QueueState deserialization from dict."""
        data = {
            "version": 2,
            "timestamp": 1234567890.0,
            "pending_jobs": [{"id": "job1"}],
            "active_jobs": [],
            "completed_jobs": [],
        }
        state = QueueState.from_dict(data)
        assert state.version == 2
        assert state.timestamp == 1234567890.0
        assert state.pending_jobs == [{"id": "job1"}]


class TestShutdownManager:
    """Test ShutdownManager class."""

    def test_shutdown_manager_creation(self) -> None:
        """Test ShutdownManager can be created."""
        manager = ShutdownManager(timeout_seconds=60.0)
        assert manager.timeout_seconds == 60.0
        assert manager.state == ShutdownState.RUNNING
        assert manager.active_job_count == 0
        assert manager.pending_job_count == 0

    def test_shutdown_manager_with_state_file(self, tmp_path: Path) -> None:
        """Test ShutdownManager with state file."""
        state_file = tmp_path / "queue_state.json"
        manager = ShutdownManager(
            timeout_seconds=30.0,
            state_file=state_file,
        )
        assert manager.state_file == state_file

    def test_is_shutting_down_initially_false(self) -> None:
        """Test is_shutting_down is False initially."""
        manager = ShutdownManager()
        assert not manager.is_shutting_down

    def test_register_job(self) -> None:
        """Test registering a job."""
        manager = ShutdownManager()
        manager.register_job("job-1", "subagent", {"task": "test"})
        assert manager.active_job_count == 1
        assert "job-1" in manager._active_jobs

    def test_register_job_during_shutdown_raises(self) -> None:
        """Test registering a job during shutdown raises error."""
        manager = ShutdownManager()
        manager._state = ShutdownState.SHUTDOWN_REQUESTED

        with pytest.raises(RuntimeError, match="Cannot register new jobs during shutdown"):
            manager.register_job("job-1", "subagent")

    def test_unregister_job(self) -> None:
        """Test unregistering a job."""
        manager = ShutdownManager()
        manager.register_job("job-1", "subagent")
        manager.unregister_job("job-1", success=True)
        assert manager.active_job_count == 0
        assert len(manager._completed_jobs) == 1

    def test_unregister_nonexistent_job(self) -> None:
        """Test unregistering a job that doesn't exist."""
        manager = ShutdownManager()
        # Should not raise
        manager.unregister_job("nonexistent", success=True)
        assert manager.active_job_count == 0

    def test_add_pending_job(self) -> None:
        """Test adding a pending job."""
        manager = ShutdownManager()
        manager.add_pending_job({"job_id": "pending-1", "task": "test"})
        assert manager.pending_job_count == 1

    def test_remove_pending_job(self) -> None:
        """Test removing a pending job."""
        manager = ShutdownManager()
        job_data = {"job_id": "pending-1", "task": "test"}
        manager.add_pending_job(job_data)
        removed = manager.remove_pending_job("pending-1")
        assert removed == job_data
        assert manager.pending_job_count == 0

    def test_remove_nonexistent_pending_job(self) -> None:
        """Test removing a pending job that doesn't exist."""
        manager = ShutdownManager()
        removed = manager.remove_pending_job("nonexistent")
        assert removed is None

    def test_register_cleanup_handler(self) -> None:
        """Test registering a cleanup handler."""
        manager = ShutdownManager()
        handler = Mock()
        manager.register_cleanup_handler(handler)
        assert handler in manager._cleanup_handlers

    def test_register_async_cleanup_handler(self) -> None:
        """Test registering an async cleanup handler."""
        manager = ShutdownManager()

        async def async_handler():
            pass

        manager.register_async_cleanup_handler(async_handler)
        assert async_handler in manager._async_cleanup_handlers

    def test_get_status(self) -> None:
        """Test getting status."""
        manager = ShutdownManager(timeout_seconds=45.0)
        manager.register_job("job-1", "subagent")
        manager.add_pending_job({"job_id": "pending-1"})

        status = manager.get_status()
        assert status["state"] == "running"
        assert status["is_shutting_down"] is False
        assert status["active_jobs"] == 1
        assert status["pending_jobs"] == 1
        assert status["completed_jobs"] == 0
        assert status["timeout_seconds"] == 45.0

    @pytest.mark.asyncio
    async def test_save_and_load_queue_state(self, tmp_path: Path) -> None:
        """Test saving and loading queue state."""
        state_file = tmp_path / "queue_state.json"
        manager = ShutdownManager(
            timeout_seconds=30.0,
            state_file=state_file,
        )

        # Add some jobs
        manager.register_job("active-1", "subagent", {"task": "active"})
        manager.add_pending_job({"job_id": "pending-1", "task": "pending"})
        manager._completed_jobs.append({"job_id": "completed-1"})

        # Save state
        await manager._save_queue_state()
        assert state_file.exists()

        # Load state
        loaded_state = manager.load_queue_state()
        assert loaded_state is not None
        assert len(loaded_state.pending_jobs) == 1
        assert loaded_state.pending_jobs[0]["job_id"] == "pending-1"
        assert len(loaded_state.active_jobs) == 1
        assert loaded_state.active_jobs[0]["job_id"] == "active-1"
        assert len(loaded_state.completed_jobs) == 1

    def test_load_queue_state_no_file(self, tmp_path: Path) -> None:
        """Test loading queue state when file doesn't exist."""
        state_file = tmp_path / "nonexistent.json"
        manager = ShutdownManager(state_file=state_file)
        loaded_state = manager.load_queue_state()
        assert loaded_state is None

    def test_clear_queue_state(self, tmp_path: Path) -> None:
        """Test clearing queue state."""
        state_file = tmp_path / "queue_state.json"
        state_file.write_text('{"version": 1}')

        manager = ShutdownManager(state_file=state_file)
        manager.clear_queue_state()
        assert not state_file.exists()

    @pytest.mark.asyncio
    async def test_run_cleanup_handlers(self) -> None:
        """Test running cleanup handlers."""
        manager = ShutdownManager()

        sync_called = False
        async_called = False

        def sync_handler():
            nonlocal sync_called
            sync_called = True

        async def async_handler():
            nonlocal async_called
            async_called = True

        manager.register_cleanup_handler(sync_handler)
        manager.register_async_cleanup_handler(async_handler)

        await manager._run_cleanup_handlers()

        assert sync_called
        assert async_called

    @pytest.mark.asyncio
    async def test_run_cleanup_handlers_with_errors(self) -> None:
        """Test running cleanup handlers when some fail."""
        manager = ShutdownManager()

        handler1_called = False
        handler2_called = False

        def failing_handler():
            raise ValueError("Handler failed")

        def success_handler():
            nonlocal handler1_called
            handler1_called = True

        async def async_success_handler():
            nonlocal handler2_called
            handler2_called = True

        manager.register_cleanup_handler(failing_handler)
        manager.register_cleanup_handler(success_handler)
        manager.register_async_cleanup_handler(async_success_handler)

        # Should not raise
        await manager._run_cleanup_handlers()

        assert handler1_called
        assert handler2_called

    @pytest.mark.asyncio
    async def test_wait_for_shutdown(self) -> None:
        """Test waiting for shutdown signal."""
        manager = ShutdownManager()

        async def trigger_shutdown():
            await asyncio.sleep(0.1)
            manager._shutdown_event.set()

        await asyncio.gather(
            manager.wait_for_shutdown(),
            trigger_shutdown(),
        )

    @pytest.mark.asyncio
    async def test_signal_handler_sets_shutdown_requested(self) -> None:
        """Test signal handler sets shutdown requested state."""
        manager = ShutdownManager()

        # Mock the graceful shutdown to avoid actual shutdown
        with patch.object(manager, "_graceful_shutdown") as mock_shutdown:
            manager._signal_handler()
            assert manager.state == ShutdownState.SHUTDOWN_REQUESTED
            mock_shutdown.assert_called_once()

    @pytest.mark.asyncio
    async def test_graceful_shutdown_idempotent(self) -> None:
        """Test graceful shutdown is idempotent."""
        manager = ShutdownManager()
        manager._state = ShutdownState.SHUTTING_DOWN

        # Should return immediately without doing anything
        await manager._graceful_shutdown()
        assert manager.state == ShutdownState.SHUTTING_DOWN

    @pytest.mark.asyncio
    async def test_graceful_shutdown_saves_state(self, tmp_path: Path) -> None:
        """Test graceful shutdown saves queue state."""
        state_file = tmp_path / "queue_state.json"
        manager = ShutdownManager(
            timeout_seconds=1.0,
            state_file=state_file,
        )

        manager.add_pending_job({"job_id": "test"})

        await manager._graceful_shutdown()

        assert state_file.exists()
        assert manager.state == ShutdownState.SHUTDOWN_COMPLETE

    @pytest.mark.asyncio
    async def test_wait_for_jobs_with_timeout(self) -> None:
        """Test waiting for jobs with timeout."""
        manager = ShutdownManager(timeout_seconds=0.1)

        # Register a job but never unregister it
        manager.register_job("slow-job", "subagent")

        # Should timeout after 0.1 seconds
        start = time.time()
        await manager._wait_for_jobs()
        elapsed = time.time() - start

        assert elapsed < 1.0  # Should not wait too long
        assert "slow-job" in manager._active_jobs  # Job still active

    @pytest.mark.asyncio
    async def test_wait_for_jobs_completes_when_jobs_finish(self) -> None:
        """Test waiting for jobs completes when jobs finish."""
        manager = ShutdownManager(timeout_seconds=5.0)

        manager.register_job("quick-job", "subagent")

        async def finish_job():
            await asyncio.sleep(0.05)
            manager.unregister_job("quick-job")

        await asyncio.gather(
            manager._wait_for_jobs(),
            finish_job(),
        )

        assert manager.active_job_count == 0


class TestGlobalShutdownManager:
    """Test global shutdown manager functions."""

    def setup_method(self) -> None:
        """Reset global state before each test."""
        reset_shutdown_manager()

    def teardown_method(self) -> None:
        """Reset global state after each test."""
        reset_shutdown_manager()

    def test_get_shutdown_manager_creates_default(self) -> None:
        """Test get_shutdown_manager creates default instance."""
        manager = get_shutdown_manager()
        assert isinstance(manager, ShutdownManager)
        assert manager.timeout_seconds == 30.0  # Default

    def test_get_shutdown_manager_returns_same_instance(self) -> None:
        """Test get_shutdown_manager returns same instance."""
        manager1 = get_shutdown_manager()
        manager2 = get_shutdown_manager()
        assert manager1 is manager2

    def test_set_shutdown_manager(self) -> None:
        """Test set_shutdown_manager sets global instance."""
        custom_manager = ShutdownManager(timeout_seconds=60.0)
        set_shutdown_manager(custom_manager)

        retrieved = get_shutdown_manager()
        assert retrieved is custom_manager
        assert retrieved.timeout_seconds == 60.0

    def test_reset_shutdown_manager(self) -> None:
        """Test reset_shutdown_manager clears global instance."""
        manager1 = get_shutdown_manager()
        reset_shutdown_manager()
        manager2 = get_shutdown_manager()

        assert manager1 is not manager2


class TestSignalHandlerIntegration:
    """Test signal handler integration (Unix only)."""

    @pytest.mark.skipif(
        not hasattr(signal, "SIGTERM"),
        reason="SIGTERM not available on this platform",
    )
    @pytest.mark.asyncio
    async def test_install_signal_handlers(self) -> None:
        """Test installing signal handlers."""
        manager = ShutdownManager()

        try:
            manager.install_signal_handlers()
            assert manager._signal_handlers_installed
        finally:
            manager.uninstall_signal_handlers()

    @pytest.mark.skipif(
        not hasattr(signal, "SIGTERM"),
        reason="SIGTERM not available on this platform",
    )
    def test_uninstall_signal_handlers(self) -> None:
        """Test uninstalling signal handlers."""
        manager = ShutdownManager()

        manager.install_signal_handlers()
        assert manager._signal_handlers_installed

        manager.uninstall_signal_handlers()
        assert not manager._signal_handlers_installed

    @pytest.mark.skipif(
        not hasattr(signal, "SIGTERM"),
        reason="SIGTERM not available on this platform",
    )
    @pytest.mark.asyncio
    async def test_signal_handler_fallback(self) -> None:
        """Test fallback signal handler."""
        manager = ShutdownManager()

        # Test with SIGINT
        manager._signal_handler_fallback(signal.SIGINT, None)
        assert manager.state == ShutdownState.SHUTDOWN_REQUESTED

        # Reset and test with SIGTERM
        manager._state = ShutdownState.RUNNING
        manager._shutdown_event.clear()

        manager._signal_handler_fallback(signal.SIGTERM, None)
        assert manager.state == ShutdownState.SHUTDOWN_REQUESTED


class TestQueueStatePersistence:
    """Test queue state persistence edge cases."""

    @pytest.mark.asyncio
    async def test_save_queue_state_creates_directories(self, tmp_path: Path) -> None:
        """Test saving queue state creates parent directories."""
        nested_dir = tmp_path / "nested" / "deep" / "path"
        state_file = nested_dir / "state.json"

        manager = ShutdownManager(state_file=state_file)
        manager.add_pending_job({"job_id": "test"})

        await manager._save_queue_state()

        assert state_file.exists()

    @pytest.mark.asyncio
    async def test_save_queue_state_invalid_path(self, tmp_path: Path) -> None:
        """Test saving queue state with invalid path."""
        # Use a path that looks like a file but can't be created
        state_file = tmp_path / "not_a_dir" / "state.json"
        state_file.mkdir(parents=True, exist_ok=True)  # Make it a directory

        manager = ShutdownManager(state_file=state_file / "file.json")
        manager.add_pending_job({"job_id": "test"})

        # Should not raise
        await manager._save_queue_state()

    def test_load_queue_state_invalid_json(self, tmp_path: Path) -> None:
        """Test loading queue state with invalid JSON."""
        state_file = tmp_path / "state.json"
        state_file.write_text("not valid json")

        manager = ShutdownManager(state_file=state_file)
        loaded = manager.load_queue_state()

        assert loaded is None

    def test_load_queue_state_missing_fields(self, tmp_path: Path) -> None:
        """Test loading queue state with missing fields."""
        state_file = tmp_path / "state.json"
        state_file.write_text('{"version": 1}')

        manager = ShutdownManager(state_file=state_file)
        loaded = manager.load_queue_state()

        assert loaded is not None
        assert loaded.pending_jobs == []
        assert loaded.active_jobs == []
        assert loaded.completed_jobs == []
