//! Acceptance tests for ADR-122 §2.2 — `Publish` trait + `publish_event`.

#![cfg(feature = "std")]

use wifi_densepose_bfld::{
    publish_event, BfldEvent, CapturePublisher, PrivacyClass, Publish, TopicMessage,
};

fn sample_event(class: PrivacyClass, with_zone: bool) -> BfldEvent {
    BfldEvent::with_privacy_gating(
        "seed-99".into(),
        1_700_000_000_000_000_000,
        true,
        0.5,
        1,
        0.8,
        if with_zone { Some("kitchen".into()) } else { None },
        class,
        Some(0.25),
        Some([0xCD; 32]),
    )
}

#[test]
fn capture_publisher_records_every_message() {
    let mut p = CapturePublisher::default();
    let count = publish_event(&mut p, &sample_event(PrivacyClass::Anonymous, true))
        .expect("publish must succeed");
    assert_eq!(count, p.published.len(), "return value must equal publish count");
    assert_eq!(count, 6, "Anonymous + zone publishes 6 topics");
}

#[test]
fn publish_returns_zero_for_raw_and_derived_events() {
    for class in [PrivacyClass::Raw, PrivacyClass::Derived] {
        let mut p = CapturePublisher::default();
        let count = publish_event(&mut p, &sample_event(class, true)).unwrap();
        assert_eq!(count, 0, "class {class:?} must publish nothing");
        assert!(p.published.is_empty());
    }
}

#[test]
fn published_topics_match_render_events_ordering() {
    // The publish loop must iterate in the same order as render_events so
    // that downstream MQTT consumers see a stable per-event topic sequence.
    let event = sample_event(PrivacyClass::Anonymous, true);
    let mut p = CapturePublisher::default();
    publish_event(&mut p, &event).unwrap();
    let rendered = wifi_densepose_bfld::render_events(&event);
    assert_eq!(p.published, rendered);
}

#[test]
fn restricted_class_publishes_no_identity_risk_topic() {
    let mut p = CapturePublisher::default();
    publish_event(&mut p, &sample_event(PrivacyClass::Restricted, true)).unwrap();
    assert!(
        !p.published.iter().any(|m| m.topic.contains("identity_risk")),
        "Restricted must not publish identity_risk, got: {:?}",
        p.published.iter().map(|m| &m.topic).collect::<Vec<_>>(),
    );
}

#[test]
fn anonymous_without_zone_publishes_five_messages() {
    let mut p = CapturePublisher::default();
    let count = publish_event(&mut p, &sample_event(PrivacyClass::Anonymous, false)).unwrap();
    assert_eq!(count, 5);
}

// --- error propagation --------------------------------------------------

struct FailingPublisher {
    fails_after: usize,
    published_so_far: usize,
}

impl Publish for FailingPublisher {
    type Error = &'static str;
    fn publish(&mut self, _msg: &TopicMessage) -> Result<(), Self::Error> {
        if self.published_so_far >= self.fails_after {
            return Err("broker offline");
        }
        self.published_so_far += 1;
        Ok(())
    }
}

#[test]
fn publisher_error_short_circuits_publish_event() {
    let mut p = FailingPublisher {
        fails_after: 2,
        published_so_far: 0,
    };
    let result = publish_event(&mut p, &sample_event(PrivacyClass::Anonymous, true));
    match result {
        Err("broker offline") => {}
        other => panic!("expected broker-offline error, got {other:?}"),
    }
    assert_eq!(
        p.published_so_far, 2,
        "exactly the first two messages should land before the error",
    );
}

// --- error type ergonomics ----------------------------------------------

#[test]
fn capture_publisher_error_type_is_infallible() {
    let mut p = CapturePublisher::default();
    let r: Result<usize, core::convert::Infallible> =
        publish_event(&mut p, &sample_event(PrivacyClass::Anonymous, false));
    assert!(r.is_ok());
}
