"""Per-request OpenAI wire client reuse across sequential LLM calls.

Building a fresh ``openai.OpenAI`` client per LLM call costs ~19-35ms (new
httpx pool, TCP+TLS handshake), so ``_create_request_openai_client`` caches
ONE reusable wire client on the agent, keyed by the effective client kwargs:

- identical kwargs → same client object handed back (the reuse win);
- kwargs change (credential rotation, provider failover) → evict + rebuild;
- cross-thread abort (#29507) poisons the slot → the owner-thread close does
  a real close and the next create rebuilds;
- non-reuse close reasons (error cleanups, stale/interrupt kills, retry
  cleanups) discard — only a request that produced a response reports a
  reuse reason (request_complete / stream_request_complete);
- vision-header copilot variant is a distinct kwargs key;
- teardown (release_clients / close) really closes the cached client, or
  detaches it to the in-flight worker's own close when checked out (#29507);
- MoA facade and Mock passthroughs never enter the cache.
"""

from unittest.mock import MagicMock, patch

from run_agent import AIAgent


class _StubClient:
    """Minimal non-Mock client: _is_openai_client_closed reads ``is_closed``."""

    def __init__(self):
        self.is_closed = False

    def close(self):
        self.is_closed = True


def _make_agent(provider="openai", base_url="https://api.openai.com/v1", model="gpt-5.4"):
    with patch("run_agent.OpenAI") as mock_openai:
        mock_openai.return_value = MagicMock()
        agent = AIAgent(
            api_key="sk-test",
            base_url=base_url,
            provider=provider,
            model=model,
            quiet_mode=True,
            skip_context_files=True,
            skip_memory=True,
        )
    # Non-Mock shared client so the Mock passthrough branch doesn't trigger.
    agent.client = _StubClient()
    return agent


class _Harness:
    """Patch the wire-client build/close/socket seams and record calls."""

    def __init__(self, agent):
        self.agent = agent
        self.built = []   # (kwargs_copy, reason)
        self.closed = []  # (client, reason)
        self._patchers = []

    def __enter__(self):
        def _fake_create(kwargs, *, reason, shared):
            # Only record per-request wire clients; teardown tests can also
            # trigger a shared-client rebuild via _ensure_primary_openai_client.
            if not shared:
                self.built.append((dict(kwargs), reason))
            return _StubClient()

        def _fake_close(client, *, reason, shared):
            self.closed.append((client, reason))

        self._patchers = [
            patch.object(self.agent, "_create_openai_client", side_effect=_fake_create),
            patch.object(self.agent, "_close_openai_client", side_effect=_fake_close),
            patch.object(self.agent, "_force_close_tcp_sockets", return_value=0),
        ]
        for p in self._patchers:
            p.start()
        return self

    def __exit__(self, *exc):
        for p in self._patchers:
            p.stop()

    def closed_clients(self):
        return [client for client, _reason in self.closed]


def test_reuse_on_identical_kwargs_same_object():
    agent = _make_agent()
    with _Harness(agent) as h:
        a = agent._create_request_openai_client(reason="chat_completion_request")
        # The agent's outer loop owns retries — the SDK loop must stay off.
        assert h.built[-1][0]["max_retries"] == 0
        agent._close_request_openai_client(a, reason="request_complete")
        assert h.closed == []  # kept for reuse, not really closed

        b = agent._create_request_openai_client(reason="chat_completion_request")
        assert b is a
        assert len(h.built) == 1




def test_rebuild_on_client_kwargs_change():
    agent = _make_agent()
    with _Harness(agent) as h:
        a = agent._create_request_openai_client(reason="r")
        agent._close_request_openai_client(a, reason="request_complete")

        # Credential rotation / provider failover mutate _client_kwargs.
        agent._client_kwargs["api_key"] = "sk-rotated"
        b = agent._create_request_openai_client(reason="r")
        assert b is not a
        # The stale cached client was really closed on eviction.
        assert a in h.closed_clients()
        assert h.built[-1][0]["api_key"] == "sk-rotated"

        # And the rotated client is itself reusable.
        agent._close_request_openai_client(b, reason="request_complete")
        c = agent._create_request_openai_client(reason="r")
        assert c is b












def test_agent_close_closes_cached_request_client():
    agent = _make_agent()
    with _Harness(agent) as h:
        a = agent._create_request_openai_client(reason="r")
        agent._close_request_openai_client(a, reason="request_complete")

        agent.close()
        assert (a, "agent_close") in h.closed

        # Idempotent: a second teardown must not double-close.
        before = len(h.closed)
        agent._close_cached_request_openai_client(reason="agent_close")
        assert len(h.closed) == before






def test_moa_passthrough_unaffected():
    agent = _make_agent()
    agent.provider = "moa"
    facade = agent.client
    with _Harness(agent) as h:
        a = agent._create_request_openai_client(reason="r")
        assert a is facade
        assert h.built == []  # facade handed back, no wire client built

        # Close behaves exactly as before the cache existed.
        agent._close_request_openai_client(facade, reason="request_complete")
        assert facade in h.closed_clients()


