//! The static assets are served out of the binary.
//!
//! The logo and the stylesheet are compiled into the executable rather than
//! read from a `static/` directory next to the process, so they are served no
//! matter where the server was started from. Every test here starts the server
//! in an empty working directory, which is exactly the situation that used to
//! answer `404`: nothing to read from disk means nothing to serve unless the
//! bytes are in the binary.
//!
//! The bodies are compared against the files on disk, so a change to an asset
//! that is not picked up by the build fails here rather than silently shipping
//! a stale copy.
//!
//! The caching tests pin both halves of revalidation: a freshness window, so
//! the browser does not ask at all while it holds a copy, and a validator, so
//! that when it does ask after the window lapses the answer is a body free
//! `304` rather than the whole file again. The bytes are compiled into the
//! binary, so the validator must also be stable - it is the content's hash,
//! not anything about the running process.

mod support;

use support::{Client, Server, free_port, get};

#[tokio::test]
async fn stylesheet_is_served_from_the_binary() -> miette::Result<()> {
    let port = free_port();
    let mut server = Server::spawn_elsewhere(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);

    let response = get(port, "/static/style.css");

    assert_eq!(response.status, 200, "stylesheet");
    assert_eq!(
        response.content_type.as_deref(),
        Some("text/css"),
        "the stylesheet should be sent as CSS"
    );
    assert_eq!(
        response.body,
        include_str!("../static/style.css"),
        "the stylesheet should be the file that was embedded"
    );

    Ok(())
}

#[tokio::test]
async fn logo_is_served_from_the_binary() -> miette::Result<()> {
    let port = free_port();
    let mut server = Server::spawn_elsewhere(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);

    let response = get(port, "/static/frogue-logo.svg");

    assert_eq!(response.status, 200, "logo");
    assert_eq!(
        response.content_type.as_deref(),
        Some("image/svg+xml"),
        "the logo should be sent as SVG"
    );
    assert_eq!(
        response.body,
        include_str!("../static/frogue-logo.svg"),
        "the logo should be the file that was embedded"
    );

    Ok(())
}

#[tokio::test]
async fn an_unknown_asset_is_not_found() -> miette::Result<()> {
    let port = free_port();
    let mut server = Server::spawn_elsewhere(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);

    let response = get(port, "/static/does-not-exist.css");

    assert_eq!(response.status, 404, "unknown asset");

    Ok(())
}

#[tokio::test]
async fn an_asset_cannot_escape_the_static_tree() -> miette::Result<()> {
    let port = free_port();
    let mut server = Server::spawn_elsewhere(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);

    // The embedded tree has no parent to walk into: anything that is not a
    // known asset is simply absent, so traversal is a 404 rather than a read of
    // something else on disk.
    let response = get(port, "/static/../Cargo.toml");

    assert_ne!(
        response.status, 200,
        "a path outside the static tree must not be served"
    );

    Ok(())
}

#[tokio::test]
async fn an_asset_is_marked_fresh_for_ten_minutes() -> miette::Result<()> {
    let port = free_port();
    let mut server = Server::spawn_elsewhere(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);

    let response = get(port, "/static/style.css");

    assert_eq!(response.status, 200, "stylesheet");
    assert_eq!(
        response.header("cache-control"),
        Some("public, max-age=600"),
        "the browser should be able to reuse its copy without asking again"
    );
    let etag = response.header("etag").unwrap_or_else(|| {
        panic!("a cached asset needs a validator so it can be revalidated later")
    });
    assert!(
        etag.starts_with('"') && etag.ends_with('"') && etag.len() > 2,
        "the etag should be a quoted strong validator, got {etag:?}"
    );

    Ok(())
}

#[tokio::test]
async fn an_unchanged_asset_revalidates_with_a_body_free_304() -> miette::Result<()> {
    let port = free_port();
    let mut server = Server::spawn_elsewhere(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);

    let first = get(port, "/static/style.css");
    let etag = first
        .header("etag")
        .expect("the stylesheet should carry an etag")
        .to_string();

    let revalidated = Client::new(port).get_with("/static/style.css", &[("If-None-Match", &etag)]);

    assert_eq!(
        revalidated.status, 304,
        "an asset whose etag matches must not be sent again"
    );
    assert!(
        revalidated.body.is_empty(),
        "a 304 carries no body, got {:?}",
        revalidated.body
    );
    assert_eq!(
        revalidated.header("etag"),
        Some(etag.as_str()),
        "the 304 should repeat the validator it matched"
    );

    Ok(())
}

#[tokio::test]
async fn a_stale_validator_gets_the_full_body() -> miette::Result<()> {
    let port = free_port();
    let mut server = Server::spawn_elsewhere(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);

    let response =
        Client::new(port).get_with("/static/style.css", &[("If-None-Match", "\"stale-etag\"")]);

    assert_eq!(
        response.status, 200,
        "a validator from an older binary must not suppress the body"
    );
    assert_eq!(
        response.body,
        include_str!("../static/style.css"),
        "the stylesheet should be the file that was embedded"
    );

    Ok(())
}

#[tokio::test]
async fn the_validator_is_stable_across_requests() -> miette::Result<()> {
    let port = free_port();
    let mut server = Server::spawn_elsewhere(&format!("127.0.0.1:{port}"));
    server.wait_for_connection(port);

    let first = get(port, "/static/style.css");
    let second = get(port, "/static/style.css");

    let first = first
        .header("etag")
        .expect("the stylesheet should carry an etag");
    let second = second
        .header("etag")
        .expect("the stylesheet should carry an etag");

    assert_eq!(
        first, second,
        "the embedded bytes cannot change under a running process, so the \
         validator must not change either"
    );

    Ok(())
}
