use db::{Db, audit, catalog};
use domain::{AuditAction, AuditActor, ObjectInput, Visibility};
use sqlx::PgPool;

fn base() -> ObjectInput {
    ObjectInput {
        object_number: "LM-1".into(),
        object_name: "vase".into(),
        number_of_objects: 1,
        brief_description: None,
        current_location: Some("shelf A1".into()),
        current_owner: None,
        recorder: None,
        recording_date: None,
        visibility: Visibility::Draft,
    }
}

#[sqlx::test]
async fn update_changes_are_audited_as_diffs(pool: PgPool) {
    let db = Db::from_pool(pool);

    let mut tx = db.pool().begin().await.unwrap();
    let id = catalog::create_object(&mut tx, AuditActor::System, &base())
        .await
        .unwrap();
    tx.commit().await.unwrap();

    let mut changed = base();
    changed.object_name = "roman vase".into();
    changed.visibility = Visibility::Public;

    let mut tx = db.pool().begin().await.unwrap();
    let updated = catalog::update_object(&mut tx, AuditActor::System, id, &changed)
        .await
        .unwrap();
    tx.commit().await.unwrap();
    assert!(updated);

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

    let history = audit::history_for(db.pool(), "object", id.to_uuid())
        .await
        .unwrap();
    assert_eq!(history.len(), 2); // created + updated
    let update = &history[1];
    assert_eq!(update.action, AuditAction::Updated);
    let mut fields: Vec<&str> = update.changes.iter().map(|c| c.field.as_str()).collect();
    fields.sort_unstable();
    assert_eq!(fields, vec!["object_name", "visibility"]);
}

#[sqlx::test]
async fn no_op_update_records_no_audit(pool: PgPool) {
    let db = Db::from_pool(pool);
    let mut tx = db.pool().begin().await.unwrap();
    let id = catalog::create_object(&mut tx, AuditActor::System, &base())
        .await
        .unwrap();
    tx.commit().await.unwrap();

    let mut tx = db.pool().begin().await.unwrap();
    let updated = catalog::update_object(&mut tx, AuditActor::System, id, &base())
        .await
        .unwrap();
    tx.commit().await.unwrap();
    assert!(updated);

    let history = audit::history_for(db.pool(), "object", id.to_uuid())
        .await
        .unwrap();
    assert_eq!(
        history.len(),
        1,
        "a no-op update must not add an audit entry"
    );
}

#[sqlx::test]
async fn update_missing_returns_false(pool: PgPool) {
    let db = Db::from_pool(pool);
    let missing = domain::ObjectId::new();

    let mut tx = db.pool().begin().await.unwrap();
    let updated = catalog::update_object(&mut tx, AuditActor::System, missing, &base())
        .await
        .unwrap();
    tx.commit().await.unwrap();
    assert!(!updated);

    let history = audit::history_for(db.pool(), "object", missing.to_uuid())
        .await
        .unwrap();
    assert!(
        history.is_empty(),
        "updating a missing object records no audit"
    );
}

#[sqlx::test]
async fn delete_missing_returns_false(pool: PgPool) {
    let db = Db::from_pool(pool);
    let mut tx = db.pool().begin().await.unwrap();
    let deleted = catalog::delete_object(&mut tx, AuditActor::System, domain::ObjectId::new())
        .await
        .unwrap();
    tx.commit().await.unwrap();
    assert!(!deleted);
}

#[sqlx::test]
async fn clearing_a_field_is_audited(pool: PgPool) {
    let db = Db::from_pool(pool);
    let mut tx = db.pool().begin().await.unwrap();
    let id = catalog::create_object(&mut tx, AuditActor::System, &base())
        .await
        .unwrap();
    tx.commit().await.unwrap();

    let mut cleared = base();
    cleared.current_location = None;

    let mut tx = db.pool().begin().await.unwrap();
    catalog::update_object(&mut tx, AuditActor::System, id, &cleared)
        .await
        .unwrap();
    tx.commit().await.unwrap();

    let history = audit::history_for(db.pool(), "object", id.to_uuid())
        .await
        .unwrap();
    let update = history.last().unwrap();
    let loc = update
        .changes
        .iter()
        .find(|c| c.field == "current_location")
        .expect("location change recorded");
    assert!(
        loc.before.is_some(),
        "cleared field should have before = Some"
    );
    assert!(
        loc.after.is_none(),
        "cleared field should have after = None"
    );
}

#[sqlx::test]
async fn delete_removes_and_audits(pool: PgPool) {
    let db = Db::from_pool(pool);
    let mut tx = db.pool().begin().await.unwrap();
    let id = catalog::create_object(&mut tx, AuditActor::System, &base())
        .await
        .unwrap();
    tx.commit().await.unwrap();

    let mut tx = db.pool().begin().await.unwrap();
    let deleted = catalog::delete_object(&mut tx, AuditActor::System, id)
        .await
        .unwrap();
    tx.commit().await.unwrap();
    assert!(deleted);

    assert!(
        catalog::object_by_id(db.pool(), id)
            .await
            .unwrap()
            .is_none()
    );
    let history = audit::history_for(db.pool(), "object", id.to_uuid())
        .await
        .unwrap();
    assert_eq!(history.last().unwrap().action, AuditAction::Deleted);
}
