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

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,
    }
}

fn labels() -> Vec<LocalizedLabel> {
    vec![
        LocalizedLabel {
            lang: "sv".into(),
            label: "material".into(),
        },
        LocalizedLabel {
            lang: "en".into(),
            label: "material".into(),
        },
    ]
}

#[sqlx::test]
async fn text_field_round_trips(pool: PgPool) {
    let db = Db::from_pool(pool);
    let mut tx = db.pool().begin().await.unwrap();
    let id = fields::create_field_definition(
        &mut tx,
        &NewFieldDefinition {
            key: "comments".into(),
            field_type: FieldType::Text,
            required: false,
            group_key: Some("identification".into()),
            labels: labels(),
        },
    )
    .await
    .unwrap();
    tx.commit().await.unwrap();

    let def = fields::field_definition_by_key(db.pool(), "comments")
        .await
        .unwrap()
        .unwrap();
    assert_eq!(def.id, id);
    assert_eq!(def.field_type, FieldType::Text);
    assert_eq!(def.group_key.as_deref(), Some("identification"));
    assert_eq!(def.labels.len(), 2);
    assert!(
        fields::field_definition_by_key(db.pool(), "nope")
            .await
            .unwrap()
            .is_none()
    );
}

#[sqlx::test]
async fn term_and_authority_fields_round_trip_their_binding(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();
    tx.commit().await.unwrap();

    let mut tx = db.pool().begin().await.unwrap();
    fields::create_field_definition(
        &mut tx,
        &NewFieldDefinition {
            key: "material".into(),
            field_type: FieldType::Term {
                vocabulary_id: material.id,
            },
            required: true,
            group_key: None,
            labels: labels(),
        },
    )
    .await
    .unwrap();
    fields::create_field_definition(
        &mut tx,
        &NewFieldDefinition {
            key: "maker".into(),
            field_type: FieldType::Authority {
                kind: Some(AuthorityKind::Person),
            },
            required: false,
            group_key: None,
            labels: vec![LocalizedLabel {
                lang: "en".into(),
                label: "maker".into(),
            }],
        },
    )
    .await
    .unwrap();
    tx.commit().await.unwrap();

    let material_def = fields::field_definition_by_key(db.pool(), "material")
        .await
        .unwrap()
        .unwrap();
    assert_eq!(
        material_def.field_type,
        FieldType::Term {
            vocabulary_id: material.id
        }
    );
    assert!(material_def.required);

    let maker_def = fields::field_definition_by_key(db.pool(), "maker")
        .await
        .unwrap()
        .unwrap();
    assert_eq!(
        maker_def.field_type,
        FieldType::Authority {
            kind: Some(AuthorityKind::Person)
        }
    );

    let all = fields::list_field_definitions(db.pool()).await.unwrap();
    assert_eq!(all.len(), 2);
}

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

    let mut tx = db.pool().begin().await.unwrap();
    fields::create_field_definition(
        &mut tx,
        &NewFieldDefinition {
            key: "donor".into(),
            field_type: FieldType::Authority { kind: None },
            required: false,
            group_key: None,
            labels: vec![],
        },
    )
    .await
    .unwrap();
    fields::create_field_definition(
        &mut tx,
        &NewFieldDefinition {
            key: "on_display".into(),
            field_type: FieldType::Boolean,
            required: false,
            group_key: None,
            labels: vec![LocalizedLabel {
                lang: "en".into(),
                label: "on display".into(),
            }],
        },
    )
    .await
    .unwrap();
    tx.commit().await.unwrap();

    let donor = fields::field_definition_by_key(db.pool(), "donor")
        .await
        .unwrap()
        .unwrap();
    assert_eq!(donor.field_type, FieldType::Authority { kind: None });
    assert!(donor.labels.is_empty());

    let on_display = fields::field_definition_by_key(db.pool(), "on_display")
        .await
        .unwrap()
        .unwrap();
    assert_eq!(on_display.field_type, FieldType::Boolean);

    // list_field_definitions is ordered by key.
    let all = fields::list_field_definitions(db.pool()).await.unwrap();
    let keys: Vec<&str> = all.iter().map(|d| d.key.as_str()).collect();
    assert_eq!(keys, vec!["donor", "on_display"]);
}

#[sqlx::test(migrations = "../db/migrations")]
async fn update_field_definition_edits_labels_group_required(pool: PgPool) {
    let db = Db::from_pool(pool);
    let mut tx = db.pool().begin().await.unwrap();

    fields::create_field_definition(
        &mut tx,
        &NewFieldDefinition {
            key: "weight".into(),
            field_type: FieldType::Integer,
            required: false,
            group_key: None,
            labels: vec![LocalizedLabel {
                lang: "sv".into(),
                label: "Vikt".into(),
            }],
        },
    )
    .await
    .unwrap();

    let existed = fields::update_field_definition(
        &mut tx,
        AuditActor::System,
        "weight",
        true,
        Some("Mått"),
        &[LocalizedLabel {
            lang: "sv".into(),
            label: "Vikt (g)".into(),
        }],
    )
    .await
    .unwrap();
    assert!(existed);

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

    let def = fields::field_definition_by_key(db.pool(), "weight")
        .await
        .unwrap()
        .unwrap();
    assert!(def.required);
    assert_eq!(def.group_key.as_deref(), Some("Mått"));
    assert_eq!(def.labels[0].label, "Vikt (g)");
}

#[sqlx::test(migrations = "../db/migrations")]
async fn delete_field_definition_blocks_when_objects_use_it(pool: PgPool) {
    let db = Db::from_pool(pool);
    let mut tx = db.pool().begin().await.unwrap();

    fields::create_field_definition(
        &mut tx,
        &NewFieldDefinition {
            key: "weight".into(),
            field_type: FieldType::Integer,
            required: false,
            group_key: None,
            labels: vec![LocalizedLabel {
                lang: "sv".into(),
                label: "Vikt".into(),
            }],
        },
    )
    .await
    .unwrap();

    let field_def_id = fields::field_definition_by_key(&mut *tx, "weight")
        .await
        .unwrap()
        .unwrap()
        .id
        .to_uuid();

    let obj = catalog::create_object(&mut tx, AuditActor::System, &sample_object_input())
        .await
        .unwrap();

    let mut map = serde_json::Map::new();
    map.insert("weight".into(), serde_json::Value::from(42));
    catalog::set_object_fields(&mut tx, AuditActor::System, obj, &map)
        .await
        .unwrap();

    assert_eq!(
        fields::delete_field_definition(&mut tx, AuditActor::System, "weight")
            .await
            .unwrap(),
        DeleteOutcome::InUse { count: 1 }
    );

    catalog::set_object_fields(&mut tx, AuditActor::System, obj, &serde_json::Map::new())
        .await
        .unwrap();

    assert_eq!(
        fields::delete_field_definition(&mut tx, AuditActor::System, "weight")
            .await
            .unwrap(),
        DeleteOutcome::Deleted
    );

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

    assert_eq!(
        fields::delete_field_definition(&mut tx, AuditActor::System, "weight")
            .await
            .unwrap(),
        DeleteOutcome::NotFound
    );
}
