"""Diagnostics for Codex Responses transport failures."""

from __future__ import annotations

import json
import logging
import re
from pathlib import Path
from types import SimpleNamespace

import httpx
import pytest
from openai import APIConnectionError

from agent.codex_runtime import _codex_request_failure_details, run_codex_stream


def test_transport_failure_without_attached_request_reports_unknown_size():
    error = httpx.RemoteProtocolError("connection closed")

    request_body_bytes, exception_chain = _codex_request_failure_details(error)

    assert request_body_bytes is None
    assert exception_chain == "RemoteProtocolError"


def test_transport_failure_logs_exact_request_bytes_and_class_chain(caplog):
    request_content = b'{"input":"payload"}'
    request = httpx.Request(
        "POST",
        "https://example.invalid/responses",
        content=request_content,
    )
    transport_error = httpx.RemoteProtocolError(
        "server disconnected without sending a response",
        request=request,
    )
    connection_error = APIConnectionError(request=request)
    connection_error.__cause__ = transport_error

    class FailingResponses:
        def create(self, **_kwargs):
            raise connection_error

    client = SimpleNamespace(responses=FailingResponses())
    agent = SimpleNamespace(
        _interrupt_requested=False,
        _current_api_request_id="request-id",
        _fallback_index=0,
        is_subagent=False,
        model="gpt-5.6-sol",
        provider="openai-codex",
        session_id="",
        _client_log_context=lambda: "",
        _buffer_diagnostic_status=lambda message: None,
    )

    with caplog.at_level(logging.WARNING, logger="agent.codex_runtime"):
        with pytest.raises(APIConnectionError):
            run_codex_stream(agent, {"model": "gpt-5.6-sol"}, client=client)

    message = caplog.messages[-1]
    assert f"serialized_request_body_bytes={len(request_content)}" in message
    assert "stream_opened=false" in message
    assert "exception_chain=APIConnectionError <- RemoteProtocolError" in message
    assert "attempt=2/2" in message
    assert "payload" not in message
    assert request_content.decode() not in message
    assert "example.invalid" not in message


def _zero_event_then_completed_client(seen_inputs: list):
    """``responses.create`` that dies before any stream event on the first call and completes on
    the second; records the ``input`` list each physical attempt was given."""
    from tests.agent.test_run_agent_codex_responses import _FakeCreateStream

    events = [
        SimpleNamespace(type="response.output_item.done", item=SimpleNamespace(
            type="message", status="completed", content=[SimpleNamespace(type="output_text", text="ok")])),
        SimpleNamespace(type="response.completed", response=SimpleNamespace(
            status="completed", usage=SimpleNamespace(input_tokens=1, output_tokens=1, total_tokens=2), id="r1")),
    ]

    def _create(**kwargs):
        seen_inputs.append(kwargs.get("input") or (kwargs.get("extra_body") or {}).get("input"))
        if len(seen_inputs) == 1:
            raise httpx.ConnectError("no first byte")
        return _FakeCreateStream(events)

    return SimpleNamespace(responses=SimpleNamespace(create=_create))


def _oversized_codex_kwargs(size: int) -> dict:
    return {"model": "gpt-5-codex", "instructions": "You are Hermes.", "store": False, "tools": None,
            "input": [{"role": "user", "content": "look"},
                      {"type": "function_call", "call_id": "call_browser", "name": "browser_exec", "arguments": "{}"},
                      {"type": "function_call_output", "call_id": "call_browser", "output": "x" * size},
                      {"role": "user", "content": "continue"}]}


def test_zero_event_retry_prunes_oversized_tool_output_and_logs_size_delta(monkeypatch, tmp_path, caplog):
    """#95429 criterion 3: a reconnect after a zero-event attempt must not resend the same oversized
    payload unchanged -- the inline tool output is spilled and the size delta is logged."""
    from tests.agent.test_run_agent_codex_responses import _build_agent
    from tools.tool_result_storage import PERSISTED_OUTPUT_TAG

    monkeypatch.setenv("HERMES_HOME", str(tmp_path))
    monkeypatch.setattr("hermes_constants.get_hermes_home", lambda: tmp_path, raising=False)
    agent = _build_agent(monkeypatch)
    seen: list = []
    agent.client = _zero_event_then_completed_client(seen)

    with caplog.at_level(logging.INFO, logger="agent.codex_runtime"):
        response = agent._run_codex_stream(_oversized_codex_kwargs(760_396))

    assert response.status == "completed" and len(seen) == 2
    first, second = (len(json.dumps(i)) for i in seen)
    assert second < first // 10
    retried_output = seen[1][2]["output"]
    assert PERSISTED_OUTPUT_TAG in retried_output and len(retried_output) < 10_000
    assert Path(tmp_path, "cache", "spillover", "call_browser.txt").read_text() == "x" * 760_396
    assert seen[0][2]["output"] == "x" * 760_396  # the caller's kwargs are not mutated
    prune_logs = [r.message for r in caplog.records if "zero-event" in r.message]
    assert len(prune_logs) == 1 and "attempt 1/2" in prune_logs[0]
    assert re.search(r"serialized_input_bytes=\d+ -> \d+", prune_logs[0])


def test_zero_event_retry_without_prunable_output_logs_unchanged_resend(monkeypatch, caplog):
    from tests.agent.test_run_agent_codex_responses import _build_agent

    agent = _build_agent(monkeypatch)
    seen: list = []
    agent.client = _zero_event_then_completed_client(seen)
    kwargs = _oversized_codex_kwargs(300_000)
    kwargs["input"][2]["output"] = ["not-a-string"]  # nothing prunable
    kwargs["input"][0]["content"] = "u" * 300_000  # still a large payload

    with caplog.at_level(logging.INFO, logger="agent.codex_runtime"):
        response = agent._run_codex_stream(kwargs)

    assert response.status == "completed" and seen[0] == seen[1]
    (log,) = [r.message for r in caplog.records if "zero-event" in r.message]
    assert "unchanged" in log and "attempt 1/2" in log and re.search(r"serialized_input_bytes=\d+", log)
