//! Integration tests for the page-level collection endpoints:
//!
//! - `POST   /v1/collections/:id/pages`
//! - `DELETE /v1/collections/:id/pages/:page_id`
//! - `GET    /v1/collections/:id/pages`
//! - `GET    /v1/pages/:id/my-collections`
//!
//! These exercise migration 0023's `collection_pages` table end-to-end,
//! plus the owner-only / 404-non-existence-leak / idempotency / cascade
//! invariants that mirror `api_collections.rs`.

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_collection(app: &axum::Router, cookie: &str, name: &str) -> Value {
    let resp = app
        .clone()
        .oneshot(common::post_json_with_cookie(
            "/api/v1/collections",
            json!({ "name": name }),
            cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::CREATED);
    common::body_json(resp).await
}

/// Create a chapter with a single page so we have a real `pages.id` to
/// attach to. Returns `(chapter_id, page_id)`.
async fn seed_chapter_with_page(
    app: &axum::Router,
    cookie: &str,
    manga_id: Uuid,
    number: i32,
) -> (String, String) {
    let resp = app
        .clone()
        .oneshot(common::post_multipart_with_cookie(
            &format!("/api/v1/mangas/{manga_id}/chapters"),
            MultipartBuilder::new()
                .add_json("metadata", json!({ "number": number }))
                .add_file("page", "1.png", "image/png", &common::fake_png_bytes()),
            cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::CREATED);
    let chapter = common::body_json(resp).await;
    let chapter_id = chapter["id"].as_str().unwrap().to_string();

    let resp = app
        .clone()
        .oneshot(common::get_with_cookie(
            &format!("/api/v1/mangas/{manga_id}/chapters/{chapter_id}/pages"),
            cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = common::body_json(resp).await;
    let page_id = body["pages"][0]["id"].as_str().unwrap().to_string();
    (chapter_id, page_id)
}

fn id_of(v: &Value) -> String {
    v["id"].as_str().unwrap().to_string()
}

#[sqlx::test(migrations = "./migrations")]
async fn add_page_then_list_returns_breadcrumb(pool: PgPool) {
    let h = common::harness(pool);
    let (_, cookie) = common::register_user(&h.app).await;
    let manga_id = common::seed_manga_via_api(&h.app, &cookie, "Berserk").await;
    let (chapter_id, page_id) = seed_chapter_with_page(&h.app, &cookie, manga_id, 1).await;
    let coll = create_collection(&h.app, &cookie, "Favorite panels").await;
    let coll_id = id_of(&coll);

    let resp = h
        .app
        .clone()
        .oneshot(common::post_json_with_cookie(
            &format!("/api/v1/collections/{coll_id}/pages"),
            json!({ "page_id": page_id }),
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::CREATED);

    let resp = h
        .app
        .oneshot(common::get_with_cookie(
            &format!("/api/v1/collections/{coll_id}/pages"),
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = common::body_json(resp).await;
    let items = body["items"].as_array().unwrap();
    assert_eq!(items.len(), 1);
    let item = &items[0];
    assert_eq!(item["page_id"], page_id);
    assert_eq!(item["chapter_id"], chapter_id);
    assert_eq!(item["manga_id"], manga_id.to_string());
    assert_eq!(item["manga_title"], "Berserk");
    assert_eq!(item["chapter_number"], 1);
    assert_eq!(item["page_number"], 1);
    assert!(
        item["storage_key"].as_str().unwrap().starts_with(&format!(
            "mangas/{manga_id}/chapters/{chapter_id}/pages/"
        )),
        "unexpected storage_key: {}",
        item["storage_key"]
    );
}

#[sqlx::test(migrations = "./migrations")]
async fn add_page_is_idempotent_and_picks_201_then_200(pool: PgPool) {
    let h = common::harness(pool);
    let (_, cookie) = common::register_user(&h.app).await;
    let manga_id = common::seed_manga_via_api(&h.app, &cookie, "M").await;
    let (_, page_id) = seed_chapter_with_page(&h.app, &cookie, manga_id, 1).await;
    let coll = create_collection(&h.app, &cookie, "C").await;
    let coll_id = id_of(&coll);

    let req = || {
        common::post_json_with_cookie(
            &format!("/api/v1/collections/{coll_id}/pages"),
            json!({ "page_id": page_id }),
            &cookie,
        )
    };
    let first = h.app.clone().oneshot(req()).await.unwrap();
    assert_eq!(first.status(), StatusCode::CREATED);
    let second = h.app.oneshot(req()).await.unwrap();
    assert_eq!(second.status(), StatusCode::OK);
}

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

    let resp = h
        .app
        .oneshot(common::post_json_with_cookie(
            &format!("/api/v1/collections/{coll_id}/pages"),
            json!({ "page_id": Uuid::new_v4().to_string() }),
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}

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

    let manga_id = common::seed_manga_via_api(&h.app, &b, "M").await;
    let (_, page_id) = seed_chapter_with_page(&h.app, &b, manga_id, 1).await;
    let coll_a = create_collection(&h.app, &a, "A's").await;
    let coll_a_id = id_of(&coll_a);

    let resp = h
        .app
        .oneshot(common::post_json_with_cookie(
            &format!("/api/v1/collections/{coll_a_id}/pages"),
            json!({ "page_id": page_id }),
            &b,
        ))
        .await
        .unwrap();
    // Same non-leak semantic as the manga-level handler.
    assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}

#[sqlx::test(migrations = "./migrations")]
async fn remove_page_is_idempotent(pool: PgPool) {
    let h = common::harness(pool);
    let (_, cookie) = common::register_user(&h.app).await;
    let manga_id = common::seed_manga_via_api(&h.app, &cookie, "M").await;
    let (_, page_id) = seed_chapter_with_page(&h.app, &cookie, manga_id, 1).await;
    let coll = create_collection(&h.app, &cookie, "C").await;
    let coll_id = id_of(&coll);

    let _ = h
        .app
        .clone()
        .oneshot(common::post_json_with_cookie(
            &format!("/api/v1/collections/{coll_id}/pages"),
            json!({ "page_id": page_id }),
            &cookie,
        ))
        .await
        .unwrap();

    let first = h
        .app
        .clone()
        .oneshot(common::delete_with_cookie(
            &format!("/api/v1/collections/{coll_id}/pages/{page_id}"),
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(first.status(), StatusCode::NO_CONTENT);
    let second = h
        .app
        .oneshot(common::delete_with_cookie(
            &format!("/api/v1/collections/{coll_id}/pages/{page_id}"),
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(second.status(), StatusCode::NO_CONTENT);
}

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

    let manga_id = common::seed_manga_via_api(&h.app, &a, "M").await;
    let (_, page_id) = seed_chapter_with_page(&h.app, &a, manga_id, 1).await;

    let a_coll = create_collection(&h.app, &a, "A").await;
    let b_coll = create_collection(&h.app, &b, "B").await;
    let a_coll_id = id_of(&a_coll);
    let b_coll_id = id_of(&b_coll);

    for (coll, cookie) in [(&a_coll_id, &a), (&b_coll_id, &b)] {
        let _ = h
            .app
            .clone()
            .oneshot(common::post_json_with_cookie(
                &format!("/api/v1/collections/{coll}/pages"),
                json!({ "page_id": page_id }),
                cookie,
            ))
            .await
            .unwrap();
    }

    let resp = h
        .app
        .oneshot(common::get_with_cookie(
            &format!("/api/v1/pages/{page_id}/my-collections"),
            &a,
        ))
        .await
        .unwrap();
    let body = common::body_json(resp).await;
    let ids: Vec<&str> = body["collection_ids"]
        .as_array()
        .unwrap()
        .iter()
        .map(|v| v.as_str().unwrap())
        .collect();
    assert_eq!(ids, vec![a_coll_id.as_str()]);
}

#[sqlx::test(migrations = "./migrations")]
async fn my_collections_for_unknown_page_returns_empty_list(pool: PgPool) {
    let h = common::harness(pool);
    let (_, cookie) = common::register_user(&h.app).await;
    let resp = h
        .app
        .oneshot(common::get_with_cookie(
            &format!("/api/v1/pages/{}/my-collections", Uuid::new_v4()),
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = common::body_json(resp).await;
    assert_eq!(body["collection_ids"], json!([]));
}

#[sqlx::test(migrations = "./migrations")]
async fn add_page_requires_authentication(pool: PgPool) {
    let h = common::harness(pool);
    let (_, cookie) = common::register_user(&h.app).await;
    let manga_id = common::seed_manga_via_api(&h.app, &cookie, "M").await;
    let (_, page_id) = seed_chapter_with_page(&h.app, &cookie, manga_id, 1).await;
    let coll = create_collection(&h.app, &cookie, "C").await;
    let coll_id = id_of(&coll);

    let resp = h
        .app
        .oneshot(common::post_json(
            &format!("/api/v1/collections/{coll_id}/pages"),
            json!({ "page_id": page_id }),
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}

#[sqlx::test(migrations = "./migrations")]
async fn list_pages_in_others_collection_is_404(pool: PgPool) {
    let h = common::harness(pool);
    let (_, a) = common::register_user(&h.app).await;
    let (_, b) = common::register_user(&h.app).await;
    let coll = create_collection(&h.app, &a, "Mine").await;
    let coll_id = id_of(&coll);

    let resp = h
        .app
        .oneshot(common::get_with_cookie(
            &format!("/api/v1/collections/{coll_id}/pages"),
            &b,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
