"""``delegation.child_timeout_seconds`` bounds INACTIVITY, not total runtime (#116001).

A configured cap used to be a dispatch-to-death stopwatch: every child that ran longer than the budget was
abandoned even while the provider was actively serving it. Measured over 219 tasks / 75 deaths, 0 of 75 deaths
happened mid-tool — all of them were waiting on an in-flight LLM completion, so the cap converted transient
provider slowness into lost work (a nearly-finished context discarded every time).

These tests pin both directions of the fix:
- a child whose activity signals keep advancing outlives a cap far shorter than its runtime;
- a child with no progress at all is still abandoned when the cap elapses (the knob keeps its teeth).
"""

from __future__ import annotations

import threading
import time
from types import SimpleNamespace

from tools import delegate_tool

_CAP_SECONDS = 0.4

class _SlowButLiveChild:
    """A provider serving multi-minute completions: the runtime outlasts the cap, but progress never stops.

    ``advance=False`` models the opposite case — a genuinely frozen child (no completed call, no tool change,
    no activity-clock tick).
    """

    def __init__(self, *, total_seconds: float = 1.2, advance: bool = True, initial_calls: int = 0) -> None:
        self.tool_progress_callback = None
        self._credential_pool = None
        self._delegate_saved_tool_names = []
        self._delegate_role = "leaf"
        self._delegate_depth = 1
        self._subagent_id = None
        self.session_id = "live-child"
        self.model = "test/model"
        self._total_seconds = total_seconds
        self._advance = advance
        self._calls = initial_calls
        self._activity_ts = time.time()
        self.interrupted = threading.Event()
        self.steers: list[tuple[float, str]] = []  # (monotonic time, text) of every budget warning received

    def steer(self, text: str) -> bool:
        self.steers.append((time.monotonic(), text))
        return True

    def run_conversation(self, **_kwargs):
        deadline = time.monotonic() + self._total_seconds
        while time.monotonic() < deadline:
            time.sleep(0.05)
            if self._advance:
                self._calls += 1
                self._activity_ts = time.time()
        return {
            "final_response": "FINISHED AFTER OUTLIVING THE CAP",
            "completed": True,
            "api_calls": self._calls,
            "messages": [],
        }

    def get_activity_summary(self):
        return {
            "api_call_count": self._calls,
            "current_tool": None,
            "last_activity_ts": self._activity_ts,
            "max_iterations": 50,
        }

    def hard_interrupt(self, *_args, **_kwargs):
        self.interrupted.set()

    def close(self):
        pass

def _run(child, monkeypatch, cap=_CAP_SECONDS):
    parent = SimpleNamespace(
        session_id="parent", _current_task_id=None, _active_children=[child],
        _active_children_lock=threading.Lock(), _touch_activity=lambda _d: None, _interrupt_requested=False,
    )
    monkeypatch.setattr(delegate_tool, "_get_child_timeout", lambda: cap)
    monkeypatch.setattr(delegate_tool, "_get_worktree_isolation", lambda: False)
    return delegate_tool._run_single_child(0, "watch the slow provider", child=child, parent_agent=parent)

def test_progressing_child_outlives_a_cap_shorter_than_its_runtime(monkeypatch):
    child = _SlowButLiveChild(total_seconds=1.2, advance=True)

    entry = _run(child, monkeypatch)

    assert entry["status"] == "completed", entry
    assert entry["summary"] == "FINISHED AFTER OUTLIVING THE CAP", entry
    # The cap really was armed and the child really did run past it: the budget reset on progress.
    assert entry["duration_seconds"] > _CAP_SECONDS, entry
    assert not child.interrupted.is_set()
    # Progress kept resetting the window, so the 80% budget warning never had cause to fire.
    assert child.steers == [], child.steers

def test_frozen_child_is_still_abandoned_when_the_cap_elapses(monkeypatch):
    """The reported death shape: calls completed earlier, then the child stops moving entirely."""
    child = _SlowButLiveChild(total_seconds=1.2, advance=False, initial_calls=49)

    entry = _run(child, monkeypatch)

    assert entry["status"] == "timeout", entry
    assert "no progress in that window" in entry["error"], entry["error"]
    assert entry["timeout_seconds"] == _CAP_SECONDS
    assert entry["last_event_age"] is not None and entry["last_event_age"] > 0.3, entry
    assert child.interrupted.is_set()
