use api::{AppState, build_app};
use axum::body::Body;
use axum::http::{Request, StatusCode};
use db::catalog;
use domain::{AuditActor, ObjectInput, Visibility};
use http_body_util::BodyExt;
use sqlx::PgPool;
use tower::ServiceExt; // for `oneshot`

fn state(pool: PgPool) -> AppState {
    AppState {
        db: db::Db::from_pool(pool),
        app_name: "Test".to_string(),
        cookie_secure: false,
        search: None,
        default_language: "sv".into(),
        default_timezone: "Europe/Stockholm".into(),
    }
}

fn object(number: &str, name: &str, visibility: Visibility) -> ObjectInput {
    ObjectInput {
        object_number: number.into(),
        object_name: name.into(),
        number_of_objects: 1,
        brief_description: Some("a description".into()),
        current_location: Some("vault B".into()), // never-public; must NOT appear in output
        current_owner: Some("the museum".into()), // never-public
        recorder: None,
        recording_date: None,
        visibility,
    }
}

async fn body_json(resp: axum::http::Response<Body>) -> serde_json::Value {
    let bytes = resp.into_body().collect().await.unwrap().to_bytes();
    serde_json::from_slice(&bytes).unwrap()
}

#[sqlx::test(migrations = "../db/migrations")]
async fn list_returns_only_public_as_public_view(pool: PgPool) {
    let db = db::Db::from_pool(pool.clone());
    let mut tx = db.pool().begin().await.unwrap();
    catalog::create_object(
        &mut tx,
        AuditActor::System,
        &object("D-1", "draft vase", Visibility::Draft),
    )
    .await
    .unwrap();
    catalog::create_object(
        &mut tx,
        AuditActor::System,
        &object("P-1", "public vase", Visibility::Public),
    )
    .await
    .unwrap();
    tx.commit().await.unwrap();

    let app = build_app(state(pool));
    let resp = app
        .oneshot(
            Request::builder()
                .uri("/api/public/objects")
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);

    let json = body_json(resp).await;
    assert_eq!(json["total"], 1);
    assert_eq!(json["items"].as_array().unwrap().len(), 1);
    let item = &json["items"][0];
    assert_eq!(item["object_number"], "P-1");
    assert_eq!(item["object_name"], "public vase");
    assert_eq!(item["brief_description"], "a description");
    assert!(item.get("current_location").is_none());
    assert!(item.get("current_owner").is_none());
    assert!(item.get("recorder").is_none());
    assert!(item.get("visibility").is_none());
}

#[sqlx::test(migrations = "../db/migrations")]
async fn get_public_object_returns_it(pool: PgPool) {
    let db = db::Db::from_pool(pool.clone());
    let mut tx = db.pool().begin().await.unwrap();
    let id = catalog::create_object(
        &mut tx,
        AuditActor::System,
        &object("P-1", "public vase", Visibility::Public),
    )
    .await
    .unwrap();
    tx.commit().await.unwrap();

    let app = build_app(state(pool));
    let resp = app
        .oneshot(
            Request::builder()
                .uri(format!("/api/public/objects/{id}"))
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::OK);
    let json = body_json(resp).await;
    assert_eq!(json["object_number"], "P-1");
    assert!(json.get("current_location").is_none());
}

#[sqlx::test(migrations = "../db/migrations")]
async fn non_public_objects_are_404(pool: PgPool) {
    let db = db::Db::from_pool(pool.clone());
    let mut tx = db.pool().begin().await.unwrap();
    let draft = catalog::create_object(
        &mut tx,
        AuditActor::System,
        &object("D-1", "draft vase", Visibility::Draft),
    )
    .await
    .unwrap();
    let internal = catalog::create_object(
        &mut tx,
        AuditActor::System,
        &object("I-1", "internal vase", Visibility::Internal),
    )
    .await
    .unwrap();
    tx.commit().await.unwrap();

    // both non-public states are hidden behind a 404 — not 403 — so existence isn't leaked
    let app = build_app(state(pool));
    for id in [draft, internal] {
        let resp = app
            .clone()
            .oneshot(
                Request::builder()
                    .uri(format!("/api/public/objects/{id}"))
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();
        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
    }
}

#[sqlx::test(migrations = "../db/migrations")]
async fn get_missing_object_is_404(pool: PgPool) {
    let app = build_app(state(pool));
    let resp = app
        .oneshot(
            Request::builder()
                .uri(format!("/api/public/objects/{}", domain::ObjectId::new()))
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}

#[sqlx::test(migrations = "../db/migrations")]
async fn openapi_lists_the_public_paths(pool: PgPool) {
    let app = build_app(state(pool));
    let resp = app
        .oneshot(
            Request::builder()
                .uri("/api-docs/openapi.json")
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();
    let json = body_json(resp).await;
    assert!(json["paths"]["/api/public/objects"].is_object());
    assert!(json["paths"]["/api/public/objects/{id}"].is_object());
}
