"""The request-logging middleware (web/app.py's revalidate_static): what gets
counted, what gets written to the access log, and what gets truncated before
it's persisted. Companion to core/test_metrics.py's classify_inbound tests --
these exercise the actual ASGI request path, not just the classifier function.
"""
import pytest
from fastapi.testclient import TestClient

from web import app as appmod

metrics = appmod.metrics   # the exact module object app.py's own calls use --
audit = appmod.audit       # see the note on module-reload isolation below.


@pytest.fixture
def client():
    return TestClient(appmod.app)


def _patch_log_access(monkeypatch):
    calls = []
    monkeypatch.setattr(audit, "log_access", lambda record: calls.append(record))
    return calls


def test_healthz_is_never_audited(monkeypatch, client):
    """/healthz is ~47% of prod's daily access log; it must never even reach
    audit.log_access (not just be cheap once there -- see classify_inbound's
    "health" category and the exclusion in revalidate_static)."""
    calls = _patch_log_access(monkeypatch)
    r = client.get("/healthz")
    assert r.status_code == 200
    assert calls == []


def test_internal_category_is_never_audited(monkeypatch, client):
    """The SSR content API's own category ("internal") is excluded from the
    access log the same way -- it's a server-to-server hop (frontend_ssr's
    api_client.py calling backend), not a page view."""
    calls = _patch_log_access(monkeypatch)
    monkeypatch.setattr(metrics, "classify_inbound", lambda path, base: "internal")
    r = client.get("/thermograph/api/version")
    assert r.status_code == 200
    assert calls == []


def test_access_log_ip_is_truncated_not_raw(monkeypatch, client):
    calls = _patch_log_access(monkeypatch)
    r = client.get("/thermograph/api/version",
                    headers={"X-Forwarded-For": "203.0.113.77, 10.0.0.1"})
    assert r.status_code == 200
    assert len(calls) == 1
    assert calls[0]["ip"] == "203.0.113.0"      # /24, not the exact address
    assert calls[0]["ip"] != "203.0.113.77"


def test_loggable_ip_truncation():
    assert appmod._loggable_ip("203.0.113.77") == "203.0.113.0"
    assert appmod._loggable_ip("2001:db8:1234:5678::1") == "2001:db8:1234::"
    assert appmod._loggable_ip(None) is None
    assert appmod._loggable_ip("") == ""
    assert appmod._loggable_ip("not-an-ip") == "not-an-ip"   # best-effort, never raises


def test_rate_limiter_still_sees_the_full_precision_ip(monkeypatch, client):
    """_client_ip (which feeds both the access log and the event rate limiter)
    is never itself truncated -- only what audit.log_access persists is. A
    /24-truncated rate-limit key would let one abuser exhaust a whole NAT'd
    office's quota, which is exactly what must NOT happen here."""
    seen = {}
    monkeypatch.setattr(metrics, "record_event", lambda name, **kw: seen.update(kw))
    r = client.post("/thermograph/api/v2/event", json={"event": "home.locate"},
                    headers={"X-Forwarded-For": "203.0.113.77"})
    assert r.status_code == 204
    assert seen["ip"] == "203.0.113.77"


def test_event_route_records_end_to_end(client):
    """Full HTTP round trip through the real ASGI route -- confirms the write
    path backend/api/event -> metrics.record_event -> the counters store works
    end to end (the existing suite only ever called record_event directly)."""
    before = metrics.snapshot()["events_total"]
    r = client.post("/thermograph/api/v2/event", json={"event": "home.locate"})
    assert r.status_code == 204
    after = metrics.snapshot()
    assert after["events_total"] == before + 1
    assert after["events"]["home.locate"]["direct"]   # no Referer sent -> "direct"
