//! Tests for `.strict_handlers()` builder flag and the
//! `.allow_unhandled::<T>()` escape hatch.
//!
//! Three behaviours to verify:
//!   1. **Default (lenient)**: missing handler → emit suspends awaiting
//!      an external worker, saga stays Running.
//!   2. **Strict**: missing handler → step fails immediately,
//!      compensation cascade fires.
//!   3. **Strict + allow_unhandled**: emit is intentionally external,
//!      saga stays Running waiting for an external worker.
//!
//! Gated on `DATABASE_URL`; skips silently when unset.

#![allow(dead_code)]

use std::time::Duration;

use mire_sagas::{PgSagaRunner, Saga, SagaBuilder, SagaInstanceStatus, SagaStepStatus};

mod common;

#[allow(unused)]
#[path = "../examples/travel_booking.rs"]
mod travel_booking;

use travel_booking::{
    Booking, BookingEvent, BookingId, CustomerId, FlightId, HotelId, Money, PaymentDeadline,
};

fn make_request(id: &str) -> BookingEvent {
    BookingEvent::Requested {
        booking_id: BookingId::new(id),
        customer_id: CustomerId::new("alice"),
        flight_id: FlightId::new("LH-100"),
        hotel_id: HotelId::new("HILTON-OSL"),
        amount: Money::from_minor(150_000),
    }
}

/// Far-future payment deadline: a sibling test's `tick_timeouts` sweeps
/// every instance of the shared saga name, and these tests assert on
/// still-Active states.
fn far_deadline<S>(b: SagaBuilder<S>) -> SagaBuilder<S>
where
    S: Saga,
{
    b.dep(PaymentDeadline(Duration::from_secs(7 * 24 * 3600)))
}

#[tokio::test]
async fn lenient_default_lets_emits_stall() {
    let Some(store) = common::store().await else {
        return;
    };
    // Strip the payment handler. Default behaviour: the step suspends
    // awaiting an external worker; saga stays Running with payment Active.
    let mut topology = travel_booking::TravelBookingSaga::build(SagaBuilder::<
        travel_booking::TravelBookingSaga,
    >::for_saga());
    topology.remove_request_handler("saga.payment.capture-requested");
    let runner = PgSagaRunner::new(far_deadline(topology), store);

    let id = runner
        .trigger::<Booking>(&make_request(&common::unique("lenient")))
        .await
        .unwrap()
        .unwrap();

    assert_eq!(
        runner.status(&id).await.unwrap(),
        Some(SagaInstanceStatus::Running)
    );
    assert_eq!(
        runner.step_status(&id, "payment").await.unwrap(),
        SagaStepStatus::Active
    );
}

#[tokio::test]
async fn strict_mode_fails_step_when_handler_missing() {
    let Some(store) = common::store().await else {
        return;
    };
    // Same setup but strict — missing payment handler should fail the
    // payment step → compensation cascade → FailedTerminal.
    let mut topology = travel_booking::TravelBookingSaga::build(SagaBuilder::<
        travel_booking::TravelBookingSaga,
    >::for_saga())
    .strict_handlers();
    topology.remove_request_handler("saga.payment.capture-requested");
    let runner = PgSagaRunner::new(topology, store);

    let id = runner
        .trigger::<Booking>(&make_request(&common::unique("strict")))
        .await
        .unwrap()
        .unwrap();

    let env = runner.load(&id).await.unwrap().unwrap();
    assert_eq!(
        env.status,
        SagaInstanceStatus::FailedTerminal,
        "strict mode should fail the saga, not stall"
    );
    assert_eq!(
        runner.step_status(&id, "payment").await.unwrap(),
        SagaStepStatus::Failed
    );
    // Compensation cascade ran on the upstreams.
    assert_eq!(
        runner.step_status(&id, "flight").await.unwrap(),
        SagaStepStatus::Compensated
    );
    assert_eq!(
        runner.step_status(&id, "hotel").await.unwrap(),
        SagaStepStatus::Compensated
    );

    // The recorded error reason mentions the missing event type so
    // operators can fix the config quickly.
    let reason = env
        .last_error
        .as_ref()
        .map(|e| format!("{e}"))
        .unwrap_or_default();
    assert!(
        reason.contains("saga.payment.capture-requested"),
        "error reason should name the missing event type: {reason}"
    );
}

#[tokio::test]
async fn strict_mode_with_allow_unhandled_lets_emit_stall() {
    let Some(store) = common::store().await else {
        return;
    };
    // Mark the payment-capture event type as intentionally external —
    // strict mode should leave it alone.
    let mut topology = travel_booking::TravelBookingSaga::build(SagaBuilder::<
        travel_booking::TravelBookingSaga,
    >::for_saga())
    .strict_handlers()
    .allow_unhandled::<travel_booking::RequestPaymentCapture>();
    topology.remove_request_handler("saga.payment.capture-requested");
    let runner = PgSagaRunner::new(far_deadline(topology), store);

    let id = runner
        .trigger::<Booking>(&make_request(&common::unique("allow")))
        .await
        .unwrap()
        .unwrap();

    // Saga is Running with payment Active — strict mode skipped the
    // failure because the user explicitly marked this emit as external.
    assert_eq!(
        runner.status(&id).await.unwrap(),
        Some(SagaInstanceStatus::Running)
    );
    assert_eq!(
        runner.step_status(&id, "payment").await.unwrap(),
        SagaStepStatus::Active
    );
}

#[tokio::test]
async fn strict_mode_passes_when_all_handlers_are_present() {
    let Some(store) = common::store().await else {
        return;
    };
    // Strict mode is opt-in safety; with all handlers registered, the
    // saga runs to terminal-success normally.
    let topology = travel_booking::TravelBookingSaga::build(SagaBuilder::<
        travel_booking::TravelBookingSaga,
    >::for_saga())
    .strict_handlers();
    let runner = PgSagaRunner::new(topology, store);

    let id = runner
        .trigger::<Booking>(&make_request(&common::unique("happy-strict")))
        .await
        .unwrap()
        .unwrap();

    assert_eq!(
        runner.status(&id).await.unwrap(),
        Some(SagaInstanceStatus::SucceededTerminal)
    );
}
