"""Regression tests for gateway preservation of provider-derived request_overrides.

Named custom providers can return request_overrides (for example
``extra_body.text.verbosity`` for OpenAI Responses). The gateway must preserve
those overrides on the runtime path and merge fast-mode overrides on top rather
than replacing them with an empty dict.
"""

from __future__ import annotations

import asyncio
import sys
import threading
import types
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

import gateway.run as gateway_run
from gateway.config import Platform
from gateway.session import SessionSource


class _CapturingAgent:
    last_init = None

    def __init__(self, *args, **kwargs):
        type(self).last_init = dict(kwargs)
        self.tools = []
        self.request_overrides = dict(kwargs.get("request_overrides") or {})

    def run_conversation(self, user_message: str, conversation_history=None, task_id=None):
        return {
            "final_response": "ok",
            "messages": [],
            "api_calls": 1,
        }


def _install_fake_agent(monkeypatch):
    fake_run_agent = types.ModuleType("run_agent")
    fake_run_agent.AIAgent = _CapturingAgent
    monkeypatch.setitem(sys.modules, "run_agent", fake_run_agent)


def _make_runner():
    runner = object.__new__(gateway_run.GatewayRunner)
    runner.adapters = {}
    runner.session_store = None
    runner.config = None
    runner._voice_mode = {}
    runner._ephemeral_system_prompt = ""
    runner._prefill_messages = []
    runner._reasoning_config = None
    runner._show_reasoning = False
    runner._provider_routing = {}
    runner._fallback_model = None
    runner._service_tier = None
    runner._running_agents = {}
    runner._running_agents_ts = {}
    runner._background_tasks = set()
    runner._session_db = None
    runner._session_model_overrides = {}
    runner._session_reasoning_overrides = {}
    runner._pending_model_notes = {}
    runner._pending_approvals = {}
    runner._agent_cache = {}
    runner._agent_cache_lock = threading.Lock()
    runner._get_or_create_gateway_honcho = lambda session_key: (None, None)
    runner.hooks = MagicMock()
    runner.hooks.emit = AsyncMock()
    runner.hooks.loaded_hooks = []
    return runner


def _make_source() -> SessionSource:
    return SessionSource(
        platform=Platform.FEISHU,
        chat_id="ou_test",
        chat_type="dm",
        user_id="user-1",
        user_name="tester",
    )


def test_resolve_runtime_agent_kwargs_preserves_request_overrides(monkeypatch):
    monkeypatch.setattr(
        "hermes_cli.runtime_provider.resolve_runtime_provider",
        lambda **_kw: {
            "api_key": "***",
            "base_url": "https://example.test/v1",
            "provider": "custom",
            "api_mode": "codex_responses",
            "command": None,
            "args": [],
            "credential_pool": None,
            "request_overrides": {
                "extra_body": {"text": {"verbosity": "low"}},
            },
        },
    )

    result = gateway_run._resolve_runtime_agent_kwargs()

    assert result["request_overrides"] == {
        "extra_body": {"text": {"verbosity": "low"}},
    }


def test_turn_route_preserves_provider_request_overrides_without_fast_mode():
    runner = _make_runner()
    runner._service_tier = None
    runtime_kwargs = {
        "api_key": "***",
        "base_url": "https://example.test/v1",
        "provider": "custom",
        "api_mode": "codex_responses",
        "command": None,
        "args": [],
        "credential_pool": None,
        "request_overrides": {
            "extra_body": {"text": {"verbosity": "low"}},
        },
    }

    route = gateway_run.GatewayRunner._resolve_turn_agent_config(
        runner,
        "hi",
        "gpt-5.4",
        runtime_kwargs,
    )

    assert route["request_overrides"] == {
        "extra_body": {"text": {"verbosity": "low"}},
    }


def test_turn_route_merges_fast_mode_with_provider_request_overrides():
    runner = _make_runner()
    runner._service_tier = "priority"
    runtime_kwargs = {
        "api_key": "***",
        "base_url": "https://example.test/v1",
        "provider": "custom",
        "api_mode": "codex_responses",
        "command": None,
        "args": [],
        "credential_pool": None,
        "request_overrides": {
            "extra_body": {"text": {"verbosity": "low"}},
        },
    }

    with patch(
        "hermes_cli.models.resolve_fast_mode_overrides",
        return_value={"service_tier": "priority"},
    ):
        route = gateway_run.GatewayRunner._resolve_turn_agent_config(
            runner,
            "hi",
            "gpt-5.4",
            runtime_kwargs,
        )

    assert route["request_overrides"] == {
        "extra_body": {"text": {"verbosity": "low"}},
        "service_tier": "priority",
    }


@pytest.mark.asyncio
async def test_run_agent_preserves_provider_request_overrides_on_gateway_path(monkeypatch):
    monkeypatch.setattr(gateway_run, "_load_gateway_config", lambda: {})
    monkeypatch.setattr(gateway_run, "_resolve_gateway_model", lambda config=None: "gpt-5.4")
    monkeypatch.setattr(
        gateway_run,
        "_resolve_runtime_agent_kwargs",
        lambda: {
            "provider": "custom",
            "api_mode": "codex_responses",
            "base_url": "https://example.test/v1",
            "api_key": "***",
            "request_overrides": {
                "extra_body": {"text": {"verbosity": "low"}},
            },
        },
    )
    _install_fake_agent(monkeypatch)

    import hermes_cli.tools_config as tools_config

    monkeypatch.setattr(tools_config, "_get_platform_tools", lambda user_config, platform_key: {"core"})

    runner = _make_runner()
    source = _make_source()
    session_key = "agent:main:feishu:dm:ou_test"

    runner.session_store = SimpleNamespace(
        get_or_create_session=lambda _source: SimpleNamespace(session_id="session-1"),
        load_transcript=lambda _session_id: [],
    )

    _CapturingAgent.last_init = None
    result = await runner._run_agent(
        message="hi",
        context_prompt="",
        history=[],
        source=source,
        session_id="session-1",
        session_key=session_key,
    )

    assert result["final_response"] == "ok"
    assert _CapturingAgent.last_init is not None
    assert _CapturingAgent.last_init["request_overrides"] == {
        "extra_body": {"text": {"verbosity": "low"}},
    }

@pytest.mark.asyncio
async def test_reused_agent_turn_merges_request_overrides_not_overwrite(monkeypatch):
    """Merge-not-overwrite regression (salvaged from PR #52432).

    A cached/reused gateway agent must keep its init-time request_overrides
    (custom-provider extra_body) across turns: a /fast turn layers
    service_tier ON TOP, and the following normal turn drops only the stale
    fast-mode key while the provider extra_body survives.
    """
    monkeypatch.setattr(gateway_run, "_load_gateway_config", lambda: {})
    monkeypatch.setattr(gateway_run, "_resolve_gateway_model", lambda config=None: "gpt-5.4")
    monkeypatch.setattr(
        gateway_run,
        "_resolve_runtime_agent_kwargs",
        lambda: {
            "provider": "custom",
            "api_mode": "codex_responses",
            "base_url": "https://example.test/v1",
            "api_key": "***",
            "request_overrides": {
                "extra_body": {"text": {"verbosity": "low"}},
            },
        },
    )
    _install_fake_agent(monkeypatch)

    import hermes_cli.tools_config as tools_config

    monkeypatch.setattr(tools_config, "_get_platform_tools", lambda user_config, platform_key: {"core"})

    runner = _make_runner()
    source = _make_source()
    session_key = "agent:main:feishu:dm:ou_test"

    runner.session_store = SimpleNamespace(
        get_or_create_session=lambda _source: SimpleNamespace(session_id="session-1"),
        load_transcript=lambda _session_id: [],
    )

    seen_agents = []
    orig_init = _CapturingAgent.__init__

    def _tracking_init(self, *args, **kwargs):
        orig_init(self, *args, **kwargs)
        seen_agents.append(self)

    monkeypatch.setattr(_CapturingAgent, "__init__", _tracking_init)

    async def run_turn():
        return await runner._run_agent(
            message="hi",
            context_prompt="",
            history=[],
            source=source,
            session_id="session-1",
            session_key=session_key,
        )

    # Turn 1: /fast active — provider extra_body AND service_tier both present.
    # The turn path re-resolves the tier per session, so stub the resolver.
    tier_box = {"tier": "priority"}
    runner._resolve_session_service_tier = lambda *a, **k: tier_box["tier"]
    with patch(
        "hermes_cli.models.resolve_fast_mode_overrides",
        return_value={"service_tier": "priority"},
    ):
        result = await run_turn()
    assert result["final_response"] == "ok"
    assert len(seen_agents) == 1
    agent = seen_agents[0]
    assert agent.request_overrides == {
        "extra_body": {"text": {"verbosity": "low"}},
        "service_tier": "priority",
    }

    # Turn 2: back to normal — the SAME cached agent must drop only the stale
    # fast-mode key; the init-time provider extra_body survives the refresh.
    tier_box["tier"] = None
    result = await run_turn()
    assert result["final_response"] == "ok"
    assert len(seen_agents) == 1, "agent should be reused from the gateway cache"
    assert agent.request_overrides == {
        "extra_body": {"text": {"verbosity": "low"}},
    }
