//! Regression test: a user handler that panics does NOT crash the host
//! process. The runner catches the panic and surfaces it as
//! `StepError::Failed` so compensation cascades cleanly.
//!
//! Gated on `DATABASE_URL`; skips silently when unset.

#![allow(dead_code)]

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

mod common;

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

#[derive(Serialize, Deserialize, Clone, EventData)]
#[mire(event_type = "source.event")]
struct SourceEvent {
    id: String,
    kind: PanicKind,
}

#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Default)]
enum PanicKind {
    #[default]
    StaticStr,
    OwnedString,
    Indexing,
}

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

#[derive(Default, Clone, Serialize, Deserialize)]
struct St {
    kind: PanicKind,
}

#[derive(Debug, Clone, Serialize, Deserialize, EventData)]
#[mire(event_type = "panic-test.request")]
struct DummyRequest;

#[derive(Debug, Clone, Serialize, Deserialize, EventData)]
#[mire(event_type = "panic-test.response")]
struct DummyResponse;

#[derive(Debug, Clone, Serialize, Deserialize)]
struct DummyOutput;

async fn panicking_step(State(s): State<St>) -> Emit {
    match s.kind {
        PanicKind::StaticStr => panic!("static str panic"),
        PanicKind::OwnedString => panic!("{}", "owned string panic".to_string()),
        PanicKind::Indexing => {
            // Force an out-of-bounds panic.
            let v: Vec<i32> = vec![];
            let _x = v[42];
            #[allow(unreachable_code)]
            Emit::event(DummyRequest)
        }
    }
}

struct PanicSaga;

impl Saga for PanicSaga {
    type State = St;
    fn name() -> &'static str {
        "panic-saga"
    }
    fn build(b: SagaBuilder<Self>) -> SagaBuilder<Self> {
        b.start_on::<Source, _>(|e: &SourceEvent| {
            Some(Start {
                instance_id: e.id.clone(),
                state: St {
                    kind: e.kind.clone(),
                },
            })
        })
        .step(
            "step",
            task(panicking_step)
                .terminal_success()
                .on_response(|_: DummyResponse| DummyOutput),
        )
    }
}

async fn trigger_kind(kind: PanicKind, id: &str) -> Option<SagaInstanceStatus> {
    let store = common::store().await?;
    let runner = PgSagaRunner::new(
        PanicSaga::build(SagaBuilder::<PanicSaga>::for_saga()),
        store,
    );
    let id_str = runner
        .trigger::<Source>(&SourceEvent {
            id: common::unique(id),
            kind,
        })
        .await
        .expect("trigger never errors")
        .expect("instance started");

    let status = runner.status(&id_str).await.unwrap().expect("instance");
    let step = runner.step_status(&id_str, "step").await.unwrap();

    // Step should be Failed (not Active, not Completed).
    assert_eq!(step, SagaStepStatus::Failed);

    // Failure reason mentions the panic.
    let env = runner.load(&id_str).await.unwrap().unwrap();
    let reason = env
        .last_error
        .as_ref()
        .map(|e| format!("{e}"))
        .unwrap_or_default();
    assert!(
        reason.to_lowercase().contains("panic"),
        "failure reason should mention panic; got: {reason}"
    );

    Some(status)
}

#[tokio::test]
async fn handler_panic_with_static_str_does_not_crash() {
    let Some(status) = trigger_kind(PanicKind::StaticStr, "panic-static").await else {
        return;
    };
    assert_eq!(status, SagaInstanceStatus::FailedTerminal);
}

#[tokio::test]
async fn handler_panic_with_owned_string_does_not_crash() {
    let Some(status) = trigger_kind(PanicKind::OwnedString, "panic-owned").await else {
        return;
    };
    assert_eq!(status, SagaInstanceStatus::FailedTerminal);
}

#[tokio::test]
async fn handler_panic_from_indexing_does_not_crash() {
    let Some(status) = trigger_kind(PanicKind::Indexing, "panic-index").await else {
        return;
    };
    assert_eq!(status, SagaInstanceStatus::FailedTerminal);
}
