mod common;

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

use common::{
    body_json, delete_with_cookie, fake_jpeg_bytes, fake_png_bytes, get, harness,
    post_multipart_with_cookie, put_multipart, put_multipart_with_cookie, register_user,
    MultipartBuilder,
};

async fn create_manga_with_cover(
    app: &axum::Router,
    cookie: &str,
    title: &str,
    cover: Option<(&str, &[u8])>,
) -> Value {
    let mut form =
        MultipartBuilder::new().add_json("metadata", json!({ "title": title }));
    if let Some((ct, bytes)) = cover {
        form = form.add_file("cover", "cover.bin", ct, bytes);
    }
    let resp = app
        .clone()
        .oneshot(post_multipart_with_cookie("/api/v1/mangas", form, cookie))
        .await
        .unwrap();
    assert_eq!(
        resp.status(),
        StatusCode::CREATED,
        "seed create_manga failed: {:?}",
        resp.status()
    );
    body_json(resp).await
}

fn id_of(body: &Value) -> Uuid {
    Uuid::parse_str(body["id"].as_str().unwrap()).unwrap()
}

fn cover_form(bytes: &[u8]) -> MultipartBuilder {
    MultipartBuilder::new().add_file("cover", "cover.bin", "application/octet-stream", bytes)
}

#[sqlx::test(migrations = "./migrations")]
async fn put_cover_sets_path_when_none_existed(pool: PgPool) {
    let h = harness(pool);
    let (_, cookie) = register_user(&h.app).await;
    let manga = create_manga_with_cover(&h.app, &cookie, "Cover Me", None).await;
    let id = id_of(&manga);
    assert!(manga["cover_image_path"].is_null());

    let bytes = fake_png_bytes();
    let resp = h
        .app
        .clone()
        .oneshot(put_multipart_with_cookie(
            &format!("/api/v1/mangas/{id}/cover"),
            cover_form(&bytes),
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = body_json(resp).await;
    let expected_key = format!("mangas/{id}/cover.png");
    assert_eq!(body["cover_image_path"], expected_key);
    assert_eq!(body["title"], "Cover Me");

    let file_resp = h
        .app
        .clone()
        .oneshot(get(&format!("/api/v1/files/{expected_key}")))
        .await
        .unwrap();
    assert_eq!(file_resp.status(), StatusCode::OK);
}

#[sqlx::test(migrations = "./migrations")]
async fn put_cover_replaces_existing_same_extension(pool: PgPool) {
    let h = harness(pool);
    let (_, cookie) = register_user(&h.app).await;
    let original = fake_png_bytes();
    let manga = create_manga_with_cover(
        &h.app,
        &cookie,
        "Replace Me",
        Some(("image/png", &original)),
    )
    .await;
    let id = id_of(&manga);
    let original_key = format!("mangas/{id}/cover.png");
    assert_eq!(manga["cover_image_path"], original_key);

    let mut replacement = fake_png_bytes();
    replacement.extend_from_slice(b"-replacement-marker");
    let resp = h
        .app
        .clone()
        .oneshot(put_multipart_with_cookie(
            &format!("/api/v1/mangas/{id}/cover"),
            cover_form(&replacement),
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = body_json(resp).await;
    assert_eq!(body["cover_image_path"], original_key);

    let file_resp = h
        .app
        .clone()
        .oneshot(get(&format!("/api/v1/files/{original_key}")))
        .await
        .unwrap();
    assert_eq!(file_resp.status(), StatusCode::OK);
    let body_bytes = http_body_util::BodyExt::collect(file_resp.into_body())
        .await
        .unwrap()
        .to_bytes();
    assert_eq!(body_bytes.as_ref(), replacement.as_slice());
}

#[sqlx::test(migrations = "./migrations")]
async fn put_cover_replaces_existing_different_extension_and_deletes_old_blob(pool: PgPool) {
    let h = harness(pool);
    let (_, cookie) = register_user(&h.app).await;
    let png = fake_png_bytes();
    let manga = create_manga_with_cover(
        &h.app,
        &cookie,
        "Switch Ext",
        Some(("image/png", &png)),
    )
    .await;
    let id = id_of(&manga);
    let old_key = format!("mangas/{id}/cover.png");
    assert_eq!(manga["cover_image_path"], old_key);

    let jpeg = fake_jpeg_bytes();
    let resp = h
        .app
        .clone()
        .oneshot(put_multipart_with_cookie(
            &format!("/api/v1/mangas/{id}/cover"),
            cover_form(&jpeg),
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = body_json(resp).await;
    let new_key = format!("mangas/{id}/cover.jpg");
    assert_eq!(body["cover_image_path"], new_key);

    let new_file = h
        .app
        .clone()
        .oneshot(get(&format!("/api/v1/files/{new_key}")))
        .await
        .unwrap();
    assert_eq!(new_file.status(), StatusCode::OK);

    let old_file = h
        .app
        .clone()
        .oneshot(get(&format!("/api/v1/files/{old_key}")))
        .await
        .unwrap();
    assert_eq!(old_file.status(), StatusCode::NOT_FOUND);
}

#[sqlx::test(migrations = "./migrations")]
async fn put_cover_rejects_unauthenticated(pool: PgPool) {
    let h = harness(pool);
    let (_, cookie) = register_user(&h.app).await;
    let manga = create_manga_with_cover(&h.app, &cookie, "Public Read", None).await;
    let id = id_of(&manga);

    let resp = h
        .app
        .clone()
        .oneshot(put_multipart(
            &format!("/api/v1/mangas/{id}/cover"),
            cover_form(&fake_png_bytes()),
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}

#[sqlx::test(migrations = "./migrations")]
async fn put_cover_404_on_unknown_id(pool: PgPool) {
    let h = harness(pool);
    let (_, cookie) = register_user(&h.app).await;
    let id = Uuid::new_v4();
    let resp = h
        .app
        .clone()
        .oneshot(put_multipart_with_cookie(
            &format!("/api/v1/mangas/{id}/cover"),
            cover_form(&fake_png_bytes()),
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}

#[sqlx::test(migrations = "./migrations")]
async fn put_cover_rejects_non_image_with_unsupported_media_type(pool: PgPool) {
    let h = harness(pool);
    let (_, cookie) = register_user(&h.app).await;
    let manga = create_manga_with_cover(&h.app, &cookie, "Not Image", None).await;
    let id = id_of(&manga);

    let pdf = b"%PDF-1.4\n%\xc4\xe5".to_vec();
    let resp = h
        .app
        .clone()
        .oneshot(put_multipart_with_cookie(
            &format!("/api/v1/mangas/{id}/cover"),
            cover_form(&pdf),
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
    let body = body_json(resp).await;
    assert_eq!(body["error"]["code"], "unsupported_media_type");
}

#[sqlx::test(migrations = "./migrations")]
async fn put_cover_rejects_oversized(pool: PgPool) {
    let h = harness(pool);
    let (_, cookie) = register_user(&h.app).await;
    let manga = create_manga_with_cover(&h.app, &cookie, "Too Big", None).await;
    let id = id_of(&manga);

    // Harness max_file_bytes is 256 KiB; 300 KiB trips the cap.
    let mut bytes = fake_png_bytes();
    bytes.resize(300 * 1024, 0);
    let resp = h
        .app
        .clone()
        .oneshot(put_multipart_with_cookie(
            &format!("/api/v1/mangas/{id}/cover"),
            cover_form(&bytes),
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
}

#[sqlx::test(migrations = "./migrations")]
async fn put_cover_rejects_missing_cover_part(pool: PgPool) {
    let h = harness(pool);
    let (_, cookie) = register_user(&h.app).await;
    let manga = create_manga_with_cover(&h.app, &cookie, "Empty Form", None).await;
    let id = id_of(&manga);

    let resp = h
        .app
        .clone()
        .oneshot(put_multipart_with_cookie(
            &format!("/api/v1/mangas/{id}/cover"),
            MultipartBuilder::new(),
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::UNPROCESSABLE_ENTITY);
    let body = body_json(resp).await;
    assert_eq!(body["error"]["code"], "validation_failed");
}

#[sqlx::test(migrations = "./migrations")]
async fn put_cover_preserves_other_metadata(pool: PgPool) {
    let h = harness(pool);
    let (_, cookie) = register_user(&h.app).await;
    let manga = create_manga_with_cover(
        &h.app,
        &cookie,
        "Keep My Fields",
        None,
    )
    .await;
    let id = id_of(&manga);

    let resp = h
        .app
        .clone()
        .oneshot(put_multipart_with_cookie(
            &format!("/api/v1/mangas/{id}/cover"),
            cover_form(&fake_png_bytes()),
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = body_json(resp).await;
    assert_eq!(body["title"], "Keep My Fields");
    assert_eq!(body["status"], "ongoing");
    assert_eq!(body["authors"], json!([]));
    assert_eq!(body["genres"], json!([]));
    assert_eq!(body["tags"], json!([]));
}

#[sqlx::test(migrations = "./migrations")]
async fn delete_cover_clears_path_and_removes_blob(pool: PgPool) {
    let h = harness(pool);
    let (_, cookie) = register_user(&h.app).await;
    let png = fake_png_bytes();
    let manga = create_manga_with_cover(
        &h.app,
        &cookie,
        "Bye Cover",
        Some(("image/png", &png)),
    )
    .await;
    let id = id_of(&manga);
    let key = format!("mangas/{id}/cover.png");

    let resp = h
        .app
        .clone()
        .oneshot(delete_with_cookie(
            &format!("/api/v1/mangas/{id}/cover"),
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = body_json(resp).await;
    assert!(body["cover_image_path"].is_null());
    assert_eq!(body["title"], "Bye Cover");

    let file_resp = h
        .app
        .clone()
        .oneshot(get(&format!("/api/v1/files/{key}")))
        .await
        .unwrap();
    assert_eq!(file_resp.status(), StatusCode::NOT_FOUND);
}

#[sqlx::test(migrations = "./migrations")]
async fn delete_cover_is_idempotent_when_no_cover_present(pool: PgPool) {
    let h = harness(pool);
    let (_, cookie) = register_user(&h.app).await;
    let manga = create_manga_with_cover(&h.app, &cookie, "Never Had One", None).await;
    let id = id_of(&manga);

    for _ in 0..2 {
        let resp = h
            .app
            .clone()
            .oneshot(delete_with_cookie(
                &format!("/api/v1/mangas/{id}/cover"),
                &cookie,
            ))
            .await
            .unwrap();
        assert_eq!(resp.status(), StatusCode::OK);
        let body = body_json(resp).await;
        assert!(body["cover_image_path"].is_null());
    }
}

#[sqlx::test(migrations = "./migrations")]
async fn delete_cover_rejects_unauthenticated(pool: PgPool) {
    let h = harness(pool);
    let (_, cookie) = register_user(&h.app).await;
    let manga = create_manga_with_cover(&h.app, &cookie, "Locked", None).await;
    let id = id_of(&manga);

    let resp = h
        .app
        .clone()
        .oneshot(
            axum::http::Request::builder()
                .method("DELETE")
                .uri(format!("/api/v1/mangas/{id}/cover"))
                .body(axum::body::Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}

#[sqlx::test(migrations = "./migrations")]
async fn delete_cover_404_on_unknown_id(pool: PgPool) {
    let h = harness(pool);
    let (_, cookie) = register_user(&h.app).await;
    let id = Uuid::new_v4();
    let resp = h
        .app
        .clone()
        .oneshot(delete_with_cookie(
            &format!("/api/v1/mangas/{id}/cover"),
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}

/// Authz: PUT /mangas/:id/cover must be uploader-only.
#[sqlx::test(migrations = "./migrations")]
async fn put_cover_forbidden_for_non_uploader(pool: PgPool) {
    let h = harness(pool);
    let (_, owner_cookie) = register_user(&h.app).await;
    let (_, intruder_cookie) = register_user(&h.app).await;

    let manga =
        create_manga_with_cover(&h.app, &owner_cookie, "Mine", None).await;
    let id = id_of(&manga);

    let resp = h
        .app
        .oneshot(put_multipart_with_cookie(
            &format!("/api/v1/mangas/{id}/cover"),
            cover_form(&fake_png_bytes()),
            &intruder_cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::FORBIDDEN);
}

/// Authz: DELETE /mangas/:id/cover must be uploader-only.
#[sqlx::test(migrations = "./migrations")]
async fn delete_cover_forbidden_for_non_uploader(pool: PgPool) {
    let h = harness(pool);
    let (_, owner_cookie) = register_user(&h.app).await;
    let (_, intruder_cookie) = register_user(&h.app).await;

    let manga = create_manga_with_cover(
        &h.app,
        &owner_cookie,
        "Mine",
        Some(("image/jpeg", &fake_jpeg_bytes())),
    )
    .await;
    let id = id_of(&manga);

    let resp = h
        .app
        .oneshot(delete_with_cookie(
            &format!("/api/v1/mangas/{id}/cover"),
            &intruder_cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::FORBIDDEN);
}
