//! Live-broker integration test for `RumqttPublisher`. ADR-122 §2.2 end-to-end.
//!
//! **Skipped silently when `BFLD_MQTT_BROKER` is unset**, so CI runs that lack
//! a broker stay green. Locally:
//!
//! ```text
//! scoop install mosquitto
//! mosquitto -v -c mosquitto-allow-anon.conf &
//! BFLD_MQTT_BROKER=tcp://localhost:1883 \
//!   cargo test -p wifi-densepose-bfld --features mqtt --test mosquitto_integration
//! ```
//!
//! Test discipline (per `feedback_mqtt_integration_test_patterns` memory):
//! - per-test unique `client_id` (current nanosecond timestamp suffix)
//! - subscriber eventloop pumped until SubAck arrives before publishing
//! - explicit `wait_for_n_messages` with timeout — never `loop { iter.recv() }`

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

use std::env;
use std::sync::mpsc::{channel, Receiver, RecvTimeoutError};
use std::thread;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};

use rumqttc::{Client, Event, Incoming, MqttOptions, Packet, QoS};
use wifi_densepose_bfld::{
    publish_event, BfldEvent, PrivacyClass, RumqttPublisher,
};

const SUBSCRIBE_TIMEOUT: Duration = Duration::from_secs(5);
const RECEIVE_TIMEOUT: Duration = Duration::from_secs(10);

fn broker_env() -> Option<(String, u16)> {
    let raw = env::var("BFLD_MQTT_BROKER").ok()?;
    let raw = raw.strip_prefix("tcp://").unwrap_or(&raw);
    let mut parts = raw.splitn(2, ':');
    let host = parts.next()?.to_string();
    let port: u16 = parts.next().unwrap_or("1883").parse().ok()?;
    Some((host, port))
}

fn unique_client_id(prefix: &str) -> String {
    let nanos = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map(|d| d.as_nanos())
        .unwrap_or(0);
    format!("{prefix}-{nanos}")
}

fn sample_event(node_id: &str) -> BfldEvent {
    BfldEvent::with_privacy_gating(
        node_id.into(),
        1_700_000_000_000_000_000,
        true,
        0.62,
        2,
        0.88,
        Some("test_zone".into()),
        PrivacyClass::Anonymous,
        Some(0.34),
        Some([0xAB; 32]),
    )
}

/// Spawn a subscriber + a pump thread. Returns the receiver of incoming
/// `(topic, payload)` pairs and a oneshot signalling SubAck arrival.
fn spawn_subscriber(
    host: &str,
    port: u16,
    topic_filter: &str,
) -> (Receiver<(String, String)>, Receiver<()>) {
    let mut opts = MqttOptions::new(unique_client_id("bfld-sub"), host, port);
    opts.set_keep_alive(Duration::from_secs(5));
    let (client, mut connection) = Client::new(opts, 64);
    client
        .subscribe(topic_filter, QoS::AtLeastOnce)
        .expect("subscribe enqueue");

    let (incoming_tx, incoming_rx) = channel();
    let (suback_tx, suback_rx) = channel();
    thread::spawn(move || {
        for notification in connection.iter() {
            match notification {
                Ok(Event::Incoming(Packet::SubAck(_))) => {
                    let _ = suback_tx.send(());
                }
                Ok(Event::Incoming(Incoming::Publish(p))) => {
                    let topic = p.topic.clone();
                    let payload = String::from_utf8_lossy(&p.payload).to_string();
                    if incoming_tx.send((topic, payload)).is_err() {
                        break;
                    }
                }
                Err(_) => break,
                _ => {}
            }
        }
    });
    (incoming_rx, suback_rx)
}

fn collect_messages(
    rx: &Receiver<(String, String)>,
    expected_count: usize,
    timeout: Duration,
) -> Vec<(String, String)> {
    let deadline = Instant::now() + timeout;
    let mut out = Vec::with_capacity(expected_count);
    while out.len() < expected_count {
        let remaining = deadline.saturating_duration_since(Instant::now());
        if remaining.is_zero() {
            break;
        }
        match rx.recv_timeout(remaining) {
            Ok(msg) => out.push(msg),
            Err(RecvTimeoutError::Timeout) => break,
            Err(RecvTimeoutError::Disconnected) => break,
        }
    }
    out
}

#[test]
fn live_broker_anonymous_event_roundtrips_all_six_topics() {
    let Some((host, port)) = broker_env() else {
        eprintln!(
            "BFLD_MQTT_BROKER unset — skipping live mosquitto roundtrip test. \
             Set e.g. BFLD_MQTT_BROKER=tcp://localhost:1883 to enable."
        );
        return;
    };

    let node_id = unique_client_id("seed");
    let filter = format!("ruview/{node_id}/bfld/+/state");

    // Subscriber first so it's ready before the publisher sends.
    let (incoming_rx, suback_rx) = spawn_subscriber(&host, port, &filter);
    suback_rx
        .recv_timeout(SUBSCRIBE_TIMEOUT)
        .expect("SubAck within 5s");

    // Publisher with its own connection. Spawn a thread iterating the
    // Connection so publishes actually reach the broker.
    let mut opts = MqttOptions::new(unique_client_id("bfld-pub"), &host, port);
    opts.set_keep_alive(Duration::from_secs(5));
    let (mut publisher, mut pub_connection) = RumqttPublisher::connect(opts, 64);
    thread::spawn(move || {
        for _ in pub_connection.iter() { /* drain protocol events */ }
    });

    // Give the publisher a brief moment to complete CONNECT before publish.
    thread::sleep(Duration::from_millis(200));

    let event = sample_event(&node_id);
    let count = publish_event(&mut publisher, &event).expect("queue publish");
    assert_eq!(count, 6, "Anonymous + zone publishes 6 topics");

    let messages = collect_messages(&incoming_rx, 6, RECEIVE_TIMEOUT);
    assert_eq!(
        messages.len(),
        6,
        "broker delivered {} of 6 expected messages",
        messages.len(),
    );

    // Topic correctness — every expected entity must appear exactly once.
    let topics: Vec<&str> = messages.iter().map(|(t, _)| t.as_str()).collect();
    for entity in [
        "presence",
        "motion",
        "person_count",
        "confidence",
        "zone_activity",
        "identity_risk",
    ] {
        assert!(
            topics
                .iter()
                .any(|t| t == &format!("ruview/{node_id}/bfld/{entity}/state").as_str()),
            "missing entity {entity} in delivered topics {topics:?}",
        );
    }
}

#[test]
fn live_broker_restricted_event_omits_identity_risk() {
    let Some((host, port)) = broker_env() else {
        eprintln!("BFLD_MQTT_BROKER unset — skipping");
        return;
    };

    let node_id = unique_client_id("seed-r");
    let filter = format!("ruview/{node_id}/bfld/+/state");

    let (incoming_rx, suback_rx) = spawn_subscriber(&host, port, &filter);
    suback_rx
        .recv_timeout(SUBSCRIBE_TIMEOUT)
        .expect("SubAck within 5s");

    let mut opts = MqttOptions::new(unique_client_id("bfld-pub-r"), &host, port);
    opts.set_keep_alive(Duration::from_secs(5));
    let (mut publisher, mut pub_connection) = RumqttPublisher::connect(opts, 64);
    thread::spawn(move || for _ in pub_connection.iter() {});
    thread::sleep(Duration::from_millis(200));

    let mut event = sample_event(&node_id);
    event.privacy_class = PrivacyClass::Restricted;
    event.apply_privacy_gating();
    publish_event(&mut publisher, &event).expect("queue publish");

    // Expect 5 messages: 6 entities minus identity_risk.
    let messages = collect_messages(&incoming_rx, 6, Duration::from_secs(3));
    assert_eq!(messages.len(), 5);
    assert!(
        !messages.iter().any(|(t, _)| t.contains("identity_risk")),
        "Restricted class must not publish identity_risk topic, got {messages:?}",
    );
}
