//! Integration tests for the page content-search endpoint
//! `GET /v1/me/page-search`: multi-tag AND across (user ∪ auto) tags,
//! weighted OCR/scene text ranking, and content-warning include/exclude.

mod common;

use axum::http::StatusCode;
use axum::Router;
use serde_json::json;
use sqlx::PgPool;
use tower::ServiceExt;
use uuid::Uuid;

use mangalord::domain::page_analysis::{OcrResult, SafetyFlag, VisionAnalysis};
use mangalord::repo;

/// Seed a manga + chapter and return the chapter id for adding pages.
async fn seed_chapter(pool: &PgPool, title: &str) -> Uuid {
    let manga_id: Uuid =
        sqlx::query_scalar("INSERT INTO mangas (title) VALUES ($1) RETURNING id")
            .bind(title)
            .fetch_one(pool)
            .await
            .unwrap();
    sqlx::query_scalar("INSERT INTO chapters (manga_id, number) VALUES ($1, 1) RETURNING id")
        .bind(manga_id)
        .fetch_one(pool)
        .await
        .unwrap()
}

async fn add_page(pool: &PgPool, chapter_id: Uuid, n: i32) -> Uuid {
    sqlx::query_scalar(
        "INSERT INTO pages (chapter_id, page_number, storage_key, content_type) \
         VALUES ($1, $2, $3, 'image/png') RETURNING id",
    )
    .bind(chapter_id)
    .bind(n)
    .bind(format!("k/{n}.png"))
    .fetch_one(pool)
    .await
    .unwrap()
}

fn analysis(
    ocr: &[(&str, &str)],
    auto_tags: &[&str],
    nsfw: bool,
    warnings: &[&str],
) -> VisionAnalysis {
    VisionAnalysis {
        ocr_results: ocr
            .iter()
            .map(|(t, k)| OcrResult {
                text: (*t).into(),
                kind: (*k).into(),
                y: None,
            })
            .collect(),
        tagging_results: auto_tags.iter().map(|t| t.to_string()).collect(),
        scene_description: String::new(),
        safety_flag: SafetyFlag {
            is_nsfw: nsfw,
            content_type: warnings.iter().map(|w| w.to_string()).collect(),
        },
    }
}

async fn user_id_for(pool: &PgPool, username: &str) -> Uuid {
    repo::user::find_by_username(pool, username)
        .await
        .unwrap()
        .unwrap()
        .id
}

async fn search(app: &Router, cookie: &str, query: &str) -> serde_json::Value {
    let resp = app
        .clone()
        .oneshot(common::get_with_cookie(
            &format!("/api/v1/me/page-search?{query}"),
            cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK, "search {query} failed");
    common::body_json(resp).await
}

fn page_ids(body: &serde_json::Value) -> Vec<String> {
    body["items"]
        .as_array()
        .unwrap()
        .iter()
        .map(|i| i["page_id"].as_str().unwrap().to_string())
        .collect()
}

#[sqlx::test(migrations = "./migrations")]
async fn multi_tag_and_across_user_and_auto_tags(pool: PgPool) {
    let h = common::harness(pool.clone());
    let (username, cookie) = common::register_user(&h.app).await;
    let uid = user_id_for(&pool, &username).await;

    let ch = seed_chapter(&pool, "M").await;
    let p1 = add_page(&pool, ch, 1).await;
    let p2 = add_page(&pool, ch, 2).await;

    // p1: auto-tag "b"; user adds tag "a" → has both a (user) and b (auto).
    repo::page_analysis::persist_analysis(&pool, p1, &analysis(&[], &["b"], false, &[]), "m")
        .await
        .unwrap();
    repo::page_tag::upsert(&pool, uid, p1, "a").await.unwrap();
    // p2: only auto-tag "a".
    repo::page_analysis::persist_analysis(&pool, p2, &analysis(&[], &["a"], false, &[]), "m")
        .await
        .unwrap();

    // tags=a,b (AND) → only p1 satisfies both.
    let body = search(&h.app, &cookie, "tags=a,b").await;
    assert_eq!(page_ids(&body), vec![p1.to_string()]);

    // tags=a alone → both (p1 via user tag, p2 via auto tag).
    let body = search(&h.app, &cookie, "tags=a").await;
    let mut ids = page_ids(&body);
    ids.sort();
    let mut want = vec![p1.to_string(), p2.to_string()];
    want.sort();
    assert_eq!(ids, want);
}

#[sqlx::test(migrations = "./migrations")]
async fn text_search_ranks_speech_above_sfx(pool: PgPool) {
    let h = common::harness(pool.clone());
    let (_u, cookie) = common::register_user(&h.app).await;
    let ch = seed_chapter(&pool, "M").await;
    let speech_page = add_page(&pool, ch, 1).await;
    let sfx_page = add_page(&pool, ch, 2).await;

    repo::page_analysis::persist_analysis(
        &pool,
        speech_page,
        &analysis(&[("alpha", "speech")], &[], false, &[]),
        "m",
    )
    .await
    .unwrap();
    repo::page_analysis::persist_analysis(
        &pool,
        sfx_page,
        &analysis(&[("alpha", "sfx")], &[], false, &[]),
        "m",
    )
    .await
    .unwrap();

    let body = search(&h.app, &cookie, "text=alpha").await;
    let ids = page_ids(&body);
    assert_eq!(ids.len(), 2, "both pages match the term");
    assert_eq!(
        ids[0],
        speech_page.to_string(),
        "speech (weight A) must rank above sfx (weight D)"
    );
}

#[sqlx::test(migrations = "./migrations")]
async fn content_warning_include_and_exclude(pool: PgPool) {
    let h = common::harness(pool.clone());
    let (_u, cookie) = common::register_user(&h.app).await;
    let ch = seed_chapter(&pool, "M").await;
    let sexual = add_page(&pool, ch, 1).await;
    let gore = add_page(&pool, ch, 2).await;

    repo::page_analysis::persist_analysis(
        &pool,
        sexual,
        &analysis(&[("x", "speech")], &["nsfw-tag"], true, &["sexual"]),
        "m",
    )
    .await
    .unwrap();
    repo::page_analysis::persist_analysis(
        &pool,
        gore,
        &analysis(&[("x", "speech")], &["nsfw-tag"], true, &["gore"]),
        "m",
    )
    .await
    .unwrap();

    // include sexual → only the sexual page.
    let body = search(&h.app, &cookie, "tags=nsfw-tag&cw_include=sexual").await;
    assert_eq!(page_ids(&body), vec![sexual.to_string()]);
    // and it carries the warning + nsfw flag in the row.
    assert_eq!(body["items"][0]["is_nsfw"], true);
    assert_eq!(body["items"][0]["content_warnings"][0], "sexual");

    // exclude sexual → only the gore page.
    let body = search(&h.app, &cookie, "tags=nsfw-tag&cw_exclude=sexual").await;
    assert_eq!(page_ids(&body), vec![gore.to_string()]);
}

#[sqlx::test(migrations = "./migrations")]
async fn text_only_search_needs_no_tags(pool: PgPool) {
    let h = common::harness(pool.clone());
    let (_u, cookie) = common::register_user(&h.app).await;
    let ch = seed_chapter(&pool, "M").await;
    let p = add_page(&pool, ch, 1).await;
    repo::page_analysis::persist_analysis(
        &pool,
        p,
        &analysis(&[("dragon", "speech")], &[], false, &[]),
        "m",
    )
    .await
    .unwrap();

    // The previously-501 text param now returns results (no tags needed).
    let body = search(&h.app, &cookie, "text=dragon").await;
    assert_eq!(page_ids(&body), vec![p.to_string()]);
}

#[sqlx::test(migrations = "./migrations")]
async fn search_without_any_filter_is_rejected(pool: PgPool) {
    let h = common::harness(pool.clone());
    let (_u, cookie) = common::register_user(&h.app).await;

    let resp = h
        .app
        .clone()
        .oneshot(common::get_with_cookie("/api/v1/me/page-search", &cookie))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::UNPROCESSABLE_ENTITY);
}

#[sqlx::test(migrations = "./migrations")]
async fn unknown_content_warning_is_rejected(pool: PgPool) {
    let h = common::harness(pool.clone());
    let (_u, cookie) = common::register_user(&h.app).await;

    let resp = h
        .app
        .clone()
        .oneshot(common::get_with_cookie(
            "/api/v1/me/page-search?cw_include=spicy",
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::UNPROCESSABLE_ENTITY);
}

#[sqlx::test(migrations = "./migrations")]
async fn requires_authentication(pool: PgPool) {
    let h = common::harness(pool.clone());
    let resp = h
        .app
        .clone()
        .oneshot(common::get("/api/v1/me/page-search?text=x"))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
    let _ = json!({}); // silence unused import in some configs
}
