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 seed_chapter(app: &axum::Router, cookie: &str, manga_id: Uuid, number: i32) -> 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 body = common::body_json(resp).await;
    body["id"].as_str().unwrap().to_string()
}

async fn upsert_progress(
    app: &axum::Router,
    cookie: &str,
    body: Value,
) -> Value {
    let resp = app
        .clone()
        .oneshot(common::put_json_with_cookie(
"/api/v1/me/read-progress",
            body,
            cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK, "upsert failed: {:?}", resp.status());
    common::body_json(resp).await
}

#[sqlx::test(migrations = "./migrations")]
async fn upsert_creates_then_overwrites(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 = seed_chapter(&h.app, &cookie, manga_id, 1).await;

    let first = upsert_progress(
        &h.app,
        &cookie,
        json!({ "manga_id": manga_id.to_string(), "chapter_id": chapter_id, "page": 5 }),
    )
    .await;
    assert_eq!(first["manga_id"], manga_id.to_string());
    assert_eq!(first["page"], 5);

    // A second upsert overwrites the page even when it moves backwards
    // — re-reading scenarios just take the latest write.
    let second = upsert_progress(
        &h.app,
        &cookie,
        json!({ "manga_id": manga_id.to_string(), "chapter_id": chapter_id, "page": 1 }),
    )
    .await;
    assert_eq!(second["page"], 1);
}

#[sqlx::test(migrations = "./migrations")]
async fn upsert_with_unknown_manga_is_404(pool: PgPool) {
    let h = common::harness(pool);
    let (_, cookie) = common::register_user(&h.app).await;
    let resp = h
        .app
        .oneshot(common::put_json_with_cookie(
"/api/v1/me/read-progress",
            json!({ "manga_id": Uuid::new_v4().to_string(), "page": 1 }),
            &cookie,
        ))
        .await
        .unwrap();
    // The FK violation in repo::upsert is mapped to NotFound.
    assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}

#[sqlx::test(migrations = "./migrations")]
async fn upsert_with_page_zero_is_422(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 resp = h
        .app
        .oneshot(common::put_json_with_cookie(
"/api/v1/me/read-progress",
            json!({ "manga_id": manga_id.to_string(), "page": 0 }),
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::UNPROCESSABLE_ENTITY);
}

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

    let _ = upsert_progress(
        &h.app,
        &cookie,
        json!({ "manga_id": m1.to_string(), "page": 1 }),
    )
    .await;
    let _ = upsert_progress(
        &h.app,
        &cookie,
        json!({ "manga_id": m2.to_string(), "page": 1 }),
    )
    .await;

    let resp = h
        .app
        .oneshot(common::get_with_cookie("/api/v1/me/read-progress", &cookie))
        .await
        .unwrap();
    let body = common::body_json(resp).await;
    let titles: Vec<&str> = body["items"]
        .as_array()
        .unwrap()
        .iter()
        .map(|r| r["manga_title"].as_str().unwrap())
        .collect();
    // Second was upserted last → it surfaces first.
    assert_eq!(titles, vec!["Second", "First"]);
    assert_eq!(body["page"]["total"], 2);
}

#[sqlx::test(migrations = "./migrations")]
async fn list_is_per_user_only(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, "Berserk").await;
    let _ = upsert_progress(
        &h.app,
        &a,
        json!({ "manga_id": manga_id.to_string(), "page": 7 }),
    )
    .await;

    let resp = h
        .app
        .oneshot(common::get_with_cookie("/api/v1/me/read-progress", &b))
        .await
        .unwrap();
    let body = common::body_json(resp).await;
    assert_eq!(body["items"], json!([]));
}

#[sqlx::test(migrations = "./migrations")]
async fn list_reports_new_chapters_since_last_read(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;
    // Three chapters exist; the reader is on chapter 1, so chapters 2 and
    // 3 are "new since last read".
    let ch1 = seed_chapter(&h.app, &cookie, manga_id, 1).await;
    let _ = seed_chapter(&h.app, &cookie, manga_id, 2).await;
    let _ = seed_chapter(&h.app, &cookie, manga_id, 3).await;
    let _ = upsert_progress(
        &h.app,
        &cookie,
        json!({ "manga_id": manga_id.to_string(), "chapter_id": ch1, "page": 1 }),
    )
    .await;

    let resp = h
        .app
        .clone()
        .oneshot(common::get_with_cookie("/api/v1/me/read-progress", &cookie))
        .await
        .unwrap();
    let body = common::body_json(resp).await;
    assert_eq!(body["items"][0]["new_chapters_count"], 2);
}

#[sqlx::test(migrations = "./migrations")]
async fn list_reports_last_read_chapter_page_count(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;
    // seed_chapter uploads a single page, so page_count is 1.
    let ch1 = seed_chapter(&h.app, &cookie, manga_id, 1).await;
    let _ = upsert_progress(
        &h.app,
        &cookie,
        json!({ "manga_id": manga_id.to_string(), "chapter_id": ch1, "page": 1 }),
    )
    .await;

    let resp = h
        .app
        .clone()
        .oneshot(common::get_with_cookie("/api/v1/me/read-progress", &cookie))
        .await
        .unwrap();
    let body = common::body_json(resp).await;
    // Exposes the last-read chapter's page count so a client can tell a
    // finished series (on the last page) from one still in progress.
    assert_eq!(body["items"][0]["chapter_page_count"], 1);
}

#[sqlx::test(migrations = "./migrations")]
async fn list_new_chapters_count_is_zero_at_latest_chapter(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 _ = seed_chapter(&h.app, &cookie, manga_id, 1).await;
    let ch2 = seed_chapter(&h.app, &cookie, manga_id, 2).await;
    // Caught up to the last chapter → nothing newer.
    let _ = upsert_progress(
        &h.app,
        &cookie,
        json!({ "manga_id": manga_id.to_string(), "chapter_id": ch2, "page": 1 }),
    )
    .await;

    let resp = h
        .app
        .clone()
        .oneshot(common::get_with_cookie("/api/v1/me/read-progress", &cookie))
        .await
        .unwrap();
    let body = common::body_json(resp).await;
    assert_eq!(body["items"][0]["new_chapters_count"], 0);
}

#[sqlx::test(migrations = "./migrations")]
async fn list_new_chapters_count_is_zero_without_a_read_chapter(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 _ = seed_chapter(&h.app, &cookie, manga_id, 1).await;
    let _ = seed_chapter(&h.app, &cookie, manga_id, 2).await;
    // Progress recorded without a chapter (manga-level) — we can't place
    // the reader among the chapters, so we don't claim any are "new".
    let _ = upsert_progress(
        &h.app,
        &cookie,
        json!({ "manga_id": manga_id.to_string(), "page": 1 }),
    )
    .await;

    let resp = h
        .app
        .clone()
        .oneshot(common::get_with_cookie("/api/v1/me/read-progress", &cookie))
        .await
        .unwrap();
    let body = common::body_json(resp).await;
    assert_eq!(body["items"][0]["new_chapters_count"], 0);
}

#[sqlx::test(migrations = "./migrations")]
async fn list_new_chapters_count_dedupes_same_numbered_scanlations(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;
    // (manga_id, number) is non-unique — two scanlations of chapter 2.
    // "New since last read" should count distinct chapter numbers past the
    // reader (2, 3 → 2), not raw rows (2, 2, 3 → 3).
    let ch1 = seed_chapter(&h.app, &cookie, manga_id, 1).await;
    let _ = seed_chapter(&h.app, &cookie, manga_id, 2).await;
    let _ = seed_chapter(&h.app, &cookie, manga_id, 2).await;
    let _ = seed_chapter(&h.app, &cookie, manga_id, 3).await;
    let _ = upsert_progress(
        &h.app,
        &cookie,
        json!({ "manga_id": manga_id.to_string(), "chapter_id": ch1, "page": 1 }),
    )
    .await;

    let resp = h
        .app
        .clone()
        .oneshot(common::get_with_cookie("/api/v1/me/read-progress", &cookie))
        .await
        .unwrap();
    let body = common::body_json(resp).await;
    assert_eq!(body["items"][0]["new_chapters_count"], 2);
}

#[sqlx::test(migrations = "./migrations")]
async fn get_single_manga_reports_new_chapters_count(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 ch1 = seed_chapter(&h.app, &cookie, manga_id, 1).await;
    let _ = seed_chapter(&h.app, &cookie, manga_id, 2).await;
    // Duplicate scanlation of chapter 3 must not inflate the count.
    let _ = seed_chapter(&h.app, &cookie, manga_id, 3).await;
    let _ = seed_chapter(&h.app, &cookie, manga_id, 3).await;
    let _ = upsert_progress(
        &h.app,
        &cookie,
        json!({ "manga_id": manga_id.to_string(), "chapter_id": ch1, "page": 1 }),
    )
    .await;

    let resp = h
        .app
        .clone()
        .oneshot(common::get_with_cookie(
            &format!("/api/v1/me/read-progress/{manga_id}"),
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = common::body_json(resp).await;
    // Distinct numbers past chapter 1: {2, 3} → 2.
    assert_eq!(body["new_chapters_count"], 2);
}

#[sqlx::test(migrations = "./migrations")]
async fn get_single_manga_new_chapters_count_zero_without_read_chapter(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 _ = seed_chapter(&h.app, &cookie, manga_id, 1).await;
    let _ = seed_chapter(&h.app, &cookie, manga_id, 2).await;
    // Manga-level progress (no chapter) → position unknown → 0.
    let _ = upsert_progress(
        &h.app,
        &cookie,
        json!({ "manga_id": manga_id.to_string(), "page": 1 }),
    )
    .await;

    let resp = h
        .app
        .clone()
        .oneshot(common::get_with_cookie(
            &format!("/api/v1/me/read-progress/{manga_id}"),
            &cookie,
        ))
        .await
        .unwrap();
    let body = common::body_json(resp).await;
    assert_eq!(body["new_chapters_count"], 0);
}

#[sqlx::test(migrations = "./migrations")]
async fn get_single_manga_returns_404_when_unread(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 resp = h
        .app
        .oneshot(common::get_with_cookie(
            &format!("/api/v1/me/read-progress/{manga_id}"),
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}

#[sqlx::test(migrations = "./migrations")]
async fn get_single_manga_returns_progress_after_upsert(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 = seed_chapter(&h.app, &cookie, manga_id, 7).await;
    let _ = upsert_progress(
        &h.app,
        &cookie,
        json!({
            "manga_id": manga_id.to_string(),
            "chapter_id": chapter_id,
            "page": 12
        }),
    )
    .await;
    let resp = h
        .app
        .oneshot(common::get_with_cookie(
            &format!("/api/v1/me/read-progress/{manga_id}"),
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = common::body_json(resp).await;
    assert_eq!(body["page"], 12);
    // chapter_number is resolved in the same round-trip so the
    // Continue CTA can render without listing chapters.
    assert_eq!(body["chapter_number"], 7);
    assert_eq!(body["chapter_id"], chapter_id);
}

#[sqlx::test(migrations = "./migrations")]
async fn upsert_rejects_chapter_from_a_different_manga(pool: PgPool) {
    let h = common::harness(pool);
    let (_, cookie) = common::register_user(&h.app).await;
    let manga_a = common::seed_manga_via_api(&h.app, &cookie, "A").await;
    let manga_b = common::seed_manga_via_api(&h.app, &cookie, "B").await;
    let chapter_of_b = seed_chapter(&h.app, &cookie, manga_b, 1).await;

    // Pair manga A with a chapter from manga B — must be rejected.
    let resp = h
        .app
        .oneshot(common::put_json_with_cookie(
            "/api/v1/me/read-progress",
            json!({
                "manga_id": manga_a.to_string(),
                "chapter_id": chapter_of_b,
                "page": 1
            }),
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::UNPROCESSABLE_ENTITY);
    let body = common::body_json(resp).await;
    assert_eq!(body["error"]["code"], "validation_failed");
}

#[sqlx::test(migrations = "./migrations")]
async fn delete_progress_on_never_read_manga_is_204(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, "Untouched").await;
    let resp = h
        .app
        .oneshot(common::delete_with_cookie(
            &format!("/api/v1/me/read-progress/{manga_id}"),
            &cookie,
        ))
        .await
        .unwrap();
    // DELETE is idempotent — clearing nothing is still success.
    assert_eq!(resp.status(), StatusCode::NO_CONTENT);
}

#[sqlx::test(migrations = "./migrations")]
async fn delete_progress_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, "Berserk").await;
    let _ = upsert_progress(
        &h.app,
        &cookie,
        json!({ "manga_id": manga_id.to_string(), "page": 1 }),
    )
    .await;
    for _ in 0..2 {
        let resp = h
            .app
            .clone()
            .oneshot(common::delete_with_cookie(
                &format!("/api/v1/me/read-progress/{manga_id}"),
                &cookie,
            ))
            .await
            .unwrap();
        assert_eq!(resp.status(), StatusCode::NO_CONTENT);
    }
}

#[sqlx::test(migrations = "./migrations")]
async fn deleted_chapter_leaves_progress_row_with_null_chapter(pool: PgPool) {
    let h = common::harness(pool.clone());
    let (_, cookie) = common::register_user(&h.app).await;
    let manga_id = common::seed_manga_via_api(&h.app, &cookie, "Berserk").await;
    let chapter_id_str = seed_chapter(&h.app, &cookie, manga_id, 1).await;
    let chapter_id = Uuid::parse_str(&chapter_id_str).unwrap();
    let _ = upsert_progress(
        &h.app,
        &cookie,
        json!({ "manga_id": manga_id.to_string(), "chapter_id": chapter_id_str, "page": 3 }),
    )
    .await;
    // Delete the chapter directly — the FK ON DELETE SET NULL keeps
    // the progress row but clears chapter_id.
    sqlx::query("DELETE FROM chapters WHERE id = $1")
        .bind(chapter_id)
        .execute(&pool)
        .await
        .unwrap();
    let resp = h
        .app
        .oneshot(common::get_with_cookie("/api/v1/me/read-progress", &cookie))
        .await
        .unwrap();
    let body = common::body_json(resp).await;
    let item = &body["items"][0];
    assert!(item["chapter_id"].is_null(), "chapter_id should be null after cascade");
    assert!(item["chapter_number"].is_null());
}

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

    // Two manga uploads with covers, then a chapter on one of them.
    let m1 = common::seed_manga_via_api(&h.app, &cookie, "Alpha").await;
    let _m2 = common::seed_manga_via_api(&h.app, &cookie, "Beta").await;
    let _ = seed_chapter(&h.app, &cookie, m1, 1).await;

    let resp = h
        .app
        .oneshot(common::get_with_cookie("/api/v1/me/uploads", &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(), 3);
    // Most recent first; the chapter upload happened after both mangas.
    assert_eq!(items[0]["kind"], "chapter");
    assert_eq!(items[1]["kind"], "manga");
    assert_eq!(items[2]["kind"], "manga");
    assert_eq!(body["page"]["total"], 3);
}

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

    let resp = h
        .app
        .oneshot(common::get_with_cookie("/api/v1/me/uploads", &b))
        .await
        .unwrap();
    let body = common::body_json(resp).await;
    assert_eq!(body["items"], json!([]));
    assert_eq!(body["page"]["total"], 0);
}

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

    let (uploaded_by,): (Option<Uuid>,) =
        sqlx::query_as("SELECT uploaded_by FROM mangas WHERE id = $1")
            .bind(manga_id)
            .fetch_one(&pool)
            .await
            .unwrap();
    assert!(uploaded_by.is_some(), "manga.uploaded_by should be set");
}

#[sqlx::test(migrations = "./migrations")]
async fn chapter_create_stamps_uploaded_by_with_current_user(pool: PgPool) {
    let h = common::harness(pool.clone());
    let (_, cookie) = common::register_user(&h.app).await;
    let manga_id = common::seed_manga_via_api(&h.app, &cookie, "Berserk").await;
    let chapter_id_str = seed_chapter(&h.app, &cookie, manga_id, 1).await;

    let (uploaded_by,): (Option<Uuid>,) =
        sqlx::query_as("SELECT uploaded_by FROM chapters WHERE id = $1")
            .bind(Uuid::parse_str(&chapter_id_str).unwrap())
            .fetch_one(&pool)
            .await
            .unwrap();
    assert!(uploaded_by.is_some(), "chapter.uploaded_by should be set");
}

#[sqlx::test(migrations = "./migrations")]
async fn read_progress_requires_authentication(pool: PgPool) {
    let h = common::harness(pool);
    for path in [
        "/api/v1/me/read-progress",
        "/api/v1/me/uploads",
    ] {
        let resp = h
            .app
            .clone()
            .oneshot(common::get(path))
            .await
            .unwrap();
        assert_eq!(resp.status(), StatusCode::UNAUTHORIZED, "{path} should require auth");
    }
}
