use pagetop::prelude::*;
use pagetop_htmx::prelude::*;

// Forces an effective language different from the default negotiated one (en-US, with no `?lang` in
// the request), so that `Context::route()` decides to propagate `?lang=...` in local routes.
fn cx_with_lang(lang: &str) -> Context {
    Context::default().with_langid(&Locale::resolve(lang))
}

fn header<'a>(response: &'a web::Response, name: &str) -> Option<&'a str> {
    response.headers().get(name)?.to_str().ok()
}

// **< HtmxResponse::new() / empty() / oob() >******************************************************

#[pagetop::test]
async fn new_renders_the_given_markup_with_an_html_content_type() {
    let response = HtmxResponse::new(html! { li #item-42 { "New item" } }).into_response();

    assert_eq!(
        header(&response, "content-type"),
        Some("text/html; charset=utf-8")
    );

    let body = web::test::read_body_text(response).await;
    assert_eq!(body, r#"<li id="item-42">New item</li>"#);
}

#[pagetop::test]
async fn empty_has_no_body_but_keeps_the_html_content_type() {
    let response = HtmxResponse::empty().into_response();

    assert_eq!(
        header(&response, "content-type"),
        Some("text/html; charset=utf-8")
    );

    let body = web::test::read_body_text(response).await;
    assert_eq!(body, "");
}

#[pagetop::test]
async fn oob_appends_markup_after_the_main_body() {
    let response = HtmxResponse::new(html! { li #item-42 { "New item" } })
        .oob(html! { span #item-count hx-swap-oob="true" { "1" } })
        .into_response();

    let body = web::test::read_body_text(response).await;
    assert_eq!(
        body,
        concat!(
            r#"<li id="item-42">New item</li>"#,
            r#"<span id="item-count" hx-swap-oob="true">1</span>"#,
        )
    );
}

#[pagetop::test]
async fn oob_can_be_called_several_times_to_accumulate_fragments() {
    let response = HtmxResponse::new(html! { p { "Main" } })
        .oob(html! { span #a hx-swap-oob="true" { "A" } })
        .oob(html! { span #b hx-swap-oob="true" { "B" } })
        .into_response();

    let body = web::test::read_body_text(response).await;
    assert_eq!(
        body,
        concat!(
            "<p>Main</p>",
            r#"<span id="a" hx-swap-oob="true">A</span>"#,
            r#"<span id="b" hx-swap-oob="true">B</span>"#,
        )
    );
}

#[pagetop::test]
async fn oob_works_from_an_empty_response() {
    let response = HtmxResponse::empty()
        .oob(html! { span #item-count hx-swap-oob="true" { "0" } })
        .into_response();

    let body = web::test::read_body_text(response).await;
    assert_eq!(body, r#"<span id="item-count" hx-swap-oob="true">0</span>"#);
}

// **< location() / location_json() >***************************************************************

#[pagetop::test]
async fn location_sets_hx_location_from_a_route_path() {
    let response = HtmxResponse::empty().location("/items").into_response();

    assert_eq!(header(&response, "hx-location"), Some("/items"));
}

#[pagetop::test]
async fn location_preserves_lang_when_built_from_context_route() {
    let cx = cx_with_lang("es-ES");

    let response = HtmxResponse::empty()
        .location(cx.route("/items"))
        .into_response();

    assert_eq!(header(&response, "hx-location"), Some("/items?lang=es-ES"));
}

#[pagetop::test]
async fn location_json_sets_hx_location_when_the_json_is_syntactically_valid() {
    let json = r##"{"path": "/items", "target": "#content"}"##;

    let response = HtmxResponse::empty().location_json(json).into_response();

    assert_eq!(header(&response, "hx-location"), Some(json));
}

#[pagetop::test]
async fn location_json_discards_the_header_when_the_json_is_malformed() {
    // Missing closing brace: invalid JSON. The header must be silently dropped rather than sending
    // a broken payload to the client.
    let response = HtmxResponse::empty()
        .location_json(r##"{"path": "/items""##)
        .into_response();

    assert_eq!(header(&response, "hx-location"), None);
}

#[pagetop::test]
async fn location_json_only_validates_syntax_not_the_expected_keys() {
    // A key HTMX does not recognize (`"tagret"` instead of `"target"`) is still valid JSON, so it
    // passes this check; the mistake would only surface client-side. This documents that limit.
    let json = r##"{"path": "/items", "tagret": "#content"}"##;

    let response = HtmxResponse::empty().location_json(json).into_response();

    assert_eq!(header(&response, "hx-location"), Some(json));
}

// **< push_url() / replace_url() / redirect() >****************************************************

#[pagetop::test]
async fn push_url_sets_hx_push_url_from_a_route_path() {
    let cx = cx_with_lang("es-ES");

    let response = HtmxResponse::empty()
        .push_url(cx.route("/items"))
        .into_response();

    assert_eq!(header(&response, "hx-push-url"), Some("/items?lang=es-ES"));
}

#[pagetop::test]
async fn push_url_accepts_the_false_sentinel_to_disable_pushing() {
    let response = HtmxResponse::empty().push_url("false").into_response();

    assert_eq!(header(&response, "hx-push-url"), Some("false"));
}

#[pagetop::test]
async fn replace_url_sets_hx_replace_url_from_a_route_path() {
    let response = HtmxResponse::empty()
        .replace_url("/items/42")
        .into_response();

    assert_eq!(header(&response, "hx-replace-url"), Some("/items/42"));
}

#[pagetop::test]
async fn redirect_sets_hx_redirect_from_a_route_path() {
    let cx = cx_with_lang("es-ES");

    let response = HtmxResponse::empty()
        .redirect(cx.route("/items"))
        .into_response();

    assert_eq!(header(&response, "hx-redirect"), Some("/items?lang=es-ES"));
}

// **< refresh() / retarget() / reswap() / reselect() >*********************************************

#[pagetop::test]
async fn refresh_sets_hx_refresh_to_true() {
    let response = HtmxResponse::empty().refresh().into_response();

    assert_eq!(header(&response, "hx-refresh"), Some("true"));
}

#[pagetop::test]
async fn retarget_reswap_and_reselect_set_the_expected_headers() {
    let response = HtmxResponse::empty()
        .retarget("#message")
        .reswap(hx::swap::BEFORE_END)
        .reselect("#fragment")
        .into_response();

    assert_eq!(header(&response, "hx-retarget"), Some("#message"));
    assert_eq!(header(&response, "hx-reswap"), Some("beforeend"));
    assert_eq!(header(&response, "hx-reselect"), Some("#fragment"));
}

// **< trigger() / trigger_after_settle() / trigger_after_swap() >**********************************

#[pagetop::test]
async fn trigger_accepts_a_single_event_name() {
    let response = HtmxResponse::empty().trigger("itemAdded").into_response();

    assert_eq!(header(&response, "hx-trigger"), Some("itemAdded"));
}

#[pagetop::test]
async fn trigger_accepts_multiple_comma_separated_events() {
    let response = HtmxResponse::empty()
        .trigger("itemAdded, listUpdated")
        .into_response();

    assert_eq!(
        header(&response, "hx-trigger"),
        Some("itemAdded, listUpdated")
    );
}

#[pagetop::test]
async fn trigger_accepts_a_json_payload_with_event_data() {
    let json = r#"{"itemAdded": {"id": 42, "name": "Example"}}"#;

    let response = HtmxResponse::empty().trigger(json).into_response();

    assert_eq!(header(&response, "hx-trigger"), Some(json));
}

#[pagetop::test]
async fn trigger_after_settle_and_trigger_after_swap_use_their_own_headers() {
    let response = HtmxResponse::empty()
        .trigger_after_settle("settled")
        .trigger_after_swap("swapped")
        .into_response();

    assert_eq!(
        header(&response, "hx-trigger-after-settle"),
        Some("settled")
    );
    assert_eq!(header(&response, "hx-trigger-after-swap"), Some("swapped"));
}

// **< Builder chaining behavior >******************************************************************

#[pagetop::test]
async fn chaining_several_methods_sets_all_their_headers_at_once() {
    let response = HtmxResponse::new(html! { ul { li { "Item 1" } li { "Item 2" } } })
        .retarget("#list")
        .reswap(hx::swap::BEFORE_END)
        .push_url("/items")
        .trigger("itemAdded")
        .into_response();

    assert_eq!(header(&response, "hx-retarget"), Some("#list"));
    assert_eq!(header(&response, "hx-reswap"), Some("beforeend"));
    assert_eq!(header(&response, "hx-push-url"), Some("/items"));
    assert_eq!(header(&response, "hx-trigger"), Some("itemAdded"));
}

#[pagetop::test]
async fn calling_the_same_method_twice_the_last_call_wins() {
    let response = HtmxResponse::empty()
        .trigger("first")
        .trigger("second")
        .into_response();

    assert_eq!(header(&response, "hx-trigger"), Some("second"));
}

#[pagetop::test]
async fn a_header_value_with_control_characters_is_silently_discarded() {
    // `\n` is forbidden in an HTTP header value; `set_header()` must drop it rather than panicking
    // or producing a malformed response.
    let response = HtmxResponse::empty().retarget("foo\nbar").into_response();

    assert_eq!(header(&response, "hx-retarget"), None);
}
