use proptest::prelude::*;
use turnframe_core::command::RiskClass;
use turnframe_core::ids::{OperationKey, OptionId};
use turnframe_core::interaction::{
FieldValue, FreeformPolicy, Interaction, InteractionKind, InteractionOption,
InteractionPayload, InteractionSpec, OptionStyle, ReviewDiffEntry, StoredInteractionAction,
TextResolutionPolicy,
};
use turnframe_core::locale::LocalizedText;
use crate::strategies::ids;
pub const CONFIRM_OPTION: &str = "confirm";
pub const DECLINE_OPTION: &str = "decline";
pub const NOTE_OPTION: &str = "note";
pub const SUPPORTED_KINDS: [InteractionKind; 9] = [
InteractionKind::Boolean,
InteractionKind::SingleSelect,
InteractionKind::Freeform,
InteractionKind::ReviewChanges,
InteractionKind::ConfirmCommand,
InteractionKind::SelectTarget,
InteractionKind::ResolveValidationError,
InteractionKind::Reauthenticate,
InteractionKind::ExternalSignature,
];
pub fn interaction_kind() -> impl Strategy<Value = InteractionKind> {
proptest::sample::select(&SUPPORTED_KINDS[..])
}
pub fn confirming_option() -> impl Strategy<Value = InteractionOption> {
ids::operation_key().prop_map(|operation| {
InteractionOption::new(
CONFIRM_OPTION,
LocalizedText::new("Confirm").with("it", "Conferma"),
StoredInteractionAction::ApplyOperation {
operation,
arguments: serde_json::Value::Null,
freeform_argument: None,
},
)
.with_style(OptionStyle::Primary)
})
}
#[must_use]
pub fn declining_option() -> InteractionOption {
InteractionOption::new(
DECLINE_OPTION,
LocalizedText::new("Not now").with("it", "Non ora"),
StoredInteractionAction::DeclineCommands,
)
.with_style(OptionStyle::Danger)
}
#[must_use]
pub fn note_option() -> InteractionOption {
InteractionOption::new(
NOTE_OPTION,
LocalizedText::new("Send the note").with("it", "Invia la nota"),
StoredInteractionAction::ResolveClarification {
answer_key: "note".to_owned(),
},
)
.with_freeform(FreeformPolicy::Required { max_len: 200 })
}
pub fn answerable_payload_for(kind: InteractionKind) -> BoxedStrategy<InteractionPayload> {
confirming_option()
.prop_map(move |confirm| {
let base = InteractionPayload::new(
LocalizedText::new("Confirm this operation?").with("it", "Confermi l'operazione?"),
)
.with_body(
LocalizedText::new("It cannot be undone.")
.with("it", "L'operazione non e reversibile."),
);
match kind {
InteractionKind::Freeform => base.with_option(note_option()).with_freeform_prompt(
LocalizedText::new("What should it say?").with("it", "Cosa deve dire?"),
),
InteractionKind::ReviewChanges => base
.with_option(confirm)
.with_option(declining_option())
.with_review_entry(ReviewDiffEntry {
field: "name".to_owned(),
label: LocalizedText::new("Subject").with("it", "Oggetto"),
before: FieldValue::present("Lisbon"),
after: FieldValue::present("Lisbon offsite"),
}),
_ => base.with_option(confirm).with_option(declining_option()),
}
})
.boxed()
}
pub fn answerable_payload() -> impl Strategy<Value = InteractionPayload> {
answerable_payload_for(InteractionKind::ConfirmCommand)
}
fn allowed_text_resolution(
kind: InteractionKind,
confirms_risk: RiskClass,
) -> BoxedStrategy<TextResolutionPolicy> {
if kind.authorizes_commands() || confirms_risk > RiskClass::ReversibleLowRisk {
return Just(TextResolutionPolicy::Never).boxed();
}
prop_oneof![
Just(TextResolutionPolicy::Never),
Just(TextResolutionPolicy::ModelInterpretedLowRisk),
]
.boxed()
}
pub fn confirms_risk() -> impl Strategy<Value = RiskClass> {
prop_oneof![
Just(RiskClass::ReadOnly),
Just(RiskClass::ReversibleLowRisk),
Just(RiskClass::SensitiveDataChange),
Just(RiskClass::Destructive),
Just(RiskClass::Irreversible),
Just(RiskClass::ExternalRegulated),
]
}
pub fn text_resolution_policy() -> impl Strategy<Value = TextResolutionPolicy> {
prop_oneof![
Just(TextResolutionPolicy::Never),
Just(TextResolutionPolicy::ModelInterpretedLowRisk),
]
}
pub fn interaction_spec() -> impl Strategy<Value = InteractionSpec> {
(interaction_kind(), confirms_risk())
.prop_flat_map(|(kind, risk)| {
(
Just(kind),
Just(risk),
ids::label(),
ids::case_ref(),
answerable_payload_for(kind),
allowed_text_resolution(kind, risk),
any::<bool>(),
any::<bool>(),
)
})
.prop_map(
|(
kind,
risk,
key,
case_ref,
payload,
text_resolution,
blocking,
revision_independent,
)| {
let spec = InteractionSpec::new(key, case_ref, kind, payload)
.with_confirms_risk(risk)
.with_text_resolution(text_resolution);
let spec = if blocking { spec } else { spec.non_blocking() };
if revision_independent {
spec.revision_independent()
} else {
spec
}
},
)
.prop_filter("the card must be answerable", |spec| {
spec.validate().is_ok()
})
}
pub fn interaction() -> impl Strategy<Value = Interaction> {
(
interaction_spec(),
ids::interaction_id(),
ids::account_id(),
ids::conversation_id(),
ids::turn_id(),
ids::instant(),
)
.prop_filter_map(
"payload must be hashable",
|(spec, id, account_id, conversation_id, turn_id, now)| {
Interaction::from_spec(spec, id, account_id, conversation_id, turn_id, now).ok()
},
)
}
#[must_use]
pub fn confirming_operation(payload: &InteractionPayload) -> Option<&OperationKey> {
match payload.option(&OptionId::from(CONFIRM_OPTION))?.action {
StoredInteractionAction::ApplyOperation { ref operation, .. } => Some(operation),
_ => None,
}
}