//! Regression tests proving the runner never panics on bad input:
//!   1. Serialization failures in a step handler's emit/output → step
//!      fails with StepError::Failed, compensation cascade fires.
//!   2. Unknown instance_id passed to deliver → no-op (`Ok(false)`),
//!      passed to signal → typed `UnknownInstance` error.
//!   3. The library has no unwrap/expect in production paths.
//!
//! Gated on `DATABASE_URL`; skips silently when unset.

#![allow(dead_code)]

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

mod common;

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

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

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

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

// =====================================================================
// Saga with a step whose emit payload fails to serialise — a real
// serialization failure that previously would have panicked via
// .expect().
// =====================================================================

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

/// A wrapper whose Serialize impl always returns an error. Forces a
/// deterministic serialisation failure inside `Emit::event`.
#[derive(Debug, Clone)]
struct AlwaysFails;

impl Serialize for AlwaysFails {
    fn serialize<S: Serializer>(&self, _: S) -> Result<S::Ok, S::Error> {
        Err(serde::ser::Error::custom(
            "test: deliberate serialise failure",
        ))
    }
}

impl<'de> Deserialize<'de> for AlwaysFails {
    fn deserialize<D: serde::de::Deserializer<'de>>(_: D) -> Result<Self, D::Error> {
        Ok(AlwaysFails)
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, EventData)]
#[mire(event_type = "bad.request")]
struct BadRequest {
    /// A field whose Serialize always errors.
    bad: AlwaysFails,
}

#[derive(Debug, Clone, Serialize, Deserialize, EventData)]
#[mire(event_type = "bad.response")]
struct BadResponse;

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

fn bad_step(State(_s): State<St>) -> Emit {
    Emit::event(BadRequest { bad: AlwaysFails })
}

async fn handle_bad(Req(_): Req<BadRequest>) -> BadResponse {
    BadResponse
}

struct BadSaga;

impl Saga for BadSaga {
    type State = St;
    fn name() -> &'static str {
        "bad-saga"
    }

    fn build(b: SagaBuilder<Self>) -> SagaBuilder<Self> {
        b.start_on::<Source, _>(|e: &SourceEvent| {
            Some(Start {
                instance_id: e.id.clone(),
                state: St {},
            })
        })
        .step(
            "bad",
            task(bad_step)
                .terminal_success()
                .on_response(|_: BadResponse| BadOutput),
        )
        .effect(handle_bad)
    }
}

#[tokio::test]
async fn serialization_failure_fails_step_does_not_panic() {
    let Some(store) = common::store().await else {
        return;
    };
    let topology = BadSaga::build(SagaBuilder::<BadSaga>::for_saga());
    let runner = PgSagaRunner::new(topology, store);

    // Trigger — the step's Emit fails to serialise its payload.
    // Previously this would panic; now the step fails gracefully and the
    // saga reaches FailedTerminal.
    let instance_id = runner
        .trigger::<Source>(&SourceEvent {
            id: common::unique("no-panic"),
        })
        .await
        .expect("trigger does not error")
        .expect("instance started");

    let env = runner.load(&instance_id).await.unwrap().expect("envelope");
    assert_eq!(env.status, SagaInstanceStatus::FailedTerminal);
    assert_eq!(
        runner.step_status(&instance_id, "bad").await.unwrap(),
        SagaStepStatus::Failed
    );
    // The failure reason names the serialisation issue.
    let reason = env
        .last_error
        .as_ref()
        .map(|e| format!("{e}"))
        .unwrap_or_default();
    assert!(
        reason.to_lowercase().contains("seriali"),
        "reason should mention serialisation: {reason}"
    );
}

// A step handler that PANICS (a user bug), not a graceful failure. The runner
// must catch it (run_caught in activate_step) and turn it into a step failure +
// compensation cascade — never abort the host (principle 9). This is the saga
// analog of the projection panicking-handler containment test.
fn panicking_step(State(_s): State<St>) -> Emit {
    panic!("boom: deliberate step-handler panic");
}

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 {},
            })
        })
        .step(
            "boom",
            task(panicking_step)
                .terminal_success()
                .on_response(|_: BadResponse| BadOutput),
        )
        .effect(handle_bad)
    }
}

#[tokio::test]
async fn panicking_step_handler_fails_the_step_does_not_panic_the_host() {
    let Some(store) = common::store().await else {
        return;
    };
    let topology = PanicSaga::build(SagaBuilder::<PanicSaga>::for_saga());
    let runner = PgSagaRunner::new(topology, store);

    // trigger drives the step inline; its handler panics. The runner catches
    // the panic and fails the step — `trigger` returns Ok (the caller's thread
    // is never unwound), the saga reaches FailedTerminal.
    let instance_id = runner
        .trigger::<Source>(&SourceEvent {
            id: common::unique("panic-step"),
        })
        .await
        .expect("trigger must not propagate the handler panic")
        .expect("instance started");

    let env = runner.load(&instance_id).await.unwrap().expect("envelope");
    assert_eq!(env.status, SagaInstanceStatus::FailedTerminal);
    assert_eq!(
        runner.step_status(&instance_id, "boom").await.unwrap(),
        SagaStepStatus::Failed
    );
    let reason = env
        .last_error
        .as_ref()
        .map(|e| format!("{e}"))
        .unwrap_or_default();
    assert!(
        reason.to_lowercase().contains("panic"),
        "the failure reason should name the panic: {reason}"
    );
}

#[tokio::test]
async fn deliver_to_unknown_instance_is_a_no_op() {
    let Some(store) = common::store().await else {
        return;
    };
    let topology = BadSaga::build(SagaBuilder::<BadSaga>::for_saga());
    let runner = PgSagaRunner::new(topology, store);

    // Durable semantics: delivering to an instance nothing awaits on is an
    // idempotent no-op — the duplicate-delivery contract, not an error.
    let consumed = runner
        .deliver_raw(
            &common::unique("does-not-exist"),
            "bad.response",
            serde_json::Value::Null,
        )
        .await
        .expect("deliver never errors on unknown instances");
    assert!(!consumed, "nothing should have consumed the event");
}

#[tokio::test]
async fn signal_to_unknown_instance_returns_typed_error_not_panic() {
    let Some(store) = common::store().await else {
        return;
    };

    #[derive(Debug, Clone, Serialize, Deserialize, EventData)]
    #[mire(event_type = "bad.cancel")]
    struct Cancel;

    struct SignallingSaga;
    impl Saga for SignallingSaga {
        type State = St;
        fn name() -> &'static str {
            "bad-saga-signalling"
        }
        fn build(b: SagaBuilder<Self>) -> SagaBuilder<Self> {
            b.start_on::<Source, _>(|e: &SourceEvent| {
                Some(Start {
                    instance_id: e.id.clone(),
                    state: St {},
                })
            })
            .step(
                "bad",
                task(bad_step)
                    .terminal_success()
                    .on_response(|_: BadResponse| BadOutput),
            )
            .signal::<Cancel>()
            .triggers_compensation()
        }
    }

    let topology = SignallingSaga::build(SagaBuilder::<SignallingSaga>::for_saga());
    let runner = PgSagaRunner::new(topology, store);

    let result = runner
        .signal(&common::unique("does-not-exist"), Cancel)
        .await;
    assert!(matches!(result, Err(PgRunnerError::UnknownInstance(_))));
}
