//! Tests for build-time extractor-requirements validation.
//!
//! These prove that `PgSagaRunner::new` catches misconfigurations BEFORE
//! any traffic arrives, with a detailed `TopologyError` that points at
//! what's wrong and what to do about it.

#![allow(dead_code)]

use std::future::Future;
use std::pin::Pin;

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

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

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

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

#[derive(Serialize, Deserialize, EventData)]
#[mire(event_type = "trig.ev")]
struct TrigEv;

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

#[derive(Serialize, Deserialize, EventData)]
#[mire(event_type = "noop.ev")]
struct Noop;

#[derive(Serialize, Deserialize, EventData)]
#[mire(event_type = "ok.ev")]
struct StepOkEv;

#[derive(Serialize, Deserialize)]
struct ProducedA;

#[derive(Serialize, Deserialize)]
struct ExpectedB; // produced by NO step in these sagas — the failure mode

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

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

// A step that produces ProducedA.
fn produce_a(_: Trigger<TrigEv>) -> Pin<Box<dyn Future<Output = Emit> + Send>> {
    Box::pin(async { Emit::event(Noop) })
}

// =====================================================================
// 1. Output<T> with no producing step — the canonical "I typo'd a type
//    name in the handler signature" case.
// =====================================================================

#[derive(Serialize, Deserialize)]
struct ConsumerOut;

fn consume_missing_b(_: Output<ExpectedB>) -> Pin<Box<dyn Future<Output = Emit> + Send>> {
    Box::pin(async { Emit::event(Noop) })
}

#[test]
fn missing_upstream_output_caught_at_validate() {
    let topology = base().step(
        "consumer",
        task(consume_missing_b)
            .on_response(|_: StepOkEv| ConsumerOut)
            .terminal_success(),
    );
    let res = topology.validate();
    let err = res.unwrap_err();
    assert!(
        matches!(
            &err,
            TopologyError::UnsatisfiableOutputExtractor {
                step: "consumer",
                ..
            }
        ),
        "got {err:?}"
    );
    // The panic message names the missing type so the user can find it.
    let msg = err.to_string();
    assert!(
        msg.contains("ExpectedB"),
        "diagnostic missing type name: {msg}"
    );
    assert!(
        msg.contains("Fixes:") || msg.contains("Fix"),
        "diagnostic missing 'Fix' hint: {msg}"
    );
}

#[test]
fn detailed_panic_message_lists_what_is_produced() {
    // Two upstream steps producing different types, plus a step that
    // tries to read a third nonexistent type. The diagnostic should
    // list both producers so the user can see what they meant to type.
    #[derive(Serialize, Deserialize)]
    struct Y;

    fn produce_y(_: Trigger<TrigEv>) -> Pin<Box<dyn Future<Output = Emit> + Send>> {
        Box::pin(async { Emit::event(Noop) })
    }

    let topology = base()
        .step("a", task(produce_a).on_response(|_: StepOkEv| ProducedA))
        .step("y", task(produce_y).on_response(|_: StepOkEv| Y).after("a"))
        .step(
            "bad",
            task(consume_missing_b)
                .on_response(|_: StepOkEv| ConsumerOut)
                .after("y")
                .terminal_success(),
        );

    let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| topology.assert_valid()));
    let panic_msg = res
        .expect_err("expected panic")
        .downcast::<String>()
        .map(|b| *b)
        .or_else(|e| e.downcast::<&'static str>().map(|s| s.to_string()))
        .unwrap();

    // The diagnostic must name the missing type AND list available
    // producers — the whole point of "details of why".
    assert!(panic_msg.contains("ExpectedB"), "msg: {panic_msg}");
    assert!(
        panic_msg.contains("`a` → ") || panic_msg.contains("ProducedA"),
        "msg should list step `a` as a producer: {panic_msg}"
    );
}

// =====================================================================
// 2. Cause in a forward step — caught at build, not at runtime.
// =====================================================================

fn forward_cause(_: Cause) -> Pin<Box<dyn Future<Output = Emit> + Send>> {
    Box::pin(async { Emit::event(Noop) })
}

#[test]
fn cause_extractor_in_forward_step_caught_at_validate() {
    let topology = base().step(
        "oops",
        task(forward_cause)
            .on_response(|_: StepOkEv| ProducedA)
            .terminal_success(),
    );
    let err = topology.validate().unwrap_err();
    assert!(
        matches!(
            &err,
            TopologyError::CauseExtractorInForwardStep { step: "oops" }
        ),
        "got {err:?}"
    );
    let msg = err.to_string();
    assert!(
        msg.contains(".compensate"),
        "diagnostic should hint at compensate(): {msg}"
    );
}

// =====================================================================
// 3. Req<R> in a forward step — caught at build, not at runtime.
// =====================================================================

#[derive(Serialize, Deserialize, EventData)]
#[mire(event_type = "some.req")]
struct SomeReq;

fn forward_req(_: Req<SomeReq>) -> Pin<Box<dyn Future<Output = Emit> + Send>> {
    Box::pin(async { Emit::event(Noop) })
}

#[test]
fn req_extractor_in_forward_step_caught_at_validate() {
    let topology = base().step(
        "wrong",
        task(forward_req)
            .on_response(|_: StepOkEv| ProducedA)
            .terminal_success(),
    );
    let err = topology.validate().unwrap_err();
    assert!(
        matches!(
            &err,
            TopologyError::ReqExtractorInForwardStep { step: "wrong" }
        ),
        "got {err:?}"
    );
    let msg = err.to_string();
    assert!(
        msg.contains(".handle("),
        "diagnostic should hint at .handle(): {msg}"
    );
}

// =====================================================================
// 4. Dep<T> with no registered dep — caught at build.
// =====================================================================

#[derive(Clone)]
struct MissingDep(i64);

fn needs_dep(_: Dep<MissingDep>) -> Pin<Box<dyn Future<Output = Emit> + Send>> {
    Box::pin(async { Emit::event(Noop) })
}

#[test]
fn missing_dep_caught_at_validate() {
    let topology = base().step(
        "needs-it",
        task(needs_dep)
            .on_response(|_: StepOkEv| ProducedA)
            .terminal_success(),
    );
    let err = topology.validate().unwrap_err();
    assert!(
        matches!(
            &err,
            TopologyError::UnsatisfiableDepExtractor {
                step: "needs-it",
                ..
            }
        ),
        "got {err:?}"
    );
    let msg = err.to_string();
    assert!(
        msg.contains("MissingDep"),
        "diagnostic must name the type: {msg}"
    );
    assert!(
        msg.contains(".dep("),
        "diagnostic should hint at .dep(): {msg}"
    );
}

#[test]
fn registered_dep_passes_validation() {
    let topology = base().dep(MissingDep(42)).step(
        "ok-now",
        task(needs_dep)
            .on_response(|_: StepOkEv| ProducedA)
            .terminal_success(),
    );
    assert_eq!(topology.validate(), Ok(()));
}

// =====================================================================
// 5. Output<T> from an upstream actually IS satisfied — happy path.
// =====================================================================

fn consume_a(_: Output<ProducedA>) -> Pin<Box<dyn Future<Output = Emit> + Send>> {
    Box::pin(async { Emit::event(Noop) })
}

#[test]
fn output_extractor_with_producer_validates() {
    // Two steps with the same output type would trip the
    // DuplicateOutputType check, so consume_a returns a different type
    // for the downstream step. Use a wrapper newtype here.
    #[derive(Serialize, Deserialize)]
    struct DownstreamOut;
    fn finish(_: Output<ProducedA>) -> Pin<Box<dyn Future<Output = Emit> + Send>> {
        Box::pin(async { Emit::event(Noop) })
    }

    let topology = base()
        .step("a", task(produce_a).on_response(|_: StepOkEv| ProducedA))
        .step(
            "b",
            task(finish)
                .on_response(|_: StepOkEv| DownstreamOut)
                .after("a")
                .terminal_success(),
        );
    assert_eq!(topology.validate(), Ok(()));
}

// =====================================================================
// 6. Compensation handler's Cause IS allowed.
// =====================================================================

fn comp_with_cause(_: Output<ProducedA>, _: Cause) -> Pin<Box<dyn Future<Output = Emit> + Send>> {
    Box::pin(async { Emit::none() })
}

#[test]
fn cause_in_compensation_handler_validates() {
    // Compensations have their own requirements collected, but Cause is
    // explicitly allowed there (it's "CompensationOnly"). This proves
    // the validator doesn't mistakenly flag valid compensation usage.
    let topology = base().step(
        "a",
        task(produce_a)
            .on_response(|_: StepOkEv| ProducedA)
            .terminal_success()
            .compensate(comp(comp_with_cause)),
    );
    assert_eq!(topology.validate(), Ok(()));
}

// =====================================================================
// 7. Compensation-handler validation symmetry with forward steps.
// =====================================================================

fn comp_needs_missing_output(_: Output<ExpectedB>) -> Pin<Box<dyn Future<Output = Emit> + Send>> {
    Box::pin(async { Emit::none() })
}

#[test]
fn compensation_missing_upstream_output_caught_at_validate() {
    let topology = base().step(
        "a",
        task(produce_a)
            .on_response(|_: StepOkEv| ProducedA)
            .terminal_success()
            .compensate(comp(comp_needs_missing_output)),
    );
    let err = topology.validate().unwrap_err();
    assert!(
        matches!(
            &err,
            TopologyError::UnsatisfiableOutputExtractorInCompensation { step: "a", .. }
        ),
        "got {err:?}"
    );
    let msg = err.to_string();
    assert!(msg.contains("ExpectedB"), "msg: {msg}");
    assert!(
        msg.contains("compensation for step `a`"),
        "msg should name compensation context: {msg}"
    );
}

fn comp_needs_missing_dep(_: Dep<MissingDep>) -> Pin<Box<dyn Future<Output = Emit> + Send>> {
    Box::pin(async { Emit::none() })
}

#[test]
fn compensation_missing_dep_caught_at_validate() {
    let topology = base().step(
        "a",
        task(produce_a)
            .on_response(|_: StepOkEv| ProducedA)
            .terminal_success()
            .compensate(comp(comp_needs_missing_dep)),
    );
    let err = topology.validate().unwrap_err();
    assert!(
        matches!(
            &err,
            TopologyError::UnsatisfiableDepExtractorInCompensation { step: "a", .. }
        ),
        "got {err:?}"
    );
}

fn comp_misuses_req(_: Req<SomeReq>) -> Pin<Box<dyn Future<Output = Emit> + Send>> {
    Box::pin(async { Emit::none() })
}

#[test]
fn compensation_using_req_extractor_caught_at_validate() {
    let topology = base().step(
        "a",
        task(produce_a)
            .on_response(|_: StepOkEv| ProducedA)
            .terminal_success()
            .compensate(comp(comp_misuses_req)),
    );
    let err = topology.validate().unwrap_err();
    assert!(
        matches!(
            &err,
            TopologyError::ReqExtractorInCompensation { step: "a" }
        ),
        "got {err:?}"
    );
}

// =====================================================================
// 8. Trigger<T> vs start_on type-match check.
// =====================================================================

// Different aggregate / event combo from Trig/TrigEv used in the base saga.
#[derive(Default, Clone)]
struct OtherAgg;

#[derive(Serialize, Deserialize, EventData)]
#[mire(event_type = "other.ev")]
struct WrongEvent;

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

// Step uses Trigger<WrongEvent> but the base() builder's start_on
// registers Trigger<TrigEv> (see `base()`).
fn step_with_wrong_trigger(_: Trigger<WrongEvent>) -> Pin<Box<dyn Future<Output = Emit> + Send>> {
    Box::pin(async { Emit::event(Noop) })
}

#[test]
fn trigger_type_mismatch_caught_at_validate() {
    // start_on::<Trig, _> registers TrigEv. The step asks for
    // Trigger<WrongEvent>. The validator should flag this *before* the
    // saga runs, with a message naming both types.
    let topology = base().step(
        "mismatch",
        task(step_with_wrong_trigger)
            .on_response(|_: StepOkEv| ProducedA)
            .terminal_success(),
    );
    let err = topology.validate().unwrap_err();
    assert!(
        matches!(
            &err,
            TopologyError::TriggerTypeMismatch {
                step: "mismatch",
                ..
            }
        ),
        "got {err:?}"
    );
    let msg = err.to_string();
    assert!(
        msg.contains("WrongEvent"),
        "msg should name requested type: {msg}"
    );
    assert!(
        msg.contains("TrigEv"),
        "msg should name registered type: {msg}"
    );
}

#[test]
fn trigger_type_match_validates() {
    // Same Trigger<TrigEv> as start_on — passes.
    let topology = base().step(
        "ok",
        task(produce_a)
            .on_response(|_: StepOkEv| ProducedA)
            .terminal_success(),
    );
    assert_eq!(topology.validate(), Ok(()));
}
