//! Multi-outcome handler returns via manual `IntoResponse` impl.
//!
//! Demonstrates that the existing `IntoResponse` trait (used by
//! `#[derive(EventData)]` for single events and by the framework for
//! `Result<Ok, Err>`) is already enough to express N-way handler
//! outcomes: the user defines an enum, each variant carries a typed
//! event, and the manual impl picks which event_type/payload/is_error
//! per variant.
//!
//! The handler picks one of three outcomes:
//!   - Approved (success) → step completes
//!   - Declined            → step fails (compensation)
//!   - PendingReview       → step fails (different reason)
//!
//! Gated on `DATABASE_URL`; skips silently when unset.

#![allow(dead_code)]

use mire::{Aggregate, EventData, IntoResponse, ResponseValue};
use mire_sagas::{
    Emit, PgSagaRunner, Req, Saga, SagaBuilder, SagaInstanceStatus, SagaStepStatus, Start, State,
    task,
};
use serde::{Deserialize, Serialize};

mod common;

// =====================================================================
// Trigger aggregate
// =====================================================================

#[derive(Default, Clone)]
struct Application;

#[derive(Serialize, Deserialize, Clone, EventData)]
#[mire(event_type = "application.submitted")]
struct ApplicationSubmitted {
    id: String,
    /// 0 = approve, 1 = decline, 2 = pending — drives which outcome
    /// the handler returns. In a real saga the handler would derive
    /// this from real data, not a steering byte.
    outcome_pick: u8,
}

impl Aggregate for Application {
    type Event = ApplicationSubmitted;
    fn stream_category() -> &'static str {
        "application"
    }
    fn apply(&mut self, _: &Self::Event) {}
}

// =====================================================================
// Saga state + request
// =====================================================================

#[derive(Default, Clone, Serialize, Deserialize)]
struct St {
    id: String,
    pick: u8,
}

#[derive(Debug, Clone, Serialize, Deserialize, EventData)]
#[mire(event_type = "decision.request")]
struct CheckApplication {
    id: String,
    pick: u8,
}

// =====================================================================
// Three response events — each is its own first-class typed event.
// =====================================================================

#[derive(Debug, Clone, Serialize, Deserialize, EventData)]
#[mire(event_type = "decision.approved")]
struct DecisionApproved {
    id: String,
    credit_score: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize, EventData)]
#[mire(event_type = "decision.declined")]
struct DecisionDeclined {
    id: String,
    reason: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, EventData)]
#[mire(event_type = "decision.pending-review")]
struct DecisionPendingReview {
    id: String,
    queue: String,
}

// =====================================================================
// User-defined N-way outcome enum + manual IntoResponse impl.
//
// The whole point of Option A: the user controls which event_type
// each variant emits as, AND whether each variant should fail the
// awaiting step (and with what reason).
// =====================================================================

#[derive(Debug)]
enum DecisionOutcome {
    Approved(DecisionApproved),
    Declined(DecisionDeclined),
    PendingReview(DecisionPendingReview),
}

impl IntoResponse for DecisionOutcome {
    fn into_response(self) -> ResponseValue {
        match self {
            // Success — wakes the awaiting step normally.
            Self::Approved(e) => e.into_response(),

            // Hard failure — set is_error so the runner fails the step
            // with a clear reason; compensation cascade fires.
            Self::Declined(e) => {
                let reason = format!("declined: {}", e.reason);
                ResponseValue {
                    event_type: <DecisionDeclined as mire::EventTypeStatic>::EVENT_TYPE,
                    payload: serde_json::to_value(&e)
                        .expect("DecisionDeclined serialises (test fixture)"),
                    is_error: true,
                    error_reason: Some(reason),
                }
            }

            // Soft failure with a different reason. Still fails the step,
            // but the audit trail clearly says "pending review" rather
            // than "declined" — useful for downstream alerting.
            Self::PendingReview(e) => {
                let reason = format!("pending manual review in queue `{}`", e.queue);
                ResponseValue {
                    event_type: <DecisionPendingReview as mire::EventTypeStatic>::EVENT_TYPE,
                    payload: serde_json::to_value(&e)
                        .expect("DecisionPendingReview serialises (test fixture)"),
                    is_error: true,
                    error_reason: Some(reason),
                }
            }
        }
    }
}

// =====================================================================
// Saga
// =====================================================================

#[derive(Debug, Clone, Serialize, Deserialize)]
struct DecisionMade {
    credit_score: u32,
}

async fn check_step(State(s): State<St>) -> Emit {
    Emit::event(CheckApplication {
        id: s.id.clone(),
        pick: s.pick,
    })
}

async fn handle_check(Req(r): Req<CheckApplication>) -> DecisionOutcome {
    match r.pick {
        0 => DecisionOutcome::Approved(DecisionApproved {
            id: r.id,
            credit_score: 800,
        }),
        1 => DecisionOutcome::Declined(DecisionDeclined {
            id: r.id,
            reason: "insufficient income".into(),
        }),
        _ => DecisionOutcome::PendingReview(DecisionPendingReview {
            id: r.id,
            queue: "manual-review".to_string(),
        }),
    }
}

struct DecisionSaga;

impl Saga for DecisionSaga {
    type State = St;
    fn name() -> &'static str {
        "decision-saga"
    }
    fn build(b: SagaBuilder<Self>) -> SagaBuilder<Self> {
        b.start_on::<Application, _>(|e: &ApplicationSubmitted| {
            Some(Start {
                instance_id: e.id.clone(),
                state: St {
                    id: e.id.clone(),
                    pick: e.outcome_pick,
                },
            })
        })
        .step(
            "check",
            task(check_step)
                .terminal_success()
                .on_response(|d: DecisionApproved| DecisionMade {
                    credit_score: d.credit_score,
                }),
        )
        .handle(handle_check)
    }
}

// =====================================================================
// Tests
// =====================================================================

fn build_runner(store: &mire::EventStore) -> PgSagaRunner<DecisionSaga> {
    PgSagaRunner::new(
        DecisionSaga::build(SagaBuilder::<DecisionSaga>::for_saga()),
        store.clone(),
    )
}

#[tokio::test]
async fn approved_variant_completes_step() {
    let Some(store) = common::store().await else {
        return;
    };
    let runner = build_runner(&store);
    let id = runner
        .trigger::<Application>(&ApplicationSubmitted {
            id: common::unique("app-approve"),
            outcome_pick: 0,
        })
        .await
        .unwrap()
        .unwrap();

    let env = runner.load(&id).await.unwrap().unwrap();
    assert_eq!(env.status, SagaInstanceStatus::SucceededTerminal);
    assert_eq!(
        runner.step_status(&id, "check").await.unwrap(),
        SagaStepStatus::Completed
    );

    // The step's typed output reflects the approved variant — the
    // handler's `DecisionApproved` was correctly mapped to the step's
    // `DecisionMade` output via `.map(|d| DecisionMade { credit_score: d.credit_score })`.
    let output = env.outputs.get("check").expect("check step has an output");
    let made: DecisionMade = serde_json::from_value(output.clone()).unwrap();
    assert_eq!(made.credit_score, 800);
}

#[tokio::test]
async fn declined_variant_fails_step_with_specific_reason() {
    let Some(store) = common::store().await else {
        return;
    };
    let runner = build_runner(&store);
    let id = runner
        .trigger::<Application>(&ApplicationSubmitted {
            id: common::unique("app-decline"),
            outcome_pick: 1,
        })
        .await
        .unwrap()
        .unwrap();

    let env = runner.load(&id).await.unwrap().unwrap();
    assert_eq!(env.status, SagaInstanceStatus::FailedTerminal);
    assert_eq!(
        runner.step_status(&id, "check").await.unwrap(),
        SagaStepStatus::Failed
    );

    // The declined event_type IS in the journey (proves the right
    // variant of the outcome enum was emitted).
    let emitted = runner.emitted(&id).await.unwrap();
    let emits: Vec<&str> = emitted.iter().map(|e| e.event_type.as_str()).collect();
    assert!(emits.contains(&"decision.declined"));
    assert!(!emits.contains(&"decision.pending-review"));

    // The error reason names the decline reason, not a generic message.
    let reason = env
        .last_error
        .as_ref()
        .map(|e| format!("{e}"))
        .unwrap_or_default();
    assert!(
        reason.contains("declined") && reason.contains("insufficient income"),
        "reason should describe decline: {reason}"
    );
}

#[tokio::test]
async fn pending_review_variant_fails_step_with_different_reason() {
    let Some(store) = common::store().await else {
        return;
    };
    let runner = build_runner(&store);
    let id = runner
        .trigger::<Application>(&ApplicationSubmitted {
            id: common::unique("app-pending"),
            outcome_pick: 2,
        })
        .await
        .unwrap()
        .unwrap();

    let env = runner.load(&id).await.unwrap().unwrap();
    assert_eq!(env.status, SagaInstanceStatus::FailedTerminal);
    assert_eq!(
        runner.step_status(&id, "check").await.unwrap(),
        SagaStepStatus::Failed
    );

    // The pending-review event_type is in the journey, NOT declined.
    let emitted = runner.emitted(&id).await.unwrap();
    let emits: Vec<&str> = emitted.iter().map(|e| e.event_type.as_str()).collect();
    assert!(emits.contains(&"decision.pending-review"));
    assert!(!emits.contains(&"decision.declined"));

    // Distinct reason — proves per-variant error_reason works.
    let reason = env
        .last_error
        .as_ref()
        .map(|e| format!("{e}"))
        .unwrap_or_default();
    assert!(
        reason.contains("pending manual review") && reason.contains("manual-review"),
        "reason should describe pending review: {reason}"
    );
}
