//! Integration tests for the isolated `/metrics` endpoint (deploy-C §5.5).
//!
//! `/metrics` lives on its own axum listener/router, so these tests drive
//! `rutster::metrics::metrics_router` directly without binding a socket —
//! the same pattern the `/readyz` tests use.

use axum::body::Body;
use axum::http::{Request, StatusCode};
use tower::ServiceExt;

/// Given no media thread behind AppState, `/metrics` must return 503.
///
/// The closed placeholder sender is the contract surface: a scraper sees
/// "media thread unresponsive" and can mark the target stale.
#[tokio::test]
async fn metrics_endpoint_returns_prometheus_text() {
    let app = rutster::metrics::metrics_router(rutster::session_map::AppState::default());
    let resp = app
        .oneshot(
            Request::builder()
                .uri("/metrics")
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::SERVICE_UNAVAILABLE);
}

/// Given a live media thread, `/metrics` returns Prometheus text with every
/// core gauge/counter and omits the event-sink drop line (TracingEventSink
/// reports `None`).
///
/// This is a synchronous `#[test]` (not `#[tokio::test]`) because
/// `MediaThread::shutdown()` uses `blocking_send`/`blocking_recv`, which
/// panic when called from inside a tokio runtime worker. The test creates
/// its own runtime only to drive the async axum oneshot.
#[test]
fn metrics_endpoint_with_live_media_thread_returns_200() {
    let rt = tokio::runtime::Runtime::new().expect("tokio runtime");
    let (state, thread) = rutster::session_map::AppState::default()
        .spawn_media_thread(rt.handle().clone())
        .expect("spawn media thread");

    let app = rutster::metrics::metrics_router(state);
    let resp = rt.block_on(async {
        app.oneshot(
            Request::builder()
                .uri("/metrics")
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap()
    });

    assert_eq!(resp.status(), StatusCode::OK);
    let ct = resp
        .headers()
        .get(axum::http::header::CONTENT_TYPE)
        .expect("content-type header present")
        .to_str()
        .expect("content-type is valid ascii");
    assert_eq!(ct, "text/plain; version=0.0.4");

    let bytes = rt
        .block_on(axum::body::to_bytes(resp.into_body(), 8192))
        .unwrap();
    let text = String::from_utf8(bytes.to_vec()).expect("metrics body is utf-8");

    let expected = [
        "# TYPE rutster_sessions_active gauge",
        "# TYPE rutster_max_sessions gauge",
        "# TYPE rutster_draining gauge",
        "# TYPE rutster_tick_overruns_total counter",
        "# TYPE rutster_last_tick_micros gauge",
        "# TYPE rutster_admission_rejects_total counter",
        "rutster_sessions_active",
        "rutster_max_sessions",
        "rutster_draining",
        "rutster_tick_overruns_total",
        "rutster_last_tick_micros",
        "rutster_admission_rejects_total",
    ];
    for needle in expected {
        assert!(
            text.contains(needle),
            "metrics output missing {needle:?}; body:\n{text}"
        );
    }

    assert!(
        !text.contains("rutster_event_sink_drops_total"),
        "TracingEventSink must not emit drop counter; body:\n{text}"
    );

    thread.shutdown();
}
