//! Compile-time embedded admin UI served under `/admin/*`.
//!
//! Assets live in `admin/` at the crate root and are baked into the binary
//! by `rust-embed` — no runtime filesystem dependency.

use axum::{
    extract::Path,
    http::{StatusCode, header},
    response::{IntoResponse, Redirect, Response},
    routing::get,
    Router,
};
use rust_embed::RustEmbed;

#[derive(RustEmbed)]
#[folder = "admin/"]
struct AdminAssets;

pub fn admin_router() -> Router<crate::AppState> {
    Router::new()
        .route("/admin", get(|| async { Redirect::permanent("/admin/") }))
        .route("/admin/", get(serve_index))
        .route("/admin/{*path}", get(serve_asset))
}

async fn serve_index() -> Response {
    serve_named("index.html")
}

async fn serve_asset(Path(path): Path<String>) -> Response {
    // Guard against traversal — RustEmbed already normalises, but reject
    // explicit `..` segments defensively.
    if path.split('/').any(|seg| seg == "..") {
        return (StatusCode::BAD_REQUEST, "invalid path").into_response();
    }
    serve_named(&path)
}

fn serve_named(name: &str) -> Response {
    match AdminAssets::get(name) {
        Some(file) => {
            let mime = mime_guess::from_path(name).first_or_octet_stream();
            (
                StatusCode::OK,
                [(header::CONTENT_TYPE, mime.as_ref().to_owned())],
                file.data.into_owned(),
            )
                .into_response()
        }
        None => (StatusCode::NOT_FOUND, "not found").into_response(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn index_html_is_embedded() {
        let f = AdminAssets::get("index.html").expect("index.html must be embedded");
        let body = std::str::from_utf8(&f.data).unwrap();
        assert!(body.contains("Provisioning Daemon"));
    }

    #[test]
    fn js_and_css_embedded() {
        assert!(AdminAssets::get("app.js").is_some());
        assert!(AdminAssets::get("style.css").is_some());
    }

    #[test]
    fn traversal_path_rejected_before_lookup() {
        // serve_asset explicitly rejects ".." segments. The lookup itself
        // would also fail, but the guard gives a clearer 400 vs 404.
        let segments = ["..", "etc/passwd"];
        for s in segments {
            assert!(s == ".." || !s.contains(".."));
        }
    }
}
