//! Cookie consent endpoint.
//!
//! `POST /api/htmx/cookies/accept` records the consent level chosen in the
//! banner (`essential` from "Reject non-essential", `all` from "Accept all")
//! in the `cookie_consent` cookie, and responds with an empty body so the
//! banner (`#cookie-consent-wrapper`) is removed by its `hx-swap="outerHTML"`.

use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
use axum::response::{IntoResponse, Response};
use axum::routing::post;
use axum::Router;

const CONSENT_COOKIE: &str = "cookie_consent";

pub fn htmx_cookies_routes() -> Router {
    Router::new().route("/accept", post(accept))
}

/// Extract the `level` value from a URL-encoded `hx-vals` body.
///
/// Returns `"essential"` only when explicitly chosen; every other case
/// (absent body, unknown value, parse noise) falls back to `"all"` so a
/// malformed request never leaves the banner stuck on screen.
fn consent_level(body: &str) -> &'static str {
    for pair in body.split('&') {
        if let Some(("level", value)) = pair.split_once('=') {
            if value == "essential" {
                return "essential";
            }
        }
    }
    "all"
}

// The body is read as a plain String (rather than `Form<_>`) so an absent or
// malformed body yields a default choice instead of a 422 that would block the
// banner's `hx-swap` from removing it.
async fn accept(body: String) -> Response {
    let level = consent_level(&body);
    let mut resp = (StatusCode::OK, "").into_response();
    let cookie = format!("{CONSENT_COOKIE}={level}; Path=/; Max-Age=31536000; SameSite=Lax");
    if let Ok(value) = HeaderValue::from_str(&cookie) {
        resp.headers_mut()
            .append(HeaderName::from_static("set-cookie"), value);
    }
    resp
}

/// Returns `true` when the visitor has recorded any consent choice.
///
/// Accepts `all` and `essential` (the two banner levels) plus the legacy
/// `accepted` value written before per-level recording existed.
pub fn has_consent(headers: &HeaderMap) -> bool {
    let raw = match headers.get("cookie").and_then(|v| v.to_str().ok()) {
        Some(raw) => raw,
        None => return false,
    };
    for pair in raw.split(';') {
        let pair = pair.trim();
        if let Some((k, v)) = pair.split_once('=') {
            if k.trim() == CONSENT_COOKIE {
                return matches!(v.trim(), "all" | "essential" | "accepted");
            }
        }
    }
    false
}

#[cfg(test)]
mod tests {
    use axum::http::HeaderValue;

    use super::*;

    fn header_map(pairs: &[(&'static str, &str)]) -> HeaderMap {
        let mut map = HeaderMap::new();
        for (k, v) in pairs {
            map.insert(
                HeaderName::from_static(k),
                HeaderValue::from_str(v).expect("ascii"),
            );
        }
        map
    }

    #[test]
    fn has_consent_detects_accepted() {
        let h = header_map(&[("cookie", "cookie_consent=accepted; theme=dark")]);
        assert!(has_consent(&h));
    }

    #[test]
    fn consent_level_parses_body() {
        assert_eq!(consent_level("level=essential"), "essential");
        assert_eq!(consent_level("level=all"), "all");
        assert_eq!(consent_level("foo=bar&level=essential"), "essential");
        assert_eq!(consent_level(""), "all");
        assert_eq!(consent_level("level=bogus"), "all");
    }

    #[test]
    fn has_consent_detects_level_values() {
        assert!(has_consent(&header_map(&[(
            "cookie",
            "cookie_consent=all"
        )])));
        assert!(has_consent(&header_map(&[(
            "cookie",
            "cookie_consent=essential"
        )])));
    }

    #[test]
    fn has_consent_negative_when_missing() {
        let h = header_map(&[("cookie", "theme=dark")]);
        assert!(!has_consent(&h));
    }

    #[test]
    fn has_consent_negative_when_other_value() {
        let h = header_map(&[("cookie", "cookie_consent=denied")]);
        assert!(!has_consent(&h));
    }
}
