"""Contract tests for the public plugin subagent lifecycle API."""

import time
from types import SimpleNamespace
from unittest.mock import Mock

import pytest

from agent.subagent_lifecycle import (
    SubagentLaunchRequest,
    SubagentLifecycleError,
    SubagentLifecycleService,
    SubagentState,
    bind_subagent_parent,
    get_active_subagent_parent,
)


class FakeChild:
    def __init__(self, ident="sa-test"):
        self._subagent_id = ident
        self._delegate_role = "leaf"
        self._delegate_depth = 1
        self.provider = "test"
        self.model = "test-model"
        self.interrupted = False

    def interrupt(self, _reason):
        self.interrupted = True


@pytest.fixture
def lifecycle(monkeypatch):
    parent = SimpleNamespace(session_id="parent-1", enabled_toolsets=["file"])
    counter = iter(range(1000))

    def build(**_kwargs):
        return FakeChild(f"sa-{next(counter)}")

    def run(_index, _goal, child, _parent):
        for _ in range(20):
            if child.interrupted:
                return {
                    "status": "interrupted",
                    "summary": None,
                    "api_calls": 0,
                    "duration_seconds": 0,
                }
            time.sleep(0.002)
        return {
            "status": "completed",
            "summary": "safe summary",
            "api_calls": 1,
            "duration_seconds": 0.01,
        }

    monkeypatch.setattr("tools.delegate_tool._build_child_agent", build)
    monkeypatch.setattr("tools.delegate_tool._run_single_child", run)
    return SubagentLifecycleService(lambda: parent)


def test_launch_wait_result_and_handle_round_trip(lifecycle):
    handle = lifecycle.launch(
        SubagentLaunchRequest(goal="x", allowed_toolsets=("file",))
    )
    assert handle.from_dict(handle.to_dict()) == handle
    assert lifecycle.wait(handle, timeout_seconds=1).state is SubagentState.SUCCEEDED
    first = lifecycle.result(handle)
    assert first.ready and first.summary == "safe summary" and first.result_hash
    assert lifecycle.result(handle) == first


def test_duplicate_correlation_and_permission_validation(lifecycle):
    handle = lifecycle.launch(SubagentLaunchRequest(goal="x", correlation_id="same"))
    with pytest.raises(SubagentLifecycleError, match="Duplicate"):
        lifecycle.launch(SubagentLaunchRequest(goal="x", correlation_id="same"))
    with pytest.raises(SubagentLifecycleError, match="broaden"):
        lifecycle.launch(
            SubagentLaunchRequest(goal="x", allowed_toolsets=("terminal",))
        )
    with pytest.raises(SubagentLifecycleError, match="working_directory"):
        lifecycle.launch(SubagentLaunchRequest(goal="x", working_directory="C:/"))
    lifecycle.wait(handle, timeout_seconds=1)


def test_cancel_is_cooperative_and_forged_handle_is_unknown(lifecycle):
    handle = lifecycle.launch(SubagentLaunchRequest(goal="x"))
    assert lifecycle.cancel(handle, reason="test").accepted
    terminal = lifecycle.wait(handle, timeout_seconds=1)
    assert terminal.state is SubagentState.CANCELLED
    forged = handle.__class__(**{**handle.to_dict(), "capability": "forged"})
    assert lifecycle.status(forged).state is SubagentState.UNKNOWN
    assert lifecycle.result(forged).error_classification == "UNKNOWN_HANDLE"
    other_parent = SimpleNamespace(session_id="different-parent")
    other_service = SubagentLifecycleService(lambda: other_parent)
    assert other_service.status(handle).state is SubagentState.UNKNOWN


def test_simultaneous_launches_are_distinct_and_reconnect_is_in_process(lifecycle):
    handles = [lifecycle.launch(SubagentLaunchRequest(goal="x")) for _ in range(10)]
    assert len({h.subagent_id for h in handles}) == 10
    assert lifecycle.reconnect(handles[0]).connected
    for handle in handles:
        lifecycle.wait(handle, timeout_seconds=1)


@pytest.mark.parametrize(
    ("field", "value"),
    [
        ("capability", []),
        ("contract_version", True),
        ("subagent_id", None),
        ("parent_session_id", []),
        ("correlation_id", []),
        ("created_at", "yesterday"),
        ("provider", []),
        ("model", []),
        ("role", []),
        ("depth", "one"),
    ],
)
def test_malformed_deserialized_handle_is_unknown(lifecycle, field, value):
    handle = lifecycle.launch(SubagentLaunchRequest(goal="x"))
    malformed = handle.from_dict({**handle.to_dict(), field: value})

    assert lifecycle.status(malformed).state is SubagentState.UNKNOWN
    assert lifecycle.result(malformed).error_classification == "UNKNOWN_HANDLE"
    lifecycle.wait(handle, timeout_seconds=1)


def test_launch_preserves_parent_tool_resolution(monkeypatch):
    import model_tools

    parent = SimpleNamespace(session_id="parent-tools", enabled_toolsets=["file"])
    model_tools._last_resolved_tool_names = ["parent_tool"]

    def build(**_kwargs):
        model_tools._last_resolved_tool_names = ["child_tool"]
        return FakeChild("sa-tools")

    monkeypatch.setattr("tools.delegate_tool._build_child_agent", build)
    monkeypatch.setattr(
        "tools.delegate_tool._run_single_child",
        lambda *_args, **_kwargs: {
            "status": "completed",
            "summary": "done",
            "api_calls": 0,
            "duration_seconds": 0,
        },
    )

    service = SubagentLifecycleService(lambda: parent)
    handle = service.launch(SubagentLaunchRequest(goal="x"))

    assert model_tools._last_resolved_tool_names == ["parent_tool"]
    assert handle.subagent_id == "sa-tools"
    service.wait(handle, timeout_seconds=1)


def test_public_lifecycle_runs_host_aggregation(monkeypatch):
    memory = Mock()
    parent = SimpleNamespace(
        session_id="parent-aggregate",
        enabled_toolsets=["file"],
        _memory_manager=memory,
        _current_turn_id="turn-1",
        session_estimated_cost_usd=1.0,
        session_cost_source="none",
        session_cost_status="unknown",
    )
    child = FakeChild("sa-aggregate")
    child.session_id = "child-session"
    hook = Mock()

    monkeypatch.setattr("tools.delegate_tool._build_child_agent", lambda **_kwargs: child)
    monkeypatch.setattr(
        "tools.delegate_tool._run_single_child",
        lambda *_args, **_kwargs: {
            "task_index": 0,
            "status": "completed",
            "summary": "aggregated",
            "api_calls": 1,
            "duration_seconds": 0.25,
            "_child_role": "leaf",
            "_child_cost_usd": 2.5,
        },
    )
    monkeypatch.setattr("hermes_cli.plugins.invoke_hook", hook)

    service = SubagentLifecycleService(lambda: parent)
    handle = service.launch(SubagentLaunchRequest(goal="aggregate me"))
    assert service.wait(handle, timeout_seconds=1).state is SubagentState.SUCCEEDED

    memory.on_delegation.assert_called_once_with(
        task="aggregate me", result="aggregated", child_session_id="child-session"
    )
    hook.assert_called_once_with(
        "subagent_stop",
        parent_session_id="parent-aggregate",
        parent_turn_id="turn-1",
        child_session_id="child-session",
        child_role="leaf",
        child_summary="aggregated",
        child_status="completed",
        # Redacted tool history rides the shared finalization pipeline
        # (#62011/#72403); empty here because the fabricated result carries
        # no tool_trace.
        tool_call_history=[],
        duration_ms=250,
    )
    assert parent.session_estimated_cost_usd == 3.5
    assert parent.session_cost_source == "subagent"
    assert parent.session_cost_status == "estimated"


def test_plugin_context_uses_turn_scoped_parent(monkeypatch):
    from hermes_cli.plugins import PluginContext, PluginManifest

    parent = SimpleNamespace(session_id="gateway-parent", enabled_toolsets=["file"])
    monkeypatch.setattr(
        "tools.delegate_tool._build_child_agent", lambda **_kwargs: FakeChild("sa-gateway")
    )
    monkeypatch.setattr(
        "tools.delegate_tool._run_single_child",
        lambda *_args, **_kwargs: {
            "status": "completed",
            "summary": "done",
            "api_calls": 0,
            "duration_seconds": 0,
        },
    )
    manager = SimpleNamespace(_cli_ref=None)
    ctx = PluginContext(PluginManifest(name="test", source="test"), manager)

    with bind_subagent_parent(parent):
        handle = ctx.subagent_lifecycle.launch(SubagentLaunchRequest(goal="x"))
        ctx.subagent_lifecycle.wait(handle, timeout_seconds=1)

    assert handle.parent_session_id == "gateway-parent"


def test_agent_turn_binds_and_clears_lifecycle_parent(monkeypatch):
    from run_agent import AIAgent

    agent = AIAgent.__new__(AIAgent)
    observed = []

    def run_conversation(parent, *_args, **_kwargs):
        observed.append(get_active_subagent_parent())
        return {"final_response": "ok"}

    monkeypatch.setattr("agent.conversation_loop.run_conversation", run_conversation)

    assert agent.run_conversation("hello") == {"final_response": "ok"}
    assert observed == [agent]
    assert get_active_subagent_parent() is None
