//! Tests for the `routes_to_step` signal policy.
//!
//! Scenario: an approval saga where a step awaits a manual decision
//! event. The decision arrives as a typed signal that gets delivered as
//! the step's awaited event.
//!
//! 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;

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

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

#[derive(Serialize, Deserialize, Clone, EventData)]
#[mire(event_type = "account.review-needed")]
struct ReviewNeeded {
    account_id: String,
}

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

// =====================================================================
// Saga state + outputs
// =====================================================================

#[derive(Default, Clone, Serialize, Deserialize)]
struct ReviewState {
    account_id: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct ReviewOutcome {
    approved: bool,
}

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

// =====================================================================
// The manual-decision signal.
//
// This is BOTH the signal type the operator sends AND the event the
// `await_decision` step is waiting on. The `routes_to_step` policy
// delivers the signal as the step's awaited event.
// =====================================================================

#[derive(Debug, Clone, Serialize, Deserialize, EventData)]
#[mire(event_type = "account.decision")]
struct AccountDecision {
    approved: bool,
}

// =====================================================================
// Steps
// =====================================================================

fn await_decision(State(_s): State<ReviewState>) -> Emit {
    // No emit — just await the operator's decision signal.
    Emit::none()
}

fn finalise(mire_sagas::Output(outcome): mire_sagas::Output<ReviewOutcome>) -> Done {
    let _approved = outcome.approved;
    Done
}

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

struct ReviewSaga;

impl Saga for ReviewSaga {
    type State = ReviewState;
    fn name() -> &'static str {
        "review-saga"
    }
    fn build(b: SagaBuilder<Self>) -> SagaBuilder<Self> {
        b.start_on::<Account, _>(|e: &ReviewNeeded| {
            Some(Start {
                instance_id: e.account_id.clone(),
                state: ReviewState {
                    account_id: e.account_id.clone(),
                },
            })
        })
        .step(
            "await_decision",
            task(await_decision).on_response(|d: AccountDecision| ReviewOutcome {
                approved: d.approved,
            }),
        )
        .step(
            "finalise",
            task(finalise).after("await_decision").terminal_success(),
        )
        // The AccountDecision signal IS the event `await_decision` awaits.
        // Operator sends `runner.signal(&id, AccountDecision { ... })` and
        // the runner routes it as the step's await event.
        .signal::<AccountDecision>()
        .routes_to_step("await_decision")
    }
}

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

fn build_runner(store: &mire::EventStore) -> PgSagaRunner<ReviewSaga> {
    let topology = ReviewSaga::build(SagaBuilder::<ReviewSaga>::for_saga());
    PgSagaRunner::new(topology, store.clone())
}

#[tokio::test]
async fn signal_routes_to_awaiting_step_as_typed_event() {
    let Some(store) = common::store().await else {
        return;
    };
    let runner = build_runner(&store);
    let instance_id = runner
        .trigger::<Account>(&ReviewNeeded {
            account_id: common::unique("acc-1"),
        })
        .await
        .unwrap()
        .unwrap();

    // await_decision is Active, awaiting AccountDecision.
    assert_eq!(
        runner
            .step_status(&instance_id, "await_decision")
            .await
            .unwrap(),
        SagaStepStatus::Active
    );
    assert_eq!(
        runner.status(&instance_id).await.unwrap(),
        Some(SagaInstanceStatus::Running)
    );

    // Operator sends the decision signal. The runner routes it as the
    // step's awaited event and reports it was consumed (DX-8/SAGA-10).
    let outcome = runner
        .signal(&instance_id, AccountDecision { approved: true })
        .await
        .unwrap();
    assert_eq!(outcome, mire_sagas::SignalOutcome::Accepted);

    // Step completed; finalise activated and reached terminal-success.
    assert_eq!(
        runner
            .step_status(&instance_id, "await_decision")
            .await
            .unwrap(),
        SagaStepStatus::Completed
    );
    assert_eq!(
        runner.step_status(&instance_id, "finalise").await.unwrap(),
        SagaStepStatus::Completed
    );
    assert_eq!(
        runner.status(&instance_id).await.unwrap(),
        Some(SagaInstanceStatus::SucceededTerminal)
    );
}

#[tokio::test]
async fn signal_to_wrong_step_errors_loudly() {
    // Build a saga whose `routes_to_step` points at a step that DOESN'T
    // await the signal's event type. Phase 2 moved this from a runtime
    // `SignalRouteMismatch` to a BUILD-TIME topology error: awaits are
    // declared on the topology, so the validator can see the target step
    // awaits nothing of the signal's type before any traffic.
    struct MisroutedSaga;
    impl Saga for MisroutedSaga {
        type State = ReviewState;
        fn name() -> &'static str {
            "misrouted-saga"
        }
        fn build(b: SagaBuilder<Self>) -> SagaBuilder<Self> {
            b.start_on::<Account, _>(|e: &ReviewNeeded| {
                Some(Start {
                    instance_id: e.account_id.clone(),
                    state: ReviewState {
                        account_id: e.account_id.clone(),
                    },
                })
            })
            // `await_decision` is the step that awaits AccountDecision,
            // but routes_to_step points elsewhere.
            .step(
                "await_decision",
                task(await_decision).on_response(|d: AccountDecision| ReviewOutcome {
                    approved: d.approved,
                }),
            )
            .step(
                "finalise",
                task(finalise).after("await_decision").terminal_success(),
            )
            .signal::<AccountDecision>()
            .routes_to_step("finalise") // ← wrong target
        }
    }

    let topology = MisroutedSaga::build(SagaBuilder::<MisroutedSaga>::for_saga());
    let err = topology
        .validate()
        .expect_err("misrouted signal must fail validation");
    assert!(
        matches!(
            err,
            mire_sagas::TopologyError::SignalRouteAwaitMismatch {
                step: "finalise",
                signal_event_type: "account.decision",
                ..
            }
        ),
        "expected SignalRouteAwaitMismatch, got {err:?}"
    );
}

#[tokio::test]
async fn signal_delivers_typed_payload() {
    // The `approved: false` case proves the payload threads through
    // correctly — the step's await closure decodes the signal and the
    // typed output reflects the operator's choice.
    let Some(store) = common::store().await else {
        return;
    };
    let runner = build_runner(&store);
    let instance_id = runner
        .trigger::<Account>(&ReviewNeeded {
            account_id: common::unique("acc-2"),
        })
        .await
        .unwrap()
        .unwrap();

    runner
        .signal(&instance_id, AccountDecision { approved: false })
        .await
        .unwrap();

    // The env's outputs map should contain the ReviewOutcome with approved=false.
    let env = runner.load(&instance_id).await.unwrap().unwrap();
    let outcome_json = env.outputs.get("await_decision").unwrap();
    let outcome: ReviewOutcome = serde_json::from_value(outcome_json.clone()).unwrap();
    assert!(!outcome.approved);
}
