458:pub enum JoinReject {
473:    SecureOnly,
484:pub fn sslonly_blocks(sslonly_set: bool, joiner_secure: bool) -> bool {
549:pub fn free_invite_allows(inviter_manages: bool, plus_g: bool) -> bool {
957:            return Some(JoinReject::SecureOnly);
3194:    /// plaintext client with [`JoinReject::SecureOnly`] — a hard transport gate not overridden
3220:            Some(JoinReject::SecureOnly),
3227:            Some(JoinReject::SecureOnly),
3248:    fn sslonly_blocks_truth_table() {
===
pub fn sslonly_blocks(sslonly_set: bool, joiner_secure: bool) -> bool {
    sslonly_set && !joiner_secure
}
    fn sslonly_blocks_truth_table() {
        assert!(sslonly_blocks(true, false), "+S + plaintext → blocked");
        assert!(!sslonly_blocks(true, true), "+S + secure → admitted");
        assert!(!sslonly_blocks(false, false), "no +S → admitted");
        assert!(!sslonly_blocks(false, true), "no +S → admitted");
    }

    /// Seed a `+R` reop mask onto an existing channel (via the trusted server path, so no
    /// op-gate gets in the way of the fixture).
    fn set_reop(ch: &Channels, name: &str, mask: &str) {
        assert!(ch.apply_modes_as_server(
            name,
            &[ModeChange::ListMask {
                add: true,
                mode: ChanMode::ReopList,
                mask: mask.into(),
            }],
        ));
    }

    /// A non-op member whose mask matches a `+R` entry is auto-opped; the UID is returned.
    #[test]
    fn apply_reop_ops_a_matching_nonop_member() {
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap(); // alice creates → op
        ch.join("#rust", &bob(), 0).unwrap(); // bob plain
        set_reop(&ch, "#rust", "*!*@host");
        let opped = ch.apply_reop("#rust", &[(bob(), "bob!u@host".into())]);
        assert_eq!(opped, vec![bob()]);
        assert!(ch.is_op("#rust", &bob()), "bob was reopped");
    }

    /// Inverse — a member whose mask matches **no** reop entry is left untouched.
    #[test]
    fn apply_reop_skips_a_nonmatching_member() {
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap();
        ch.join("#rust", &bob(), 0).unwrap();
        set_reop(&ch, "#rust", "*!*@trusted.example");
        let opped = ch.apply_reop("#rust", &[(bob(), "bob!u@host".into())]);
        assert!(opped.is_empty(), "no match → no grant");
        assert!(!ch.is_op("#rust", &bob()), "bob stays a plain member");
    }

    /// Inverse — an already-opped member is not re-opped (no duplicate broadcast).
    #[test]
    fn apply_reop_skips_an_existing_op() {
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap(); // alice is already op
        set_reop(&ch, "#rust", "*!*@host");
        let opped = ch.apply_reop("#rust", &[(alice(), "alice!u@host".into())]);
        assert!(opped.is_empty(), "already op → nothing newly granted");
    }

    /// Inverse — a candidate who is not on the channel is skipped.
    #[test]
    fn apply_reop_skips_a_nonmember() {
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap();
        set_reop(&ch, "#rust", "*!*@host");
        let opped = ch.apply_reop("#rust", &[(carol(), "carol!u@host".into())]);
        assert!(opped.is_empty(), "non-member can't be opped");
    }

    /// Inverse — with no reop masks set, a perfectly-matching candidate is still ignored.
    #[test]
    fn apply_reop_is_a_noop_without_masks() {
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap();
        ch.join("#rust", &bob(), 0).unwrap();
        let opped = ch.apply_reop("#rust", &[(bob(), "bob!u@host".into())]);
        assert!(opped.is_empty(), "empty reop list → no grants");
        assert!(!ch.is_op("#rust", &bob()));
    }

    /// Inverse — an unknown channel yields no grants (and does not panic).
    #[test]
    fn apply_reop_on_unknown_channel_is_empty() {
        let ch = Channels::new();
        assert!(ch
            .apply_reop("#ghost", &[(bob(), "bob!u@host".into())])
            .is_empty());
    }

    #[test]
    fn channel_mode_string_hides_params_from_outsiders() {
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap();
        apply(
            &ch,
            "#rust",
            &alice(),
            &[
                ModeChange::Flag {
                    add: true,
                    mode: ChanMode::NoExternalMessages,
                },
                ModeChange::Flag {
                    add: true,
                    mode: ChanMode::TopicOpsOnly,
                },
                ModeChange::Key {
                    add: true,
                    key: "sekrit".into(),
                },
                ModeChange::Limit {
                    add: true,
                    value: Some(10),
                },
            ],
        );
        // Member view shows the params; outsider view shows only the letters.
        let (m_in, p_in) = ch.channel_mode_string("#rust", true).unwrap();
        assert_eq!(m_in, "+ntlk");
        assert_eq!(p_in, "10 sekrit");
        let (m_out, p_out) = ch.channel_mode_string("#rust", false).unwrap();
        assert_eq!(m_out, "+ntlk");
        assert_eq!(p_out, "");
    }

    // --- can_send (+n / +m / +b speech mute) ---

    /// `can_send` with a plain (no-account, not-oper, not-ssl) subject and a fixed mask — the
    /// `+n`/`+m` tests never consult the mask (no bans), so one helper covers them.
    fn sendable(ch: &Channels, name: &str, uid: &Uid) -> CanSend {
        ch.can_send(name, uid, "n!u@h", &extban::SubjectAttrs::plain(false))
    }

    /// `can_send` against an explicit `mask` + `attrs`, for the ban-mute tests.
    fn sendable_as(
        ch: &Channels,
        name: &str,
        uid: &Uid,
        mask: &str,
        attrs: &extban::SubjectAttrs,
    ) -> CanSend {
        ch.can_send(name, uid, mask, attrs)
    }

    #[test]
    fn blocks_ctcp_tracks_the_plus_c_flag() {
        // +C (no-CTCP, slice 220) query — true only when the channel exists and carries +C.
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap();
        assert!(!ch.blocks_ctcp("#rust"), "no +C set");
        assert!(!ch.blocks_ctcp("#ghost"), "missing channel is not +C");
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::Flag {
                add: true,
                mode: ChanMode::NoCtcp,
            }],
        );
        assert!(ch.blocks_ctcp("#rust"), "+C set");
        // Inverse: clearing -C drops the flag again.
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::Flag {
                add: false,
                mode: ChanMode::NoCtcp,
            }],
        );
        assert!(!ch.blocks_ctcp("#rust"), "-C cleared");
    }

    #[test]
    fn blocks_color_tracks_the_plus_c_flag() {
        // +c (no-color, slice 232) query — true only when the channel exists and carries +c.
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap();
        assert!(!ch.blocks_color("#rust"), "no +c set");
        assert!(!ch.blocks_color("#ghost"), "missing channel is not +c");
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::Flag {
                add: true,
                mode: ChanMode::NoColor,
            }],
        );
        assert!(ch.blocks_color("#rust"), "+c set");
        // +c is independent of +C: setting +c does not set +C.
        assert!(!ch.blocks_ctcp("#rust"), "+c must not imply +C");
        // Inverse: clearing -c drops the flag again.
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::Flag {
                add: false,
                mode: ChanMode::NoColor,
            }],
        );
        assert!(!ch.blocks_color("#rust"), "-c cleared");
    }

    #[test]
    fn blocks_notice_tracks_the_plus_t_flag() {
        // +T (no-NOTICE, slice 239) query — true only when the channel exists and carries +T.
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap();
        assert!(!ch.blocks_notice("#rust"), "no +T set");
        assert!(!ch.blocks_notice("#ghost"), "missing channel is not +T");
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::Flag {
                add: true,
                mode: ChanMode::NoNotice,
            }],
        );
        assert!(ch.blocks_notice("#rust"), "+T set");
        // +T is independent of +C/+c: setting +T sets neither.
        assert!(!ch.blocks_ctcp("#rust"), "+T must not imply +C");
        assert!(!ch.blocks_color("#rust"), "+T must not imply +c");
        // Inverse: clearing -T drops the flag again.
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::Flag {
                add: false,
                mode: ChanMode::NoNotice,
            }],
        );
        assert!(!ch.blocks_notice("#rust"), "-T cleared");
    }

    #[test]
    fn notice_blocked_predicate_truth_table() {
        // +T drops a NOTICE (any case) iff the flag is set; a PRIVMSG is never dropped.
        assert!(notice_blocked(true, "NOTICE"));
        assert!(notice_blocked(true, "notice"));
        assert!(!notice_blocked(false, "NOTICE"), "no +T → never dropped");
        assert!(!notice_blocked(true, "PRIVMSG"), "PRIVMSG passes +T");
        assert!(!notice_blocked(true, "TAGMSG"), "non-NOTICE passes +T");
        assert!(!notice_blocked(false, "PRIVMSG"));
    }

    #[test]
    fn opmod_redirects_only_blocked_verdicts_when_set() {
        // +z (op-moderation, slice 235): redirect a *blocked* verdict to ops, and only when set.
        for verdict in [CanSend::External, CanSend::Moderated, CanSend::Banned] {
            assert!(opmod_redirects(true, verdict), "{verdict:?} + z → redirect");
            assert!(!opmod_redirects(false, verdict), "{verdict:?} no z → no redirect");
        }
        // Never a non-blocking verdict, set or not.
        for verdict in [CanSend::Ok, CanSend::NoSuchChannel] {
            assert!(!opmod_redirects(true, verdict), "{verdict:?} is not redirectable");
            assert!(!opmod_redirects(false, verdict));
        }
    }

    #[test]
    fn op_moderated_tracks_the_plus_z_flag() {
        // +z query — true only when the channel exists and carries +z.
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap();
        assert!(!ch.op_moderated("#rust"), "no +z set");
        assert!(!ch.op_moderated("#ghost"), "missing channel is not +z");
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::Flag {
                add: true,
                mode: ChanMode::OpModerate,
            }],
        );
        assert!(ch.op_moderated("#rust"), "+z set");
        // Inverse: clearing -z drops the flag again.
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::Flag {
                add: false,
                mode: ChanMode::OpModerate,
            }],
        );
        assert!(!ch.op_moderated("#rust"), "-z cleared");
    }

    #[test]
    fn op_members_returns_only_rank_op_and_above() {
        // alice is the creator (→ op); bob is a plain member; carol is granted op.
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap();
        ch.join("#rust", &bob(), 0).unwrap();
        ch.join("#rust", &carol(), 0).unwrap();
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::Member {
                add: true,
                mode: ChanMode::Op,
                uid: carol(),
            }],
        );
        let ops = ch.op_members("#rust");
        assert!(ops.contains(&alice()), "creator is op");
        assert!(ops.contains(&carol()), "granted op");
        assert!(!ops.contains(&bob()), "plain member is not an op recipient");
        // Every op is a member, and every op holds rank ≥ +o.
        let members = ch.members("#rust").unwrap();
        for op in &ops {
            assert!(members.contains(op), "op_members ⊆ members");
            assert!(
                ch.member_status("#rust", op).unwrap().rank() >= ChanMode::Op.rank(),
                "op_members only holds rank ≥ op"
            );
        }
        assert!(ch.op_members("#ghost").is_empty(), "unknown channel → no ops");
    }

    #[test]
    fn admit_join_passes_through_without_a_throttle() {
        // No +j set → every join is admitted (the gate short-circuits before the counter).
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap();
        assert_eq!(ch.join_throttle_param("#rust"), None);
        for now in 0..100 {
            assert!(ch.admit_join("#rust", now), "no +j must never throttle");
        }
        assert!(ch.admit_join("#ghost", 0), "missing channel is not throttled");
    }

    #[test]
    fn admit_join_enforces_and_clears_the_plus_j_throttle() {
        // +j 2:60 — two joins per 60s window; the third is throttled until the window elapses.
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap();
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::JoinThrottle {
                add: true,
                value: Some((2, 60)),
            }],
        );
        assert_eq!(ch.join_throttle_param("#rust"), Some((2, 60)));
        assert!(ch.admit_join("#rust", 1000)); // 1
        assert!(ch.admit_join("#rust", 1001)); // 2
        assert!(!ch.admit_join("#rust", 1002), "3rd join exceeds 2/60s");
        assert!(ch.admit_join("#rust", 1061), "fresh window once 60s elapsed");
        // Inverse: clearing -j removes the budget, so the gate stops throttling entirely.
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::JoinThrottle {
                add: false,
                value: None,
            }],
        );
        assert_eq!(ch.join_throttle_param("#rust"), None);
        for now in 2000..2100 {
            assert!(ch.admit_join("#rust", now), "-j must never throttle");
        }
    }

    #[test]
    fn has_control_codes_truth_table() {
        use super::has_control_codes;
        // Plain text — including printable punctuation and the CTCP delimiter (a `+C` concern,
        // not `+c`) — carries no formatting codes.
        assert!(!has_control_codes(""));
        assert!(!has_control_codes("plain message"));
        assert!(!has_control_codes("\u{1}ACTION waves\u{1}")); // CTCP delimiter is not a format code
        // Each mIRC formatting code is detected.
        assert!(has_control_codes("\u{2}bold\u{2}"));
        assert!(has_control_codes("\u{3}04red"));
        assert!(has_control_codes("hex\u{4}FF0000"));
        assert!(has_control_codes("reset\u{f}"));
        assert!(has_control_codes("\u{11}mono"));
        assert!(has_control_codes("\u{16}reverse"));
        assert!(has_control_codes("\u{1d}italic"));
        assert!(has_control_codes("\u{1e}strike"));
        assert!(has_control_codes("\u{1f}underline"));
    }

    #[test]
    fn can_send_honours_no_external_and_moderated() {
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap(); // alice op
        ch.join("#rust", &bob(), 0).unwrap(); // bob plain member
                                              // Default: anyone may speak.
        assert_eq!(sendable(&ch, "#rust", &carol()), CanSend::Ok);
        // +n: a non-member (carol) is blocked; members still ok.
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::Flag {
                add: true,
                mode: ChanMode::NoExternalMessages,
            }],
        );
        assert_eq!(sendable(&ch, "#rust", &carol()), CanSend::External);
        assert_eq!(sendable(&ch, "#rust", &bob()), CanSend::Ok);
        // +m: bob (no voice) blocked; alice (op) still ok.
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::Flag {
                add: true,
                mode: ChanMode::Moderated,
            }],
        );
        assert_eq!(sendable(&ch, "#rust", &bob()), CanSend::Moderated);
        assert_eq!(sendable(&ch, "#rust", &alice()), CanSend::Ok);
        // Voicing bob lets him speak again (inverse of the +m block).
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::Member {
                add: true,
                mode: ChanMode::Voice,
                uid: bob(),
            }],
        );
        assert_eq!(sendable(&ch, "#rust", &bob()), CanSend::Ok);
        // No such channel.
        assert_eq!(sendable(&ch, "#ghost", &alice()), CanSend::NoSuchChannel);
    }

    #[test]
    fn can_send_mutes_a_banned_member() {
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap(); // alice op
        ch.join("#rust", &bob(), 0).unwrap(); // bob plain member
        let plain = extban::SubjectAttrs::plain(false);
        // Before any ban bob may speak.
        assert_eq!(
            sendable_as(&ch, "#rust", &bob(), "bob!u@h", &plain),
            CanSend::Ok
        );
        // Ban bob's mask (set *after* he joined — the JOIN gate could not catch this).
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::ListMask {
                add: true,
                mode: ChanMode::Ban,
                mask: "bob!*@*".into(),
            }],
        );
        // bob is now muted on the speech plane; alice (op) and a non-matching nick are not.
        assert_eq!(
            sendable_as(&ch, "#rust", &bob(), "bob!u@h", &plain),
            CanSend::Banned
        );
        assert_eq!(
            sendable_as(&ch, "#rust", &alice(), "alice!u@h", &plain),
            CanSend::Ok
        );
        assert_eq!(
            sendable_as(&ch, "#rust", &bob(), "carol!u@h", &plain),
            CanSend::Ok
        );
        // Voicing bob bypasses the mute (voice+ may speak through a ban).
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::Member {
                add: true,
                mode: ChanMode::Voice,
                uid: bob(),
            }],
        );
        assert_eq!(
            sendable_as(&ch, "#rust", &bob(), "bob!u@h", &plain),
            CanSend::Ok
        );
        // Devoice → muted again; then unban → speaks again (round-trip, inverse of the mute).
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::Member {
                add: false,
                mode: ChanMode::Voice,
                uid: bob(),
            }],
        );
        assert_eq!(
            sendable_as(&ch, "#rust", &bob(), "bob!u@h", &plain),
            CanSend::Banned
        );
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::ListMask {
                add: false,
                mode: ChanMode::Ban,
                mask: "bob!*@*".into(),
            }],
        );
        assert_eq!(
            sendable_as(&ch, "#rust", &bob(), "bob!u@h", &plain),
            CanSend::Ok
        );
    }

    #[test]
    fn can_send_ban_exception_clears_the_mute() {
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap();
        ch.join("#rust", &bob(), 0).unwrap();
        let plain = extban::SubjectAttrs::plain(false);
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::ListMask {
                add: true,
                mode: ChanMode::Ban,
                mask: "bob!*@*".into(),
            }],
        );
        assert_eq!(
            sendable_as(&ch, "#rust", &bob(), "bob!u@h", &plain),
            CanSend::Banned
        );
        // A +e exception covering bob clears the ban on the speech plane, exactly as at JOIN.
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::ListMask {
                add: true,
                mode: ChanMode::Exception,
                mask: "bob!*@*".into(),
            }],
        );
        assert_eq!(
            sendable_as(&ch, "#rust", &bob(), "bob!u@h", &plain),
            CanSend::Ok
        );
    }

    #[test]
    fn can_send_mutes_via_account_extban() {
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap();
        ch.join("#rust", &bob(), 0).unwrap();
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::ListMask {
                add: true,
                mode: ChanMode::Ban,
                mask: "$a:spammer".into(),
            }],
        );
        let signed_in = extban::SubjectAttrs {
            account: Some("spammer"),
            ..extban::SubjectAttrs::plain(false)
        };
        let other = extban::SubjectAttrs {
            account: Some("regular"),
            ..extban::SubjectAttrs::plain(false)
        };
        // The dynamic extban mutes the matching account but not a different one — a mute the
        // JOIN gate can't catch once the user is already in the channel.
        assert_eq!(
            sendable_as(&ch, "#rust", &bob(), "bob!u@h", &signed_in),
            CanSend::Banned
        );
        assert_eq!(
            sendable_as(&ch, "#rust", &bob(), "bob!u@h", &other),
            CanSend::Ok
        );
    }

    // --- check_join (+k / +l / +i / +b) ---

    #[test]
    fn check_join_enforces_key() {
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap();
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::Key {
                add: true,
                key: "letmein".into(),
            }],
        );
        assert_eq!(
            ch.check_join(
                "#rust",
                &bob(),
                "bob!u@h",
                None,
                &crate::extban::SubjectAttrs::plain(false)
            ),
            Some(JoinReject::BadKey)
        );
        assert_eq!(
            ch.check_join(
                "#rust",
                &bob(),
                "bob!u@h",
                Some("wrong"),
                &crate::extban::SubjectAttrs::plain(false)
            ),
            Some(JoinReject::BadKey)
        );
        assert_eq!(
            ch.check_join(
                "#rust",
                &bob(),
                "bob!u@h",
                Some("letmein"),
                &crate::extban::SubjectAttrs::plain(false)
            ),
            None
        );
    }

    #[test]
    fn check_join_enforces_limit() {
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap();
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::Limit {
                add: true,
                value: Some(1),
            }],
        );
        // One member already; a second is full.
        assert_eq!(
            ch.check_join(
                "#rust",
                &bob(),
                "bob!u@h",
                None,
                &crate::extban::SubjectAttrs::plain(false)
            ),
            Some(JoinReject::Full)
        );
        // Inverse: raising the limit reopens it.
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::Limit {
                add: true,
                value: Some(5),
            }],
        );
        assert_eq!(
            ch.check_join(
                "#rust",
                &bob(),
                "bob!u@h",
                None,
                &crate::extban::SubjectAttrs::plain(false)
            ),
            None
        );
    }

    #[test]
    fn check_join_enforces_ban_and_invite_only() {
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap();
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::ListMask {
                add: true,
                mode: ChanMode::Ban,
                mask: "*!*@evil.example".into(),
            }],
        );
        assert_eq!(
            ch.check_join(
                "#rust",
                &bob(),
                "bob!u@evil.example",
                None,
                &crate::extban::SubjectAttrs::plain(false)
            ),
            Some(JoinReject::Banned)
        );
        assert_eq!(
            ch.check_join(
                "#rust",
                &bob(),
                "bob!u@good.example",
                None,
                &crate::extban::SubjectAttrs::plain(false)
            ),
            None
        );
        // +i blocks an un-invited joiner.
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::Flag {
                add: true,
                mode: ChanMode::InviteOnly,
            }],
        );
        assert_eq!(
            ch.check_join(
                "#rust",
                &bob(),
                "bob!u@good.example",
                None,
                &crate::extban::SubjectAttrs::plain(false)
            ),
            Some(JoinReject::InviteOnly)
        );
    }

    #[test]
    fn check_join_on_a_fresh_channel_is_always_allowed() {
        let ch = Channels::new();
        // Channel does not exist yet → creating it is allowed regardless of key.
        assert_eq!(
            ch.check_join(
                "#new",
                &alice(),
                "alice!u@h",
                None,
                &crate::extban::SubjectAttrs::plain(false)
            ),
            None
        );
    }

    // --- invite ---

    #[test]
    fn invite_outcomes_and_op_guard() {
        let ch = Channels::new();
        // No such channel → courtesy outcome, nothing stored.
        assert_eq!(
            ch.invite("#ghost", &alice(), &bob()),
            InviteOutcome::NoSuchChannel
        );
        // alice creates #rust (chanop); bob is a non-member.
        ch.join("#rust", &alice(), 0).unwrap();
        // Inviter not on the channel → 442.
        assert_eq!(
            ch.invite("#rust", &carol(), &bob()),
            InviteOutcome::NotOnChannel
        );
        // Chanop invites a non-member → stored.
        assert_eq!(
            ch.invite("#rust", &alice(), &bob()),
            InviteOutcome::Invited {
                display: "#rust".into(),
                stored: true
            }
        );
        // Target already a member → 443 (alice invites herself-as-member case: invite bob
        // after he joins).
        ch.join("#rust", &bob(), 0).unwrap();
        assert_eq!(
            ch.invite("#rust", &alice(), &bob()),
            InviteOutcome::AlreadyOnChannel
        );
    }

    #[test]
    fn invite_needs_ops_on_invite_only_channel() {
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap(); // alice = op
        ch.join("#rust", &bob(), 0).unwrap(); // bob = plain member
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::Flag {
                add: true,
                mode: ChanMode::InviteOnly,
            }],
        );
        // A non-op member cannot invite to a +i channel.
        assert_eq!(ch.invite("#rust", &bob(), &carol()), InviteOutcome::NeedOps);
        // A non-op member CAN invite to a non-+i channel, but stores no token.
        let ch2 = Channels::new();
        ch2.join("#open", &alice(), 0).unwrap();
        ch2.join("#open", &bob(), 0).unwrap();
        assert_eq!(
            ch2.invite("#open", &bob(), &carol()),
            InviteOutcome::Invited {
                display: "#open".into(),
                stored: false
            }
        );
    }

    /// `+g` free-invite (slice 240): a non-op member of a `+i +g` channel may invite, and the
    /// invite is authoritative — it stores the one-shot `+i` bypass token.
    #[test]
    fn free_invite_lets_a_non_op_invite_to_plus_i() {
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap(); // alice = op
        ch.join("#rust", &bob(), 0).unwrap(); // bob = plain member
        apply(
            &ch,
            "#rust",
            &alice(),
            &[
                ModeChange::Flag {
                    add: true,
                    mode: ChanMode::InviteOnly,
                },
                ModeChange::Flag {
                    add: true,
                    mode: ChanMode::FreeInvite,
                },
            ],
        );
        // bob (non-op) may now invite carol, and the bypass token is stored.
        assert_eq!(
            ch.invite("#rust", &bob(), &carol()),
            InviteOutcome::Invited {
                display: "#rust".into(),
                stored: true
            }
        );
        // The stored token lets carol cross +i on her next join.
        assert_eq!(
            ch.check_join(
                "#rust",
                &carol(),
                "carol!u@clean.example",
                None,
                &crate::extban::SubjectAttrs::plain(false)
            ),
            None,
            "the +g invite must record the +i bypass token"
        );
    }

    /// Inverse: clearing `+g` restores the chanop requirement on a `+i` channel.
    #[test]
    fn without_free_invite_a_non_op_still_needs_ops() {
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap();
        ch.join("#rust", &bob(), 0).unwrap();
        apply(
            &ch,
            "#rust",
            &alice(),
            &[
                ModeChange::Flag {
                    add: true,
                    mode: ChanMode::InviteOnly,
                },
                ModeChange::Flag {
                    add: true,
                    mode: ChanMode::FreeInvite,
                },
                ModeChange::Flag {
                    add: false,
                    mode: ChanMode::FreeInvite,
                },
            ],
        );
        assert_eq!(ch.invite("#rust", &bob(), &carol()), InviteOutcome::NeedOps);
    }

    #[test]
    fn free_invite_predicate_truth_table() {
        assert!(free_invite_allows(true, false)); // chanop, no +g
        assert!(free_invite_allows(true, true)); // chanop, +g
        assert!(free_invite_allows(false, true)); // non-op, +g
        assert!(!free_invite_allows(false, false)); // non-op, no +g
    }

    #[test]
    fn invite_token_bypasses_i_b_l_but_not_k_and_is_consumed() {
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap();
        // +i only: un-invited carol is blocked by +i.
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::Flag {
                add: true,
                mode: ChanMode::InviteOnly,
            }],
        );
        assert_eq!(
            ch.check_join(
                "#rust",
                &carol(),
                "carol!u@clean.example",
                None,
                &crate::extban::SubjectAttrs::plain(false)
            ),
            Some(JoinReject::InviteOnly)
        );
        // Invite carol → the +i gate is now bypassed.
        ch.invite("#rust", &alice(), &carol());
        assert_eq!(
            ch.check_join(
                "#rust",
                &carol(),
                "carol!u@clean.example",
                None,
                &crate::extban::SubjectAttrs::plain(false)
            ),
            None
        );
        // Add +l 1 (alice already fills it) and a +b on carol's host: an invited carol
        // still bypasses BOTH the limit and the ban.
        apply(
            &ch,
            "#rust",
            &alice(),
            &[
                ModeChange::Limit {
                    add: true,
                    value: Some(1),
                },
                ModeChange::ListMask {
                    add: true,
                    mode: ChanMode::Ban,
                    mask: "*!*@blocked.example".into(),
                },
            ],
        );
        assert_eq!(
            ch.check_join(
                "#rust",
                &carol(),
                "carol!u@blocked.example",
                None,
                &crate::extban::SubjectAttrs::plain(false)
            ),
            None
        );
        // ...but +k is never bypassed.
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::Key {
                add: true,
                key: "secret".into(),
            }],
        );
        assert_eq!(
            ch.check_join(
                "#rust",
                &carol(),
                "carol!u@blocked.example",
                None,
                &crate::extban::SubjectAttrs::plain(false)
            ),
            Some(JoinReject::BadKey)
        );
        // With the key supplied, the rest is still bypassed.
        assert_eq!(
            ch.check_join(
                "#rust",
                &carol(),
                "carol!u@blocked.example",
                Some("secret"),
                &crate::extban::SubjectAttrs::plain(false)
            ),
            None
        );
        // The token is consumed on a successful join: after carol joins (with the key) and
        // parts, a fresh join attempt is blocked by +i again (no lingering token). Lift +l
        // and use a clean host so +i is unambiguously the gate that re-blocks her.
        ch.join("#rust", &carol(), 0).unwrap();
        ch.part("#rust", &carol());
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::Limit {
                add: true,
                value: Some(10),
            }],
        );
        assert_eq!(
            ch.check_join(
                "#rust",
                &carol(),
                "carol!u@clean.example",
                Some("secret"),
                &crate::extban::SubjectAttrs::plain(false)
            ),
            Some(JoinReject::InviteOnly)
        );
    }

    // --- topic ---

    #[test]
    fn topic_query_on_fresh_channel_is_no_topic() {
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap();
        match ch.topic("#rust") {
            TopicQuery::NoTopic { display } => assert_eq!(display, "#rust"),
            other => panic!("expected NoTopic, got {other:?}"),
        }
        assert_eq!(ch.topic("#ghost"), TopicQuery::NoSuchChannel);
    }

    #[test]
    fn set_topic_then_query_round_trips() {
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap();
        let set = ch.set_topic("#rust", &alice(), "alice!u@h", "be excellent", 7_000);
        match set {
            SetTopic::Set {
                display,
                members,
                topic,
            } => {
                assert_eq!(display, "#rust");
                assert_eq!(members, vec![alice()]);
                let t = topic.unwrap();
                assert_eq!(t.text, "be excellent");
                assert_eq!(t.setter, "alice!u@h");
                assert_eq!(t.set_at, 7_000);
            }
            other => panic!("expected Set, got {other:?}"),
        }
        match ch.topic("#rust") {
            TopicQuery::Topic { display, topic } => {
                assert_eq!(display, "#rust");
                assert_eq!(
                    topic,
                    Topic {
                        text: "be excellent".to_string(),
                        setter: "alice!u@h".to_string(),
                        set_at: 7_000,
                    }
                );
            }
            other => panic!("expected Topic, got {other:?}"),
        }
    }

    #[test]
    fn empty_topic_clears_and_query_returns_no_topic() {
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap();
        ch.set_topic("#rust", &alice(), "alice!u@h", "something", 1);
        assert!(matches!(ch.topic("#rust"), TopicQuery::Topic { .. }));
        match ch.set_topic("#rust", &alice(), "alice!u@h", "", 2) {
            SetTopic::Set { topic, .. } => assert_eq!(topic, None),
            other => panic!("expected Set with None topic, got {other:?}"),
        }
        assert!(matches!(ch.topic("#rust"), TopicQuery::NoTopic { .. }));
    }

    #[test]
    fn set_topic_requires_membership_and_existence() {
        let ch = Channels::new();
        assert_eq!(
            ch.set_topic("#ghost", &alice(), "alice!u@h", "x", 0),
            SetTopic::NoSuchChannel
        );
        ch.join("#rust", &bob(), 0).unwrap();
        assert_eq!(
            ch.set_topic("#rust", &alice(), "alice!u@h", "x", 0),
            SetTopic::NotOnChannel
        );
        assert!(matches!(ch.topic("#rust"), TopicQuery::NoTopic { .. }));
    }

    /// +t gates the topic: a non-op member is refused; the op may set it.
    #[test]
    fn set_topic_on_plus_t_channel_requires_chanop() {
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap(); // alice op
        ch.join("#rust", &bob(), 0).unwrap(); // bob plain
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::Flag {
                add: true,
                mode: ChanMode::TopicOpsOnly,
            }],
        );
        assert_eq!(
            ch.set_topic("#rust", &bob(), "bob!u@h", "nope", 1),
            SetTopic::NeedOps
        );
        // The op can still set it.
        assert!(matches!(
            ch.set_topic("#rust", &alice(), "alice!u@h", "yep", 2),
            SetTopic::Set { .. }
        ));
        // Inverse: clearing +t lets bob set it again.
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::Flag {
                add: false,
                mode: ChanMode::TopicOpsOnly,
            }],
        );
        assert!(matches!(
            ch.set_topic("#rust", &bob(), "bob!u@h", "ok now", 3),
            SetTopic::Set { .. }
        ));
    }

    #[test]
    fn member_channels_lists_every_channel_a_uid_is_in() {
        let ch = Channels::new();
        ch.join("#a", &alice(), 0).unwrap();
        ch.join("#b", &alice(), 0).unwrap();
        ch.join("#b", &bob(), 0).unwrap();
        let mut chans = ch.member_channels(&alice());
        chans.sort();
        assert_eq!(chans, vec!["#a", "#b"]);
        assert_eq!(ch.member_channels(&bob()), vec!["#b"]);
        assert!(ch.member_channels(&carol()).is_empty());
    }

    #[test]
    fn shares_channel_true_only_with_a_common_channel() {
        let ch = Channels::new();
        ch.join("#a", &alice(), 0).unwrap();
        ch.join("#a", &bob(), 0).unwrap();
        ch.join("#b", &carol(), 0).unwrap();
        // alice & bob share #a.
        assert!(ch.shares_channel(&alice(), &bob()));
        assert!(ch.shares_channel(&bob(), &alice())); // symmetric
        // carol is in a different channel; alice & carol share nothing.
        assert!(!ch.shares_channel(&alice(), &carol()));
        // A user in no channel shares nothing.
        let dave = uid("0ABCAAAAD");
        assert!(!ch.shares_channel(&alice(), &dave));
        // Inverse: once they part the common channel, they no longer share it.
        ch.part("#a", &bob());
        assert!(!ch.shares_channel(&alice(), &bob()));
    }

    #[test]
    fn co_members_unions_and_excludes_self() {
        let ch = Channels::new();
        ch.join("#a", &alice(), 0).unwrap();
        ch.join("#a", &bob(), 0).unwrap();
        ch.join("#b", &alice(), 0).unwrap();
        ch.join("#b", &carol(), 0).unwrap();
        ch.join("#b", &bob(), 0).unwrap();
        let co = ch.co_members(&alice());
        assert_eq!(co.into_iter().collect::<Vec<_>>(), vec![bob(), carol()]);
        let dave = uid("0ABCAAAAD");
        ch.join("#solo", &dave, 0).unwrap();
        assert!(ch.co_members(&dave).is_empty());
    }

    #[test]
    fn remove_everywhere_clears_membership_and_drops_empties() {
        let ch = Channels::new();
        ch.join("#a", &alice(), 0).unwrap();
        ch.join("#b", &alice(), 0).unwrap();
        ch.join("#b", &bob(), 0).unwrap();
        let mut left = ch.remove_everywhere(&alice());
        left.sort();
        assert_eq!(left, vec!["#a", "#b"]);
        assert!(ch.members("#a").is_none());
        assert_eq!(ch.members("#b").unwrap(), vec![bob()]);
        assert_eq!(ch.len(), 1);
        assert!(ch.remove_everywhere(&carol()).is_empty());
        assert_eq!(ch.len(), 1);
    }

    #[test]
    fn deop_clears_only_the_op_bit_and_reports_membership() {
        let ch = Channels::new();
        ch.join("#a", &alice(), 0).unwrap(); // alice creates → op
        ch.join("#a", &bob(), 0).unwrap(); // bob plain member
        assert!(ch.is_op("#a", &alice()));
        // Deopping the creator clears `@` and reports she was a member.
        assert!(ch.deop("#a", &alice()));
        assert!(!ch.is_op("#a", &alice()));
        assert!(ch.is_member("#a", &alice()), "deop keeps membership");
        // Idempotent: deopping an already-plain member is a no-op that still returns true.
        assert!(ch.deop("#a", &bob()));
        assert!(!ch.is_op("#a", &bob()));
        // Inverse: a non-member and a nonexistent channel both report false, change nothing.
        assert!(!ch.deop("#a", &carol()));
        assert!(!ch.deop("#nope", &alice()));
    }

    /// A NICK-change keeps the UID, so membership, status, and metadata are untouched.
    #[test]
    fn membership_and_metadata_survive_a_nick_change_without_rekeying() {
        let ch = Channels::new();
        ch.join("#a", &alice(), 42).unwrap();
        ch.join("#b", &alice(), 0).unwrap();
        ch.set_topic("#a", &alice(), "alice!u@h", "hi", 100);
        assert!(ch.is_member("#a", &alice()));
        assert!(ch.is_member("#b", &alice()));
        assert!(ch.is_op("#a", &alice()), "creator op survives a rename");
        assert_eq!(ch.members("#a").unwrap(), vec![alice()]);
        assert_eq!(ch.created_at("#a"), Some(42));
        match ch.topic("#a") {
            TopicQuery::Topic { topic, .. } => assert_eq!(topic.text, "hi"),
            other => panic!("expected Topic, got {other:?}"),
        }
    }

    // --- NAMES / LIST queries ---

    fn set_flag(ch: &Channels, name: &str, actor: &Uid, mode: ChanMode) {
        apply(ch, name, actor, &[ModeChange::Flag { add: true, mode }]);
    }

    #[test]
    fn names_query_public_channel_shows_roster_with_equals_sigil() {
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap();
        ch.join("#rust", &bob(), 0).unwrap();
        match ch.names_query("#RUST", &carol()) {
            NamesReply::Visible(nc) => {
                assert_eq!(nc.display, "#rust");
                assert_eq!(nc.visibility, '=');
                // Roster in UID order, creator op first.
                assert_eq!(nc.members, vec![(alice(), op()), (bob(), plain())]);
            }
            other => panic!("expected Visible, got {other:?}"),
        }
    }

    #[test]
    fn names_query_missing_channel_is_hidden() {
        let ch = Channels::new();
        assert_eq!(ch.names_query("#ghost", &alice()), NamesReply::Hidden);
    }

    #[test]
    fn names_query_private_channel_shows_star_to_anyone() {
        let ch = Channels::new();
        ch.join("#p", &alice(), 0).unwrap();
        set_flag(&ch, "#p", &alice(), ChanMode::Private);
        // A non-member still sees a +p channel (with the `*` sigil), matching the oracle.
        match ch.names_query("#p", &bob()) {
            NamesReply::Visible(nc) => assert_eq!(nc.visibility, '*'),
            other => panic!("expected Visible, got {other:?}"),
        }
    }

    #[test]
    fn names_query_secret_channel_hidden_from_nonmember_shown_to_member() {
        let ch = Channels::new();
        ch.join("#s", &alice(), 0).unwrap();
        set_flag(&ch, "#s", &alice(), ChanMode::Secret);
        // Non-member: hidden entirely.
        assert_eq!(ch.names_query("#s", &bob()), NamesReply::Hidden);
        // Member: visible with the `@` sigil.
        match ch.names_query("#s", &alice()) {
            NamesReply::Visible(nc) => assert_eq!(nc.visibility, '@'),
            other => panic!("expected Visible, got {other:?}"),
        }
    }

    #[test]
    fn names_all_excludes_secret_nonmember_includes_own_secret() {
        let ch = Channels::new();
        ch.join("#pub", &alice(), 0).unwrap();
        ch.join("#sec", &bob(), 0).unwrap();
        set_flag(&ch, "#sec", &bob(), ChanMode::Secret);
        // alice (not on #sec) sees only #pub.
        let names: Vec<String> = ch
            .names_all(&alice())
            .into_iter()
            .map(|n| n.display)
            .collect();
        assert_eq!(names, vec!["#pub"]);
        // bob (on #sec) sees both; folded-name ordering → #pub, #sec.
        let names: Vec<String> = ch
            .names_all(&bob())
            .into_iter()
            .map(|n| n.display)
            .collect();
        assert_eq!(names, vec!["#pub", "#sec"]);
    }

    #[test]
    fn list_one_hides_secret_and_private_from_nonmembers() {
        let ch = Channels::new();
        ch.join("#s", &alice(), 0).unwrap();
        ch.join("#p", &alice(), 0).unwrap();
        set_flag(&ch, "#s", &alice(), ChanMode::Secret);
        set_flag(&ch, "#p", &alice(), ChanMode::Private);
        // Non-member: both hidden from LIST.
        assert_eq!(ch.list_one("#s", &bob()), None);
        assert_eq!(ch.list_one("#p", &bob()), None);
        // Member: both visible.
        assert!(ch.list_one("#s", &alice()).is_some());
        assert!(ch.list_one("#p", &alice()).is_some());
        // Missing channel: None.
        assert_eq!(ch.list_one("#ghost", &alice()), None);
    }

    #[test]
    fn list_one_reports_count_and_topic() {
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap();
        ch.join("#rust", &bob(), 0).unwrap();
        ch.set_topic("#rust", &alice(), "alice!u@h", "be excellent", 0);
        let l = ch.list_one("#rust", &carol()).unwrap();
        assert_eq!(l.name, "#rust");
        assert_eq!(l.members, 2);
        assert_eq!(l.topic.as_deref(), Some("be excellent"));
    }

    #[test]
    fn list_visible_filters_and_orders_by_folded_name() {
        let ch = Channels::new();
        ch.join("#b", &alice(), 0).unwrap();
        ch.join("#a", &alice(), 0).unwrap();
        ch.join("#sec", &bob(), 0).unwrap();
        set_flag(&ch, "#sec", &bob(), ChanMode::Secret);
        // alice (not on #sec): sees #a, #b in folded order; #sec hidden.
        let names: Vec<String> = ch
            .list_visible(&alice())
            .into_iter()
            .map(|l| l.name)
            .collect();
        assert_eq!(names, vec!["#a", "#b"]);
        // bob: sees his #sec too.
        let names: Vec<String> = ch
            .list_visible(&bob())
            .into_iter()
            .map(|l| l.name)
            .collect();
        assert_eq!(names, vec!["#a", "#b", "#sec"]);
    }

    /// Inverse: a channel that empties out (last part) never appears in any query.
    #[test]
    fn emptied_channel_vanishes_from_names_and_list() {
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap();
        assert_eq!(ch.list_visible(&bob()).len(), 1);
        assert_eq!(ch.names_all(&bob()).len(), 1);
        ch.part("#rust", &alice());
        assert!(ch.list_visible(&bob()).is_empty());
        assert!(ch.names_all(&bob()).is_empty());
        assert_eq!(ch.names_query("#rust", &bob()), NamesReply::Hidden);
        assert_eq!(ch.list_one("#rust", &bob()), None);
    }

    // --- whois_channels (the 319 projection) ---

    #[test]
    fn whois_channels_shows_status_prefix_and_folded_order() {
        let ch = Channels::new();
        // alice is op of #rust (creator); voiced on #go.
        ch.join("#rust", &alice(), 0).unwrap();
        ch.join("#go", &bob(), 0).unwrap(); // bob creates #go (op)
        ch.join("#go", &alice(), 0).unwrap(); // alice joins plain
        apply(
            &ch,
            "#go",
            &bob(),
            &[ModeChange::Member {
                add: true,
                mode: ChanMode::Voice,
                uid: alice(),
            }],
        );
        // A requester who shares both channels sees both, folded-sorted, with prefixes.
        assert_eq!(
            ch.whois_channels(&alice(), &bob(), false),
            vec!["+#go".to_string(), "@#rust".to_string()]
        );
        // A plain member shows no prefix.
        ch.join("#rust", &carol(), 0).unwrap();
        assert_eq!(
            ch.whois_channels(&carol(), &alice(), false),
            vec!["#rust".to_string()]
        );
    }

    #[test]
    fn whois_channels_hides_secret_and_private_from_non_members() {
        let ch = Channels::new();
        ch.join("#sec", &alice(), 0).unwrap();
        apply(
            &ch,
            "#sec",
            &alice(),
            &[ModeChange::Flag {
                add: true,
                mode: ChanMode::Secret,
            }],
        );
        ch.join("#priv", &alice(), 0).unwrap();
        apply(
            &ch,
            "#priv",
            &alice(),
            &[ModeChange::Flag {
                add: true,
                mode: ChanMode::Private,
            }],
        );
        ch.join("#pub", &alice(), 0).unwrap();
        // bob shares none of them → sees only the public one.
        assert_eq!(
            ch.whois_channels(&alice(), &bob(), false),
            vec!["@#pub".to_string()]
        );
        // A fellow member sees the hidden channels too.
        ch.join("#sec", &bob(), 0).unwrap();
        ch.join("#priv", &bob(), 0).unwrap();
        assert_eq!(
            ch.whois_channels(&alice(), &bob(), false),
            vec![
                "@#priv".to_string(),
                "@#pub".to_string(),
                "@#sec".to_string()
            ]
        );
    }

    /// IRCv3 `multi-prefix`: an op+voice member shows `@+` with the cap and only `@`
    /// (the single highest, the inverse) without it — pinned in one test.
    #[test]
    fn member_status_prefixes_multi_vs_single() {
        let op_voice = MemberStatus::default()
            .with(ChanMode::Op, true)
            .with(ChanMode::Voice, true);
        assert_eq!(op_voice.prefixes(true), "@+");
        assert_eq!(op_voice.prefixes(false), "@", "single = highest only");
        let op = MemberStatus::default()
            .with(ChanMode::Op, true)
            .with(ChanMode::Voice, false);
        assert_eq!(op.prefixes(true), "@");
        assert_eq!(op.prefixes(false), "@");
        let voice = MemberStatus::default()
            .with(ChanMode::Op, false)
            .with(ChanMode::Voice, true);
        assert_eq!(voice.prefixes(true), "+");
        assert_eq!(voice.prefixes(false), "+");
        let plain = MemberStatus::default();
        assert_eq!(plain.prefixes(true), "");
        assert_eq!(plain.prefixes(false), "");
    }

    /// `whois_channels` honours `multi-prefix`: an op+voice target's channel is `@+#chan`
    /// with the cap, `@#chan` without it.
    #[test]
    fn whois_channels_multi_prefix() {
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap(); // alice is op (creator)
        apply(
            &ch,
            "#rust",
            &alice(),
            &[ModeChange::Member {
                add: true,
                mode: ChanMode::Voice,
                uid: alice(), // voice alice too -> op+voice
            }],
        );
        ch.join("#rust", &bob(), 0).unwrap();
        assert_eq!(
            ch.whois_channels(&alice(), &bob(), true),
            vec!["@+#rust".to_string()],
            "multi-prefix shows both sigils"
        );
        assert_eq!(
            ch.whois_channels(&alice(), &bob(), false),
            vec!["@#rust".to_string()],
            "without the cap, only the highest"
        );
    }

    /// Inverse: once the target parts, the channel drops out of its WHOIS list.
    #[test]
    fn whois_channels_drops_parted_channel() {
        let ch = Channels::new();
        ch.join("#rust", &alice(), 0).unwrap();
        ch.join("#rust", &bob(), 0).unwrap();
        assert_eq!(
            ch.whois_channels(&alice(), &bob(), false),
            vec!["@#rust".to_string()]
        );
        ch.part("#rust", &alice());
        assert!(
            ch.whois_channels(&alice(), &bob(), false).is_empty(),
            "parted channel gone"
        );
        // A channelless / unknown target has no channels.
        assert!(ch.whois_channels(&carol(), &bob(), false).is_empty());
    }

    // --- burst_snapshot (the S2S outbound-burst projection) ---

    #[test]
    fn burst_snapshot_lists_every_channel_in_folded_order_with_full_rosters() {
        let ch = Channels::new();
        // Created out of order; the snapshot must come back folded-name sorted (#a, #b).
        ch.join("#b", &alice(), 0).unwrap(); // alice op
        ch.join("#b", &bob(), 0).unwrap(); // bob plain
        ch.join("#a", &carol(), 0).unwrap();

        let snap = ch.burst_snapshot();
        let names: Vec<&str> = snap.iter().map(|c| c.name.as_str()).collect();
        assert_eq!(names, ["#a", "#b"], "ascending folded-name order");

        // #b carries its full ascending-UID roster with status.
        let b = &snap[1];
        assert_eq!(b.members, vec![(alice(), op()), (bob(), plain())]);
        assert_eq!(b.modes, "+", "a fresh channel has no flags");
        assert!(b.mode_params.is_empty());
    }

    #[test]
    fn burst_snapshot_is_visibility_agnostic_and_renders_modes_with_params() {
        let ch = Channels::new();
        ch.join("#secret", &alice(), 0).unwrap(); // alice is op
                                                  // +s (secret) + a user limit of 25.
        apply(
            &ch,
            "#secret",
            &alice(),
            &[
                ModeChange::Flag {
                    add: true,
                    mode: ChanMode::Secret,
                },
                ModeChange::Limit {
                    add: true,
                    value: Some(25),
                },
            ],
        );
        let snap = ch.burst_snapshot();
        // A +s channel is STILL in the burst (a server learns everything) — unlike names_all,
        // which would hide it from a non-member requester.
        assert_eq!(snap.len(), 1);
        let c = &snap[0];
        assert_eq!(c.name, "#secret");
        // The mode string carries the flag and the limit letter; the value is in the params.
        assert_eq!(c.modes, "+sl");
        assert_eq!(c.mode_params, "25");
    }

    #[test]
    fn burst_snapshot_of_an_empty_table_is_empty() {
        let ch = Channels::new();
        assert!(ch.burst_snapshot().is_empty());
    }

    // --- reconcile_channel_ts: channel-merge timestamp arbitration (P11 slice 132) ---

    fn sid(s: &str) -> Sid {
        Sid::try_from(s).unwrap()
    }

    /// Build a `#x` with an op on **our** side (`0ABCAAAAA`) and an op on **their** side
    /// (`0XYZAAAAA`), stamped at `created_at == ts`. Both are ops (the merge precondition).
    fn merged_channel(ts: u64) -> Channels {
        let ch = Channels::new();
        ch.njoin_member("#x", &uid("0ABCAAAAA"), op(), ts);
        ch.njoin_member("#x", &uid("0XYZAAAAA"), op(), ts);
        assert_eq!(ch.created_at("#x"), Some(ts));
        ch
    }

    /// Their TS is older → **they** win: our side's op is dropped, their op survives, and the
    /// channel adopts the older timestamp.
    #[test]
    fn reconcile_older_remote_ts_deops_our_side() {
        let ch = merged_channel(1000);
        let out = ch.reconcile_channel_ts("#x", 500, &sid("0XYZ"));
        assert_eq!(
            out,
            ChanTsOutcome::Reconciled {
                demoted: vec![(uid("0ABCAAAAA"), op())]
            }
        );
        assert!(
            !ch.is_op("#x", &uid("0ABCAAAAA")),
            "our op was dropped (we lost)"
        );
        assert!(
            ch.is_op("#x", &uid("0XYZAAAAA")),
            "their op survives (they won)"
        );
        assert_eq!(ch.created_at("#x"), Some(500), "adopted the older TS");
    }

    /// Their TS is newer → **we** win: their side's op is dropped, our op survives, and we
    /// keep our (older) timestamp.
    #[test]
    fn reconcile_newer_remote_ts_deops_their_side() {
        let ch = merged_channel(1000);
        let out = ch.reconcile_channel_ts("#x", 2000, &sid("0XYZ"));
        assert_eq!(
            out,
            ChanTsOutcome::Reconciled {
                demoted: vec![(uid("0XYZAAAAA"), op())]
            }
        );
        assert!(
            ch.is_op("#x", &uid("0ABCAAAAA")),
            "our op survives (we won)"
        );
        assert!(
            !ch.is_op("#x", &uid("0XYZAAAAA")),
            "their op dropped (they lost)"
        );
        assert_eq!(ch.created_at("#x"), Some(1000), "kept our older TS");
    }

    /// Equal TS → genuine same-age merge: nobody is deopped (the inverse of a conflict).
    #[test]
    fn reconcile_equal_ts_keeps_both_ops() {
        let ch = merged_channel(1000);
        let out = ch.reconcile_channel_ts("#x", 1000, &sid("0XYZ"));
        assert_eq!(out, ChanTsOutcome::Merged);
        assert!(ch.is_op("#x", &uid("0ABCAAAAA")));
        assert!(ch.is_op("#x", &uid("0XYZAAAAA")));
        assert_eq!(ch.created_at("#x"), Some(1000));
    }

    /// An unknown channel reconciles to nothing (no panic, no creation).
    #[test]
    fn reconcile_unknown_channel_is_unknown() {
        let ch = Channels::new();
        assert_eq!(
            ch.reconcile_channel_ts("#ghost", 500, &sid("0XYZ")),
            ChanTsOutcome::Unknown
        );
        assert!(ch.created_at("#ghost").is_none());
    }

    /// A plain (non-op) member is never in the deopped set — only chanops lose, on the losing
    /// side. Here their side wins but a *plain* member of ours is untouched.
    #[test]
    fn reconcile_only_deops_chanops_on_the_losing_side() {
        let ch = Channels::new();
        ch.njoin_member("#x", &uid("0ABCAAAAA"), op(), 1000); // our op (loses)
        ch.njoin_member("#x", &uid("0ABCAAAAB"), plain(), 1000); // our plain member (untouched)
        ch.njoin_member("#x", &uid("0XYZAAAAA"), op(), 1000); // their op (wins)
        let out = ch.reconcile_channel_ts("#x", 500, &sid("0XYZ"));
        // Only the chanop on the losing side, never the plain member.
        assert_eq!(
            out,
            ChanTsOutcome::Reconciled {
                demoted: vec![(uid("0ABCAAAAA"), op())]
            }
        );
        assert!(
            ch.members("#x").unwrap().contains(&uid("0ABCAAAAB")),
            "plain member stays"
        );
    }

    use proptest::prelude::*;

    proptest! {
        #![proptest_config(ProptestConfig::with_cases(256))]

        /// Fuzz the arbitration over arbitrary timestamps: after reconcile the channel always
        /// holds `min(ours, theirs)`, the surviving chanops are **exactly** the winning side's
        /// (their side iff they are at least as old — actually strictly older — else ours),
        /// and the deopped set is exactly the complement among the prior ops. Never panics.
        #[test]
        fn reconcile_converges_on_the_older_side(our_ts in 0u64..1_000_000, their_ts in 0u64..1_000_000) {
            let ch = merged_channel(our_ts);
            let our = uid("0ABCAAAAA");
            let theirs = uid("0XYZAAAAA");
            let out = ch.reconcile_channel_ts("#x", their_ts, &sid("0XYZ"));
            // Channel adopts the lower timestamp regardless of outcome.
            prop_assert_eq!(ch.created_at("#x"), Some(our_ts.min(their_ts)));
            match out {
                ChanTsOutcome::Merged => {
                    prop_assert_eq!(our_ts, their_ts);
                    // Same-age: both keep op.
                    prop_assert!(ch.is_op("#x", &our) && ch.is_op("#x", &theirs));
                }
                ChanTsOutcome::Reconciled { demoted } => {
                    prop_assert_ne!(our_ts, their_ts);
                    if their_ts < our_ts {
                        // They win: our op dropped (with its op bit recorded), theirs survives.
                        prop_assert_eq!(demoted, vec![(our.clone(), op())]);
                        prop_assert!(!ch.is_op("#x", &our) && ch.is_op("#x", &theirs));
                    } else {
                        // We win: their op dropped, ours survives.
                        prop_assert_eq!(demoted, vec![(theirs.clone(), op())]);
                        prop_assert!(ch.is_op("#x", &our) && !ch.is_op("#x", &theirs));
                    }
                }
                ChanTsOutcome::Unknown => prop_assert!(false, "the channel exists"),
            }
        }
    }

    // ----- SAMODE override (slice 151): apply_modes_oper -----

    /// A small representative spread of mode changes the SAMODE proptest samples a
    /// subsequence of. Covers every `ModeChange` arm, no-op churn (set-then-clear), a
    /// non-member `+o`/`+v` (→ `not_members`), and a `+k` over a key (→ `key_set`).
    fn samode_candidate(i: usize) -> ModeChange {
        let ban = |m: &str| ModeChange::ListMask {
            add: true,
            mode: ChanMode::Ban,
            mask: m.to_string(),
        };
        let unban = |m: &str| ModeChange::ListMask {
            add: false,
            mode: ChanMode::Ban,
            mask: m.to_string(),
        };
        match i {
            0 => ModeChange::Flag {
                add: true,
                mode: ChanMode::Moderated,
            },
            1 => ModeChange::Flag {
                add: false,
                mode: ChanMode::Moderated,
            },
            2 => ModeChange::Flag {
                add: true,
                mode: ChanMode::InviteOnly,
            },
            3 => ModeChange::Member {
                add: false,
                mode: ChanMode::Op,
                uid: alice(),
            },
            4 => ModeChange::Member {
                add: true,
                mode: ChanMode::Op,
                uid: bob(),
            },
            5 => ModeChange::Member {
                add: true,
                mode: ChanMode::Voice,
                uid: bob(),
            },
            6 => ModeChange::Member {
                add: true,
                mode: ChanMode::Op,
                uid: carol(),
            }, // non-member → 441
            7 => ModeChange::Key {
                add: true,
                key: "first".to_string(),
            },
            8 => ModeChange::Key {
                add: true,
                key: "second".to_string(),
            }, // over a key → 467
            9 => ModeChange::Key {
                add: false,
                key: String::new(),
            },
            10 => ModeChange::Limit {
                add: true,
                value: Some(7),
            },
            11 => ModeChange::Limit {
                add: false,
                value: None,
            },
            12 => ban("*!*@a.example"),
            13 => ban("*!*@a.example"), // duplicate → no-op
            14 => unban("*!*@a.example"),
            _ => ban("*!*@b.example"),
        }
    }
    const SAMODE_CANDIDATES: usize = 16;

    /// A channel `#x` with alice (chanop) + bob (plain member); carol is **not** a member.
    fn samode_roster_channel() -> Channels {
        let ch = Channels::new();
        ch.njoin_member("#x", &alice(), op(), 1_000);
        ch.njoin_member("#x", &bob(), plain(), 1_000);
        ch
    }

    proptest! {
        #![proptest_config(ProptestConfig::with_cases(256))]

        /// The SAMODE override (`apply_modes_oper`, no op-gate) differs from a chanop's
        /// `MODE` (`apply_modes` by an op actor) **only** in the gate: over an arbitrary
        /// change sequence the two produce the identical `ApplyOutcome` (same effective
        /// set, same `not_members`/`key_set`) and leave the channel in the identical state.
        /// And the override never returns `NeedOps`.
        #[test]
        fn samode_override_equals_a_real_ops_mode(
            picks in prop::collection::vec(0usize..SAMODE_CANDIDATES, 0..14)
        ) {
            let changes: Vec<ModeChange> = picks.iter().map(|&i| samode_candidate(i)).collect();
            let a = samode_roster_channel();
            let b = samode_roster_channel();
            let via_op = a.apply_modes("#x", &alice(), &changes); // alice is op → gate passes
            let via_oper = b.apply_modes_oper("#x", &changes);    // no gate
            prop_assert!(!matches!(via_oper, ApplyOutcome::NeedOps), "override never gates");
            prop_assert_eq!(&via_op, &via_oper, "override == an op's MODE outcome");
            // Resulting state is identical along every axis the changes can touch.
            prop_assert_eq!(
                a.channel_mode_string("#x", true),
                b.channel_mode_string("#x", true)
            );
            prop_assert_eq!(a.is_op("#x", &alice()), b.is_op("#x", &alice()));
            prop_assert_eq!(a.is_op("#x", &bob()), b.is_op("#x", &bob()));
            prop_assert_eq!(
                a.list_masks("#x", ChanMode::Ban),
                b.list_masks("#x", ChanMode::Ban)
            );
        }

        /// TS6 merge-loss list reset (P11 slice 174): a losing channel carrying an arbitrary set
        /// of list masks `L` is wiped, then the winner's arbitrary masks `W` are adopted (the
        /// post-`CHANTS` burst MODE lines). The merged channel must end with **exactly** `W` on
        /// every list — no element of `L\W` survives — proving the wipe leaves no loser residue.
        /// Never panics; the wipe is total even when `L` and `W` overlap.
        #[test]
        fn loss_wipe_then_adopt_leaves_no_loser_residue(
            loser in prop::collection::vec(("[be]", "[a-c]!\\*@\\*"), 0..6),
            winner in prop::collection::vec(("[be]", "[b-d]!\\*@\\*"), 0..6),
        ) {
            let letter = |c: &str| match c { "b" => ChanMode::Ban, _ => ChanMode::Exception };
            let ch = Channels::new();
            ch.join("#x", &alice(), 0).unwrap();
            // The loser's pre-merge list state.
            for (c, mask) in &loser {
                ch.apply_modes_as_server("#x", &[ModeChange::ListMask {
                    add: true, mode: letter(c), mask: mask.clone(),
                }]);
            }
            // Lose the merge → wipe → adopt the winner's masks.
            ch.wipe_list_masks("#x");
            for (c, mask) in &winner {
                ch.apply_modes_as_server("#x", &[ModeChange::ListMask {
                    add: true, mode: letter(c), mask: mask.clone(),
                }]);
            }
            // Each list now equals the winner's distinct masks for that list, in insertion order.
            for mode in [ChanMode::Ban, ChanMode::Exception] {
                let mut want: Vec<String> = Vec::new();
                for (c, mask) in &winner {
                    if letter(c) == mode && !want.contains(mask) { want.push(mask.clone()); }
                }
                prop_assert_eq!(ch.list_masks("#x", mode), Some(want), "{:?} == winner's", mode);
            }
        }
    }

    /// `apply_modes_oper` on an unknown channel is `NoSuchChannel` (parity with
    /// `apply_modes`), never a panic or a silent success.
    #[test]
    fn apply_modes_oper_on_missing_channel_is_nosuchchannel() {
        let ch = Channels::new();
        assert_eq!(
            ch.apply_modes_oper(
                "#ghost",
                &[ModeChange::Flag {
                    add: true,
                    mode: ChanMode::Moderated
                }]
            ),
            ApplyOutcome::NoSuchChannel
        );
    }
}
