use db::catalog::FieldError;
use db::{Db, audit, catalog, fields, vocab};
use domain::{
    AuditAction, AuditActor, FieldType, LocalizedLabel, NewFieldDefinition, ObjectInput, Visibility,
};
use sqlx::PgPool;

fn obj_input() -> ObjectInput {
    ObjectInput {
        object_number: "LM-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,
    }
}

fn label(text: &str) -> Vec<LocalizedLabel> {
    vec![LocalizedLabel {
        lang: "en".into(),
        label: text.into(),
    }]
}

async fn setup_object(db: &Db) -> domain::ObjectId {
    let mut tx = db.pool().begin().await.unwrap();
    let id = catalog::create_object(&mut tx, AuditActor::System, &obj_input())
        .await
        .unwrap();
    tx.commit().await.unwrap();
    id
}

async fn define(db: &Db, key: &str, field_type: FieldType) {
    let mut tx = db.pool().begin().await.unwrap();
    fields::create_field_definition(
        &mut tx,
        &NewFieldDefinition {
            key: key.into(),
            field_type,
            required: false,
            group_key: None,
            labels: label(key),
        },
    )
    .await
    .unwrap();
    tx.commit().await.unwrap();
}

#[sqlx::test]
async fn sets_scalar_fields_and_audits(pool: PgPool) {
    let db = Db::from_pool(pool);
    let id = setup_object(&db).await;
    define(&db, "comments", FieldType::Text).await;
    define(&db, "year", FieldType::Integer).await;
    define(&db, "on_display", FieldType::Boolean).await;

    let values = serde_json::json!({ "comments": "nice", "year": 1850, "on_display": true });

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

    let obj = catalog::object_by_id(db.pool(), id).await.unwrap().unwrap();
    assert_eq!(obj.fields["comments"], "nice");
    assert_eq!(obj.fields["year"], 1850);
    assert_eq!(obj.fields["on_display"], true);

    let history = audit::history_for(db.pool(), "object", id.to_uuid())
        .await
        .unwrap();
    assert_eq!(history.last().unwrap().action, AuditAction::Updated);
    let changed: Vec<&str> = history
        .last()
        .unwrap()
        .changes
        .iter()
        .map(|c| c.field.as_str())
        .collect();
    assert!(
        changed.contains(&"comments")
            && changed.contains(&"year")
            && changed.contains(&"on_display")
    );
}

#[sqlx::test]
async fn term_field_must_resolve_in_its_vocabulary(pool: PgPool) {
    let db = Db::from_pool(pool);
    let id = setup_object(&db).await;
    let mut tx = db.pool().begin().await.unwrap();
    let material = vocab::create_vocabulary(&mut tx, AuditActor::System, "material")
        .await
        .unwrap();
    tx.commit().await.unwrap();

    define(
        &db,
        "material",
        FieldType::Term {
            vocabulary_id: material.id,
        },
    )
    .await;

    let mut tx = db.pool().begin().await.unwrap();
    let wood = vocab::add_term(
        &mut tx,
        AuditActor::System,
        &domain::NewTerm {
            vocabulary_id: material.id,
            external_uri: None,
            labels: label("wood"),
        },
    )
    .await
    .unwrap();
    tx.commit().await.unwrap();

    let ok = serde_json::json!({ "material": wood.to_string() });
    let mut tx = db.pool().begin().await.unwrap();
    catalog::set_object_fields(&mut tx, AuditActor::System, id, ok.as_object().unwrap())
        .await
        .unwrap();
    tx.commit().await.unwrap();

    let bad = serde_json::json!({ "material": domain::TermId::new().to_string() });
    let mut tx = db.pool().begin().await.unwrap();
    let err =
        catalog::set_object_fields(&mut tx, AuditActor::System, id, bad.as_object().unwrap()).await;
    assert!(matches!(err, Err(FieldError::Unresolved { .. })));
}

#[sqlx::test]
async fn unknown_field_and_type_mismatch_are_rejected(pool: PgPool) {
    let db = Db::from_pool(pool);
    let id = setup_object(&db).await;
    define(&db, "year", FieldType::Integer).await;

    let unknown = serde_json::json!({ "nope": "x" });
    let mut tx = db.pool().begin().await.unwrap();
    assert!(matches!(
        catalog::set_object_fields(
            &mut tx,
            AuditActor::System,
            id,
            unknown.as_object().unwrap()
        )
        .await,
        Err(FieldError::UnknownField(_))
    ));
    drop(tx);

    let wrong = serde_json::json!({ "year": "not a number" });
    let mut tx = db.pool().begin().await.unwrap();
    assert!(matches!(
        catalog::set_object_fields(&mut tx, AuditActor::System, id, wrong.as_object().unwrap())
            .await,
        Err(FieldError::TypeMismatch { .. })
    ));
    drop(tx);
}

#[sqlx::test]
async fn authority_field_enforces_kind(pool: PgPool) {
    let db = Db::from_pool(pool);
    let id = setup_object(&db).await;
    define(
        &db,
        "maker",
        FieldType::Authority {
            kind: Some(domain::AuthorityKind::Person),
        },
    )
    .await;

    let mut tx = db.pool().begin().await.unwrap();
    let person = db::authority::create_authority(
        &mut tx,
        AuditActor::System,
        &domain::NewAuthority {
            kind: domain::AuthorityKind::Person,
            external_uri: None,
            labels: label("Carl"),
        },
    )
    .await
    .unwrap();
    let place = db::authority::create_authority(
        &mut tx,
        AuditActor::System,
        &domain::NewAuthority {
            kind: domain::AuthorityKind::Place,
            external_uri: None,
            labels: label("Stockholm"),
        },
    )
    .await
    .unwrap();
    tx.commit().await.unwrap();

    let ok = serde_json::json!({ "maker": person.to_string() });
    let mut tx = db.pool().begin().await.unwrap();
    catalog::set_object_fields(&mut tx, AuditActor::System, id, ok.as_object().unwrap())
        .await
        .unwrap();
    tx.commit().await.unwrap();

    let bad = serde_json::json!({ "maker": place.to_string() });
    let mut tx = db.pool().begin().await.unwrap();
    assert!(matches!(
        catalog::set_object_fields(&mut tx, AuditActor::System, id, bad.as_object().unwrap()).await,
        Err(FieldError::Unresolved { .. })
    ));
}

#[sqlx::test]
async fn term_from_wrong_vocabulary_is_rejected(pool: PgPool) {
    let db = Db::from_pool(pool);
    let id = setup_object(&db).await;
    let mut tx = db.pool().begin().await.unwrap();
    let material = vocab::create_vocabulary(&mut tx, AuditActor::System, "material")
        .await
        .unwrap();
    let technique = vocab::create_vocabulary(&mut tx, AuditActor::System, "technique")
        .await
        .unwrap();
    tx.commit().await.unwrap();
    define(
        &db,
        "material",
        FieldType::Term {
            vocabulary_id: material.id,
        },
    )
    .await;

    // a real term, but in the WRONG vocabulary
    let mut tx = db.pool().begin().await.unwrap();
    let other = vocab::add_term(
        &mut tx,
        AuditActor::System,
        &domain::NewTerm {
            vocabulary_id: technique.id,
            external_uri: None,
            labels: label("forged"),
        },
    )
    .await
    .unwrap();
    tx.commit().await.unwrap();

    let bad = serde_json::json!({ "material": other.to_string() });
    let mut tx = db.pool().begin().await.unwrap();
    assert!(matches!(
        catalog::set_object_fields(&mut tx, AuditActor::System, id, bad.as_object().unwrap()).await,
        Err(FieldError::Unresolved { .. })
    ));
}

#[sqlx::test]
async fn localized_text_round_trips(pool: PgPool) {
    let db = Db::from_pool(pool);
    let id = setup_object(&db).await;
    define(&db, "title", FieldType::LocalizedText).await;

    let values = serde_json::json!({ "title": { "sv": "Vas", "en": "Vase" } });
    let mut tx = db.pool().begin().await.unwrap();
    catalog::set_object_fields(&mut tx, AuditActor::System, id, values.as_object().unwrap())
        .await
        .unwrap();
    tx.commit().await.unwrap();

    let obj = catalog::object_by_id(db.pool(), id).await.unwrap().unwrap();
    assert_eq!(obj.fields["title"]["sv"], "Vas");
    assert_eq!(obj.fields["title"]["en"], "Vase");

    let bad = serde_json::json!({ "title": { "sv": 5 } });
    let mut tx = db.pool().begin().await.unwrap();
    assert!(matches!(
        catalog::set_object_fields(&mut tx, AuditActor::System, id, bad.as_object().unwrap()).await,
        Err(FieldError::TypeMismatch { .. })
    ));
}

#[sqlx::test]
async fn replace_semantics_remove_a_field_and_audit_it(pool: PgPool) {
    let db = Db::from_pool(pool);
    let id = setup_object(&db).await;
    define(&db, "comments", FieldType::Text).await;
    define(&db, "year", FieldType::Integer).await;

    let mut tx = db.pool().begin().await.unwrap();
    catalog::set_object_fields(
        &mut tx,
        AuditActor::System,
        id,
        serde_json::json!({ "comments": "x", "year": 1850 })
            .as_object()
            .unwrap(),
    )
    .await
    .unwrap();
    tx.commit().await.unwrap();

    let mut tx = db.pool().begin().await.unwrap();
    catalog::set_object_fields(
        &mut tx,
        AuditActor::System,
        id,
        serde_json::json!({ "comments": "x" }).as_object().unwrap(),
    )
    .await
    .unwrap();
    tx.commit().await.unwrap();

    let obj = catalog::object_by_id(db.pool(), id).await.unwrap().unwrap();
    assert!(obj.fields.get("year").is_none());

    let history = audit::history_for(db.pool(), "object", id.to_uuid())
        .await
        .unwrap();
    let last = history.last().unwrap();
    let year = last
        .changes
        .iter()
        .find(|c| c.field == "year")
        .expect("year removal recorded");
    assert!(year.before.is_some());
    assert!(year.after.is_none());
}

#[sqlx::test]
async fn no_op_set_records_no_audit(pool: PgPool) {
    let db = Db::from_pool(pool);
    let id = setup_object(&db).await;
    define(&db, "comments", FieldType::Text).await;

    let values = serde_json::json!({ "comments": "x" });
    let mut tx = db.pool().begin().await.unwrap();
    catalog::set_object_fields(&mut tx, AuditActor::System, id, values.as_object().unwrap())
        .await
        .unwrap();
    tx.commit().await.unwrap();

    let before = audit::history_for(db.pool(), "object", id.to_uuid())
        .await
        .unwrap()
        .len();

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

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

#[sqlx::test]
async fn set_on_missing_object_errors(pool: PgPool) {
    let db = Db::from_pool(pool);
    let mut tx = db.pool().begin().await.unwrap();
    let err = catalog::set_object_fields(
        &mut tx,
        AuditActor::System,
        domain::ObjectId::new(),
        serde_json::json!({}).as_object().unwrap(),
    )
    .await;
    assert!(matches!(err, Err(FieldError::ObjectNotFound)));
}
