"""Tests for timeout watchdog.

Tests for shell.timeout_watchdog module including:
- TimeoutWatchdog (threading-based)
- AsyncTimeoutWatchdog (asyncio-based)
- create_timeout_watchdog factory
"""

import asyncio
import threading
import time

import pytest

from returns.result import Failure, Success

from anima.shell.timeout_watchdog import (
    AsyncTimeoutWatchdog,
    TimeoutWatchdog,
    create_timeout_watchdog,
    create_async_timeout_watchdog,
)


class TestTimeoutWatchdog:
    """Tests for threading-based TimeoutWatchdog."""

    # -------------------------------------------------------------------------
    # Basic functionality
    # -------------------------------------------------------------------------

    def test_create_watchdog_with_timeout(self) -> None:
        """Create watchdog with timeout."""
        called: list[str] = []
        watchdog = TimeoutWatchdog(60, "inst_123", lambda aid: called.append(aid))
        assert watchdog.timeout_seconds == 60
        assert watchdog.instance_id == "inst_123"
        assert not watchdog.started
        assert not watchdog.cancelled

    def test_create_watchdog_no_timeout(self) -> None:
        """Create watchdog without timeout."""
        watchdog = TimeoutWatchdog(None, "ag_456", lambda _: None)
        assert watchdog.timeout_seconds is None

    def test_start_watchdog(self) -> None:
        """Start watchdog starts timer."""
        called: list[str] = []
        watchdog = TimeoutWatchdog(60, "ag_123", lambda aid: called.append(aid))
        result = watchdog.start()
        assert isinstance(result, Success)
        assert watchdog.started
        assert watchdog.timer is not None

        # Clean up
        watchdog.cancel()

    def test_start_no_timeout(self) -> None:
        """Start with no timeout returns success without starting."""
        watchdog = TimeoutWatchdog(None, "ag_456", lambda _: None)
        result = watchdog.start()
        assert isinstance(result, Success)
        assert not watchdog.started  # No timer started

    def test_start_already_started(self) -> None:
        """Start when already started returns failure."""
        called: list[str] = []
        watchdog = TimeoutWatchdog(60, "ag_123", lambda aid: called.append(aid))
        _ = watchdog.start()
        result = watchdog.start()
        assert isinstance(result, Failure)
        watchdog.cancel()

    def test_cancel_watchdog(self) -> None:
        """Cancel watchdog stops timer."""
        called: list[str] = []
        watchdog = TimeoutWatchdog(60, "ag_123", lambda aid: called.append(aid))
        _ = watchdog.start()
        result = watchdog.cancel()
        assert isinstance(result, Success)
        assert watchdog.cancelled
        assert not watchdog.started
        assert watchdog.timer is None

    def test_cancel_before_start(self) -> None:
        """Cancel before start is idempotent."""
        watchdog = TimeoutWatchdog(60, "ag_123", lambda _: None)
        result = watchdog.cancel()
        assert isinstance(result, Success)
        assert watchdog.cancelled

    def test_is_active(self) -> None:
        """is_active reflects watchdog state."""
        called: list[str] = []
        watchdog = TimeoutWatchdog(60, "ag_123", lambda aid: called.append(aid))
        assert not watchdog.is_active

        watchdog.start()
        assert watchdog.is_active

        watchdog.cancel()
        assert not watchdog.is_active

    def test_timeout_fires(self) -> None:
        """Timeout callback fires after duration."""
        called: list[str] = []
        event = threading.Event()

        def on_timeout(agent_id: str) -> None:
            called.append(agent_id)
            event.set()

        # Use very short timeout for testing
        watchdog = TimeoutWatchdog(0.1, "ag_test", on_timeout)
        _ = watchdog.start()

        # Wait for timeout
        assert event.wait(timeout=1.0), "Timeout should fire within 1 second"
        assert "ag_test" in called

    def test_timeout_transitions_state(self) -> None:
        """Timeout callback can safely call state transition."""
        # Track state transitions
        transitions: list[tuple[str, str]] = []

        def on_timeout(agent_id: str) -> None:
            transitions.append((agent_id, "timeout"))

        watchdog = TimeoutWatchdog(0.1, "ag_test", on_timeout)
        _ = watchdog.start()

        # Wait briefly for timeout
        time.sleep(0.3)

        assert ("ag_test", "timeout") in transitions

    def test_cancel_before_timeout(self) -> None:
        """Cancel prevents timeout callback."""
        called: list[str] = []

        # Use longer timeout so we can cancel before firing
        watchdog = TimeoutWatchdog(5.0, "ag_test", lambda aid: called.append(aid))
        _ = watchdog.start()
        watchdog.cancel()

        # Wait briefly
        time.sleep(0.1)

        # Timeout should not have fired
        assert "ag_test" not in called

    def test_multiple_cancel_is_idempotent(self) -> None:
        """Cancel can be called multiple times."""
        called: list[str] = []
        watchdog = TimeoutWatchdog(60, "ag_123", lambda aid: called.append(aid))
        _ = watchdog.start()

        result1 = watchdog.cancel()
        result2 = watchdog.cancel()
        result3 = watchdog.cancel()

        assert isinstance(result1, Success)
        assert isinstance(result2, Success)
        assert isinstance(result3, Success)

    def test_cancel_after_fired(self) -> None:
        """Cancel after timeout fired returns success (no-op since timeout fired)."""
        called: list[str] = []
        event = threading.Event()

        def on_timeout(agent_id: str) -> None:
            called.append(agent_id)
            event.set()

        watchdog = TimeoutWatchdog(0.05, "ag_test", on_timeout)
        _ = watchdog.start()

        # Wait for timeout to fire
        event.wait(timeout=1.0)

        # After firing, started is False and timer is None
        # Cancel returns Success (no-op)
        result = watchdog.cancel()
        assert isinstance(result, Success)

    # -------------------------------------------------------------------------
    # Edge cases
    # -------------------------------------------------------------------------

    def test_timeout_zero_seconds(self) -> None:
        """Zero timeout fires immediately."""
        called: list[str] = []
        event = threading.Event()

        def on_timeout(agent_id: str) -> None:
            called.append(agent_id)
            event.set()

        watchdog = TimeoutWatchdog(0, "ag_test", on_timeout)
        _ = watchdog.start()

        # Zero timeout should fire very quickly
        assert event.wait(timeout=1.0), "Zero timeout should fire within 1 second"
        assert "ag_test" in called

    def test_callback_exception_does_not_raise(self) -> None:
        """Callback exceptions are caught and do not raise."""

        def bad_callback(agent_id: str) -> None:
            raise RuntimeError("Callback error")

        # Should not raise on creation or start
        watchdog = TimeoutWatchdog(0.05, "ag_test", bad_callback)
        result = watchdog.start()
        assert isinstance(result, Success)

        # Wait for timeout to fire
        time.sleep(0.2)

        # Clean up
        watchdog.cancel()


class TestCreateTimeoutWatchdog:
    """Tests for create_timeout_watchdog factory."""

    def test_create_with_valid_timeout(self) -> None:
        """Create watchdog with valid timeout string."""
        called: list[str] = []
        result = create_timeout_watchdog(
            "10m", "ag_123", lambda aid: called.append(aid)
        )
        assert isinstance(result, Success)
        watchdog = result.unwrap()
        assert watchdog.timeout_seconds == 600

    def test_create_with_none_timeout(self) -> None:
        """Create watchdog with None timeout."""
        result = create_timeout_watchdog(None, "ag_456", lambda _: None)
        assert isinstance(result, Success)
        watchdog = result.unwrap()
        assert watchdog.timeout_seconds is None

    def test_create_with_various_formats(self) -> None:
        """Create watchdog with various timeout formats."""
        # Seconds
        result1 = create_timeout_watchdog("30s", "ag_1", lambda _: None)
        assert isinstance(result1, Success)
        assert result1.unwrap().timeout_seconds == 30

        # Minutes
        result2 = create_timeout_watchdog("5m", "ag_2", lambda _: None)
        assert isinstance(result2, Success)
        assert result2.unwrap().timeout_seconds == 300

        # Hours
        result3 = create_timeout_watchdog("2h", "ag_3", lambda _: None)
        assert isinstance(result3, Success)
        assert result3.unwrap().timeout_seconds == 7200

        # Bare number
        result4 = create_timeout_watchdog("60", "ag_4", lambda _: None)
        assert isinstance(result4, Success)
        assert result4.unwrap().timeout_seconds == 60

    def test_create_with_invalid_timeout(self) -> None:
        """Create watchdog with invalid timeout string returns failure."""
        # "invalid" - parse_timeout raises ValueError, caught by create_timeout_watchdog
        result = create_timeout_watchdog("invalid", "ag_abc", lambda _: None)
        assert isinstance(result, Failure)
        assert "Invalid timeout" in str(result.failure())


class TestAsyncTimeoutWatchdog:
    """Tests for asyncio-based AsyncTimeoutWatchdog."""

    @pytest.mark.asyncio
    async def test_create_async_watchdog(self) -> None:
        """Create async watchdog with timeout."""
        called: list[str] = []
        watchdog = AsyncTimeoutWatchdog(60, "ag_123", lambda aid: called.append(aid))
        assert watchdog.timeout_seconds == 60
        assert not watchdog.started

    @pytest.mark.asyncio
    async def test_start_async_watchdog(self) -> None:
        """Start async watchdog starts timeout task."""
        called: list[str] = []
        watchdog = AsyncTimeoutWatchdog(60, "ag_123", lambda aid: called.append(aid))
        result = await watchdog.start()
        assert isinstance(result, Success)
        assert watchdog.started

        # Clean up
        await watchdog.cancel()

    @pytest.mark.asyncio
    async def test_start_no_timeout_async(self) -> None:
        """Start with no timeout returns success without starting."""
        watchdog = AsyncTimeoutWatchdog(None, "ag_456", lambda _: None)
        result = await watchdog.start()
        assert isinstance(result, Success)
        assert not watchdog.started

    @pytest.mark.asyncio
    async def test_cancel_async_watchdog(self) -> None:
        """Cancel async watchdog stops task."""
        called: list[str] = []
        watchdog = AsyncTimeoutWatchdog(60, "ag_123", lambda aid: called.append(aid))
        _ = await watchdog.start()
        result = await watchdog.cancel()
        assert isinstance(result, Success)
        assert watchdog.cancelled

    @pytest.mark.asyncio
    async def test_timeout_fires_async(self) -> None:
        """Async timeout callback fires after duration."""
        called: list[str] = []

        def on_timeout(agent_id: str) -> None:
            called.append(agent_id)

        # Use very short timeout
        watchdog = AsyncTimeoutWatchdog(0.05, "ag_test", on_timeout)
        _ = await watchdog.start()

        # Wait for timeout
        await asyncio.sleep(0.2)

        assert "ag_test" in called

    @pytest.mark.asyncio
    async def test_cancel_before_timeout_async(self) -> None:
        """Cancel prevents async timeout callback."""
        called: list[str] = []

        # Use longer timeout so we can cancel
        watchdog = AsyncTimeoutWatchdog(5.0, "ag_test", lambda aid: called.append(aid))
        _ = await watchdog.start()
        await watchdog.cancel()

        # Wait briefly
        await asyncio.sleep(0.1)

        # Timeout should not have fired
        assert "ag_test" not in called


class TestCreateAsyncTimeoutWatchdog:
    """Tests for create_async_timeout_watchdog factory."""

    @pytest.mark.asyncio
    async def test_create_with_valid_timeout(self) -> None:
        """Create async watchdog with valid timeout string."""
        called: list[str] = []
        result = await create_async_timeout_watchdog(
            "10m", "ag_123", lambda aid: called.append(aid)
        )
        assert isinstance(result, Success)
        watchdog = result.unwrap()
        assert watchdog.timeout_seconds == 600

    @pytest.mark.asyncio
    async def test_create_with_none_timeout(self) -> None:
        """Create async watchdog with None timeout."""
        result = await create_async_timeout_watchdog(None, "ag_456", lambda _: None)
        assert isinstance(result, Success)
        watchdog = result.unwrap()
        assert watchdog.timeout_seconds is None

    @pytest.mark.asyncio
    async def test_create_with_invalid_timeout(self) -> None:
        """Create async watchdog with invalid timeout string returns failure."""
        # "invalid" - parse_timeout raises ValueError, caught by create_async_timeout_watchdog
        result = await create_async_timeout_watchdog(
            "invalid", "ag_abc", lambda _: None
        )
        assert isinstance(result, Failure)
        assert "Invalid timeout" in str(result.failure())
