// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2026 ndaal Gesellschaft für Sicherheit in der Informationstechnik mbH & Co KG, Cologne
// SPDX-FileCopyrightText: Author: Pierre Gronau <Pierre.Gronau@ndaal.eu>

//! Static assets, embedded into the binary at compile time.
//!
//! Everything the UI needs — Bootstrap, Bootstrap Icons, the colour-mode
//! switcher and the Roboto faces (which also back the PDF exporter) — ships
//! inside the executable: no CDN, no filesystem reads. The asset table is a
//! fixed compile-time map, so path traversal cannot reach anything outside it
//! by construction; the `.`/`..` check below is belt-and-braces. Carried over
//! from the ndaal OSCAL viewer's `static_files`.

use bytes::Bytes;
use http::header::{self, HeaderValue};
use http::{Response, StatusCode};
use http_body_util::Full;

use crate::router::{text_response, Body};

/// `(url path, content type, bytes)` for every embedded asset.
const ASSETS: &[(&str, &str, &[u8])] = &[
    (
        "css/bootstrap.min.css",
        "text/css; charset=utf-8",
        include_bytes!("static/css/bootstrap.min.css"),
    ),
    (
        "css/bootstrap-icons.min.css",
        "text/css; charset=utf-8",
        include_bytes!("static/css/bootstrap-icons.min.css"),
    ),
    (
        "js/bootstrap.bundle.min.js",
        "text/javascript; charset=utf-8",
        include_bytes!("static/js/bootstrap.bundle.min.js"),
    ),
    (
        "js/theme.js",
        "text/javascript; charset=utf-8",
        include_bytes!("static/js/theme.js"),
    ),
    (
        "fonts/bootstrap-icons.woff2",
        "font/woff2",
        include_bytes!("static/fonts/bootstrap-icons.woff2"),
    ),
    (
        "fonts/bootstrap-icons.woff",
        "font/woff",
        include_bytes!("static/fonts/bootstrap-icons.woff"),
    ),
    // Aliases: the unmodified bootstrap-icons.min.css references its fonts
    // relative to the css/ directory (`url("fonts/bootstrap-icons.woff2")`).
    (
        "css/fonts/bootstrap-icons.woff2",
        "font/woff2",
        include_bytes!("static/fonts/bootstrap-icons.woff2"),
    ),
    (
        "css/fonts/bootstrap-icons.woff",
        "font/woff",
        include_bytes!("static/fonts/bootstrap-icons.woff"),
    ),
    (
        "fonts/roboto/roboto-regular.ttf",
        "font/ttf",
        include_bytes!("static/fonts/roboto/roboto-regular.ttf"),
    ),
    (
        "fonts/roboto/roboto-bold.ttf",
        "font/ttf",
        include_bytes!("static/fonts/roboto/roboto-bold.ttf"),
    ),
];

/// Serve an embedded asset given the path beyond `/static/`.
///
/// Unknown paths (and any path containing `.` / `..` / empty segments) yield
/// `404` — the embedded table is the only source, so nothing outside the
/// binary can ever be read.
#[must_use]
pub fn serve(url_path: &str) -> Response<Body> {
    let relative = url_path.strip_prefix('/').unwrap_or(url_path);
    if relative
        .split('/')
        .any(|seg| seg == ".." || seg == "." || seg.is_empty())
    {
        return text_response(StatusCode::NOT_FOUND, "File not found");
    }
    let Some((_, content_type, bytes)) = ASSETS.iter().find(|(path, _, _)| *path == relative) else {
        return text_response(StatusCode::NOT_FOUND, "File not found");
    };
    let mut response = Response::new(Full::new(Bytes::from_static(bytes)));
    let headers = response.headers_mut();
    headers.insert(
        header::CONTENT_TYPE,
        HeaderValue::from_str(content_type)
            .unwrap_or_else(|_| HeaderValue::from_static("application/octet-stream")),
    );
    headers.insert(
        header::CACHE_CONTROL,
        HeaderValue::from_static("public, max-age=3600"),
    );
    response
}

#[cfg(test)]
mod tests {
    use super::serve;
    use http::StatusCode;

    #[test]
    fn known_assets_are_served() {
        for path in [
            "css/bootstrap.min.css",
            "css/bootstrap-icons.min.css",
            "js/bootstrap.bundle.min.js",
            "js/theme.js",
            "css/fonts/bootstrap-icons.woff2",
            "fonts/roboto/roboto-regular.ttf",
        ] {
            assert_eq!(serve(path).status(), StatusCode::OK, "missing: {path}");
        }
    }

    #[test]
    fn unknown_is_404() {
        assert_eq!(serve("js/evil.js").status(), StatusCode::NOT_FOUND);
    }

    #[test]
    fn traversal_attempts_are_rejected() {
        for path in ["../Cargo.toml", "css/../../etc/passwd", "./css/x", "//x"] {
            assert_eq!(serve(path).status(), StatusCode::NOT_FOUND, "{path}");
        }
    }
}
