//! Acceptance tests for ADR-122 §2.2 — MQTT topic routing.

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

use wifi_densepose_bfld::{render_events, BfldEvent, PrivacyClass, TopicMessage};

fn sample_event(class: PrivacyClass, with_zone: bool) -> BfldEvent {
    BfldEvent::with_privacy_gating(
        "seed-01".into(),
        1_700_000_000_000_000_000,
        true,
        0.72,
        2,
        0.91,
        if with_zone { Some("living_room".into()) } else { None },
        class,
        Some(0.34),
        Some([0xAB; 32]),
    )
}

fn topics_for(class: PrivacyClass) -> Vec<String> {
    render_events(&sample_event(class, true))
        .into_iter()
        .map(|m| m.topic)
        .collect()
}

// --- topic shape ---------------------------------------------------------

#[test]
fn topic_format_is_ruview_node_bfld_entity_state() {
    let t = TopicMessage::ruview_topic("seed-42", "presence");
    assert_eq!(t, "ruview/seed-42/bfld/presence/state");
}

#[test]
fn anonymous_class_publishes_six_topics_with_zone() {
    let topics = topics_for(PrivacyClass::Anonymous);
    assert_eq!(topics.len(), 6, "got {topics:?}");
    let expected: Vec<&str> = vec![
        "ruview/seed-01/bfld/presence/state",
        "ruview/seed-01/bfld/motion/state",
        "ruview/seed-01/bfld/person_count/state",
        "ruview/seed-01/bfld/confidence/state",
        "ruview/seed-01/bfld/zone_activity/state",
        "ruview/seed-01/bfld/identity_risk/state",
    ];
    for t in &expected {
        assert!(topics.contains(&t.to_string()), "missing topic {t}");
    }
}

#[test]
fn anonymous_class_without_zone_omits_zone_activity_topic() {
    let topics: Vec<String> = render_events(&sample_event(PrivacyClass::Anonymous, false))
        .into_iter()
        .map(|m| m.topic)
        .collect();
    assert!(!topics.iter().any(|t| t.contains("zone_activity")));
    assert_eq!(topics.len(), 5);
}

// --- class-gated routing -------------------------------------------------

#[test]
fn restricted_class_omits_identity_risk_topic() {
    let topics = topics_for(PrivacyClass::Restricted);
    assert!(
        !topics.iter().any(|t| t.contains("identity_risk")),
        "Restricted (class 3) must NOT publish identity_risk: {topics:?}",
    );
    // Other entities still present.
    assert!(topics.iter().any(|t| t.contains("presence")));
    assert!(topics.iter().any(|t| t.contains("motion")));
}

#[test]
fn raw_and_derived_classes_publish_nothing() {
    // Raw (0) and Derived (1) are local-only / research — never on the
    // public topic tree.
    let raw = render_events(&sample_event(PrivacyClass::Raw, true));
    assert!(raw.is_empty(), "Raw class must publish nothing");
    let derived = render_events(&sample_event(PrivacyClass::Derived, true));
    assert!(derived.is_empty(), "Derived class must publish nothing");
}

// --- payload shape -------------------------------------------------------

#[test]
fn presence_payload_is_lowercase_json_bool() {
    let msgs = render_events(&sample_event(PrivacyClass::Anonymous, false));
    let pres = msgs
        .iter()
        .find(|m| m.topic.contains("presence"))
        .expect("presence topic");
    assert_eq!(pres.payload, "true");
}

#[test]
fn motion_payload_is_fixed_precision_decimal() {
    let msgs = render_events(&sample_event(PrivacyClass::Anonymous, false));
    let motion = msgs
        .iter()
        .find(|m| m.topic.contains("motion"))
        .expect("motion topic");
    assert_eq!(motion.payload, "0.720000");
}

#[test]
fn person_count_payload_is_bare_integer() {
    let msgs = render_events(&sample_event(PrivacyClass::Anonymous, false));
    let pc = msgs
        .iter()
        .find(|m| m.topic.contains("person_count"))
        .expect("person_count topic");
    assert_eq!(pc.payload, "2");
}

#[test]
fn zone_payload_is_json_string_with_quotes() {
    let msgs = render_events(&sample_event(PrivacyClass::Anonymous, true));
    let zone = msgs
        .iter()
        .find(|m| m.topic.contains("zone_activity"))
        .expect("zone_activity topic");
    assert_eq!(zone.payload, "\"living_room\"");
}

#[test]
fn identity_risk_payload_is_fixed_precision_decimal() {
    let msgs = render_events(&sample_event(PrivacyClass::Anonymous, false));
    let risk = msgs
        .iter()
        .find(|m| m.topic.contains("identity_risk"))
        .expect("identity_risk topic");
    assert_eq!(risk.payload, "0.340000");
}
