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

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

    let found = vocab::vocabulary_by_key(db.pool(), "material")
        .await
        .unwrap()
        .unwrap();

    assert_eq!(found.id, v.id);
    assert_eq!(found.key, "material");
    assert!(
        vocab::vocabulary_by_key(db.pool(), "nope")
            .await
            .unwrap()
            .is_none()
    );
}

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

    let mut tx = db.pool().begin().await.unwrap();
    let term_id = vocab::add_term(
        &mut tx,
        AuditActor::System,
        &NewTerm {
            vocabulary_id: v.id,
            external_uri: Some("http://vocab.getty.edu/aat/300011914".into()),
            labels: vec![
                LocalizedLabel {
                    lang: "sv".into(),
                    label: "trä".into(),
                },
                LocalizedLabel {
                    lang: "en".into(),
                    label: "wood".into(),
                },
            ],
        },
    )
    .await
    .unwrap();
    tx.commit().await.unwrap();

    let term = vocab::term_by_id(db.pool(), term_id)
        .await
        .unwrap()
        .unwrap();

    assert_eq!(term.vocabulary_id, v.id);
    assert_eq!(
        term.external_uri.as_deref(),
        Some("http://vocab.getty.edu/aat/300011914")
    );
    assert_eq!(term.labels.len(), 2);
    assert_eq!(domain::pick_label(&term.labels, "sv", "en"), Some("trä"));
    assert_eq!(domain::pick_label(&term.labels, "de", "en"), Some("wood"));

    let listed = vocab::list_terms(db.pool(), v.id).await.unwrap();

    assert_eq!(listed.len(), 1);
    assert_eq!(listed[0].id, term_id);
}

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

    let mut tx = db.pool().begin().await.unwrap();
    let term_id = vocab::add_term(
        &mut tx,
        AuditActor::System,
        &NewTerm {
            vocabulary_id: v.id,
            external_uri: None,
            labels: vec![],
        },
    )
    .await
    .unwrap();
    tx.commit().await.unwrap();

    let term = vocab::term_by_id(db.pool(), term_id)
        .await
        .unwrap()
        .unwrap();
    assert!(term.labels.is_empty());
}

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

    let mut tx = db.pool().begin().await.unwrap();
    let err = vocab::create_vocabulary(&mut tx, AuditActor::System, "material")
        .await
        .unwrap_err();
    assert!(
        matches!(err, sqlx::Error::Database(_)),
        "expected a unique-violation DB error, got: {err:?}"
    );
}

#[sqlx::test]
async fn resolve_term_checks_vocabulary_membership(pool: PgPool) {
    let db = Db::from_pool(pool);
    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();

    let mut tx = db.pool().begin().await.unwrap();
    let term_id = vocab::add_term(
        &mut tx,
        AuditActor::System,
        &NewTerm {
            vocabulary_id: material.id,
            external_uri: None,
            labels: vec![LocalizedLabel {
                lang: "en".into(),
                label: "wood".into(),
            }],
        },
    )
    .await
    .unwrap();
    tx.commit().await.unwrap();

    assert!(
        vocab::resolve_term(db.pool(), material.id, term_id)
            .await
            .unwrap()
            .is_some()
    );
    assert!(
        vocab::resolve_term(db.pool(), technique.id, term_id)
            .await
            .unwrap()
            .is_none()
    );
}

fn sample_object_input() -> ObjectInput {
    ObjectInput {
        object_number: "X.1".into(),
        object_name: "Test".into(),
        number_of_objects: 1,
        brief_description: None,
        current_location: None,
        current_owner: None,
        recorder: None,
        recording_date: None,
        visibility: Visibility::Draft,
    }
}

#[sqlx::test(migrations = "../db/migrations")]
async fn update_term_changes_labels_and_uri(pool: PgPool) {
    let db = Db::from_pool(pool);
    let mut tx = db.pool().begin().await.unwrap();
    let vocab = vocab::create_vocabulary(&mut tx, AuditActor::System, "material")
        .await
        .unwrap();
    let term_id = vocab::add_term(
        &mut tx,
        AuditActor::System,
        &NewTerm {
            vocabulary_id: vocab.id,
            external_uri: None,
            labels: vec![LocalizedLabel {
                lang: "sv".into(),
                label: "Trä".into(),
            }],
        },
    )
    .await
    .unwrap();

    let existed = vocab::update_term(
        &mut tx,
        AuditActor::System,
        vocab.id,
        term_id,
        Some("https://example.org/wood"),
        &[LocalizedLabel {
            lang: "sv".into(),
            label: "Träslag".into(),
        }],
    )
    .await
    .unwrap();
    assert!(existed);

    let history = audit::history_for(&mut *tx, "term", term_id.to_uuid())
        .await
        .unwrap();
    assert!(
        history.iter().any(|e| e.action == AuditAction::Updated),
        "expected an Updated audit entry for the term"
    );

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

    let term = vocab::term_by_id(db.pool(), term_id)
        .await
        .unwrap()
        .unwrap();
    assert_eq!(
        term.external_uri.as_deref(),
        Some("https://example.org/wood")
    );
    assert_eq!(term.labels.len(), 1);
    assert_eq!(term.labels[0].label, "Träslag");
}

#[sqlx::test(migrations = "../db/migrations")]
async fn delete_term_blocks_when_referenced_then_succeeds(pool: PgPool) {
    use db::DeleteOutcome;

    let db = Db::from_pool(pool);
    let mut tx = db.pool().begin().await.unwrap();
    let vocab = vocab::create_vocabulary(&mut tx, AuditActor::System, "material")
        .await
        .unwrap();
    let term_id = vocab::add_term(
        &mut tx,
        AuditActor::System,
        &NewTerm {
            vocabulary_id: vocab.id,
            external_uri: None,
            labels: vec![LocalizedLabel {
                lang: "sv".into(),
                label: "Trä".into(),
            }],
        },
    )
    .await
    .unwrap();

    fields::create_field_definition(
        &mut tx,
        &NewFieldDefinition {
            key: "material".into(),
            field_type: FieldType::Term {
                vocabulary_id: vocab.id,
            },
            required: false,
            group_key: None,
            labels: vec![LocalizedLabel {
                lang: "sv".into(),
                label: "Material".into(),
            }],
        },
    )
    .await
    .unwrap();
    let obj = catalog::create_object(&mut tx, AuditActor::System, &sample_object_input())
        .await
        .unwrap();
    let mut map = serde_json::Map::new();
    map.insert(
        "material".into(),
        serde_json::Value::String(term_id.to_string()),
    );
    catalog::set_object_fields(&mut tx, AuditActor::System, obj, &map)
        .await
        .unwrap();

    let blocked = vocab::delete_term(&mut tx, AuditActor::System, vocab.id, term_id)
        .await
        .unwrap();
    assert_eq!(blocked, DeleteOutcome::InUse { count: 1 });

    catalog::set_object_fields(&mut tx, AuditActor::System, obj, &serde_json::Map::new())
        .await
        .unwrap();
    let ok = vocab::delete_term(&mut tx, AuditActor::System, vocab.id, term_id)
        .await
        .unwrap();
    assert_eq!(ok, DeleteOutcome::Deleted);
    assert!(
        vocab::term_by_id(&mut *tx, term_id)
            .await
            .unwrap()
            .is_none()
    );

    let history = audit::history_for(&mut *tx, "term", term_id.to_uuid())
        .await
        .unwrap();
    assert!(
        history.iter().any(|e| e.action == AuditAction::Deleted),
        "expected a Deleted audit entry for the term"
    );

    let gone = vocab::delete_term(&mut tx, AuditActor::System, vocab.id, term_id)
        .await
        .unwrap();
    assert_eq!(gone, DeleteOutcome::NotFound);
}

#[sqlx::test(migrations = "../db/migrations")]
async fn rename_vocabulary_changes_key(pool: PgPool) {
    let db = Db::from_pool(pool);
    let mut tx = db.pool().begin().await.unwrap();
    let v = vocab::create_vocabulary(&mut tx, AuditActor::System, "old")
        .await
        .unwrap();
    let existed = vocab::rename_vocabulary(&mut tx, AuditActor::System, v.id, "new")
        .await
        .unwrap();
    assert!(existed);
    tx.commit().await.unwrap();

    assert!(
        vocab::vocabulary_by_key(db.pool(), "new")
            .await
            .unwrap()
            .is_some()
    );
    assert!(
        vocab::vocabulary_by_key(db.pool(), "old")
            .await
            .unwrap()
            .is_none()
    );
}

#[sqlx::test(migrations = "../db/migrations")]
async fn delete_vocabulary_blocks_when_it_has_terms(pool: PgPool) {
    use db::DeleteOutcome;

    let db = Db::from_pool(pool);
    let mut tx = db.pool().begin().await.unwrap();
    let v = vocab::create_vocabulary(&mut tx, AuditActor::System, "material")
        .await
        .unwrap();
    vocab::add_term(
        &mut tx,
        AuditActor::System,
        &NewTerm {
            vocabulary_id: v.id,
            external_uri: None,
            labels: vec![LocalizedLabel {
                lang: "sv".into(),
                label: "Trä".into(),
            }],
        },
    )
    .await
    .unwrap();

    let blocked = vocab::delete_vocabulary(&mut tx, AuditActor::System, v.id)
        .await
        .unwrap();
    assert_eq!(blocked, DeleteOutcome::InUse { count: 1 });

    let empty = vocab::create_vocabulary(&mut tx, AuditActor::System, "empty")
        .await
        .unwrap();
    assert_eq!(
        vocab::delete_vocabulary(&mut tx, AuditActor::System, empty.id)
            .await
            .unwrap(),
        DeleteOutcome::Deleted
    );

    let gone = vocab::delete_vocabulary(&mut tx, AuditActor::System, empty.id)
        .await
        .unwrap();
    assert_eq!(gone, DeleteOutcome::NotFound);
}
