//! Integration test for `MdnsDiscovery::advertise_with_addr` (peat#897).
//!
//! Two `MdnsDiscovery` instances on the same host advertise on
//! **loopback** addresses and must discover each other end-to-end.
//!
//! Closed by the mdns_sd 0.19 upgrade: the daemon now exposes
//! `IfKind::LoopbackV4` / `IfKind::LoopbackV6` and peat-mesh's
//! `MdnsDiscovery::start()` calls `enable_interface` for both, so the
//! daemon joins the loopback multicast group on top of its default
//! non-loopback interface set. With both daemons on the same host
//! listening on `lo` plus their default interfaces, announces published
//! at `127.0.0.1:PORT` reach the other daemon's browse path.
//!
//! `#[ignore]` is retained as a CI guardrail — GitHub Actions and most
//! containerized CI runners block multicast joins, so this test needs
//! bare-metal hardware (or a runner with multicast enabled). Locally:
//!
//! ```sh
//! cargo test --test mdns_advertise_with_addr -- --ignored
//! ```

use peat_mesh::discovery::{DiscoveryStrategy, MdnsDiscovery};
use std::time::{Duration, Instant};

/// Grab an OS-assigned free loopback UDP port. The socket is dropped
/// immediately; we only want the port number for the advertised address
/// (mDNS advertises the addr as record metadata — nothing binds to it).
fn free_loopback_port() -> u16 {
    std::net::UdpSocket::bind("127.0.0.1:0")
        .expect("bind ephemeral udp port")
        .local_addr()
        .expect("local_addr")
        .port()
}

#[tokio::test]
#[ignore = "requires multicast in CI; run with --ignored on bare metal"]
async fn two_nodes_mutual_loopback_discovery() {
    // Unique per run: two P2P-functional CI jobs sharing the multicast group
    // (e.g. a merge-push CI run racing the tag/release CI run on the same
    // self-hosted runner) must not collide on mDNS service-instance names or
    // advertised ports. A run-scoped suffix isolates node ids, and OS-assigned
    // ports isolate the advertised addresses, so each run only ever discovers
    // its own pair.
    let uniq = format!(
        "{}-{}",
        std::process::id(),
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_nanos()
    );
    let id_a = format!("node-A-{uniq}");
    let id_b = format!("node-B-{uniq}");
    let port_a = free_loopback_port();
    let port_b = free_loopback_port();

    let mut a = MdnsDiscovery::new().expect("create MdnsDiscovery A");
    let mut b = MdnsDiscovery::new().expect("create MdnsDiscovery B");

    a.start().await.expect("start A");
    b.start().await.expect("start B");

    a.advertise_with_addr(&id_a, format!("127.0.0.1:{port_a}").parse().unwrap(), None)
        .expect("advertise A");
    b.advertise_with_addr(&id_b, format!("127.0.0.1:{port_b}").parse().unwrap(), None)
        .expect("advertise B");

    // mDNS announce + browse round-trip is typically 1-3s. Allow up to 20s
    // before failing, polling every 250ms for cheaper failure feedback.
    // 20s (was 10s) leaves headroom for a cold-start announce on the
    // self-hosted runner now that the job is serialized repo-wide (#222);
    // the round-trip is contention-sensitive, and serialization removes the
    // dominant contention source.
    // Surface the real round-trip latency. Discovery is normally 1-3s; under
    // the 20s ceiling a large regression (e.g. ~7x) would otherwise pass
    // silently. Measure from here and warn loudly if it exceeds a healthy
    // budget — same hard deadline, but degradation shows as a visible delta
    // rather than only as a timeout (#229 QA suggestion).
    let start = Instant::now();
    let deadline = start + Duration::from_secs(20);
    loop {
        let a_peers = a.discovered_peers().await;
        let b_peers = b.discovered_peers().await;

        let a_sees_b = a_peers.iter().any(|p| {
            p.node_id == id_b
                && p.addresses
                    .iter()
                    .any(|s| s.ip().is_loopback() && s.port() == port_b)
        });
        let b_sees_a = b_peers.iter().any(|p| {
            p.node_id == id_a
                && p.addresses
                    .iter()
                    .any(|s| s.ip().is_loopback() && s.port() == port_a)
        });

        if a_sees_b && b_sees_a {
            // Both sides observe the other at the exact loopback `IP:port`
            // we advertised — the peat#897 regression gate is closed.
            let elapsed = start.elapsed();
            if elapsed > Duration::from_secs(8) {
                eprintln!(
                    "[mdns][WARN] mutual loopback discovery took {elapsed:?} — well above the \
                     typical 1-3s round-trip; investigate runner contention or an mDNS regression"
                );
            } else {
                eprintln!("[mdns] mutual loopback discovery in {elapsed:?}");
            }
            return;
        }
        if Instant::now() > deadline {
            panic!(
                "timed out after {:?} waiting for mutual loopback discovery: a_sees_b={a_sees_b} b_sees_a={b_sees_a}\nA peers: {a_peers:?}\nB peers: {b_peers:?}",
                start.elapsed()
            );
        }
        tokio::time::sleep(Duration::from_millis(250)).await;
    }
}
