//! Tests for the framework-owned [`SagaEnvelope`] aggregate.

use mire::{Aggregate, EventData};
use mire_sagas::{Saga, SagaBuilder, SagaEnvelope, SagaInstanceStatus, SagaStepStatus};
use serde::{Deserialize, Serialize};

/// Minimal saga used to parameterise `SagaEnvelope<TestSaga>`. Topology
/// isn't exercised by these tests — we only validate the aggregate side.
struct TestSaga;

#[derive(Default, Clone, Serialize, Deserialize)]
struct TestState {
    counter: i64,
}

impl Saga for TestSaga {
    type State = TestState;
    fn name() -> &'static str {
        "test-saga"
    }
    fn build(b: SagaBuilder<Self>) -> SagaBuilder<Self> {
        b
    }
}

fn ev_started(counter: i64) -> mire_sagas::envelope::SagaEnvelopeEvent {
    mire_sagas::envelope::SagaEnvelopeEvent::Started {
        state: serde_json::to_value(TestState { counter }).unwrap(),
        trigger: None,
    }
}

#[test]
fn envelope_default_is_notstarted() {
    let env: SagaEnvelope<TestSaga> = SagaEnvelope::default();
    assert_eq!(env.status, SagaInstanceStatus::NotStarted);
    assert_eq!(env.state.counter, 0);
    assert!(env.steps.is_empty());
    assert!(env.outputs.is_empty());
    assert!(env.last_error.is_none());
}

#[test]
fn apply_started_initialises_state_and_moves_to_running() {
    let mut env: SagaEnvelope<TestSaga> = SagaEnvelope::default();
    env.apply(&ev_started(42));
    assert_eq!(env.status, SagaInstanceStatus::Running);
    assert_eq!(env.state.counter, 42);
}

/// C6/DX-3/SAGA-17: when the persisted `Started` state no longer
/// deserializes into the current `S::State` (schema drift), the envelope
/// must *record* the failure rather than silently running on `Default`.
/// The durable runner reads this flag and refuses to act on the instance.
#[test]
fn apply_started_with_undecodable_state_is_flagged_not_silently_defaulted() {
    use mire_sagas::envelope::SagaEnvelopeEvent;

    let mut env: SagaEnvelope<TestSaga> = SagaEnvelope::default();
    // `counter` is `i64`; a string is a decode failure (simulated drift).
    let drifted = SagaEnvelopeEvent::Started {
        state: serde_json::json!({ "counter": "not-an-integer" }),
        trigger: None,
    };
    env.apply(&drifted);

    assert!(
        env.state_decode_error().is_some(),
        "decode failure must be recorded so the runner can refuse to act"
    );
    // It must NOT have silently adopted a default and pretended all is well.
    assert_eq!(env.state.counter, 0, "state stays default but is flagged");

    // A well-typed Started leaves the flag clear.
    let mut ok: SagaEnvelope<TestSaga> = SagaEnvelope::default();
    ok.apply(&ev_started(7));
    assert!(ok.state_decode_error().is_none());
    assert_eq!(ok.state.counter, 7);
}

/// D5: a `Cancelled` saga where some step's compensation failed must end in
/// the *faulty* `FailedTerminalNeedsIntervention` status (not a clean
/// `FailedTerminal`), and expose which compensations need manual handling.
#[test]
fn compensation_failure_yields_needs_intervention_terminal() {
    use mire_sagas::envelope::SagaEnvelopeEvent::*;
    use mire_sagas::result::StepError;

    let mut env: SagaEnvelope<TestSaga> = SagaEnvelope::default();
    env.apply(&ev_started(1));
    // refund compensated cleanly; unbook could not.
    env.apply(&StepCompensated {
        step: "refund".into(),
    });
    env.apply(&StepCompensationFailed {
        step: "unbook".into(),
        error: StepError::Failed {
            step: "unbook".into(),
            reason: "airline API down".into(),
        },
    });
    assert_eq!(
        env.step_status("unbook"),
        SagaStepStatus::CompensationFailed
    );
    assert_eq!(env.step_status("refund"), SagaStepStatus::Compensated);

    env.apply(&Cancelled {
        reason: "hotel failed".into(),
    });
    assert_eq!(
        env.status,
        SagaInstanceStatus::FailedTerminalNeedsIntervention
    );
    assert!(env.needs_intervention());
    assert_eq!(env.failed_compensations(), vec!["unbook"]);
}

/// A clean rollback (no compensation failed) stays `FailedTerminal`.
#[test]
fn clean_rollback_is_plain_failed_terminal() {
    use mire_sagas::envelope::SagaEnvelopeEvent::*;

    let mut env: SagaEnvelope<TestSaga> = SagaEnvelope::default();
    env.apply(&ev_started(1));
    env.apply(&StepCompensated {
        step: "refund".into(),
    });
    env.apply(&Cancelled {
        reason: "hotel failed".into(),
    });
    assert_eq!(env.status, SagaInstanceStatus::FailedTerminal);
    assert!(!env.needs_intervention());
    assert!(env.failed_compensations().is_empty());
}

/// `StepResolving` marks the transient claimed-but-not-resolved state.
#[test]
fn step_resolving_marks_transient_claim() {
    use mire_sagas::envelope::SagaEnvelopeEvent::*;

    let mut env: SagaEnvelope<TestSaga> = SagaEnvelope::default();
    env.apply(&ev_started(1));
    env.apply(&StepActivated {
        step: "capture".into(),
    });
    env.apply(&StepResolving {
        step: "capture".into(),
    });
    assert_eq!(env.step_status("capture"), SagaStepStatus::Resolving);
}

#[test]
fn step_lifecycle_is_recorded() {
    use mire_sagas::envelope::SagaEnvelopeEvent::*;
    let mut env: SagaEnvelope<TestSaga> = SagaEnvelope::default();
    env.apply(&ev_started(1));
    env.apply(&StepActivated {
        step: "flight".into(),
    });
    assert_eq!(env.step_status("flight"), SagaStepStatus::Active);

    let output = serde_json::json!({ "hold_id": "fh-1" });
    env.apply(&StepCompleted {
        step: "flight".into(),
        output: output.clone(),
    });
    assert_eq!(env.step_status("flight"), SagaStepStatus::Completed);
    assert_eq!(env.outputs.get("flight"), Some(&output));
}

#[test]
fn step_failure_records_last_error() {
    use mire_sagas::StepError;
    use mire_sagas::envelope::SagaEnvelopeEvent::*;

    let mut env: SagaEnvelope<TestSaga> = SagaEnvelope::default();
    env.apply(&ev_started(1));
    env.apply(&StepActivated {
        step: "payment".into(),
    });
    let err = StepError::Timeout {
        step: "payment".into(),
        timeout_ms: 1000,
    };
    env.apply(&StepFailed {
        step: "payment".into(),
        error: err.clone(),
    });

    assert_eq!(env.step_status("payment"), SagaStepStatus::Failed);
    assert!(env.last_error.is_some());
    assert!(env.originating_error.is_some());
}

#[test]
fn cascade_failures_preserve_originating_but_overwrite_last_error() {
    use mire_sagas::StepError;
    use mire_sagas::envelope::SagaEnvelopeEvent::*;

    let mut env: SagaEnvelope<TestSaga> = SagaEnvelope::default();
    env.apply(&ev_started(1));

    // First failure — the *originating* error.
    let first = StepError::Timeout {
        step: "payment".into(),
        timeout_ms: 1000,
    };
    env.apply(&StepFailed {
        step: "payment".into(),
        error: first.clone(),
    });

    // Second failure during cascade — overwrites last_error but
    // NOT originating_error.
    let second = StepError::Failed {
        step: "release-hold".into(),
        reason: "downstream unreachable".into(),
    };
    env.apply(&StepFailed {
        step: "release-hold".into(),
        error: second.clone(),
    });

    // originating preserves the FIRST failure (why compensation started).
    let orig = env.originating_error.as_ref().unwrap();
    assert!(matches!(orig, StepError::Timeout { .. }), "got {orig:?}");

    // last_error reflects the most recent failure.
    let last = env.last_error.as_ref().unwrap();
    assert!(matches!(last, StepError::Failed { .. }), "got {last:?}");
}

#[test]
fn confirmed_transitions_to_succeeded_terminal() {
    use mire_sagas::envelope::SagaEnvelopeEvent::*;
    let mut env: SagaEnvelope<TestSaga> = SagaEnvelope::default();
    env.apply(&ev_started(1));
    env.apply(&Confirmed);
    assert_eq!(env.status, SagaInstanceStatus::SucceededTerminal);
}

#[test]
fn cancelled_transitions_to_failed_terminal() {
    use mire_sagas::envelope::SagaEnvelopeEvent::*;
    let mut env: SagaEnvelope<TestSaga> = SagaEnvelope::default();
    env.apply(&ev_started(1));
    env.apply(&Cancelled {
        reason: "operator".into(),
    });
    assert_eq!(env.status, SagaInstanceStatus::FailedTerminal);
}

#[test]
fn compensating_then_compensated_transitions_step_status() {
    use mire_sagas::envelope::SagaEnvelopeEvent::*;
    let mut env: SagaEnvelope<TestSaga> = SagaEnvelope::default();
    env.apply(&ev_started(1));
    env.apply(&StepActivated {
        step: "flight".into(),
    });
    env.apply(&StepCompleted {
        step: "flight".into(),
        output: serde_json::json!({}),
    });
    env.apply(&StepCompensating {
        step: "flight".into(),
    });
    assert_eq!(env.step_status("flight"), SagaStepStatus::Compensating);
    env.apply(&StepCompensated {
        step: "flight".into(),
    });
    assert_eq!(env.step_status("flight"), SagaStepStatus::Compensated);
}

#[test]
fn stream_category_is_saga_name() {
    assert_eq!(
        <SagaEnvelope<TestSaga> as Aggregate>::stream_category(),
        "test-saga"
    );
}

#[test]
fn envelope_event_types_are_stable_strings() {
    use mire_sagas::envelope::SagaEnvelopeEvent::*;
    let cases: Vec<(_, &str)> = vec![
        (
            Started {
                state: serde_json::Value::Null,
                trigger: None,
            },
            "saga.started",
        ),
        (StepActivated { step: "x".into() }, "saga.step-activated"),
        (
            StepCompleted {
                step: "x".into(),
                output: serde_json::Value::Null,
            },
            "saga.step-completed",
        ),
        (
            StepFailed {
                step: "x".into(),
                error: mire_sagas::StepError::Timeout {
                    step: "x".into(),
                    timeout_ms: 0,
                },
            },
            "saga.step-failed",
        ),
        (
            StepCompensating { step: "x".into() },
            "saga.step-compensating",
        ),
        (
            StepCompensated { step: "x".into() },
            "saga.step-compensated",
        ),
        (Confirmed, "saga.confirmed"),
        (Cancelled { reason: "".into() }, "saga.cancelled"),
    ];
    for (event, expected) in cases {
        assert_eq!(event.event_type(), expected, "mismatch for {event:?}");
    }
}

#[test]
fn unknown_step_status_is_pending() {
    let env: SagaEnvelope<TestSaga> = SagaEnvelope::default();
    assert_eq!(env.step_status("never-registered"), SagaStepStatus::Pending);
}
