"""Unit-level coverage for runner plumbing that does not need a live pi.

pi owns the conversation loop and event surfacing; we still prove our
wrapper assembles the right ``pi`` argv, resolves per-node tool scope,
sums usage, and builds the system prompt — all without spawning pi.
"""

from __future__ import annotations

from pathlib import Path

from attractor.agent import AgentConfig
from attractor.agent.runner import (
    _accumulate_usage,  # pyright: ignore[reportPrivateUsage]
    _build_pi_args,  # pyright: ignore[reportPrivateUsage]
    _build_system_prompt,  # pyright: ignore[reportPrivateUsage]
    _resolve_tool_allowlist,  # pyright: ignore[reportPrivateUsage]
)
from attractor.agent.tool import REPORT_OUTCOME_TOOL_NAME


# [unit->REQ-AGENT-SESSION-EVENTS]
# [unit->REQ-LLM-SURFACE]  -- proves we consume pi's per-turn usage
# payloads rather than calling a provider API directly.
def test_accumulate_usage_sums_pi_keys() -> None:
    """pi's assistant-message usage uses input / output."""
    totals: list[int] = [0, 0]
    _accumulate_usage({"input": 120, "output": 30}, totals)
    _accumulate_usage({"input": 80, "output": 20}, totals)
    assert totals == [200, 50]


# [unit->REQ-AGENT-SESSION-EVENTS]
def test_accumulate_usage_tolerates_anthropic_and_legacy_keys() -> None:
    """input_tokens / prompt_tokens shapes still count (shape drift)."""
    totals: list[int] = [0, 0]
    _accumulate_usage({"input_tokens": 10, "output_tokens": 5}, totals)
    _accumulate_usage({"prompt_tokens": 1, "completion_tokens": 2}, totals)
    assert totals == [11, 7]


# [unit->REQ-AGENT-SESSION-EVENTS]
def test_accumulate_usage_ignores_unknown_shape() -> None:
    """Token accounting is observability — bad data must not crash."""
    totals: list[int] = [0, 0]
    _accumulate_usage({}, totals)
    _accumulate_usage(None, totals)
    _accumulate_usage({"input": "not-a-number"}, totals)
    assert totals == [0, 0]


# [unit->REQ-AGENT-SESSION-API]
def test_system_prompt_returns_none_when_no_inputs() -> None:
    """Falling back to None lets pi use its default coding prompt."""
    assert _build_system_prompt(None, None) is None
    assert _build_system_prompt("", None) is None


# [unit->REQ-AGENT-SESSION-API]
def test_system_prompt_includes_goal() -> None:
    prompt = _build_system_prompt("Ship v0.1 by Friday.", None)
    assert prompt is not None
    assert "Workflow goal" in prompt
    assert "Ship v0.1 by Friday." in prompt


# [unit->REQ-EXEC-REVISIT-CONTEXT]
def test_system_prompt_includes_revisit_context_when_present() -> None:
    prompt = _build_system_prompt(
        "ship",
        {"visit_n": 2, "prior_outcome": "FAILURE", "captured_output": "x"},
    )
    assert prompt is not None
    assert "Revisit context" in prompt
    assert "<context>" in prompt
    assert "FAILURE" in prompt


def _cfg(**kwargs: object) -> AgentConfig:
    base: dict[str, object] = {
        "model": "anthropic/claude-haiku-4-5",
        "workflow_goal": None,
        "node_prompt": "p",
        "cwd": Path("."),
    }
    base.update(kwargs)
    return AgentConfig(**base)  # type: ignore[arg-type]


# [unit->REQ-AGENT-NODE-SCOPE]
def test_allowlist_is_none_when_no_scope_set() -> None:
    """No per-node restriction and no extras → don't pass --tools."""
    assert _resolve_tool_allowlist(_cfg()) is None


# [unit->REQ-AGENT-NODE-SCOPE]
def test_node_allowed_tools_becomes_restriction_plus_report_outcome() -> None:
    """node_allowed_tools is a strict allowlist (extra_allowed_tools ignored)."""
    allow = _resolve_tool_allowlist(
        _cfg(extra_allowed_tools=("grep",), node_allowed_tools=("read", "edit"))
    )
    assert allow == ("read", "edit", REPORT_OUTCOME_TOOL_NAME)


# [unit->REQ-AGENT-NODE-SCOPE]
def test_empty_node_allowed_tools_clears_to_report_outcome_only() -> None:
    """An explicit empty tuple restricts to just report_outcome."""
    allow = _resolve_tool_allowlist(_cfg(node_allowed_tools=()))
    assert allow == (REPORT_OUTCOME_TOOL_NAME,)


# [unit->REQ-AGENT-NODE-SCOPE]
def test_extra_allowed_tools_are_additive_to_pi_defaults() -> None:
    """Host-level extras add to pi's defaults rather than replacing them."""
    allow = _resolve_tool_allowlist(_cfg(extra_allowed_tools=("grep",)))
    assert allow is not None
    assert "read" in allow and "bash" in allow  # pi defaults preserved
    assert "grep" in allow
    assert allow[-1] == REPORT_OUTCOME_TOOL_NAME


# [unit->REQ-LLM-SURFACE]
# [unit->REQ-LLM-MODEL-STRINGS]
def test_build_pi_args_core_flags_and_model() -> None:
    args = _build_pi_args(_cfg(model="openai/gpt-5.2"))
    assert args[:6] == [
        "--mode",
        "rpc",
        "--no-session",
        "--offline",
        "--no-extensions",
        "--extension",
    ]
    # report_outcome extension path is the 7th arg.
    assert args[6].endswith("report_outcome.ts")
    # provider/model passed verbatim to --model.
    assert "--model" in args
    assert args[args.index("--model") + 1] == "openai/gpt-5.2"


# [unit->REQ-LLM-MODEL-STRINGS]
def test_build_pi_args_omits_model_when_empty() -> None:
    """Empty model → no --model flag; pi uses its configured default."""
    args = _build_pi_args(_cfg(model=""))
    assert "--model" not in args


# [unit->REQ-AGENT-NODE-SCOPE]
def test_build_pi_args_emits_tools_for_node_restriction() -> None:
    args = _build_pi_args(_cfg(node_allowed_tools=("read", "bash")))
    assert "--tools" in args
    tools_value = args[args.index("--tools") + 1]
    assert tools_value == f"read,bash,{REPORT_OUTCOME_TOOL_NAME}"


# [unit->REQ-AGENT-SESSION-API]
def test_build_pi_args_appends_system_prompt_when_goal_present() -> None:
    args = _build_pi_args(_cfg(workflow_goal="Do the thing."))
    assert "--append-system-prompt" in args
    sp = args[args.index("--append-system-prompt") + 1]
    assert "Do the thing." in sp


# [unit->REQ-AGENT-SESSION-STEERING]
def test_single_shot_prompt_is_the_steering_seam() -> None:
    """v0.1 sends one ``prompt``; pi's ``steer``/``follow_up`` RPC commands
    are the additive seam for surfacing steering later without changing
    the AgentConfig/AgentOutcome contract. The node prompt is therefore
    sent as a command, not pre-baked into the pi argv.
    """
    cfg = _cfg(node_prompt="do the thing now")
    args = _build_pi_args(cfg)
    assert "do the thing now" not in args
    assert cfg.node_prompt == "do the thing now"
