"""Tests for HERMES_API_URL remote gateway probe (#3281, #3355)."""
from __future__ import annotations

import json
from unittest import mock

import pytest

from api import agent_health


@pytest.fixture(autouse=True)
def _clear_cache():
    agent_health._reset_remote_probe_cache_for_tests()
    yield
    agent_health._reset_remote_probe_cache_for_tests()


class _FakeResp:
    def __init__(self, status: int = 200, body: bytes = b""):
        self.status = status
        self._body = body

    def getcode(self) -> int:
        return self.status

    def read(self, amt: int | None = None) -> bytes:
        if amt is None:
            return self._body
        return self._body[:amt]

    def __enter__(self):
        return self

    def __exit__(self, *_a):
        return False


def test_remote_gateway_healthy_when_200(monkeypatch):
    monkeypatch.setenv("HERMES_API_URL", "http://gateway:8080")
    calls: list[str] = []

    def fake_urlopen(req, timeout=None):
        calls.append(req.full_url)
        return _FakeResp(200)

    with mock.patch.object(agent_health.urllib_request, "urlopen", fake_urlopen):
        payload = agent_health.build_agent_health_payload()

    assert payload["alive"] is True
    assert payload["details"]["reason"] == "remote_gateway"
    assert payload["details"]["status_code"] == 200
    assert calls and calls[0].startswith("http://gateway:8080/")


def test_remote_gateway_unreachable_when_network_error(monkeypatch):
    monkeypatch.setenv("HERMES_API_URL", "http://gateway:8080/")

    def fake_urlopen(req, timeout=None):
        raise OSError("connection refused")

    with mock.patch.object(agent_health.urllib_request, "urlopen", fake_urlopen):
        payload = agent_health.build_agent_health_payload()

    assert payload["alive"] is False
    assert payload["details"]["reason"] == "remote_gateway_unreachable"
    assert payload["details"]["endpoint"] == "http://gateway:8080"
    assert "error" in payload["details"]


def test_falls_back_to_local_when_no_env(monkeypatch):
    monkeypatch.delenv("HERMES_API_URL", raising=False)

    # Force the local importlib path to fail so we hit the well-known
    # "gateway_not_configured" terminal state — proving the remote probe was
    # NOT invoked and the legacy local path ran.
    def boom(name):
        raise ModuleNotFoundError(name)

    with mock.patch.object(agent_health.importlib, "import_module", boom):
        payload = agent_health.build_agent_health_payload()

    assert payload["alive"] is None
    assert payload["details"]["reason"] == "gateway_status_unavailable"


def test_remote_probe_result_cached_for_5s(monkeypatch):
    monkeypatch.setenv("HERMES_API_URL", "http://gateway:8080")
    call_count = {"n": 0}

    def fake_urlopen(req, timeout=None):
        call_count["n"] += 1
        return _FakeResp(200)

    with mock.patch.object(agent_health.urllib_request, "urlopen", fake_urlopen):
        first = agent_health.build_agent_health_payload()
        second = agent_health.build_agent_health_payload()

    assert first["alive"] is True
    assert second["alive"] is True
    assert second["details"]["reason"] == "remote_gateway"
    # Second call must NOT have hit the network.
    assert call_count["n"] == 1
    # checked_at is refreshed even on cache hit so the UI shows a current time.
    assert "checked_at" in second


# ── #3355: gateway_state extraction and probe-order tests ─────────────────


def test_gateway_state_populated_from_health_detailed(monkeypatch):
    """Remote probe should extract gateway_state from /health/detailed JSON body."""
    monkeypatch.setenv("HERMES_API_URL", "http://fake-gateway:8642")
    body = json.dumps({"gateway_state": "running", "uptime": 3600}).encode()

    def fake_urlopen(req, timeout=None):
        return _FakeResp(200, body=body)

    with mock.patch.object(agent_health.urllib_request, "urlopen", fake_urlopen):
        payload = agent_health.build_agent_health_payload()

    assert payload["alive"] is True
    assert payload["details"]["gateway_state"] == "running"
    assert payload["details"]["reason"] == "remote_gateway"


def test_probe_order_prefers_health_detailed(monkeypatch):
    """First probe path should be /health/detailed so gateway_state is available."""
    monkeypatch.setenv("HERMES_API_URL", "http://fake-gateway:8642")
    probed_urls: list[str] = []

    def fake_urlopen(req, timeout=None):
        probed_urls.append(req.full_url)
        # Return 200 on first hit so the loop stops immediately
        return _FakeResp(200, body=b'{"gateway_state": "running"}')

    with mock.patch.object(agent_health.urllib_request, "urlopen", fake_urlopen):
        agent_health.build_agent_health_payload()

    assert len(probed_urls) == 1
    assert probed_urls[0] == "http://fake-gateway:8642/health/detailed"


def test_health_detailed_probe_sends_bearer_when_api_key_configured(monkeypatch):
    """/health/detailed must include Bearer auth when a gateway API key is set (#5418)."""
    monkeypatch.setenv("HERMES_API_URL", "http://fake-gateway:8642")
    monkeypatch.setenv("HERMES_WEBUI_GATEWAY_API_KEY", "probe-secret")
    captured_headers: list[dict[str, str]] = []

    def fake_urlopen(req, timeout=None):
        captured_headers.append(dict(req.header_items()))
        return _FakeResp(200, body=json.dumps({"gateway_state": "running"}).encode())

    with mock.patch.object(agent_health.urllib_request, "urlopen", fake_urlopen):
        payload = agent_health.build_agent_health_payload()

    assert payload["alive"] is True
    assert payload["details"]["gateway_state"] == "running"
    assert captured_headers
    auth = captured_headers[0].get("Authorization")
    assert auth == "Bearer probe-secret"


def test_health_detailed_falls_back_to_api_server_key(monkeypatch):
    monkeypatch.setenv("HERMES_API_URL", "http://fake-gateway:8642")
    monkeypatch.delenv("HERMES_WEBUI_GATEWAY_API_KEY", raising=False)
    monkeypatch.setenv("API_SERVER_KEY", "shared-secret")
    captured_headers: list[dict[str, str]] = []

    def fake_urlopen(req, timeout=None):
        captured_headers.append(dict(req.header_items()))
        return _FakeResp(200, body=b'{"gateway_state": "running"}')

    with mock.patch.object(agent_health.urllib_request, "urlopen", fake_urlopen):
        agent_health.build_agent_health_payload()

    assert captured_headers[0].get("Authorization") == "Bearer shared-secret"


def test_health_probe_does_not_send_bearer_without_api_key(monkeypatch):
    monkeypatch.setenv("HERMES_API_URL", "http://fake-gateway:8642")
    monkeypatch.delenv("HERMES_WEBUI_GATEWAY_API_KEY", raising=False)
    monkeypatch.delenv("API_SERVER_KEY", raising=False)
    captured_headers: list[dict[str, str]] = []

    def fake_urlopen(req, timeout=None):
        captured_headers.append(dict(req.header_items()))
        return _FakeResp(200, body=b'{"gateway_state": "running"}')

    with mock.patch.object(agent_health.urllib_request, "urlopen", fake_urlopen):
        agent_health.build_agent_health_payload()

    assert "Authorization" not in captured_headers[0]


def test_gateway_health_url_env_used(monkeypatch):
    """GATEWAY_HEALTH_URL should be respected by the remote probe."""
    monkeypatch.delenv("HERMES_API_URL", raising=False)
    monkeypatch.delenv("HERMES_GATEWAY_HEALTH_URL", raising=False)
    monkeypatch.setenv("GATEWAY_HEALTH_URL", "http://custom:9999")
    probed_urls: list[str] = []

    def fake_urlopen(req, timeout=None):
        probed_urls.append(req.full_url)
        return _FakeResp(200, body=b'{}')

    with mock.patch.object(agent_health.urllib_request, "urlopen", fake_urlopen):
        payload = agent_health.build_agent_health_payload()

    assert payload["alive"] is True
    assert probed_urls[0].startswith("http://custom:9999/")


def test_default_url_when_no_env(monkeypatch):
    """Without any gateway env vars, should fall back to local detection (not remote)."""
    monkeypatch.delenv("HERMES_API_URL", raising=False)
    monkeypatch.delenv("GATEWAY_HEALTH_URL", raising=False)
    monkeypatch.delenv("HERMES_GATEWAY_HEALTH_URL", raising=False)

    # Force the local importlib path to fail so we hit "gateway_status_unavailable",
    # proving the remote probe was NOT invoked.
    def boom(name):
        raise ModuleNotFoundError(name)

    with mock.patch.object(agent_health.importlib, "import_module", boom):
        payload = agent_health.build_agent_health_payload()

    assert payload["alive"] is None
    assert payload["details"]["reason"] == "gateway_status_unavailable"


def test_gateway_health_url_with_health_suffix_is_normalized(monkeypatch):
    """A gateway env var that already points AT a health path must not produce
    a doubled path like /health/health/detailed (#3355 Codex follow-up)."""
    monkeypatch.delenv("HERMES_API_URL", raising=False)
    monkeypatch.delenv("HERMES_GATEWAY_HEALTH_URL", raising=False)
    monkeypatch.setenv("GATEWAY_HEALTH_URL", "http://gw:8642/health")
    probed: list[str] = []

    def fake_urlopen(req, timeout=None):
        probed.append(req.full_url)
        if req.full_url == "http://gw:8642/health/detailed":
            return _FakeResp(200, body=json.dumps({"gateway_state": "running"}).encode())
        return _FakeResp(404)

    with mock.patch.object(agent_health.urllib_request, "urlopen", fake_urlopen):
        payload = agent_health.build_agent_health_payload()

    assert all("/health/health" not in u for u in probed), probed
    assert "http://gw:8642/health/detailed" in probed
    assert payload["alive"] is True
    assert payload["details"].get("gateway_state") == "running"


def test_gateway_webui_base_url_env_is_used_for_remote_probe(monkeypatch):
    """`HERMES_WEBUI_GATEWAY_BASE_URL` should be treated like a gateway health base URL."""
    monkeypatch.delenv("HERMES_API_URL", raising=False)
    monkeypatch.delenv("HERMES_GATEWAY_HEALTH_URL", raising=False)
    monkeypatch.delenv("GATEWAY_HEALTH_URL", raising=False)
    monkeypatch.setenv("HERMES_WEBUI_GATEWAY_BASE_URL", "http://container-gw:8642")
    seen = []

    def fake_urlopen(req, timeout=None):
        seen.append(req.full_url)
        return _FakeResp(200, body=b'{"gateway_state":"running"}')

    with mock.patch.object(agent_health.urllib_request, "urlopen", fake_urlopen):
        payload = agent_health.build_agent_health_payload()

    assert payload["alive"] is True
    assert payload["details"]["reason"] == "remote_gateway"
    assert seen[0].startswith("http://container-gw:8642/")


def test_oversized_remote_body_does_not_hang_and_skips_parse(monkeypatch):
    """A 2xx response with a huge body must be capped (not read unbounded) and
    still report the gateway alive, just without a parsed gateway_state."""
    monkeypatch.setenv("HERMES_API_URL", "http://gateway:8080")
    huge = b"x" * (agent_health._REMOTE_PROBE_BODY_LIMIT_BYTES * 4)
    captured_amt: list[int | None] = []

    class _HugeResp(_FakeResp):
        def read(self, amt: int | None = None):
            captured_amt.append(amt)
            return huge[:amt] if amt is not None else huge

    def fake_urlopen(req, timeout=None):
        return _HugeResp(200, body=huge)

    with mock.patch.object(agent_health.urllib_request, "urlopen", fake_urlopen):
        payload = agent_health.build_agent_health_payload()

    assert captured_amt and all(a is not None for a in captured_amt)
    assert payload["alive"] is True
    assert "gateway_state" not in payload["details"]
