//! **Steady-state command relay** — the bidirectional bridge between a local client's
//! actions and the linked network, once the burst is over.
//!
//! Two directions, both routed through [`PeerLinks`](super::PeerLinks):
//!
//! - **Outbound** (`local_*`): a local client did something observable network-wide — joined
//!   a channel, spoke, changed a mode/topic, kicked, renamed, quit. The command handler calls
//!   the matching `local_*` builder, which emits the **S2S form** of the event — a line
//!   prefixed with the actor's [`Uid`] — to every linked peer (`broadcast` with no origin to
//!   skip). On a single-server boot ([`PeerLinks::is_empty`](super::PeerLinks::is_empty))
//!   every builder is an early no-op, so the relay costs nothing until a link exists.
//! - **Inbound** (`inbound_*`): a peer forwarded a remote user's live action. We apply it to
//!   the shared local state, fan the **client form** (`:nick!user@host VERB …`) out to the
//!   *local* members it concerns, and **onward-relay** the original UID-form line to every
//!   *other* peer (split-horizon — never back to the origin).
//!
//! # The local-vs-remote fan-out rule (load-bearing)
//!
//! A channel's membership mixes local and remote [`Uid`]s. Fan-out must deliver the
//! client-form line **only to local members** ([`ServerContext::is_local_uid`]); a remote
//! member is reached exactly once by the peer relay, never by a per-member mailbox push (which
//! would send a client-form line to the peer socket, in the wrong direction and the wrong
//! format). [`fanout_local`] enforces this.
//!
//! # Divergences from the oracle (documented)
//!
//! - **Single-uplink topology.** leveva forms one direct link in practice. The onward-relay
//!   (`broadcast(except origin)`) is spine-correct but, with one peer, a no-op. Per-server
//!   membership routing (relaying a channel message only to peers that carry a member, and
//!   loop avoidance in a multi-peer mesh) is therefore out of scope.
//! - **Clean UID forms.** A steady-state single join relays as `:<uid> JOIN <chan>`; `NJOIN`
//!   stays burst-only ([`super::njoin`]). No channel-TS arbitration.
//! - **No client numerics to a server.** A relayed line that cannot be applied (unknown
//!   channel/user) is dropped silently — a server is never answered with a `4xx`.

use crate::channel::{MemberStatus, ModeChange};
use crate::ident::{Sid, Uid};
use crate::server::ServerContext;
use crate::Message;

use super::{squit, PeerLink};

/// Whether `target` names a channel (the leading-sigil test shared with the command layer).
fn is_channel(target: &str) -> bool {
    matches!(target.chars().next(), Some('#' | '&' | '+' | '!'))
}

// ---------------------------------------------------------------------------
// Outbound — a local client's action → the linked network (UID-prefixed S2S form).
// ---------------------------------------------------------------------------

/// Relay a local client's `JOIN <chan>` to every peer as `:<uid> JOIN <chan>`.
pub fn local_join(ctx: &ServerContext, from: &Uid, chan: &str) {
    if ctx.peers.is_empty() {
        return;
    }
    let wire = Message::builder("JOIN")
        .prefix(from.as_str())
        .param(chan)
        .to_wire();
    ctx.peers.broadcast(None, wire);
}

/// Relay a local client's `PART <chan> [:reason]`.
pub fn local_part(ctx: &ServerContext, from: &Uid, chan: &str, reason: Option<&str>) {
    if ctx.peers.is_empty() {
        return;
    }
    let mut b = Message::builder("PART").prefix(from.as_str()).param(chan);
    if let Some(r) = reason.filter(|r| !r.is_empty()) {
        b = b.trailing(r);
    }
    ctx.peers.broadcast(None, b.to_wire());
}

/// Relay a local client's `TOPIC <chan> :<text>`.
pub fn local_topic(ctx: &ServerContext, from: &Uid, chan: &str, text: &str) {
    if ctx.peers.is_empty() {
        return;
    }
    let wire = Message::builder("TOPIC")
        .prefix(from.as_str())
        .param(chan)
        .trailing(text)
        .to_wire();
    ctx.peers.broadcast(None, wire);
}

/// Relay a local client's `KICK <chan> <victim> :<reason>`, addressing the victim by its
/// stable [`Uid`] on the wire (the peer resolves it).
pub fn local_kick(ctx: &ServerContext, from: &Uid, chan: &str, victim: &Uid, reason: &str) {
    if ctx.peers.is_empty() {
        return;
    }
    let wire = Message::builder("KICK")
        .prefix(from.as_str())
        .param(chan)
        .param(victim.as_str())
        .trailing(reason)
        .to_wire();
    ctx.peers.broadcast(None, wire);
}

/// Relay a local client's disconnect as `:<uid> QUIT :<reason>`.
pub fn local_quit(ctx: &ServerContext, from: &Uid, reason: &str) {
    if ctx.peers.is_empty() {
        return;
    }
    let wire = Message::builder("QUIT")
        .prefix(from.as_str())
        .trailing(reason)
        .to_wire();
    ctx.peers.broadcast(None, wire);
}

/// Relay a local client's `NICK <new>` as `:<uid> NICK <new>`.
pub fn local_nick(ctx: &ServerContext, from: &Uid, new_nick: &str) {
    if ctx.peers.is_empty() {
        return;
    }
    let wire = Message::builder("NICK")
        .prefix(from.as_str())
        .param(new_nick)
        .to_wire();
    ctx.peers.broadcast(None, wire);
}

/// Relay a local client's channel message (`PRIVMSG`/`NOTICE <chan> :<text>`) to every peer
/// (single-uplink: a channel message goes to all peers; each fans it to its own members).
pub fn local_channel_message(ctx: &ServerContext, from: &Uid, verb: &str, chan: &str, text: &str) {
    if ctx.peers.is_empty() {
        return;
    }
    let wire = Message::builder(verb)
        .prefix(from.as_str())
        .param(chan)
        .trailing(text)
        .to_wire();
    ctx.peers.broadcast(None, wire);
}

/// Route a local client's message **to a remote user** to the single peer in whose subtree
/// that user lives. `target_field` is what the peer should see in the target slot (the typed
/// nick), `from` the local sender's [`Uid`]. A no-op if the target's uplink is not linked.
pub fn route_message_to_remote(
    ctx: &ServerContext,
    target: &Uid,
    from: &Uid,
    verb: &str,
    target_field: &str,
    text: &str,
) {
    if let Some(sid) = uplink_of(ctx, target) {
        let wire = Message::builder(verb)
            .prefix(from.as_str())
            .param(target_field)
            .trailing(text)
            .to_wire();
        ctx.peers.route(&sid, wire);
    }
}

/// Relay a local chanop's effective channel-mode changes as a server-authoritative
/// `:<uid> MODE <chan> <modes> [<uid/value params>]`. Member (`+o`/`+v`) params are rendered
/// as **UIDs** (the network-wide identifier the peer keys on). A no-op if nothing effective.
pub fn local_channel_mode(ctx: &ServerContext, from: &Uid, chan: &str, changes: &[ModeChange]) {
    if ctx.peers.is_empty() || changes.is_empty() {
        return;
    }
    let (modestr, params) = render_modes_uid(changes);
    if modestr.is_empty() {
        return;
    }
    let mut b = Message::builder("MODE")
        .prefix(from.as_str())
        .param(chan)
        .param(&modestr);
    for p in &params {
        b = b.param(p);
    }
    ctx.peers.broadcast(None, b.to_wire());
}

/// The directly-linked peer SID through which the remote user `uid` is reached — its server's
/// [`uplink`](super::RemoteServer::uplink), falling back to the user's own server SID (a
/// direct peer is its own uplink). `None` only if the SID prefix is malformed.
fn uplink_of(ctx: &ServerContext, uid: &Uid) -> Option<Sid> {
    let server_sid = Sid::try_from(uid.as_str().get(..4)?).ok()?;
    Some(
        ctx.net
            .server(&server_sid)
            .map(|s| s.uplink)
            .unwrap_or(server_sid),
    )
}

/// Render effective [`ModeChange`]s to an S2S `(modestring, params)` pair: sign-tracked mode
/// letters with `+o`/`+v` rendered as **UID** params, `+k`/`+l value`/`+b mask` as their
/// value params. The inverse of the S2S parser in [`super::mode`].
fn render_modes_uid(changes: &[ModeChange]) -> (String, Vec<String>) {
    let mut modestr = String::new();
    let mut params: Vec<String> = Vec::new();
    let mut sign: Option<bool> = None;
    let push_sign = |modestr: &mut String, add: bool, sign: &mut Option<bool>| {
        if *sign != Some(add) {
            modestr.push(if add { '+' } else { '-' });
            *sign = Some(add);
        }
    };
    for change in changes {
        match change {
            ModeChange::Flag { add, mode } => {
                push_sign(&mut modestr, *add, &mut sign);
                modestr.push(mode.as_char());
            }
            ModeChange::Member { add, op, uid } => {
                push_sign(&mut modestr, *add, &mut sign);
                modestr.push(if *op { 'o' } else { 'v' });
                params.push(uid.as_str().to_string());
            }
            ModeChange::Key { add, key } => {
                push_sign(&mut modestr, *add, &mut sign);
                modestr.push('k');
                params.push(key.clone());
            }
            ModeChange::Limit { add, value } => {
                push_sign(&mut modestr, *add, &mut sign);
                modestr.push('l');
                if let Some(v) = value {
                    params.push(v.to_string());
                }
            }
            ModeChange::ListMask { add, mode, mask } => {
                push_sign(&mut modestr, *add, &mut sign);
                modestr.push(mode.as_char());
                params.push(mask.clone());
            }
        }
    }
    (modestr, params)
}

// ---------------------------------------------------------------------------
// Inbound — a peer forwarding a remote user's action → local clients + onward.
// ---------------------------------------------------------------------------

/// Resolve a relayed line's source prefix (a remote user's [`Uid`]) to its display
/// `nick!user@host`, for the client-form fan-out. Falls back to the [`Network`](super::Network)
/// mirror, then the registry (a local UID, unusual on an inbound line), then the bare prefix.
fn source_mask(ctx: &ServerContext, prefix: &str) -> String {
    if let Ok(uid) = Uid::try_from(prefix) {
        if let Some(u) = ctx.net.user(&uid) {
            return format!("{}!{}@{}", u.nick, u.user, u.host);
        }
        if let Some(r) = ctx.registry.record_of(&uid) {
            return format!("{}!{}@{}", r.nick, r.user, r.host);
        }
    }
    prefix.to_string()
}

/// Deliver `wire` (client form) to every **local** member of `chan`, optionally skipping
/// `skip`. Remote members are not pushed here — they are served by the onward relay.
fn fanout_local(ctx: &ServerContext, chan: &str, skip: Option<&Uid>, wire: &[u8]) {
    if let Some(members) = ctx.channels.members(chan) {
        for m in &members {
            if ctx.is_local_uid(m) && Some(m) != skip {
                ctx.registry.deliver_uid(m, wire.to_vec());
            }
        }
    }
}

/// Forward an inbound line to every peer except the one it arrived on (split-horizon).
fn onward(ctx: &ServerContext, origin: &Sid, wire: Vec<u8>) {
    ctx.peers.broadcast(Some(origin), wire);
}

/// Resolve a relayed member reference (a [`Uid`] on the wire, or a nick) to a [`Uid`].
fn resolve_member(ctx: &ServerContext, field: &str) -> Option<Uid> {
    if let Ok(uid) = Uid::try_from(field) {
        return Some(uid);
    }
    ctx.registry.uid_of(field)
}

/// Inbound `:<uid> JOIN <chan>`: add the remote member, fan a client `JOIN` out to local
/// members, and onward-relay.
pub fn inbound_join(link: &mut PeerLink, msg: &Message, ctx: &ServerContext) -> Vec<u8> {
    let (Some(prefix), Some(chan)) = (msg.prefix(), msg.params().first()) else {
        return Vec::new();
    };
    let Ok(uid) = Uid::try_from(prefix) else {
        return Vec::new();
    };
    ctx.channels
        .njoin_member(chan, &uid, MemberStatus::default(), crate::clock::unixnano());
    let mask = source_mask(ctx, prefix);
    let local = Message::builder("JOIN")
        .prefix(&mask)
        .param(chan)
        .to_wire();
    fanout_local(ctx, chan, Some(&uid), &local);
    onward(ctx, &link.sid, msg.to_wire());
    Vec::new()
}

/// Inbound `:<uid> PART <chan> [:reason]`: fan a client `PART` out to local members, remove
/// the remote member, and onward-relay.
pub fn inbound_part(link: &mut PeerLink, msg: &Message, ctx: &ServerContext) -> Vec<u8> {
    let (Some(prefix), Some(chan)) = (msg.prefix(), msg.params().first()) else {
        return Vec::new();
    };
    let Ok(uid) = Uid::try_from(prefix) else {
        return Vec::new();
    };
    let mask = source_mask(ctx, prefix);
    let mut b = Message::builder("PART").prefix(&mask).param(chan);
    if let Some(r) = msg.trailing().filter(|r| !r.is_empty()) {
        b = b.trailing(r);
    }
    let local = b.to_wire();
    // Fan out before removal (the parter is remote, so it is never a local member anyway).
    fanout_local(ctx, chan, None, &local);
    ctx.channels.part(chan, &uid);
    onward(ctx, &link.sid, msg.to_wire());
    Vec::new()
}

/// Inbound `:<uid> PRIVMSG/NOTICE <targets> :<text>`: deliver to each local channel's members
/// and to each local nick target, then onward-relay the whole line (split-horizon).
pub fn inbound_message(
    link: &mut PeerLink,
    msg: &Message,
    ctx: &ServerContext,
    verb: &str,
) -> Vec<u8> {
    let (Some(prefix), Some(targets)) = (msg.prefix(), msg.params().first()) else {
        return Vec::new();
    };
    let Some(text) = msg
        .trailing()
        .or_else(|| msg.params().get(1).map(String::as_str))
    else {
        return Vec::new();
    };
    let mask = source_mask(ctx, prefix);
    for target in targets.split(',').filter(|t| !t.is_empty()) {
        let line = Message::builder(verb)
            .prefix(&mask)
            .param(target)
            .trailing(text)
            .to_wire();
        if is_channel(target) {
            fanout_local(ctx, target, None, &line);
        } else if let Some(uid) = ctx.registry.uid_of(target) {
            // Only a *local* nick is delivered here; a remote target is reached by the onward
            // relay below (it lives behind another peer).
            if ctx.is_local_uid(&uid) {
                ctx.registry.deliver_uid(&uid, line);
            }
        }
    }
    onward(ctx, &link.sid, msg.to_wire());
    Vec::new()
}

/// Inbound `:<uid> TOPIC <chan> :<text>`: set the topic server-authoritatively, fan a client
/// `TOPIC` out to local members, and onward-relay.
pub fn inbound_topic(link: &mut PeerLink, msg: &Message, ctx: &ServerContext) -> Vec<u8> {
    let (Some(prefix), Some(chan)) = (msg.prefix(), msg.params().first()) else {
        return Vec::new();
    };
    let text = msg.trailing().unwrap_or("");
    let mask = source_mask(ctx, prefix);
    if ctx
        .channels
        .set_topic_as_server(chan, &mask, text, crate::clock::unixnano())
        .is_some()
    {
        let local = Message::builder("TOPIC")
            .prefix(&mask)
            .param(chan)
            .trailing(text)
            .to_wire();
        fanout_local(ctx, chan, None, &local);
    }
    onward(ctx, &link.sid, msg.to_wire());
    Vec::new()
}

/// Inbound `:<uid> KICK <chan> <victim> :<reason>`: fan a client `KICK` (victim by nick) out
/// to local members, remove the victim, and onward-relay.
pub fn inbound_kick(link: &mut PeerLink, msg: &Message, ctx: &ServerContext) -> Vec<u8> {
    let p = msg.params();
    let (Some(prefix), Some(chan), Some(victim_field)) = (msg.prefix(), p.first(), p.get(1)) else {
        return Vec::new();
    };
    let Some(victim) = resolve_member(ctx, victim_field) else {
        return Vec::new();
    };
    let reason = msg
        .trailing()
        .or_else(|| p.get(2).map(String::as_str))
        .unwrap_or("");
    let mask = source_mask(ctx, prefix);
    let victim_nick = ctx
        .registry
        .nick_of(&victim)
        .or_else(|| ctx.net.user(&victim).map(|u| u.nick))
        .unwrap_or_else(|| victim_field.clone());
    let local = Message::builder("KICK")
        .prefix(&mask)
        .param(chan)
        .param(&victim_nick)
        .trailing(reason)
        .to_wire();
    // Fan out before removal so the victim (if local) sees its own KICK.
    fanout_local(ctx, chan, None, &local);
    ctx.channels.kick(chan, &victim);
    onward(ctx, &link.sid, msg.to_wire());
    Vec::new()
}

/// Inbound `:<uid> NICK <new>`: rename the remote user in the mirror + registry, fan a client
/// `NICK` (old mask as source) out to local co-members, and onward-relay.
pub fn inbound_nick(link: &mut PeerLink, msg: &Message, ctx: &ServerContext) -> Vec<u8> {
    let Some(prefix) = msg.prefix() else {
        return Vec::new();
    };
    let new = msg
        .params()
        .first()
        .map(String::as_str)
        .or_else(|| msg.trailing())
        .filter(|s| !s.is_empty());
    let (Ok(uid), Some(new)) = (Uid::try_from(prefix), new) else {
        return Vec::new();
    };
    let old_mask = source_mask(ctx, prefix); // old nick!user@host (before the rename)
    if let Some(mut u) = ctx.net.user(&uid) {
        u.nick = new.to_string();
        ctx.net.insert_user(u);
    }
    let _ = ctx.registry.try_rename(&uid, new);
    let line = Message::builder("NICK")
        .prefix(&old_mask)
        .param(new)
        .to_wire();
    for co in ctx.channels.co_members(&uid) {
        if ctx.is_local_uid(&co) {
            ctx.registry.deliver_uid(&co, line.clone());
        }
    }
    onward(ctx, &link.sid, msg.to_wire());
    Vec::new()
}

/// Inbound `:<uid> QUIT :<reason>`: tear the remote user down (relaying its `QUIT` to local
/// co-members, freeing its channels + nick) and onward-relay.
pub fn inbound_quit(link: &mut PeerLink, msg: &Message, ctx: &ServerContext) -> Vec<u8> {
    let Some(prefix) = msg.prefix() else {
        return Vec::new();
    };
    let Ok(uid) = Uid::try_from(prefix) else {
        return Vec::new();
    };
    let reason = msg
        .trailing()
        .or_else(|| msg.params().first().map(String::as_str))
        .filter(|s| !s.is_empty())
        .unwrap_or("Quit");
    squit::quit_one_remote_user(ctx, &uid, reason);
    onward(ctx, &link.sid, msg.to_wire());
    Vec::new()
}

/// Post-burst channel `MODE` fan-out: re-prefix the line with the source `nick!user@host`,
/// translate any UID member-params to their display nicks for local clients, fan out to local
/// members, and onward-relay. Called from [`dispatch`](super::dispatch) **after** the state
/// has already been applied by [`super::mode::handle_mode`], and only once the burst is over.
pub fn after_mode(link: &mut PeerLink, msg: &Message, ctx: &ServerContext) {
    let (Some(prefix), Some(chan)) = (msg.prefix(), msg.params().first()) else {
        return;
    };
    if !is_channel(chan) {
        return; // a user MODE from a server is not relayed to clients
    }
    let Some(modestr) = msg.params().get(1) else {
        return;
    };
    let mask = source_mask(ctx, prefix);
    let mut b = Message::builder("MODE").prefix(&mask).param(chan).param(modestr);
    for p in msg.params().iter().skip(2) {
        b = b.param(localize_param(ctx, p));
    }
    let local = b.to_wire();
    fanout_local(ctx, chan, None, &local);
    onward(ctx, &link.sid, msg.to_wire());
}

/// Translate a MODE param to its client-facing form: a known [`Uid`] (an `+o`/`+v` member
/// reference) becomes that member's display nick; anything else (a key, limit, ban mask) is
/// returned unchanged.
fn localize_param(ctx: &ServerContext, param: &str) -> String {
    if let Ok(uid) = Uid::try_from(param) {
        if let Some(nick) = ctx
            .registry
            .nick_of(&uid)
            .or_else(|| ctx.net.user(&uid).map(|u| u.nick))
        {
            return nick;
        }
    }
    param.to_string()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::channel::MemberStatus;
    use crate::ident::ServerName;
    use crate::mode::ChanMode;
    use crate::registry::Envelope;
    use crate::s2s::network::{RemoteServer, RemoteUser};
    use std::sync::Arc;
    use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver};

    fn ctx() -> Arc<ServerContext> {
        let kdl = r#"
            server { name "leveva.test"; description "Leveva Test"; sid "0ABC" }
            admin { name "A"; email "a@test"; network "Net" }
            class "c" { ping-freq 90; max-links 10; sendq 1000 }
        "#;
        let cfg = crate::config::Config::from_kdl(kdl).expect("valid test config");
        ServerContext::from_config(&cfg, "now".to_string(), None)
    }

    fn uid(s: &str) -> Uid {
        Uid::try_from(s).unwrap()
    }

    /// A linked peer `0XYZ`, registered in both the network mirror and the [`PeerLinks`]
    /// routing table; returns the receiver so a test can read what we relay to the peer.
    fn link_peer(ctx: &ServerContext) -> (PeerLink, UnboundedReceiver<Envelope>) {
        let (link, rx) = PeerLink::new(
            Sid::try_from("0XYZ").unwrap(),
            ServerName::try_from("peer.test").unwrap(),
        );
        ctx.net.insert_server(RemoteServer {
            sid: link.sid.clone(),
            name: link.name.clone(),
            description: "Peer".into(),
            hopcount: 1,
            uplink: link.sid.clone(),
        });
        ctx.peers.link(link.sid.clone(), link.mailbox_tx.clone());
        (link, rx)
    }

    /// Introduce a remote user behind the peer: into the mirror + a registry nick claim
    /// routed to the peer's mailbox (exactly as UNICK does).
    fn introduce(ctx: &ServerContext, link: &PeerLink, uid_s: &str, nick: &str) {
        let u = uid(uid_s);
        ctx.net.insert_user(RemoteUser {
            uid: u.clone(),
            nick: nick.into(),
            user: nick.into(),
            host: "rh".into(),
            ip: "0".into(),
            umodes: 0,
            realname: nick.into(),
            server: RemoteUser::sid_of_uid(&u),
        });
        ctx.registry
            .try_claim(&u, nick, nick, "rh", nick, link.mailbox_tx.clone())
            .unwrap();
    }

    /// A local client, returning its UID + an observable mailbox receiver.
    fn local_client(ctx: &ServerContext, uid_s: &str, nick: &str) -> (Uid, UnboundedReceiver<Envelope>) {
        let u = uid(uid_s);
        let (tx, rx) = unbounded_channel();
        ctx.registry.try_claim(&u, nick, nick, "lh", nick, tx).unwrap();
        (u, rx)
    }

    fn next(rx: &mut UnboundedReceiver<Envelope>) -> String {
        match rx.try_recv().expect("a frame was delivered") {
            Envelope::Wire(b) => String::from_utf8(b).unwrap(),
            other => panic!("expected wire, got {other:?}"),
        }
    }

    fn parse(line: &str) -> Message {
        Message::parse(line).unwrap()
    }

    // ---- outbound ----

    #[test]
    fn local_builders_are_noops_with_no_peers() {
        let ctx = ctx();
        // No peers linked → every builder is a silent no-op (single-server boot).
        local_join(&ctx, &uid("0ABCAAAAA"), "#rust");
        local_quit(&ctx, &uid("0ABCAAAAA"), "bye");
        assert_eq!(ctx.peers.count(), 0);
    }

    #[test]
    fn local_join_part_quit_relay_uid_form_to_peers() {
        let ctx = ctx();
        let (_link, mut rx) = link_peer(&ctx);
        let a = uid("0ABCAAAAA");
        local_join(&ctx, &a, "#rust");
        assert_eq!(next(&mut rx), ":0ABCAAAAA JOIN #rust\r\n");
        local_part(&ctx, &a, "#rust", Some("later"));
        assert_eq!(next(&mut rx), ":0ABCAAAAA PART #rust :later\r\n");
        local_quit(&ctx, &a, "Connection closed");
        assert_eq!(next(&mut rx), ":0ABCAAAAA QUIT :Connection closed\r\n");
        // Inverse: a bare PART carries no trailing.
        local_part(&ctx, &a, "#rust", None);
        assert_eq!(next(&mut rx), ":0ABCAAAAA PART #rust\r\n");
    }

    #[test]
    fn local_channel_message_broadcasts_uid_form() {
        let ctx = ctx();
        let (_link, mut rx) = link_peer(&ctx);
        local_channel_message(&ctx, &uid("0ABCAAAAA"), "PRIVMSG", "#rust", "hi all");
        assert_eq!(next(&mut rx), ":0ABCAAAAA PRIVMSG #rust :hi all\r\n");
    }

    #[test]
    fn route_message_to_remote_reaches_only_its_uplink() {
        let ctx = ctx();
        let (link, mut rx) = link_peer(&ctx);
        introduce(&ctx, &link, "0XYZAAAAA", "rover");
        route_message_to_remote(&ctx, &uid("0XYZAAAAA"), &uid("0ABCAAAAA"), "PRIVMSG", "rover", "hey");
        assert_eq!(next(&mut rx), ":0ABCAAAAA PRIVMSG rover :hey\r\n");
        assert!(rx.try_recv().is_err(), "exactly one routed frame");
    }

    #[test]
    fn local_channel_mode_renders_uid_member_params() {
        let ctx = ctx();
        let (_link, mut rx) = link_peer(&ctx);
        let changes = vec![
            ModeChange::Flag { add: true, mode: ChanMode::NoExternalMessages },
            ModeChange::Member { add: true, op: true, uid: uid("0ABCAAAAB") },
        ];
        local_channel_mode(&ctx, &uid("0ABCAAAAA"), "#rust", &changes);
        assert_eq!(next(&mut rx), ":0ABCAAAAA MODE #rust +no 0ABCAAAAB\r\n");
        // Inverse: an empty change set relays nothing.
        local_channel_mode(&ctx, &uid("0ABCAAAAA"), "#rust", &[]);
        assert!(rx.try_recv().is_err());
    }

    // ---- inbound ----

    #[test]
    fn inbound_join_adds_member_and_fans_out_to_local() {
        let ctx = ctx();
        let (mut link, _peer_rx) = link_peer(&ctx);
        introduce(&ctx, &link, "0XYZAAAAA", "rover");
        let (local, mut local_rx) = local_client(&ctx, "0ABCAAAAA", "alice");
        ctx.channels.join("#rust", &local, 0); // alice already on #rust

        inbound_join(&mut link, &parse(":0XYZAAAAA JOIN #rust"), &ctx);
        // rover is now a member, and alice saw the client-form JOIN.
        assert!(ctx.channels.is_member("#rust", &uid("0XYZAAAAA")));
        assert_eq!(next(&mut local_rx), ":rover!rover@rh JOIN #rust\r\n");
        // Inverse: a PART removes rover and alice sees it.
        inbound_part(&mut link, &parse(":0XYZAAAAA PART #rust :bye"), &ctx);
        assert!(!ctx.channels.is_member("#rust", &uid("0XYZAAAAA")));
        assert_eq!(next(&mut local_rx), ":rover!rover@rh PART #rust :bye\r\n");
    }

    #[test]
    fn inbound_channel_message_fans_out_to_local_members_only() {
        let ctx = ctx();
        let (mut link, mut peer_rx) = link_peer(&ctx);
        introduce(&ctx, &link, "0XYZAAAAA", "rover");
        let (local, mut local_rx) = local_client(&ctx, "0ABCAAAAA", "alice");
        // Both rover (remote) and alice (local) are on #rust.
        ctx.channels.njoin_member("#rust", &uid("0XYZAAAAA"), MemberStatus::default(), 0);
        ctx.channels.join("#rust", &local, 0);

        inbound_message(&mut link, &parse(":0XYZAAAAA PRIVMSG #rust :hello"), &ctx, "PRIVMSG");
        // alice (local) gets the client form; the remote member is NOT pushed onto the mailbox.
        assert_eq!(next(&mut local_rx), ":rover!rover@rh PRIVMSG #rust :hello\r\n");
        // Single-uplink: the onward relay (broadcast except origin) reaches no other peer.
        assert!(peer_rx.try_recv().is_err(), "no echo back to the origin peer");
    }

    #[test]
    fn inbound_private_message_to_local_nick_delivers() {
        let ctx = ctx();
        let (mut link, _peer_rx) = link_peer(&ctx);
        introduce(&ctx, &link, "0XYZAAAAA", "rover");
        let (_local, mut local_rx) = local_client(&ctx, "0ABCAAAAA", "alice");
        inbound_message(&mut link, &parse(":0XYZAAAAA PRIVMSG alice :psst"), &ctx, "PRIVMSG");
        assert_eq!(next(&mut local_rx), ":rover!rover@rh PRIVMSG alice :psst\r\n");
    }

    #[test]
    fn inbound_topic_sets_and_fans_out() {
        let ctx = ctx();
        let (mut link, _peer_rx) = link_peer(&ctx);
        introduce(&ctx, &link, "0XYZAAAAA", "rover");
        let (local, mut local_rx) = local_client(&ctx, "0ABCAAAAA", "alice");
        ctx.channels.join("#rust", &local, 0);
        inbound_topic(&mut link, &parse(":0XYZAAAAA TOPIC #rust :hello world"), &ctx);
        assert_eq!(next(&mut local_rx), ":rover!rover@rh TOPIC #rust :hello world\r\n");
    }

    #[test]
    fn inbound_kick_removes_victim_and_fans_out_with_nick() {
        let ctx = ctx();
        let (mut link, _peer_rx) = link_peer(&ctx);
        introduce(&ctx, &link, "0XYZAAAAA", "rover");
        let (local, mut local_rx) = local_client(&ctx, "0ABCAAAAA", "alice");
        ctx.channels.join("#rust", &local, 0);
        // rover kicks alice (a local member, addressed by UID on the wire).
        inbound_kick(&mut link, &parse(":0XYZAAAAA KICK #rust 0ABCAAAAA :spam"), &ctx);
        assert_eq!(next(&mut local_rx), ":rover!rover@rh KICK #rust alice :spam\r\n");
        assert!(!ctx.channels.is_member("#rust", &local), "victim removed");
    }

    #[test]
    fn inbound_nick_renames_and_relays_old_mask() {
        let ctx = ctx();
        let (mut link, _peer_rx) = link_peer(&ctx);
        introduce(&ctx, &link, "0XYZAAAAA", "rover");
        let (local, mut local_rx) = local_client(&ctx, "0ABCAAAAA", "alice");
        ctx.channels.njoin_member("#rust", &uid("0XYZAAAAA"), MemberStatus::default(), 0);
        ctx.channels.join("#rust", &local, 0);

        inbound_nick(&mut link, &parse(":0XYZAAAAA NICK landrover"), &ctx);
        // The registry + mirror now know the new nick; the old mask is the relay source.
        assert_eq!(ctx.net.user(&uid("0XYZAAAAA")).unwrap().nick, "landrover");
        assert!(ctx.registry.contains("landrover") && !ctx.registry.contains("rover"));
        assert_eq!(next(&mut local_rx), ":rover!rover@rh NICK landrover\r\n");
    }

    #[test]
    fn inbound_quit_tears_down_remote_user() {
        let ctx = ctx();
        let (mut link, _peer_rx) = link_peer(&ctx);
        introduce(&ctx, &link, "0XYZAAAAA", "rover");
        let (local, mut local_rx) = local_client(&ctx, "0ABCAAAAA", "alice");
        ctx.channels.njoin_member("#rust", &uid("0XYZAAAAA"), MemberStatus::default(), 0);
        ctx.channels.join("#rust", &local, 0);

        inbound_quit(&mut link, &parse(":0XYZAAAAA QUIT :gone"), &ctx);
        assert_eq!(next(&mut local_rx), ":rover!rover@rh QUIT :gone\r\n");
        // Inverse: rover is gone from the mirror, the registry, and the channel.
        assert!(ctx.net.user(&uid("0XYZAAAAA")).is_none());
        assert!(!ctx.registry.contains("rover"));
        assert!(!ctx.channels.is_member("#rust", &uid("0XYZAAAAA")));
        assert!(ctx.channels.is_member("#rust", &local), "alice survives");
    }

    #[test]
    fn after_mode_fans_out_with_member_uid_translated_to_nick() {
        let ctx = ctx();
        let (mut link, _peer_rx) = link_peer(&ctx);
        link.end_burst(); // steady state
        introduce(&ctx, &link, "0XYZAAAAA", "rover");
        let (local, mut local_rx) = local_client(&ctx, "0ABCAAAAA", "alice");
        ctx.channels.njoin_member("#rust", &uid("0XYZAAAAA"), MemberStatus::default(), 0);
        ctx.channels.join("#rust", &local, 0);
        // rover ops alice (a local member); the wire param is alice's UID.
        after_mode(&mut link, &parse(":0XYZAAAAA MODE #rust +o 0ABCAAAAA"), &ctx);
        assert_eq!(next(&mut local_rx), ":rover!rover@rh MODE #rust +o alice\r\n");
    }

    #[test]
    fn malformed_inbound_lines_are_dropped_cleanly() {
        let ctx = ctx();
        let (mut link, _peer_rx) = link_peer(&ctx);
        // No prefix / no channel / bad UID — each a clean no-op, no panic.
        assert!(inbound_join(&mut link, &parse("JOIN #x"), &ctx).is_empty());
        assert!(inbound_join(&mut link, &parse(":0XYZAAAAA JOIN"), &ctx).is_empty());
        assert!(inbound_kick(&mut link, &parse(":0XYZAAAAA KICK #x"), &ctx).is_empty());
        assert!(inbound_quit(&mut link, &parse("QUIT :x"), &ctx).is_empty());
    }
}
