//! End-to-end test of step handler dispatch:
//! - register an `async fn` handler via the builder
//! - synthesise a StepContext
//! - invoke the registered handler through the erased shape
//! - check the dispatch envelope and the typed output round-trip

use std::any::TypeId;
use std::collections::BTreeMap;
use std::sync::Arc;

use mire::{Aggregate, EventData};
use mire_sagas::handler::ErasedStepOutcome;
use mire_sagas::{
    Dep, Emit, ExtractError, Saga, SagaBuilder, Start, State, StepContext, Trigger, task,
};
#[allow(unused_imports)]
use serde::{Deserialize as _, Serialize as _};
use serde::{Deserialize, Serialize};

// =====================================================================
// Fixtures
// =====================================================================

#[derive(Default, Clone, Serialize, Deserialize, Debug)]
struct St {
    booking_id: String,
}

// A typed request event using the single-derive form.
#[derive(Serialize, Deserialize, Debug, EventData)]
#[mire(event_type = "test.reserve-requested")]
struct ReserveRequest {
    booking_id: String,
}

struct TS;
impl Saga for TS {
    type State = St;
    fn name() -> &'static str {
        "ts"
    }
    fn build(b: SagaBuilder<Self>) -> SagaBuilder<Self> {
        b
    }
}

// Trigger aggregate.
#[derive(Default, Clone)]
struct Trig;
#[derive(Serialize, Deserialize, EventData)]
#[mire(event_type = "trig.ev")]
struct TrigEv {
    booking_id: String,
}
impl Aggregate for Trig {
    type Event = TrigEv;
    fn stream_category() -> &'static str {
        "trig"
    }
    fn apply(&mut self, _: &Self::Event) {}
}

// Awaited event — single-derive form.
#[derive(Serialize, Deserialize, EventData)]
#[mire(event_type = "thing.held")]
struct Held {
    hold_id: String,
}

#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct OutReserved {
    hold_id: String,
}

// The handler under test — emits only; the await + map are declared on
// the topology at registration.
async fn reserve(Trigger(t): Trigger<TrigEv>, State(state): State<St>) -> Emit {
    assert_eq!(t.booking_id, state.booking_id);
    Emit::event(ReserveRequest {
        booking_id: state.booking_id.clone(),
    })
}

// =====================================================================
// Test
// =====================================================================

#[tokio::test]
async fn registered_handler_dispatches_and_returns_typed_output() {
    let topology = SagaBuilder::<TS>::for_saga()
        .start_on::<Trig, _>(|e: &TrigEv| {
            Some(Start {
                instance_id: e.booking_id.clone(),
                state: St {
                    booking_id: e.booking_id.clone(),
                },
            })
        })
        .step(
            "reserve",
            task(reserve)
                .on_response(|h: Held| OutReserved { hold_id: h.hold_id })
                .terminal_success(),
        );

    topology.assert_valid();

    // Synthesise a context that mimics what the runner would build at
    // step-activation time.
    let state = St {
        booking_id: "b-7".into(),
    };
    let trigger_json = serde_json::to_value(TrigEv {
        booking_id: "b-7".into(),
    })
    .unwrap();
    let outputs: BTreeMap<TypeId, serde_json::Value> = BTreeMap::new();
    let deps: BTreeMap<TypeId, Arc<dyn std::any::Any + Send + Sync>> = BTreeMap::new();
    let ctx: StepContext<'_, TS> =
        StepContext::for_testing(&state, Some(&trigger_json), &outputs, &deps, None);

    // Pull the erased handler out of the topology and invoke it directly.
    let entries = topology.step_entries();
    let entry = entries.iter().find(|e| e.name == "reserve").unwrap();
    let erased = (entry.handler)(ctx).await.expect("handler runs");

    // The handler emitted the ReserveRequest event; the await is declared
    // on the topology (`.on_response`), visible via the topology accessor.
    let ErasedStepOutcome::Emit { dispatch, .. } = erased else {
        panic!("emit handler must produce an Emit outcome");
    };
    assert_eq!(dispatch.len(), 1);
    assert_eq!(dispatch[0].event_type, Some("test.reserve-requested"));
    let dispatched: ReserveRequest = serde_json::from_value(dispatch[0].payload.clone()).unwrap();
    assert_eq!(dispatched.booking_id, "b-7");

    assert_eq!(
        topology.step_await_event_type("reserve"),
        Some("thing.held")
    );
}

#[tokio::test]
async fn extractor_failure_propagates_as_extract_error_not_panic() {
    // Handler asks for a Dep that was never registered. The runner should
    // surface a clean ExtractError, NOT panic.
    #[derive(Clone, Debug)]
    struct MissingDep;

    async fn needs_dep(_: Trigger<TrigEv>, _: Dep<MissingDep>) -> Emit {
        unreachable!("handler body should not run when extractor fails")
    }

    let topology = SagaBuilder::<TS>::for_saga()
        .start_on::<Trig, _>(|e: &TrigEv| {
            Some(Start {
                instance_id: e.booking_id.clone(),
                state: St::default(),
            })
        })
        .step("needs_dep", task(needs_dep).terminal_success());

    let state = St::default();
    let trigger_json = serde_json::to_value(TrigEv {
        booking_id: "b".into(),
    })
    .unwrap();
    let outputs: BTreeMap<TypeId, serde_json::Value> = BTreeMap::new();
    let deps: BTreeMap<TypeId, Arc<dyn std::any::Any + Send + Sync>> = BTreeMap::new();
    let ctx: StepContext<'_, TS> =
        StepContext::for_testing(&state, Some(&trigger_json), &outputs, &deps, None);

    let entries = topology.step_entries();
    let entry = entries.iter().find(|e| e.name == "needs_dep").unwrap();
    let err = match (entry.handler)(ctx).await {
        Err(e) => e,
        Ok(_) => panic!("missing dep should fail extraction"),
    };
    assert!(matches!(err, ExtractError::MissingDep(_)));
}

#[tokio::test]
async fn handler_with_immediate_complete_returns_output() {
    // A value handler — completes right away with its return value.
    async fn quick(_: Trigger<TrigEv>) -> OutReserved {
        OutReserved {
            hold_id: "instant".into(),
        }
    }

    let topology = SagaBuilder::<TS>::for_saga()
        .start_on::<Trig, _>(|e: &TrigEv| {
            Some(Start {
                instance_id: e.booking_id.clone(),
                state: St::default(),
            })
        })
        .step("quick", task(quick).terminal_success());

    topology.assert_valid();

    let state = St::default();
    let trigger_json = serde_json::to_value(TrigEv {
        booking_id: "b".into(),
    })
    .unwrap();
    let outputs: BTreeMap<TypeId, serde_json::Value> = BTreeMap::new();
    let deps: BTreeMap<TypeId, Arc<dyn std::any::Any + Send + Sync>> = BTreeMap::new();
    let ctx: StepContext<'_, TS> =
        StepContext::for_testing(&state, Some(&trigger_json), &outputs, &deps, None);

    let entries = topology.step_entries();
    let entry = entries.iter().find(|e| e.name == "quick").unwrap();
    let erased = (entry.handler)(ctx).await.expect("handler runs");

    let ErasedStepOutcome::Value(output) = erased else {
        panic!("value handler must produce a Value outcome");
    };
    assert!(topology.step_await_event_type("quick").is_none());
    let out: OutReserved = serde_json::from_value(output.expect("serialises")).unwrap();
    assert_eq!(
        out,
        OutReserved {
            hold_id: "instant".into()
        }
    );
}
