//! HTMX request inspection and response helpers for the htmx-ssr rendering
//! profile.

use axum::http::{HeaderMap, HeaderValue, StatusCode};
use axum::response::{IntoResponse, Response};

/// Header name set by htmx on every AJAX request it initiates.
pub const HX_REQUEST: &str = "HX-Request";

/// Header set by htmx when the request originates from `hx-boost`-decorated
/// navigation.
pub const HX_BOOSTED: &str = "HX-Boosted";

/// Header carrying the CSS selector of the element that triggered the request.
pub const HX_TARGET: &str = "HX-Target";

/// Header carrying the current page URL when the request was triggered.
pub const HX_CURRENT_URL: &str = "HX-Current-URL";

/// Response header overriding the swap target on the client.
pub const HX_RETARGET: &str = "HX-Retarget";

/// Response header overriding the swap strategy on the client.
pub const HX_RESWAP: &str = "HX-Reswap";

/// Response header instructing htmx to perform a hard redirect.
pub const HX_REDIRECT: &str = "HX-Redirect";

/// Response header instructing htmx to push a new URL into the browser history.
pub const HX_PUSH_URL: &str = "HX-Push-Url";

/// Returns true when the request was issued by the htmx client runtime.
pub fn is_htmx_request(headers: &HeaderMap) -> bool {
    headers
        .get(HX_REQUEST)
        .is_some_and(|v| v.as_bytes().eq_ignore_ascii_case(b"true"))
}

/// Returns true when the request was issued via `hx-boost` link/form
/// navigation.
pub fn is_htmx_boosted(headers: &HeaderMap) -> bool {
    headers
        .get(HX_BOOSTED)
        .is_some_and(|v| v.as_bytes().eq_ignore_ascii_case(b"true"))
}

/// Returns the CSS selector of the element that triggered the request, if
/// present.
pub fn htmx_target(headers: &HeaderMap) -> Option<&str> {
    headers.get(HX_TARGET).and_then(|v| v.to_str().ok())
}

/// Returns the page URL that was active when the request was triggered, if
/// present.
pub fn htmx_current_url(headers: &HeaderMap) -> Option<&str> {
    headers.get(HX_CURRENT_URL).and_then(|v| v.to_str().ok())
}

/// Wraps a fragment in a `hx-swap-oob` container targeting another DOM region.
///
/// Use for cross-region updates returned from a single endpoint — for example,
/// refreshing the nav and footer when the language is switched.
pub fn wrap_oob(target_selector: &str, swap: &str, fragment: &str) -> String {
    format!(
        r#"<div hx-swap-oob="{swap}:{target_selector}">{fragment}</div>"#,
        swap = swap,
        target_selector = target_selector,
        fragment = fragment,
    )
}

/// Builds an error response suitable for htmx swap targets.
///
/// When the request includes `HX-Request`, the body is returned without the
/// page shell and the client is instructed to retarget the swap to
/// `#htmx-error` with `outerHTML`. Non-htmx callers receive a plain status with
/// the message body.
pub fn htmx_error_response(
    headers: &HeaderMap,
    status: StatusCode,
    message: impl Into<String>,
) -> Response {
    let message = message.into();
    if is_htmx_request(headers) {
        let body = format!(
            r#"<div id="htmx-error" class="htmx-error" role="alert">{}</div>"#,
            html_escape(&message),
        );
        let mut resp = (status, body).into_response();
        let headers_mut = resp.headers_mut();
        headers_mut.insert(HX_RETARGET, HeaderValue::from_static("#htmx-error"));
        headers_mut.insert(HX_RESWAP, HeaderValue::from_static("outerHTML"));
        resp
    } else {
        (status, message).into_response()
    }
}

/// Returns true when the response should omit the full HTML shell.
///
/// A response is fragment-bound when the request was an htmx AJAX call AND it
/// was not a boosted navigation (boosted navigations replace the full body and
/// thus still benefit from the shell-injected head metadata).
pub fn wants_fragment(headers: &HeaderMap) -> bool {
    is_htmx_request(headers) && !is_htmx_boosted(headers)
}

fn html_escape(input: &str) -> String {
    let mut out = String::with_capacity(input.len());
    for ch in input.chars() {
        match ch {
            '<' => out.push_str("&lt;"),
            '>' => out.push_str("&gt;"),
            '&' => out.push_str("&amp;"),
            '"' => out.push_str("&quot;"),
            '\'' => out.push_str("&#39;"),
            other => out.push(other),
        }
    }
    out
}

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

    use super::*;

    fn headers_with(pairs: &[(&'static str, &str)]) -> HeaderMap {
        let mut map = HeaderMap::new();
        for (k, v) in pairs {
            let value =
                HeaderValue::from_str(v).expect("static test header value must be valid ASCII");
            map.insert(HeaderName::from_static(k), value);
        }
        map
    }

    #[test]
    fn detects_htmx_request() {
        assert!(is_htmx_request(&headers_with(&[("hx-request", "true")])));
        assert!(is_htmx_request(&headers_with(&[("hx-request", "TRUE")])));
        assert!(!is_htmx_request(&headers_with(&[("hx-request", "false")])));
        assert!(!is_htmx_request(&HeaderMap::new()));
    }

    #[test]
    fn detects_boosted() {
        assert!(is_htmx_boosted(&headers_with(&[("hx-boosted", "true")])));
        assert!(!is_htmx_boosted(&HeaderMap::new()));
    }

    #[test]
    fn fragment_only_for_non_boosted_htmx() {
        assert!(wants_fragment(&headers_with(&[("hx-request", "true")])));
        assert!(!wants_fragment(&headers_with(&[
            ("hx-request", "true"),
            ("hx-boosted", "true"),
        ])));
        assert!(!wants_fragment(&HeaderMap::new()));
    }

    #[test]
    fn extracts_target_and_url() {
        let h = headers_with(&[
            ("hx-target", "#main"),
            ("hx-current-url", "https://example.com/blog"),
        ]);
        assert_eq!(htmx_target(&h), Some("#main"));
        assert_eq!(htmx_current_url(&h), Some("https://example.com/blog"));
    }

    #[test]
    fn wrap_oob_emits_attribute() {
        let html = wrap_oob("#nav", "outerHTML", "<nav>x</nav>");
        assert!(html.contains(r#"hx-swap-oob="outerHTML:#nav""#));
        assert!(html.contains("<nav>x</nav>"));
    }

    #[test]
    fn html_escape_handles_specials() {
        assert_eq!(
            html_escape(r#"<a href="x">&y'</a>"#),
            "&lt;a href=&quot;x&quot;&gt;&amp;y&#39;&lt;/a&gt;"
        );
    }

    #[test]
    fn error_response_retargets_for_htmx() {
        let headers = headers_with(&[("hx-request", "true")]);
        let resp = htmx_error_response(&headers, StatusCode::NOT_FOUND, "missing");
        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
        assert_eq!(
            resp.headers()
                .get(HX_RETARGET)
                .expect("HX-Retarget header must be present"),
            &HeaderValue::from_static("#htmx-error")
        );
        assert_eq!(
            resp.headers()
                .get(HX_RESWAP)
                .expect("HX-Reswap header must be present"),
            &HeaderValue::from_static("outerHTML")
        );
    }

    #[test]
    fn error_response_passes_through_non_htmx() {
        let resp = htmx_error_response(&HeaderMap::new(), StatusCode::NOT_FOUND, "missing");
        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
        assert!(resp.headers().get(HX_RETARGET).is_none());
    }
}
