//! Tests for the extractors. We synthesise a `StepContext` via the
//! `for_testing` constructor and check that each extractor pulls correctly
//! (and reports clean errors when its input is missing).

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

use mire_sagas::{
    Cause, Dep, Extract, ExtractError, Output, Saga, SagaBuilder, State, StepContext, StepError,
    Trigger,
};
use serde::{Deserialize, Serialize};

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

#[derive(Default, Clone, Serialize, Deserialize, Debug, PartialEq)]
struct St {
    n: i32,
}

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

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

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

#[derive(Debug, Clone, PartialEq)]
struct MyDep(pub i32);

fn empty_outputs() -> BTreeMap<TypeId, serde_json::Value> {
    BTreeMap::new()
}

fn empty_deps() -> BTreeMap<TypeId, Arc<dyn std::any::Any + Send + Sync>> {
    BTreeMap::new()
}

// =====================================================================
// Trigger<T>
// =====================================================================

#[test]
fn trigger_extracts_typed_payload() {
    let state = St { n: 1 };
    let trigger = serde_json::to_value(TriggerPayload {
        booking_id: "b-1".into(),
    })
    .unwrap();
    let outputs = empty_outputs();
    let deps = empty_deps();
    let ctx: StepContext<'_, S> =
        StepContext::for_testing(&state, Some(&trigger), &outputs, &deps, None);

    let Trigger(p) = <Trigger<TriggerPayload> as Extract<S>>::extract(&ctx).unwrap();
    assert_eq!(p.booking_id, "b-1");
}

#[test]
fn trigger_missing_returns_no_trigger() {
    let state = St::default();
    let outputs = empty_outputs();
    let deps = empty_deps();
    let ctx: StepContext<'_, S> = StepContext::for_testing(&state, None, &outputs, &deps, None);

    let err = <Trigger<TriggerPayload> as Extract<S>>::extract(&ctx).unwrap_err();
    assert!(matches!(err, ExtractError::NoTrigger(_)));
}

#[test]
fn trigger_wrong_schema_returns_deserialise_error() {
    let state = St::default();
    let bad = serde_json::json!({ "wrong_field": 42 });
    let outputs = empty_outputs();
    let deps = empty_deps();
    let ctx: StepContext<'_, S> =
        StepContext::for_testing(&state, Some(&bad), &outputs, &deps, None);

    let err = <Trigger<TriggerPayload> as Extract<S>>::extract(&ctx).unwrap_err();
    assert!(matches!(err, ExtractError::Deserialize { .. }));
}

// =====================================================================
// Output<T> (the re-export of From<T>)
// =====================================================================

#[test]
fn output_extracts_by_typeid() {
    let state = St::default();
    let trigger: Option<&serde_json::Value> = None;
    let mut outputs = empty_outputs();
    outputs.insert(
        TypeId::of::<UpstreamOut>(),
        serde_json::to_value(UpstreamOut {
            hold_id: "fh-1".into(),
        })
        .unwrap(),
    );
    let deps = empty_deps();
    let ctx: StepContext<'_, S> = StepContext::for_testing(&state, trigger, &outputs, &deps, None);

    let Output(o) = <Output<UpstreamOut> as Extract<S>>::extract(&ctx).unwrap();
    assert_eq!(o.hold_id, "fh-1");
}

#[test]
fn output_missing_returns_missing_output() {
    let state = St::default();
    let outputs = empty_outputs();
    let deps = empty_deps();
    let ctx: StepContext<'_, S> = StepContext::for_testing(&state, None, &outputs, &deps, None);

    let err = <Output<UpstreamOut> as Extract<S>>::extract(&ctx).unwrap_err();
    assert!(matches!(err, ExtractError::MissingOutput(_)));
}

// =====================================================================
// State<S::State>
// =====================================================================

#[test]
fn state_extracts_clone() {
    let state = St { n: 7 };
    let outputs = empty_outputs();
    let deps = empty_deps();
    let ctx: StepContext<'_, S> = StepContext::for_testing(&state, None, &outputs, &deps, None);

    let State(s) = <State<St> as Extract<S>>::extract(&ctx).unwrap();
    assert_eq!(s, state);
}

// =====================================================================
// Dep<T>
// =====================================================================

#[test]
fn dep_extracts_registered_value() {
    let state = St::default();
    let outputs = empty_outputs();
    let mut deps: BTreeMap<TypeId, Arc<dyn std::any::Any + Send + Sync>> = BTreeMap::new();
    deps.insert(TypeId::of::<MyDep>(), Arc::new(MyDep(42)));
    let ctx: StepContext<'_, S> = StepContext::for_testing(&state, None, &outputs, &deps, None);

    let Dep(d) = <Dep<MyDep> as Extract<S>>::extract(&ctx).unwrap();
    assert_eq!(d, MyDep(42));
}

#[test]
fn dep_missing_returns_missing_dep() {
    let state = St::default();
    let outputs = empty_outputs();
    let deps = empty_deps();
    let ctx: StepContext<'_, S> = StepContext::for_testing(&state, None, &outputs, &deps, None);

    let err = <Dep<MyDep> as Extract<S>>::extract(&ctx).unwrap_err();
    assert!(matches!(err, ExtractError::MissingDep(_)));
}

// =====================================================================
// Req<R>
// =====================================================================

#[test]
fn req_extracts_typed_request_payload() {
    use mire_sagas::Req;
    let state = St::default();
    let request_json = serde_json::json!({ "booking_id": "b-7" });
    let deps = empty_deps();
    let ctx: StepContext<'_, S> =
        StepContext::for_testing_with_request(&state, &request_json, &deps);

    let Req(p) = <Req<TriggerPayload> as Extract<S>>::extract(&ctx).unwrap();
    assert_eq!(p.booking_id, "b-7");
}

#[test]
fn req_missing_returns_no_request() {
    use mire_sagas::Req;
    let state = St::default();
    let outputs = empty_outputs();
    let deps = empty_deps();
    let ctx: StepContext<'_, S> = StepContext::for_testing(&state, None, &outputs, &deps, None);

    let err = <Req<TriggerPayload> as Extract<S>>::extract(&ctx).unwrap_err();
    assert!(matches!(err, ExtractError::NoRequest(_)));
}

// =====================================================================
// Cause
// =====================================================================

#[test]
fn cause_extracts_step_error() {
    let state = St::default();
    let outputs = empty_outputs();
    let deps = empty_deps();
    let cause = StepError::Timeout {
        step: "payment".into(),
        timeout_ms: 600_000,
    };
    let ctx: StepContext<'_, S> =
        StepContext::for_testing(&state, None, &outputs, &deps, Some(&cause));

    let Cause(c) = <Cause as Extract<S>>::extract(&ctx).unwrap();
    assert!(matches!(c, StepError::Timeout { .. }));
}

#[test]
fn cause_missing_returns_no_cause() {
    let state = St::default();
    let outputs = empty_outputs();
    let deps = empty_deps();
    let ctx: StepContext<'_, S> = StepContext::for_testing(&state, None, &outputs, &deps, None);

    let err = <Cause as Extract<S>>::extract(&ctx).unwrap_err();
    assert!(matches!(err, ExtractError::NoCause));
}
