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

fn object(number: &str, visibility: Visibility) -> ObjectInput {
    ObjectInput {
        object_number: number.into(),
        object_name: "vase".into(),
        number_of_objects: 1,
        brief_description: None,
        current_location: None,
        current_owner: None,
        recorder: None,
        recording_date: None,
        visibility,
    }
}

#[sqlx::test]
async fn publish_steps_through_internal_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,
        &object("LM-1", Visibility::Draft),
    )
    .await
    .unwrap();
    catalog::set_visibility(&mut tx, AuditActor::System, id, Visibility::Internal)
        .await
        .unwrap();
    catalog::set_visibility(&mut tx, AuditActor::System, id, Visibility::Public)
        .await
        .unwrap();
    tx.commit().await.unwrap();

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

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

#[sqlx::test]
async fn skipping_a_step_is_rejected_and_unchanged(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,
        &object("LM-1", Visibility::Draft),
    )
    .await
    .unwrap();
    tx.commit().await.unwrap();

    let mut tx = db.pool().begin().await.unwrap();
    let err = catalog::set_visibility(&mut tx, AuditActor::System, id, Visibility::Public)
        .await
        .unwrap_err();
    tx.commit().await.unwrap();
    assert!(matches!(
        err,
        catalog::VisibilityError::Illegal(IllegalTransition {
            from: Visibility::Draft,
            to: Visibility::Public
        })
    ));

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

#[sqlx::test]
async fn set_visibility_on_missing_object_errors(pool: PgPool) {
    let db = Db::from_pool(pool);
    let mut tx = db.pool().begin().await.unwrap();
    let err = catalog::set_visibility(
        &mut tx,
        AuditActor::System,
        domain::ObjectId::new(),
        Visibility::Internal,
    )
    .await
    .unwrap_err();
    tx.commit().await.unwrap();
    assert!(matches!(err, catalog::VisibilityError::ObjectNotFound));
}

#[sqlx::test]
async fn no_op_set_to_current_visibility_writes_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,
        &object("LM-1", Visibility::Draft),
    )
    .await
    .unwrap();
    catalog::set_visibility(&mut tx, AuditActor::System, id, Visibility::Draft)
        .await
        .unwrap();
    tx.commit().await.unwrap();

    let history = audit::history_for(db.pool(), "object", id.to_uuid())
        .await
        .unwrap();
    assert_eq!(history.len(), 1); // only `created`; the no-op transition recorded nothing
}

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

    let mut tx = db.pool().begin().await.unwrap();
    let draft = catalog::create_object(
        &mut tx,
        AuditActor::System,
        &object("D-1", Visibility::Draft),
    )
    .await
    .unwrap();
    let pub_id = catalog::create_object(
        &mut tx,
        AuditActor::System,
        &object("P-1", Visibility::Public),
    )
    .await
    .unwrap();
    let internal = catalog::create_object(
        &mut tx,
        AuditActor::System,
        &object("I-1", Visibility::Internal),
    )
    .await
    .unwrap();
    tx.commit().await.unwrap();

    assert!(
        catalog::public_object_by_id(db.pool(), pub_id)
            .await
            .unwrap()
            .is_some()
    );
    assert!(
        catalog::public_object_by_id(db.pool(), draft)
            .await
            .unwrap()
            .is_none()
    );

    let listed = catalog::list_public_objects(db.pool(), 50, 0)
        .await
        .unwrap();
    assert_eq!(listed.len(), 1);
    assert_eq!(listed[0].id, pub_id);
    assert_eq!(catalog::count_public_objects(db.pool()).await.unwrap(), 1);

    assert!(
        catalog::list_public_objects(db.pool(), 50, 1)
            .await
            .unwrap()
            .is_empty()
    );

    // internal records are excluded from public reads too (not just draft)
    assert!(
        catalog::public_object_by_id(db.pool(), internal)
            .await
            .unwrap()
            .is_none()
    );
}

#[sqlx::test]
async fn publishing_requires_all_required_fields_present(pool: PgPool) {
    use db::fields;
    use domain::{FieldType, LocalizedLabel, NewFieldDefinition};

    let db = Db::from_pool(pool);

    let mut tx = db.pool().begin().await.unwrap();

    // a required flexible field
    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,
        &object("LM-1", Visibility::Draft),
    )
    .await
    .unwrap();

    catalog::set_visibility(&mut tx, AuditActor::System, id, Visibility::Internal)
        .await
        .unwrap();

    // publishing without the required field present is rejected
    let err = catalog::set_visibility(&mut tx, AuditActor::System, id, Visibility::Public)
        .await
        .unwrap_err();
    assert!(
        matches!(err, catalog::VisibilityError::MissingRequiredFields(ref keys) if keys == &["inscription"])
    );

    // the object is still not public
    let still = catalog::object_by_id(&mut *tx, id).await.unwrap().unwrap();
    assert_eq!(still.visibility, Visibility::Internal);

    // set the required field, then publishing succeeds
    catalog::set_object_fields(
        &mut tx,
        AuditActor::System,
        id,
        serde_json::json!({ "inscription": "To the gods" })
            .as_object()
            .unwrap(),
    )
    .await
    .unwrap();

    catalog::set_visibility(&mut tx, AuditActor::System, id, Visibility::Public)
        .await
        .unwrap();

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

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

#[sqlx::test]
async fn republishing_a_public_object_is_a_noop_even_with_a_new_required_field(pool: PgPool) {
    use db::fields;
    use domain::{FieldType, LocalizedLabel, NewFieldDefinition};

    let db = Db::from_pool(pool);

    let mut tx = db.pool().begin().await.unwrap();

    // an already-public object (created public directly at the db layer)
    let id = catalog::create_object(
        &mut tx,
        AuditActor::System,
        &object("LM-2", Visibility::Public),
    )
    .await
    .unwrap();

    // a required field is introduced AFTER the object is already public
    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();

    // setting visibility to its current value stays an idempotent no-op — the publish
    // gate only fires on an actual transition into public, not on a re-set.
    catalog::set_visibility(&mut tx, AuditActor::System, id, Visibility::Public)
        .await
        .unwrap();

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

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