mod common;

use axum::http::StatusCode;
use serde_json::{json, Value};
use sqlx::PgPool;
use tower::ServiceExt;
use uuid::Uuid;

use common::MultipartBuilder;

async fn create_manga(
    app: &axum::Router,
    cookie: &str,
    metadata: Value,
) -> Value {
    let resp = app
        .clone()
        .oneshot(common::post_multipart_with_cookie(
            "/api/v1/mangas",
            MultipartBuilder::new().add_json("metadata", metadata),
            cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::CREATED);
    common::body_json(resp).await
}

fn first_author_id(manga: &Value) -> String {
    manga["authors"][0]["id"].as_str().unwrap().to_string()
}

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

    let m1 = create_manga(
        &h.app,
        &cookie,
        json!({ "title": "Berserk", "authors": ["Kentaro Miura"] }),
    )
    .await;
    // A second manga by the same author bumps the count to 2.
    let _ = create_manga(
        &h.app,
        &cookie,
        json!({ "title": "Berserk Prelude", "authors": ["Kentaro Miura"] }),
    )
    .await;
    let author_id = first_author_id(&m1);

    let resp = h
        .app
        .oneshot(common::get(&format!("/api/v1/authors/{author_id}")))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = common::body_json(resp).await;
    assert_eq!(body["id"], author_id);
    assert_eq!(body["name"], "Kentaro Miura");
    assert_eq!(body["manga_count"], 2);
}

#[sqlx::test(migrations = "./migrations")]
async fn get_unknown_id_is_404_with_envelope(pool: PgPool) {
    let h = common::harness(pool);
    let resp = h
        .app
        .oneshot(common::get(&format!("/api/v1/authors/{}", Uuid::new_v4())))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::NOT_FOUND);
    let body = common::body_json(resp).await;
    assert_eq!(body["error"]["code"], "not_found");
}

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

    let m_miura = create_manga(
        &h.app,
        &cookie,
        json!({ "title": "Berserk", "authors": ["Kentaro Miura"] }),
    )
    .await;
    let _ = create_manga(
        &h.app,
        &cookie,
        json!({ "title": "One Piece", "authors": ["Eiichiro Oda"] }),
    )
    .await;
    let _ = create_manga(
        &h.app,
        &cookie,
        json!({ "title": "Berserk Prelude", "authors": ["Kentaro Miura"] }),
    )
    .await;
    let author_id = first_author_id(&m_miura);

    let resp = h
        .app
        .oneshot(common::get(&format!("/api/v1/authors/{author_id}/mangas")))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = common::body_json(resp).await;
    let titles: Vec<&str> = body["items"]
        .as_array()
        .unwrap()
        .iter()
        .map(|m| m["title"].as_str().unwrap())
        .collect();
    // Sorted by created_at DESC — Berserk Prelude was created last.
    assert_eq!(titles, vec!["Berserk Prelude", "Berserk"]);
    assert_eq!(body["page"]["total"], 2);
}

#[sqlx::test(migrations = "./migrations")]
async fn list_mangas_paginates(pool: PgPool) {
    let h = common::harness(pool);
    let (_, cookie) = common::register_user(&h.app).await;
    let mut author_id = String::new();
    for i in 0..5 {
        let body = create_manga(
            &h.app,
            &cookie,
            json!({ "title": format!("Vol {i}"), "authors": ["Solo"] }),
        )
        .await;
        if author_id.is_empty() {
            author_id = first_author_id(&body);
        }
    }

    let resp = h
        .app
        .oneshot(common::get(&format!(
            "/api/v1/authors/{author_id}/mangas?limit=2&offset=1"
        )))
        .await
        .unwrap();
    let body = common::body_json(resp).await;
    assert_eq!(body["items"].as_array().unwrap().len(), 2);
    assert_eq!(body["page"]["limit"], 2);
    assert_eq!(body["page"]["offset"], 1);
    // Total reflects unfiltered count, not page size.
    assert_eq!(body["page"]["total"], 5);
}

#[sqlx::test(migrations = "./migrations")]
async fn list_mangas_for_unknown_author_returns_empty(pool: PgPool) {
    let h = common::harness(pool);
    let resp = h
        .app
        .oneshot(common::get(&format!(
            "/api/v1/authors/{}/mangas",
            Uuid::new_v4()
        )))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = common::body_json(resp).await;
    assert_eq!(body["items"], json!([]));
    assert_eq!(body["page"]["total"], 0);
}

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

    for name in ["Kentaro Miura", "Eiichiro Oda", "Makoto Yukimura"] {
        let _ = create_manga(
            &h.app,
            &cookie,
            json!({ "title": format!("Book of {name}"), "authors": [name] }),
        )
        .await;
    }

    // Substring is case-insensitive (ILIKE) — "miura" picks Kentaro
    // Miura but NOT Makoto Yukimura (whose surname is Yukimura, not
    // "Yumiura"). The trigram path is exercised in api_mangas.rs.
    let resp = h
        .app
        .clone()
        .oneshot(common::get("/api/v1/authors?search=miura"))
        .await
        .unwrap();
    let body = common::body_json(resp).await;
    let names: Vec<&str> = body
        .as_array()
        .unwrap()
        .iter()
        .map(|a| a["name"].as_str().unwrap())
        .collect();
    assert_eq!(names, vec!["Kentaro Miura"]);

    // "yuki" matches only Makoto Yukimura.
    let resp = h
        .app
        .oneshot(common::get("/api/v1/authors?search=yuki"))
        .await
        .unwrap();
    let body = common::body_json(resp).await;
    let names: Vec<&str> = body
        .as_array()
        .unwrap()
        .iter()
        .map(|a| a["name"].as_str().unwrap())
        .collect();
    assert_eq!(names, vec!["Makoto Yukimura"]);
}

#[sqlx::test(migrations = "./migrations")]
async fn search_authors_default_limit_caps_at_10(pool: PgPool) {
    let h = common::harness(pool);
    let (_, cookie) = common::register_user(&h.app).await;
    for i in 0..15 {
        let _ = create_manga(
            &h.app,
            &cookie,
            json!({ "title": format!("Book {i}"), "authors": [format!("Author {i}")] }),
        )
        .await;
    }

    let resp = h
        .app
        .oneshot(common::get("/api/v1/authors"))
        .await
        .unwrap();
    let body = common::body_json(resp).await;
    // No search term: returns the default page (10), not all 15.
    assert_eq!(body.as_array().unwrap().len(), 10);
}

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

    let m1 = create_manga(
        &h.app,
        &cookie,
        json!({ "title": "Vol 1", "authors": ["Solo"] }),
    )
    .await;
    let _ = create_manga(
        &h.app,
        &cookie,
        json!({ "title": "Vol 2", "authors": ["Solo"] }),
    )
    .await;
    let author_id = first_author_id(&m1);
    let manga_id = m1["id"].as_str().unwrap().to_string();

    // No DELETE endpoint yet — delete via SQL to simulate cleanup.
    sqlx::query("DELETE FROM mangas WHERE id = $1")
        .bind(Uuid::parse_str(&manga_id).unwrap())
        .execute(&pool)
        .await
        .unwrap();

    let resp = h
        .app
        .oneshot(common::get(&format!("/api/v1/authors/{author_id}")))
        .await
        .unwrap();
    let body = common::body_json(resp).await;
    // FK cascade on manga_authors keeps the count honest.
    assert_eq!(body["manga_count"], 1);
}
