//! PR 4 (feat/admin-system-api) integration tests.
//!
//! Shape-only assertions — we don't mock the system, just call the
//! endpoint and check the response envelope. Threshold-triggering of
//! alerts would require faking statvfs / sysinfo, which is more
//! plumbing than the test gives back.

mod common;

use axum::http::StatusCode;
use axum::Router;
use sqlx::PgPool;
use tower::ServiceExt;

use mangalord::repo;

async fn seed_admin(pool: &PgPool, app: &Router) -> String {
    let (username, cookie) = common::register_user(app).await;
    let u = repo::user::find_by_username(pool, &username)
        .await
        .unwrap()
        .unwrap();
    repo::user::set_is_admin_unchecked(pool, u.id, true).await.unwrap();
    cookie
}

#[sqlx::test(migrations = "./migrations")]
async fn requires_admin(pool: PgPool) {
    let h = common::harness(pool);
    let (_u, cookie) = common::register_user(&h.app).await;
    let resp = h
        .app
        .oneshot(common::get_with_cookie("/api/v1/admin/system", &cookie))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::FORBIDDEN);
}

#[sqlx::test(migrations = "./migrations")]
async fn unauthenticated_request_is_rejected(pool: PgPool) {
    let h = common::harness(pool);
    let resp = h
        .app
        .oneshot(common::get("/api/v1/admin/system"))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}

#[sqlx::test(migrations = "./migrations")]
async fn returns_disk_memory_cpu_alerts_shape(pool: PgPool) {
    let h = common::harness(pool.clone());
    let cookie = seed_admin(&pool, &h.app).await;

    let resp = h
        .app
        .oneshot(common::get_with_cookie("/api/v1/admin/system", &cookie))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = common::body_json(resp).await;

    // Disk: harness uses LocalStorage on a tempdir, so disk SHOULD be
    // populated. Validate the field shape and percent range.
    let disk = body
        .get("disk")
        .expect("disk key present")
        .as_object()
        .expect("disk is an object (LocalStorage exposes a path)");
    assert!(disk["total_bytes"].as_u64().unwrap() > 0);
    let pct = disk["percent_used"].as_f64().unwrap();
    assert!(
        (0.0..=100.0).contains(&pct),
        "percent_used outside [0,100]: {pct}"
    );

    let mem = body.get("memory").expect("memory key").as_object().unwrap();
    assert!(mem["total_bytes"].as_u64().unwrap() > 0);
    let mpct = mem["percent_used"].as_f64().unwrap();
    assert!((0.0..=100.0).contains(&mpct));

    let cpu = body.get("cpu").expect("cpu key").as_object().unwrap();
    let cpu_pct = cpu["percent_used"].as_f64().unwrap();
    assert!(
        (0.0..=100.0).contains(&cpu_pct),
        "cpu out of range: {cpu_pct}"
    );

    // Load average: three numeric keys. Values can legitimately be 0 on
    // an idle box, so only assert presence + non-negativity.
    let load = cpu["load_avg"].as_object().expect("cpu.load_avg object");
    for k in ["one", "five", "fifteen"] {
        let v = load[k].as_f64().unwrap_or_else(|| panic!("load_avg.{k} numeric"));
        assert!(v >= 0.0, "load_avg.{k} negative: {v}");
    }

    // Per-core usage: an array of percentages in [0,100].
    let cores = cpu["per_core"].as_array().expect("cpu.per_core array");
    assert!(!cores.is_empty(), "expected at least one core");
    for c in cores {
        let p = c.as_f64().unwrap();
        assert!((0.0..=100.0).contains(&p), "per_core out of range: {p}");
    }

    // Temperatures: an array (possibly empty in CI / containers without
    // /sys sensors). Each present entry has a string label + numeric °C.
    let temps = body
        .get("temperatures")
        .expect("temperatures key")
        .as_array()
        .expect("temperatures is an array");
    for t in temps {
        assert!(t["label"].is_string(), "temp label is string");
        assert!(t["celsius"].as_f64().is_some(), "temp celsius is numeric");
    }

    let alerts = body.get("alerts").expect("alerts key").as_array().unwrap();
    // Don't assert on length — the box may genuinely be >90% on memory
    // when the test runs. Just confirm shape of any present entry.
    for alert in alerts {
        assert!(alert["level"].is_string());
        assert!(alert["message"].is_string());
    }
}
