//! Site-wide auth gate (`PRIVATE_MODE=true`).
//!
//! With private mode on, every API path except a small allowlist
//! (`/health`, `/auth/config`, `/auth/login`, `/auth/logout`) requires
//! a valid session cookie or bearer token, and `/auth/register` is
//! force-blocked regardless of `ALLOW_SELF_REGISTER`. With private mode
//! off (the default), nothing changes — the `public_mode_*` test
//! pins that regression guard.

mod common;

use serde_json::json;
use sqlx::PgPool;
use tower::ServiceExt;

use axum::http::StatusCode;

#[sqlx::test(migrations = "./migrations")]
async fn private_mode_blocks_anonymous_manga_list(pool: PgPool) {
    let h = common::harness_with_private_mode(pool);
    let resp = h.app.oneshot(common::get("/api/v1/mangas")).await.unwrap();
    assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}

#[sqlx::test(migrations = "./migrations")]
async fn private_mode_blocks_anonymous_files(pool: PgPool) {
    let h = common::harness_with_private_mode(pool);
    // The path doesn't have to exist — the guard runs before routing,
    // so the response is 401 (not 404). That's the property the test
    // is pinning: nothing leaks via crafted URLs.
    let resp = h
        .app
        .oneshot(common::get("/api/v1/files/anything.png"))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}

#[sqlx::test(migrations = "./migrations")]
async fn private_mode_allows_session_cookie_read(pool: PgPool) {
    // Register through a non-private harness sharing the same DB pool
    // so the session row exists. Then exercise the gate using a fresh
    // private-mode harness against the same DB.
    let public = common::harness(pool.clone());
    let (_, cookie) = common::register_user(&public.app).await;

    let private = common::harness_with_private_mode(pool);
    let resp = private
        .app
        .oneshot(common::get_with_cookie("/api/v1/mangas", &cookie))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
}

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

    let resp = public
        .app
        .clone()
        .oneshot(common::post_json_with_cookie(
            "/api/v1/auth/tokens",
            json!({ "name": "private-mode-bot" }),
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::CREATED);
    let body = common::body_json(resp).await;
    let bearer = body["bearer"].as_str().unwrap().to_string();

    let private = common::harness_with_private_mode(pool);
    let resp = private
        .app
        .oneshot(common::get_with_bearer("/api/v1/mangas", &bearer))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
}

#[sqlx::test(migrations = "./migrations")]
async fn private_mode_allows_login_endpoint_anonymous(pool: PgPool) {
    // Seed a user via the public harness so login has credentials to
    // verify against.
    let public = common::harness(pool.clone());
    let _ = public
        .app
        .clone()
        .oneshot(common::post_json(
            "/api/v1/auth/register",
            json!({ "username": "alice", "password": "hunter2hunter2" }),
        ))
        .await
        .unwrap();

    let private = common::harness_with_private_mode(pool);
    let resp = private
        .app
        .oneshot(common::post_json(
            "/api/v1/auth/login",
            json!({ "username": "alice", "password": "hunter2hunter2" }),
        ))
        .await
        .unwrap();
    // Reaches the login handler and succeeds — *not* 401 from the
    // gate. That's the property we're pinning.
    assert_eq!(resp.status(), StatusCode::OK);
}

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

    let r = h
        .app
        .clone()
        .oneshot(common::get("/api/v1/health"))
        .await
        .unwrap();
    assert_eq!(r.status(), StatusCode::OK);

    let r = h
        .app
        .oneshot(common::get("/api/v1/auth/config"))
        .await
        .unwrap();
    assert_eq!(r.status(), StatusCode::OK);
}

#[sqlx::test(migrations = "./migrations")]
async fn private_mode_blocks_register_even_when_self_register_enabled(pool: PgPool) {
    // harness_with_private_mode keeps `allow_self_register=true` (the
    // default) — private mode is supposed to force-block register
    // regardless. That's what this test pins.
    let h = common::harness_with_private_mode(pool);
    let resp = h
        .app
        .oneshot(common::post_json(
            "/api/v1/auth/register",
            json!({ "username": "alice", "password": "hunter2hunter2" }),
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::FORBIDDEN);
    let body = common::body_json(resp).await;
    assert_eq!(body["error"]["code"], "forbidden");
}

#[sqlx::test(migrations = "./migrations")]
async fn private_mode_serves_files_as_private_not_public_cacheable(pool: PgPool) {
    // In private mode /files is auth-gated, so a served blob must NOT carry a
    // `public` Cache-Control: a shared cache / CDN would otherwise store it and
    // hand it to anonymous clients, defeating the gate. It should be `private`.
    //
    // Register via a public harness on the shared pool so the session exists,
    // then upload + fetch a cover through the private harness (same storage).
    let public = common::harness(pool.clone());
    let (_, cookie) = common::register_user(&public.app).await;

    let private = common::harness_with_private_mode(pool);
    let form = common::MultipartBuilder::new()
        .add_json("metadata", json!({ "title": "Secret Library" }))
        .add_file("cover", "cover.png", "image/png", &common::fake_png_bytes());
    let created = private
        .app
        .clone()
        .oneshot(common::post_multipart_with_cookie(
            "/api/v1/mangas",
            form,
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(created.status(), StatusCode::CREATED);
    let body = common::body_json(created).await;
    let key = body["cover_image_path"].as_str().unwrap().to_string();

    let resp = private
        .app
        .oneshot(common::get_with_cookie(
            &format!("/api/v1/files/{key}"),
            &cookie,
        ))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let cc = resp
        .headers()
        .get(axum::http::header::CACHE_CONTROL)
        .unwrap()
        .to_str()
        .unwrap();
    assert!(
        cc.starts_with("private") && !cc.contains("public"),
        "private-mode blobs must be `private`, not `public`; got: {cc}"
    );
}

#[sqlx::test(migrations = "./migrations")]
async fn auth_config_reports_private_mode_and_effective_self_register(pool: PgPool) {
    let h = common::harness_with_private_mode(pool);
    let resp = h
        .app
        .oneshot(common::get("/api/v1/auth/config"))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = common::body_json(resp).await;
    assert_eq!(body["private_mode"], true);
    // Effective value: `allow_self_register && !private_mode` is false
    // here even though the raw `allow_self_register` is true.
    assert_eq!(body["self_register_enabled"], false);
}

#[sqlx::test(migrations = "./migrations")]
async fn public_mode_does_not_gate_anonymous_reads(pool: PgPool) {
    // Regression guard: with private_mode off (the default), the gate
    // must be a no-op so existing public deployments stay public.
    let h = common::harness(pool);
    let resp = h.app.oneshot(common::get("/api/v1/mangas")).await.unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
}

#[sqlx::test(migrations = "./migrations")]
async fn public_mode_reports_private_mode_false(pool: PgPool) {
    let h = common::harness(pool);
    let resp = h
        .app
        .oneshot(common::get("/api/v1/auth/config"))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let body = common::body_json(resp).await;
    assert_eq!(body["private_mode"], false);
    assert_eq!(body["self_register_enabled"], true);
}
