//! Tests for `SagaBuilder` topology validation.

use mire::{Aggregate, EventData};
use mire_sagas::{Emit, Output, Saga, SagaBuilder, Start, TopologyError, Trigger, comp, task};
use serde::{Deserialize, Serialize};

// =====================================================================
// Test fixtures
// =====================================================================

#[derive(Default, Clone, Serialize, Deserialize)]
struct TestState;

// Typed emit event used by the topology tests' handlers.
#[derive(Serialize, Deserialize, EventData)]
#[mire(event_type = "test.noop")]
struct NoopRequest;

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

// A minimal trigger aggregate to satisfy `start_on`.
#[derive(Default, Clone)]
struct Triggerer;

#[derive(Serialize, Deserialize, EventData)]
#[mire(event_type = "triggerer.event")]
struct TriggererEvent;

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

// Awaited events — one per step so parallel fixtures don't trip the
// build-time ambiguous-await check (each step's response type must be
// distinguishable when steps can be concurrently active).
#[derive(Serialize, Deserialize, EventData)]
#[mire(event_type = "an.event-a")]
struct AnEventA;
#[derive(Serialize, Deserialize, EventData)]
#[mire(event_type = "an.event-b")]
struct AnEventB;
#[derive(Serialize, Deserialize, EventData)]
#[mire(event_type = "an.event-c")]
struct AnEventC;

#[derive(Serialize, Deserialize)]
struct OutputA;
#[derive(Serialize, Deserialize)]
struct OutputB;
#[derive(Serialize, Deserialize)]
struct OutputC;

// One emit handler is enough — outputs now come from each task's
// `.on_response(...)` declaration, so distinct output types are a
// registration concern, not a handler concern.
fn handler_a(Trigger(_): Trigger<TriggererEvent>) -> Emit {
    Emit::event(NoopRequest)
}

fn handler_b(Trigger(_): Trigger<TriggererEvent>) -> Emit {
    Emit::event(NoopRequest)
}

fn handler_c(Trigger(_): Trigger<TriggererEvent>) -> Emit {
    Emit::event(NoopRequest)
}

fn comp_a(Output(_): Output<OutputA>) -> Emit {
    Emit::none()
}

fn b() -> SagaBuilder<TestSaga> {
    SagaBuilder::<TestSaga>::for_saga().start_on::<Triggerer, _>(|_e| {
        Some(Start {
            instance_id: "x".into(),
            state: TestState,
        })
    })
}

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

#[test]
fn happy_path_validates() {
    let topology = b()
        .step("a", task(handler_a).on_response(|_: AnEventA| OutputA))
        .step(
            "b",
            task(handler_b)
                .on_response(|_: AnEventB| OutputB)
                .after("a")
                .terminal_success(),
        );
    assert_eq!(topology.validate(), Ok(()));
}

#[test]
fn missing_start_on_fails() {
    let topology = SagaBuilder::<TestSaga>::for_saga().step(
        "a",
        task(handler_a)
            .on_response(|_: AnEventA| OutputA)
            .terminal_success(),
    );
    assert_eq!(topology.validate(), Err(TopologyError::MissingStartOn));
}

#[test]
fn duplicate_step_name_fails() {
    let topology = b()
        .step("a", task(handler_a).on_response(|_: AnEventA| OutputA))
        .step(
            "a",
            task(handler_a)
                .on_response(|_: AnEventA| OutputA)
                .terminal_success(),
        );
    assert_eq!(
        topology.validate(),
        Err(TopologyError::DuplicateStepName("a"))
    );
}

#[test]
fn unknown_after_ref_fails() {
    let topology = b().step(
        "a",
        task(handler_a)
            .on_response(|_: AnEventA| OutputA)
            .after("missing")
            .terminal_success(),
    );
    assert_eq!(
        topology.validate(),
        Err(TopologyError::UnknownAfterRef {
            step: "a",
            unknown: "missing",
        })
    );
}

#[test]
fn self_after_ref_fails() {
    // A step naming itself in `.after()` is caught by the SelfAfterRef
    // check, which runs BEFORE the forward/cycle check. Pin the exact
    // variant so we notice if the validator order changes.
    let topology = b().step(
        "a",
        task(handler_a)
            .on_response(|_: AnEventA| OutputA)
            .after("a")
            .terminal_success(),
    );
    assert_eq!(topology.validate(), Err(TopologyError::SelfAfterRef("a")),);
}

#[test]
fn unknown_gated_by_ref_fails() {
    #[derive(Serialize, Deserialize)]
    struct DummyOut;
    let topology = b()
        .step("a", task(handler_a).on_response(|_: AnEventA| OutputA))
        .step(
            "b",
            task(handler_b)
                .on_response(|_: AnEventB| OutputB)
                .after("a")
                .gated_by("nope", |_: &DummyOut| true)
                .terminal_success(),
        );
    assert!(matches!(
        topology.validate(),
        Err(TopologyError::UnknownGateRef {
            step: "b",
            unknown: "nope"
        })
    ));
}

#[test]
fn duplicate_output_type_fails() {
    // Two steps producing the same output type. Pre-fix this silently
    // collapsed in the Output<T> extractor view; now it's caught at
    // build time.
    //
    // handler_a and handler_b have distinct OutputA / OutputB types in
    // this test file, so reuse handler_a twice to share OutputA.
    let topology = b()
        .step("first", task(handler_a).on_response(|_: AnEventA| OutputA))
        .step(
            "second",
            task(handler_a)
                .on_response(|_: AnEventA| OutputA)
                .after("first")
                .terminal_success(),
        );
    assert!(matches!(
        topology.validate(),
        Err(TopologyError::DuplicateOutputType {
            first: "first",
            second: "second",
            ..
        })
    ));
}

#[test]
fn unknown_signal_route_fails() {
    #[derive(Serialize, Deserialize, EventData)]
    #[mire(event_type = "test.my-signal")]
    struct MySignal;

    let topology = b()
        .step(
            "a",
            task(handler_a)
                .on_response(|_: AnEventA| OutputA)
                .terminal_success(),
        )
        .signal::<MySignal>()
        .routes_to_step("missing");
    assert!(matches!(
        topology.validate(),
        Err(TopologyError::UnknownSignalRoute {
            unknown: "missing",
            ..
        })
    ));
}

#[test]
fn duplicate_start_on_panics() {
    // start_on twice is a build-time config error. The panic is
    // catch_unwind-able since builder configuration happens at boot
    // before the runner exists.
    let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
        let _ = SagaBuilder::<TestSaga>::for_saga()
            .start_on::<Triggerer, _>(|_e| {
                Some(Start {
                    instance_id: "x".into(),
                    state: TestState,
                })
            })
            .start_on::<Triggerer, _>(|_e| {
                Some(Start {
                    instance_id: "y".into(),
                    state: TestState,
                })
            });
    }));
    assert!(res.is_err(), "second start_on should panic");
}

#[test]
fn duplicate_signal_registration_fails() {
    #[derive(Serialize, Deserialize)]
    struct MySignal;

    let topology = b()
        .step(
            "a",
            task(handler_a)
                .on_response(|_: AnEventA| OutputA)
                .terminal_success(),
        )
        .signal::<MySignal>()
        .triggers_compensation()
        .signal::<MySignal>()
        .triggers_compensation();
    assert!(matches!(
        topology.validate(),
        Err(TopologyError::DuplicateSignal(_))
    ));
}

#[test]
fn no_terminal_step_fails() {
    let topology = b().step("a", task(handler_a).on_response(|_: AnEventA| OutputA));
    assert_eq!(topology.validate(), Err(TopologyError::NoTerminalStep));
}

#[test]
fn forward_after_ref_now_validates() {
    // Pre-Kahn's the validator rejected `.after()` references to a
    // step declared later. Post-Kahn's that's just style — the runner
    // re-evaluates each pass — and validation passes as long as no
    // cycle exists. This documents the relaxed contract.
    let topology = b()
        .step(
            "a",
            task(handler_a)
                .on_response(|_: AnEventA| OutputA)
                .after("b"),
        )
        .step(
            "b",
            task(handler_b)
                .on_response(|_: AnEventB| OutputB)
                .terminal_success(),
        );
    assert_eq!(topology.validate(), Ok(()));
}

#[test]
fn true_cycle_is_detected() {
    // a → b → a. Both register fine individually but the after refs
    // create a cycle. Kahn's catches it.
    let topology = b()
        .step(
            "a",
            task(handler_a)
                .on_response(|_: AnEventA| OutputA)
                .after("b"),
        )
        .step(
            "b",
            task(handler_b)
                .on_response(|_: AnEventB| OutputB)
                .after("a")
                .terminal_success(),
        );
    let res = topology.validate();
    assert!(
        matches!(&res, Err(TopologyError::CycleDetected { steps }) if steps.contains(&"a") && steps.contains(&"b")),
        "expected CycleDetected covering a + b, got {res:?}"
    );
}

#[test]
fn self_loop_cycle_is_caught_by_self_check() {
    // A self-reference (a.after(a)) is caught by the more-specific
    // SelfAfterRef check before Kahn's runs.
    let topology = b().step(
        "a",
        task(handler_a)
            .on_response(|_: AnEventA| OutputA)
            .after("a")
            .terminal_success(),
    );
    assert_eq!(topology.validate(), Err(TopologyError::SelfAfterRef("a")));
}

#[test]
fn assert_valid_panics_on_invalid_topology() {
    let topology = SagaBuilder::<TestSaga>::for_saga();
    let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| topology.assert_valid()));
    assert!(res.is_err(), "expected panic on missing start_on");
}

#[test]
fn assert_valid_succeeds_on_valid_topology() {
    let topology = b().step(
        "a",
        task(handler_a)
            .on_response(|_: AnEventA| OutputA)
            .terminal_success(),
    );
    topology.assert_valid(); // must not panic
}

#[test]
fn compensation_registers_against_known_step() {
    let topology = b().step(
        "a",
        task(handler_a)
            .on_response(|_: AnEventA| OutputA)
            .terminal_success()
            .compensate(comp(comp_a)),
    );
    assert_eq!(topology.validate(), Ok(()));
}

#[test]
fn after_accepts_array_of_names() {
    let topology = b()
        .step("a", task(handler_a).on_response(|_: AnEventA| OutputA))
        .step("b", task(handler_b).on_response(|_: AnEventB| OutputB))
        .step(
            "c",
            task(handler_c)
                .on_response(|_: AnEventC| OutputC)
                .after(["a", "b"])
                .terminal_success(),
        );
    assert_eq!(topology.validate(), Ok(()));
}

#[test]
fn after_accepts_single_name() {
    let topology = b()
        .step("a", task(handler_a).on_response(|_: AnEventA| OutputA))
        .step(
            "b",
            task(handler_b)
                .on_response(|_: AnEventB| OutputB)
                .after("a")
                .terminal_success(),
        );
    assert_eq!(topology.validate(), Ok(()));
}
