use api::{AppState, build_app, migrate_sessions};
use axum::body::Body;
use axum::http::{Request, StatusCode, header};
use db::{catalog, users};
use domain::{AuditActor, Email, NewUser, ObjectInput, Role, Visibility};
use http_body_util::BodyExt;
use sqlx::PgPool;
use tower::ServiceExt;

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

async fn seed_user(pool: &PgPool, email: &str, password: &str, role: Role) {
    let db = db::Db::from_pool(pool.clone());
    let mut tx = db.pool().begin().await.unwrap();

    users::create_user(
        &mut tx,
        AuditActor::System,
        &NewUser {
            email: Email::parse(email).unwrap(),
            password_hash: auth::hash_password(password).unwrap(),
            role,
        },
    )
    .await
    .unwrap();

    tx.commit().await.unwrap();
}

fn login_request(email: &str, password: &str) -> Request<Body> {
    Request::builder()
        .method("POST")
        .uri("/api/admin/login")
        .header(header::CONTENT_TYPE, "application/json")
        .body(Body::from(format!(
            r#"{{"email":"{email}","password":"{password}"}}"#
        )))
        .unwrap()
}

fn session_cookie(resp: &axum::http::Response<Body>) -> String {
    let raw = resp
        .headers()
        .get(header::SET_COOKIE)
        .expect("Set-Cookie")
        .to_str()
        .unwrap();

    raw.split(';').next().unwrap().to_owned()
}

#[sqlx::test(migrations = "../db/migrations")]
async fn login_then_me_returns_identity(pool: PgPool) {
    migrate_sessions(&db::Db::from_pool(pool.clone()))
        .await
        .unwrap();

    seed_user(&pool, "admin@example.com", "s3cret-passw0rd", Role::Admin).await;

    let app = build_app(state(pool));

    let resp = app
        .clone()
        .oneshot(login_request("admin@example.com", "s3cret-passw0rd"))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::NO_CONTENT);

    let cookie = session_cookie(&resp);

    let me = app
        .oneshot(
            Request::builder()
                .uri("/api/admin/me")
                .header(header::COOKIE, &cookie)
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();
    assert_eq!(me.status(), StatusCode::OK);

    let json: serde_json::Value =
        serde_json::from_slice(&me.into_body().collect().await.unwrap().to_bytes()).unwrap();
    assert_eq!(json["email"], "admin@example.com");
    assert_eq!(json["role"], "admin");
}

#[sqlx::test(migrations = "../db/migrations")]
async fn me_without_session_is_401(pool: PgPool) {
    migrate_sessions(&db::Db::from_pool(pool.clone()))
        .await
        .unwrap();

    let app = build_app(state(pool));

    let resp = app
        .oneshot(
            Request::builder()
                .uri("/api/admin/me")
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}

#[sqlx::test(migrations = "../db/migrations")]
async fn wrong_password_is_401(pool: PgPool) {
    migrate_sessions(&db::Db::from_pool(pool.clone()))
        .await
        .unwrap();

    seed_user(&pool, "admin@example.com", "right", Role::Admin).await;

    let app = build_app(state(pool));

    let resp = app
        .oneshot(login_request("admin@example.com", "wrong"))
        .await
        .unwrap();
    assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}

#[sqlx::test(migrations = "../db/migrations")]
async fn editor_cannot_list_users_but_admin_can(pool: PgPool) {
    migrate_sessions(&db::Db::from_pool(pool.clone()))
        .await
        .unwrap();

    seed_user(&pool, "editor@example.com", "pw-editor-123", Role::Editor).await;
    seed_user(&pool, "admin@example.com", "pw-admin-123", Role::Admin).await;

    let app = build_app(state(pool));

    let resp = app
        .clone()
        .oneshot(login_request("editor@example.com", "pw-editor-123"))
        .await
        .unwrap();
    let editor_cookie = session_cookie(&resp);

    let listed = app
        .clone()
        .oneshot(
            Request::builder()
                .uri("/api/admin/users")
                .header(header::COOKIE, &editor_cookie)
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();
    assert_eq!(listed.status(), StatusCode::FORBIDDEN);

    let resp = app
        .clone()
        .oneshot(login_request("admin@example.com", "pw-admin-123"))
        .await
        .unwrap();
    let admin_cookie = session_cookie(&resp);

    let listed = app
        .oneshot(
            Request::builder()
                .uri("/api/admin/users")
                .header(header::COOKIE, &admin_cookie)
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();
    assert_eq!(listed.status(), StatusCode::OK);
}

#[sqlx::test(migrations = "../db/migrations")]
async fn editor_can_publish_via_admin_endpoint(pool: PgPool) {
    migrate_sessions(&db::Db::from_pool(pool.clone()))
        .await
        .unwrap();

    seed_user(&pool, "editor@example.com", "pw-editor-123", Role::Editor).await;

    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,
        &ObjectInput {
            object_number: "P-1".into(),
            object_name: "vase".into(),
            number_of_objects: 1,
            brief_description: None,
            current_location: None,
            current_owner: None,
            recorder: None,
            recording_date: None,
            visibility: Visibility::Internal,
        },
    )
    .await
    .unwrap();

    tx.commit().await.unwrap();

    let app = build_app(state(pool));

    let resp = app
        .clone()
        .oneshot(login_request("editor@example.com", "pw-editor-123"))
        .await
        .unwrap();
    let cookie = session_cookie(&resp);

    let publish = app
        .oneshot(
            Request::builder()
                .method("POST")
                .uri(format!("/api/admin/objects/{id}/visibility"))
                .header(header::COOKIE, &cookie)
                .header(header::CONTENT_TYPE, "application/json")
                .body(Body::from(r#"{"visibility":"public"}"#))
                .unwrap(),
        )
        .await
        .unwrap();
    assert_eq!(publish.status(), StatusCode::NO_CONTENT);

    let obj = catalog::object_by_id(db.pool(), id).await.unwrap().unwrap();
    assert_eq!(obj.visibility, Visibility::Public);
}

#[sqlx::test(migrations = "../db/migrations")]
async fn logout_invalidates_the_session(pool: PgPool) {
    migrate_sessions(&db::Db::from_pool(pool.clone()))
        .await
        .unwrap();
    seed_user(&pool, "admin@example.com", "s3cret-passw0rd", Role::Admin).await;
    let app = build_app(state(pool));

    let resp = app
        .clone()
        .oneshot(login_request("admin@example.com", "s3cret-passw0rd"))
        .await
        .unwrap();
    let cookie = session_cookie(&resp);

    // logout with the session cookie
    let out = app
        .clone()
        .oneshot(
            Request::builder()
                .method("POST")
                .uri("/api/admin/logout")
                .header(header::COOKIE, &cookie)
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();
    assert_eq!(out.status(), StatusCode::NO_CONTENT);

    // the old cookie no longer authenticates
    let me = app
        .oneshot(
            Request::builder()
                .uri("/api/admin/me")
                .header(header::COOKIE, &cookie)
                .body(Body::empty())
                .unwrap(),
        )
        .await
        .unwrap();
    assert_eq!(me.status(), StatusCode::UNAUTHORIZED);
}

#[sqlx::test(migrations = "../db/migrations")]
async fn illegal_visibility_transition_is_409(pool: PgPool) {
    migrate_sessions(&db::Db::from_pool(pool.clone()))
        .await
        .unwrap();
    seed_user(&pool, "editor@example.com", "pw-editor-123", Role::Editor).await;

    // a draft object — draft -> public in one step is illegal (must pass through internal)
    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,
        &ObjectInput {
            object_number: "D-1".into(),
            object_name: "vase".into(),
            number_of_objects: 1,
            brief_description: None,
            current_location: None,
            current_owner: None,
            recorder: None,
            recording_date: None,
            visibility: Visibility::Draft,
        },
    )
    .await
    .unwrap();
    tx.commit().await.unwrap();

    let app = build_app(state(pool));
    let resp = app
        .clone()
        .oneshot(login_request("editor@example.com", "pw-editor-123"))
        .await
        .unwrap();
    let cookie = session_cookie(&resp);

    let publish = app
        .oneshot(
            Request::builder()
                .method("POST")
                .uri(format!("/api/admin/objects/{id}/visibility"))
                .header(header::COOKIE, &cookie)
                .header(header::CONTENT_TYPE, "application/json")
                .body(Body::from(r#"{"visibility":"public"}"#))
                .unwrap(),
        )
        .await
        .unwrap();
    assert_eq!(publish.status(), StatusCode::CONFLICT);
}

#[sqlx::test(migrations = "../db/migrations")]
async fn publishing_without_required_field_is_422(pool: PgPool) {
    use domain::{FieldType, LocalizedLabel, NewFieldDefinition};

    migrate_sessions(&db::Db::from_pool(pool.clone()))
        .await
        .unwrap();

    seed_user(&pool, "editor@example.com", "pw-editor-123", Role::Editor).await;

    let db = db::Db::from_pool(pool.clone());
    let mut tx = db.pool().begin().await.unwrap();

    db::fields::create_field_definition(
        &mut tx,
        &NewFieldDefinition {
            key: "inscription".into(),
            field_type: FieldType::Text,
            required: true,
            group_key: None,
            labels: vec![LocalizedLabel {
                lang: "en".into(),
                label: "Inscription".into(),
            }],
        },
    )
    .await
    .unwrap();

    let id = catalog::create_object(
        &mut tx,
        AuditActor::System,
        &ObjectInput {
            object_number: "P-2".into(),
            object_name: "vase".into(),
            number_of_objects: 1,
            brief_description: None,
            current_location: None,
            current_owner: None,
            recorder: None,
            recording_date: None,
            visibility: Visibility::Internal,
        },
    )
    .await
    .unwrap();

    tx.commit().await.unwrap();

    let app = build_app(state(pool.clone()));

    let resp = app
        .clone()
        .oneshot(login_request("editor@example.com", "pw-editor-123"))
        .await
        .unwrap();
    let cookie = session_cookie(&resp);

    // publishing while a required field has no value -> 422, visibility unchanged
    let publish = app
        .oneshot(
            Request::builder()
                .method("POST")
                .uri(format!("/api/admin/objects/{id}/visibility"))
                .header(header::COOKIE, &cookie)
                .header(header::CONTENT_TYPE, "application/json")
                .body(Body::from(r#"{"visibility":"public"}"#))
                .unwrap(),
        )
        .await
        .unwrap();
    assert_eq!(publish.status(), StatusCode::UNPROCESSABLE_ENTITY);

    let obj = catalog::object_by_id(db.pool(), id).await.unwrap().unwrap();
    assert_eq!(obj.visibility, Visibility::Internal);
}
