mod common;

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

#[sqlx::test(migrations = "./migrations")]
async fn get_returns_defaults_for_new_user(pool: PgPool) {
    let h = common::harness(pool);
    let (_, cookie) = common::register_user(&h.app).await;
    let resp = h
        .app
        .oneshot(common::get_with_cookie("/api/v1/auth/me/preferences", &cookie))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = common::body_json(resp).await;
    assert_eq!(body["reader_mode"], "single");
    assert_eq!(body["reader_page_gap"], "none");
    assert!(body.get("updated_at").is_some());
    // user_id is server-internal — must not leak.
    assert!(body.get("user_id").is_none());
}

#[sqlx::test(migrations = "./migrations")]
async fn patch_persists_mode_and_gap(pool: PgPool) {
    let h = common::harness(pool);
    let (_, cookie) = common::register_user(&h.app).await;
    let resp = h
        .app
        .clone()
        .oneshot(common::patch_json_with_cookie(
            "/api/v1/auth/me/preferences",
            json!({ "reader_mode": "continuous", "reader_page_gap": "large" }),
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = common::body_json(resp).await;
    assert_eq!(body["reader_mode"], "continuous");
    assert_eq!(body["reader_page_gap"], "large");

    // A fresh GET reads the persisted values.
    let resp = h
        .app
        .oneshot(common::get_with_cookie("/api/v1/auth/me/preferences", &cookie))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = common::body_json(resp).await;
    assert_eq!(body["reader_mode"], "continuous");
    assert_eq!(body["reader_page_gap"], "large");
}

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

    // Seed both fields away from their defaults.
    let _ = h
        .app
        .clone()
        .oneshot(common::patch_json_with_cookie(
            "/api/v1/auth/me/preferences",
            json!({ "reader_mode": "continuous", "reader_page_gap": "medium" }),
            &cookie,
        ))
        .await
        .unwrap();

    // PATCH only the mode — the gap must not regress to default.
    let resp = h
        .app
        .clone()
        .oneshot(common::patch_json_with_cookie(
            "/api/v1/auth/me/preferences",
            json!({ "reader_mode": "single" }),
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = common::body_json(resp).await;
    assert_eq!(body["reader_mode"], "single");
    assert_eq!(body["reader_page_gap"], "medium");
}

#[sqlx::test(migrations = "./migrations")]
async fn patch_rejects_invalid_mode(pool: PgPool) {
    let h = common::harness(pool);
    let (_, cookie) = common::register_user(&h.app).await;
    let resp = h
        .app
        .oneshot(common::patch_json_with_cookie(
            "/api/v1/auth/me/preferences",
            json!({ "reader_mode": "tachiyomi" }),
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
    let body = common::body_json(resp).await;
    assert_eq!(body["error"]["code"], "invalid_input");
}

#[sqlx::test(migrations = "./migrations")]
async fn patch_rejects_invalid_gap(pool: PgPool) {
    let h = common::harness(pool);
    let (_, cookie) = common::register_user(&h.app).await;
    let resp = h
        .app
        .oneshot(common::patch_json_with_cookie(
            "/api/v1/auth/me/preferences",
            json!({ "reader_page_gap": "huge" }),
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
    let body = common::body_json(resp).await;
    assert_eq!(body["error"]["code"], "invalid_input");
}

#[sqlx::test(migrations = "./migrations")]
async fn requires_authentication(pool: PgPool) {
    let h = common::harness(pool);

    let resp = h
        .app
        .clone()
        .oneshot(common::get("/api/v1/auth/me/preferences"))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);

    let resp = h
        .app
        .oneshot(common::patch_json(
            "/api/v1/auth/me/preferences",
            json!({ "reader_mode": "continuous" }),
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}

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

    // A switches to continuous + large; B is untouched and must keep defaults.
    let _ = h
        .app
        .clone()
        .oneshot(common::patch_json_with_cookie(
            "/api/v1/auth/me/preferences",
            json!({ "reader_mode": "continuous", "reader_page_gap": "large" }),
            &cookie_a,
        ))
        .await
        .unwrap();

    let resp_b = h
        .app
        .clone()
        .oneshot(common::get_with_cookie("/api/v1/auth/me/preferences", &cookie_b))
        .await
        .unwrap();
    let body_b = common::body_json(resp_b).await;
    assert_eq!(body_b["reader_mode"], "single");
    assert_eq!(body_b["reader_page_gap"], "none");

    // A still sees their own update.
    let resp_a = h
        .app
        .oneshot(common::get_with_cookie("/api/v1/auth/me/preferences", &cookie_a))
        .await
        .unwrap();
    let body_a = common::body_json(resp_a).await;
    assert_eq!(body_a["reader_mode"], "continuous");
    assert_eq!(body_a["reader_page_gap"], "large");
}

/// Direct repo + SQL: a deleted user must cascade-delete their preferences
/// row. Guards against accidental loss of `ON DELETE CASCADE` in future
/// migrations.
#[sqlx::test(migrations = "./migrations")]
async fn preferences_cascade_on_user_delete(pool: PgPool) {
    use mangalord::repo;

    // Insert a user directly so we don't need the API harness here.
    let user_id: uuid::Uuid = sqlx::query_scalar(
        "INSERT INTO users (username, password_hash) VALUES ($1, $2) RETURNING id",
    )
    .bind("cascade-test")
    .bind("not-a-real-hash")
    .fetch_one(&pool)
    .await
    .unwrap();

    repo::user_preferences::upsert(&pool, user_id, "continuous", "small")
        .await
        .unwrap();
    assert!(repo::user_preferences::find(&pool, user_id)
        .await
        .unwrap()
        .is_some());

    sqlx::query("DELETE FROM users WHERE id = $1")
        .bind(user_id)
        .execute(&pool)
        .await
        .unwrap();

    assert!(repo::user_preferences::find(&pool, user_id)
        .await
        .unwrap()
        .is_none());
}
