// Copyright 2024 Saorsa Labs Ltd.
//
// This Saorsa Network Software is licensed under the General Public License (GPL), version 3.
// Please see the file LICENSE-GPL, or visit <http://www.gnu.org/licenses/> for the full text.
//
// Full details available at https://saorsalabs.com/licenses

#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use std::{
    cmp,
    collections::VecDeque,
    convert::TryFrom,
    fmt, io, mem,
    net::{IpAddr, SocketAddr},
    sync::Arc,
};

use bytes::{Bytes, BytesMut};
use frame::StreamMetaVec;
// Removed qlog feature

use rand::{Rng, SeedableRng, rngs::StdRng};
use thiserror::Error;
use tracing::{debug, error, info, trace, trace_span, warn};

use crate::{
    Dir, Duration, EndpointConfig, Frame, INITIAL_MTU, Instant, MAX_CID_SIZE, MAX_STREAM_COUNT,
    MIN_INITIAL_SIZE, MtuDiscoveryConfig, Side, StreamId, TIMER_GRANULARITY, TokenStore, Transmit,
    TransportError, TransportErrorCode, VarInt,
    cid_generator::ConnectionIdGenerator,
    cid_queue::CidQueue,
    coding::BufMutExt,
    config::{ServerConfig, TransportConfig},
    crypto::{self, KeyPair, Keys, PacketKey},
    endpoint::AddressDiscoveryStats,
    frame::{self, Close, Datagram, FrameStruct, NewToken},
    nat_traversal_api::PeerId,
    packet::{
        FixedLengthConnectionIdParser, Header, InitialHeader, InitialPacket, LongType, Packet,
        PacketNumber, PartialDecode, SpaceId,
    },
    range_set::ArrayRangeSet,
    shared::{
        ConnectionEvent, ConnectionEventInner, ConnectionId, DatagramConnectionEvent, EcnCodepoint,
        EndpointEvent, EndpointEventInner,
    },
    token::{ResetToken, Token, TokenPayload},
    transport_parameters::TransportParameters,
};

mod ack_frequency;
use ack_frequency::AckFrequencyState;

pub mod port_prediction;
pub use self::port_prediction::{PortPredictor, PortPredictorConfig};

pub(crate) mod nat_traversal;
use nat_traversal::NatTraversalState;
// v0.13.0: NatTraversalRole removed - all nodes are symmetric P2P nodes
pub(crate) use nat_traversal::{CoordinationPhase, NatTraversalError};

mod assembler;
pub use assembler::Chunk;

mod cid_state;
use cid_state::CidState;

mod datagrams;
use datagrams::DatagramState;
pub use datagrams::{Datagrams, SendDatagramError};

mod mtud;
use mtud::MtuDiscovery;

mod pacing;

mod packet_builder;
use packet_builder::PacketBuilder;

mod packet_crypto;
use packet_crypto::{PrevCrypto, ZeroRttCrypto};

mod paths;
pub use paths::RttEstimator;
use paths::{PathData, PathResponses};

mod send_buffer;

mod spaces;
#[cfg(fuzzing)]
pub use spaces::Retransmits;
#[cfg(not(fuzzing))]
use spaces::Retransmits;
use spaces::{PacketNumberFilter, PacketSpace, SendableFrames, SentPacket, ThinRetransmits};

mod stats;
pub use stats::{ConnectionStats, FrameStats, PathStats, UdpStats};

mod streams;
#[cfg(fuzzing)]
pub use streams::StreamsState;
#[cfg(not(fuzzing))]
use streams::StreamsState;
pub use streams::{
    Chunks, ClosedStream, FinishError, ReadError, ReadableError, RecvStream, SendStream,
    ShouldTransmit, StreamEvent, Streams, WriteError, Written,
};

mod timer;
use crate::congestion::Controller;
use timer::{Timer, TimerTable};

/// Protocol state and logic for a single QUIC connection
///
/// Objects of this type receive [`ConnectionEvent`]s and emit [`EndpointEvent`]s and application
/// [`Event`]s to make progress. To handle timeouts, a `Connection` returns timer updates and
/// expects timeouts through various methods. A number of simple getter methods are exposed
/// to allow callers to inspect some of the connection state.
///
/// `Connection` has roughly 4 types of methods:
///
/// - A. Simple getters, taking `&self`
/// - B. Handlers for incoming events from the network or system, named `handle_*`.
/// - C. State machine mutators, for incoming commands from the application. For convenience we
///   refer to this as "performing I/O" below, however as per the design of this library none of the
///   functions actually perform system-level I/O. For example, [`read`](RecvStream::read) and
///   [`write`](SendStream::write), but also things like [`reset`](SendStream::reset).
/// - D. Polling functions for outgoing events or actions for the caller to
///   take, named `poll_*`.
///
/// The simplest way to use this API correctly is to call (B) and (C) whenever
/// appropriate, then after each of those calls, as soon as feasible call all
/// polling methods (D) and deal with their outputs appropriately, e.g. by
/// passing it to the application or by making a system-level I/O call. You
/// should call the polling functions in this order:
///
/// 1. [`poll_transmit`](Self::poll_transmit)
/// 2. [`poll_timeout`](Self::poll_timeout)
/// 3. [`poll_endpoint_events`](Self::poll_endpoint_events)
/// 4. [`poll`](Self::poll)
///
/// Currently the only actual dependency is from (2) to (1), however additional
/// dependencies may be added in future, so the above order is recommended.
///
/// (A) may be called whenever desired.
///
/// Care should be made to ensure that the input events represent monotonically
/// increasing time. Specifically, calling [`handle_timeout`](Self::handle_timeout)
/// with events of the same [`Instant`] may be interleaved in any order with a
/// call to [`handle_event`](Self::handle_event) at that same instant; however
/// events or timeouts with different instants must not be interleaved.
pub struct Connection {
    endpoint_config: Arc<EndpointConfig>,
    config: Arc<TransportConfig>,
    rng: StdRng,
    crypto: Box<dyn crypto::Session>,
    /// The CID we initially chose, for use during the handshake
    handshake_cid: ConnectionId,
    /// The CID the peer initially chose, for use during the handshake
    rem_handshake_cid: ConnectionId,
    /// The "real" local IP address which was was used to receive the initial packet.
    /// This is only populated for the server case, and if known
    local_ip: Option<IpAddr>,
    path: PathData,
    /// Whether MTU detection is supported in this environment
    allow_mtud: bool,
    prev_path: Option<(ConnectionId, PathData)>,
    state: State,
    side: ConnectionSide,
    /// Whether or not 0-RTT was enabled during the handshake. Does not imply acceptance.
    zero_rtt_enabled: bool,
    /// Set if 0-RTT is supported, then cleared when no longer needed.
    zero_rtt_crypto: Option<ZeroRttCrypto>,
    key_phase: bool,
    /// How many packets are in the current key phase. Used only for `Data` space.
    key_phase_size: u64,
    /// Transport parameters set by the peer
    peer_params: TransportParameters,
    /// Source ConnectionId of the first packet received from the peer
    orig_rem_cid: ConnectionId,
    /// Destination ConnectionId sent by the client on the first Initial
    initial_dst_cid: ConnectionId,
    /// The value that the server included in the Source Connection ID field of a Retry packet, if
    /// one was received
    retry_src_cid: Option<ConnectionId>,
    /// Total number of outgoing packets that have been deemed lost
    lost_packets: u64,
    events: VecDeque<Event>,
    endpoint_events: VecDeque<EndpointEventInner>,
    /// Whether the spin bit is in use for this connection
    spin_enabled: bool,
    /// Outgoing spin bit state
    spin: bool,
    /// Packet number spaces: initial, handshake, 1-RTT
    spaces: [PacketSpace; 3],
    /// Highest usable packet number space
    highest_space: SpaceId,
    /// 1-RTT keys used prior to a key update
    prev_crypto: Option<PrevCrypto>,
    /// 1-RTT keys to be used for the next key update
    ///
    /// These are generated in advance to prevent timing attacks and/or DoS by third-party attackers
    /// spoofing key updates.
    next_crypto: Option<KeyPair<Box<dyn PacketKey>>>,
    accepted_0rtt: bool,
    /// Whether the idle timer should be reset the next time an ack-eliciting packet is transmitted.
    permit_idle_reset: bool,
    /// Negotiated idle timeout
    idle_timeout: Option<Duration>,
    timers: TimerTable,
    /// Number of packets received which could not be authenticated
    authentication_failures: u64,
    /// Why the connection was lost, if it has been
    error: Option<ConnectionError>,
    /// Identifies Data-space packet numbers to skip. Not used in earlier spaces.
    packet_number_filter: PacketNumberFilter,

    //
    // Queued non-retransmittable 1-RTT data
    //
    /// Responses to PATH_CHALLENGE frames
    path_responses: PathResponses,
    close: bool,

    //
    // ACK frequency
    //
    ack_frequency: AckFrequencyState,

    //
    // Loss Detection
    //
    /// The number of times a PTO has been sent without receiving an ack.
    pto_count: u32,

    //
    // Congestion Control
    //
    /// Whether the most recently received packet had an ECN codepoint set
    receiving_ecn: bool,
    /// Number of packets authenticated
    total_authed_packets: u64,
    /// Whether the last `poll_transmit` call yielded no data because there was
    /// no outgoing application data.
    app_limited: bool,

    streams: StreamsState,
    /// Surplus remote CIDs for future use on new paths
    rem_cids: CidQueue,
    // Attributes of CIDs generated by local peer
    local_cid_state: CidState,
    /// State of the unreliable datagram extension
    datagrams: DatagramState,
    /// Connection level statistics
    stats: ConnectionStats,
    /// QUIC version used for the connection.
    version: u32,

    /// NAT traversal state for establishing direct P2P connections
    nat_traversal: Option<NatTraversalState>,

    /// NAT traversal frame format configuration
    nat_traversal_frame_config: frame::nat_traversal_unified::NatTraversalFrameConfig,

    /// Address discovery state for tracking observed addresses
    address_discovery_state: Option<AddressDiscoveryState>,

    /// PQC state for tracking post-quantum cryptography support
    pqc_state: PqcState,

    /// Trace context for this connection
    #[cfg(feature = "trace")]
    trace_context: crate::tracing::TraceContext,

    /// Event log for tracing
    #[cfg(feature = "trace")]
    event_log: Arc<crate::tracing::EventLog>,

    /// Qlog writer
    #[cfg(feature = "__qlog")]
    qlog_streamer: Option<Box<dyn std::io::Write + Send + Sync>>,

    /// Optional bound peer identity for NEW_TOKEN v2 issuance
    peer_id_for_tokens: Option<PeerId>,
    /// When true, NEW_TOKEN frames are delayed until channel binding
    /// sets `peer_id_for_tokens`, avoiding legacy tokens in v2 mode.
    delay_new_token_until_binding: bool,
}

impl Connection {
    pub(crate) fn new(
        endpoint_config: Arc<EndpointConfig>,
        config: Arc<TransportConfig>,
        init_cid: ConnectionId,
        loc_cid: ConnectionId,
        rem_cid: ConnectionId,
        remote: SocketAddr,
        local_ip: Option<IpAddr>,
        crypto: Box<dyn crypto::Session>,
        cid_gen: &dyn ConnectionIdGenerator,
        now: Instant,
        version: u32,
        allow_mtud: bool,
        rng_seed: [u8; 32],
        side_args: SideArgs,
    ) -> Self {
        let pref_addr_cid = side_args.pref_addr_cid();
        let path_validated = side_args.path_validated();
        let connection_side = ConnectionSide::from(side_args);
        let side = connection_side.side();
        let initial_space = PacketSpace {
            crypto: Some(crypto.initial_keys(&init_cid, side)),
            ..PacketSpace::new(now)
        };
        let state = State::Handshake(state::Handshake {
            rem_cid_set: side.is_server(),
            expected_token: Bytes::new(),
            client_hello: None,
        });
        let mut rng = StdRng::from_seed(rng_seed);
        let mut this = Self {
            endpoint_config,
            crypto,
            handshake_cid: loc_cid,
            rem_handshake_cid: rem_cid,
            local_cid_state: CidState::new(
                cid_gen.cid_len(),
                cid_gen.cid_lifetime(),
                now,
                if pref_addr_cid.is_some() { 2 } else { 1 },
            ),
            path: PathData::new(remote, allow_mtud, None, now, &config),
            allow_mtud,
            local_ip,
            prev_path: None,
            state,
            side: connection_side,
            zero_rtt_enabled: false,
            zero_rtt_crypto: None,
            key_phase: false,
            // A small initial key phase size ensures peers that don't handle key updates correctly
            // fail sooner rather than later. It's okay for both peers to do this, as the first one
            // to perform an update will reset the other's key phase size in `update_keys`, and a
            // simultaneous key update by both is just like a regular key update with a really fast
            // response. Inspired by quic-go's similar behavior of performing the first key update
            // at the 100th short-header packet.
            key_phase_size: rng.gen_range(10..1000),
            peer_params: TransportParameters::default(),
            orig_rem_cid: rem_cid,
            initial_dst_cid: init_cid,
            retry_src_cid: None,
            lost_packets: 0,
            events: VecDeque::new(),
            endpoint_events: VecDeque::new(),
            spin_enabled: config.allow_spin && rng.gen_ratio(7, 8),
            spin: false,
            spaces: [initial_space, PacketSpace::new(now), PacketSpace::new(now)],
            highest_space: SpaceId::Initial,
            prev_crypto: None,
            next_crypto: None,
            accepted_0rtt: false,
            permit_idle_reset: true,
            idle_timeout: match config.max_idle_timeout {
                None | Some(VarInt(0)) => None,
                Some(dur) => Some(Duration::from_millis(dur.0)),
            },
            timers: TimerTable::default(),
            authentication_failures: 0,
            error: None,
            #[cfg(test)]
            packet_number_filter: match config.deterministic_packet_numbers {
                false => PacketNumberFilter::new(&mut rng),
                true => PacketNumberFilter::disabled(),
            },
            #[cfg(not(test))]
            packet_number_filter: PacketNumberFilter::new(&mut rng),

            path_responses: PathResponses::default(),
            close: false,

            ack_frequency: AckFrequencyState::new(get_max_ack_delay(
                &TransportParameters::default(),
            )),

            pto_count: 0,

            app_limited: false,
            receiving_ecn: false,
            total_authed_packets: 0,

            streams: StreamsState::new(
                side,
                config.max_concurrent_uni_streams,
                config.max_concurrent_bidi_streams,
                config.send_window,
                config.receive_window,
                config.stream_receive_window,
            ),
            datagrams: DatagramState::default(),
            config,
            rem_cids: CidQueue::new(rem_cid),
            rng,
            stats: ConnectionStats::default(),
            version,
            nat_traversal: None, // Will be initialized when NAT traversal is negotiated
            nat_traversal_frame_config:
                frame::nat_traversal_unified::NatTraversalFrameConfig::default(),
            address_discovery_state: {
                // Initialize with default config for now
                // Will be updated when transport parameters are negotiated
                Some(AddressDiscoveryState::new(
                    &crate::transport_parameters::AddressDiscoveryConfig::default(),
                    now,
                ))
            },
            pqc_state: PqcState::new(),

            #[cfg(feature = "trace")]
            trace_context: crate::tracing::TraceContext::new(crate::tracing::TraceId::new()),

            #[cfg(feature = "trace")]
            event_log: crate::tracing::global_log(),

            #[cfg(feature = "__qlog")]
            qlog_streamer: None,

            peer_id_for_tokens: None,
            delay_new_token_until_binding: false,
        };

        // Trace connection creation
        #[cfg(feature = "trace")]
        {
            use crate::trace_event;
            use crate::tracing::{Event, EventData, socket_addr_to_bytes, timestamp_now};
            // Tracing imports handled by macros
            let _peer_id = {
                let mut id = [0u8; 32];
                let addr_bytes = match remote {
                    SocketAddr::V4(addr) => addr.ip().octets().to_vec(),
                    SocketAddr::V6(addr) => addr.ip().octets().to_vec(),
                };
                id[..addr_bytes.len().min(32)]
                    .copy_from_slice(&addr_bytes[..addr_bytes.len().min(32)]);
                id
            };

            let (addr_bytes, addr_type) = socket_addr_to_bytes(remote);
            trace_event!(
                &this.event_log,
                Event {
                    timestamp: timestamp_now(),
                    trace_id: this.trace_context.trace_id(),
                    sequence: 0,
                    _padding: 0,
                    node_id: [0u8; 32], // Will be set by endpoint
                    event_data: EventData::ConnInit {
                        endpoint_bytes: addr_bytes,
                        addr_type,
                        _padding: [0u8; 45],
                    },
                }
            );
        }

        if path_validated {
            this.on_path_validated();
        }
        if side.is_client() {
            // Kick off the connection
            this.write_crypto();
            this.init_0rtt();
        }
        this
    }

    /// Set up qlog for this connection
    #[cfg(feature = "__qlog")]
    pub fn set_qlog(
        &mut self,
        writer: Box<dyn std::io::Write + Send + Sync>,
        _title: Option<String>,
        _description: Option<String>,
        _now: Instant,
    ) {
        self.qlog_streamer = Some(writer);
    }

    /// Emit qlog recovery metrics
    #[cfg(feature = "__qlog")]
    fn emit_qlog_recovery_metrics(&mut self, _now: Instant) {
        // TODO: Implement actual qlog recovery metrics emission
        // For now, this is a stub to allow compilation
    }

    /// Returns the next time at which `handle_timeout` should be called
    ///
    /// The value returned may change after:
    /// - the application performed some I/O on the connection
    /// - a call was made to `handle_event`
    /// - a call to `poll_transmit` returned `Some`
    /// - a call was made to `handle_timeout`
    #[must_use]
    pub fn poll_timeout(&mut self) -> Option<Instant> {
        let mut next_timeout = self.timers.next_timeout();

        // Check NAT traversal timeouts
        if let Some(nat_state) = &self.nat_traversal {
            if let Some(nat_timeout) = nat_state.get_next_timeout(Instant::now()) {
                // Schedule NAT traversal timer
                self.timers.set(Timer::NatTraversal, nat_timeout);
                next_timeout = Some(next_timeout.map_or(nat_timeout, |t| t.min(nat_timeout)));
            }
        }

        next_timeout
    }

    /// Returns application-facing events
    ///
    /// Connections should be polled for events after:
    /// - a call was made to `handle_event`
    /// - a call was made to `handle_timeout`
    #[must_use]
    pub fn poll(&mut self) -> Option<Event> {
        if let Some(x) = self.events.pop_front() {
            return Some(x);
        }

        if let Some(event) = self.streams.poll() {
            return Some(Event::Stream(event));
        }

        if let Some(err) = self.error.take() {
            return Some(Event::ConnectionLost { reason: err });
        }

        None
    }

    /// Return endpoint-facing events
    #[must_use]
    pub fn poll_endpoint_events(&mut self) -> Option<EndpointEvent> {
        self.endpoint_events.pop_front().map(EndpointEvent)
    }

    /// Provide control over streams
    #[must_use]
    pub fn streams(&mut self) -> Streams<'_> {
        Streams {
            state: &mut self.streams,
            conn_state: &self.state,
        }
    }

    // Removed unused trace accessors to eliminate dead_code warnings

    /// Provide control over streams
    #[must_use]
    pub fn recv_stream(&mut self, id: StreamId) -> RecvStream<'_> {
        assert!(id.dir() == Dir::Bi || id.initiator() != self.side.side());
        RecvStream {
            id,
            state: &mut self.streams,
            pending: &mut self.spaces[SpaceId::Data].pending,
        }
    }

    /// Provide control over streams
    #[must_use]
    pub fn send_stream(&mut self, id: StreamId) -> SendStream<'_> {
        assert!(id.dir() == Dir::Bi || id.initiator() == self.side.side());
        SendStream {
            id,
            state: &mut self.streams,
            pending: &mut self.spaces[SpaceId::Data].pending,
            conn_state: &self.state,
        }
    }

    /// Returns packets to transmit
    ///
    /// Connections should be polled for transmit after:
    /// - the application performed some I/O on the connection
    /// - a call was made to `handle_event`
    /// - a call was made to `handle_timeout`
    ///
    /// `max_datagrams` specifies how many datagrams can be returned inside a
    /// single Transmit using GSO. This must be at least 1.
    #[must_use]
    pub fn poll_transmit(
        &mut self,
        now: Instant,
        max_datagrams: usize,
        buf: &mut Vec<u8>,
    ) -> Option<Transmit> {
        assert!(max_datagrams != 0);
        let max_datagrams = match self.config.enable_segmentation_offload {
            false => 1,
            true => max_datagrams,
        };

        let mut num_datagrams = 0;
        // Position in `buf` of the first byte of the current UDP datagram. When coalescing QUIC
        // packets, this can be earlier than the start of the current QUIC packet.
        let mut datagram_start = 0;
        let mut segment_size = usize::from(self.path.current_mtu());

        // Check for NAT traversal coordination timeouts
        if let Some(nat_traversal) = &mut self.nat_traversal {
            if nat_traversal.check_coordination_timeout(now) {
                trace!("NAT traversal coordination timed out, may retry");
            }
        }

        // First priority: NAT traversal PATH_CHALLENGE packets (includes coordination)
        if let Some(challenge) = self.send_nat_traversal_challenge(now, buf) {
            return Some(challenge);
        }

        if let Some(challenge) = self.send_path_challenge(now, buf) {
            return Some(challenge);
        }

        // If we need to send a probe, make sure we have something to send.
        for space in SpaceId::iter() {
            let request_immediate_ack =
                space == SpaceId::Data && self.peer_supports_ack_frequency();
            self.spaces[space].maybe_queue_probe(request_immediate_ack, &self.streams);
        }

        // Check whether we need to send a close message
        let close = match self.state {
            State::Drained => {
                self.app_limited = true;
                return None;
            }
            State::Draining | State::Closed(_) => {
                // self.close is only reset once the associated packet had been
                // encoded successfully
                if !self.close {
                    self.app_limited = true;
                    return None;
                }
                true
            }
            _ => false,
        };

        // Check whether we need to send an ACK_FREQUENCY frame
        if let Some(config) = &self.config.ack_frequency_config {
            self.spaces[SpaceId::Data].pending.ack_frequency = self
                .ack_frequency
                .should_send_ack_frequency(self.path.rtt.get(), config, &self.peer_params)
                && self.highest_space == SpaceId::Data
                && self.peer_supports_ack_frequency();
        }

        // Reserving capacity can provide more capacity than we asked for. However, we are not
        // allowed to write more than `segment_size`. Therefore the maximum capacity is tracked
        // separately.
        let mut buf_capacity = 0;

        let mut coalesce = true;
        let mut builder_storage: Option<PacketBuilder> = None;
        let mut sent_frames = None;
        let mut pad_datagram = false;
        let mut pad_datagram_to_mtu = false;
        let mut congestion_blocked = false;

        // Iterate over all spaces and find data to send
        let mut space_idx = 0;
        let spaces = [SpaceId::Initial, SpaceId::Handshake, SpaceId::Data];
        // This loop will potentially spend multiple iterations in the same `SpaceId`,
        // so we cannot trivially rewrite it to take advantage of `SpaceId::iter()`.
        while space_idx < spaces.len() {
            let space_id = spaces[space_idx];
            // Number of bytes available for frames if this is a 1-RTT packet. We're guaranteed to
            // be able to send an individual frame at least this large in the next 1-RTT
            // packet. This could be generalized to support every space, but it's only needed to
            // handle large fixed-size frames, which only exist in 1-RTT (application datagrams). We
            // don't account for coalesced packets potentially occupying space because frames can
            // always spill into the next datagram.
            let pn = self.packet_number_filter.peek(&self.spaces[SpaceId::Data]);
            let frame_space_1rtt =
                segment_size.saturating_sub(self.predict_1rtt_overhead(Some(pn)));

            // Is there data or a close message to send in this space?
            let can_send = self.space_can_send(space_id, frame_space_1rtt);
            if can_send.is_empty() && (!close || self.spaces[space_id].crypto.is_none()) {
                space_idx += 1;
                continue;
            }

            let mut ack_eliciting = !self.spaces[space_id].pending.is_empty(&self.streams)
                || self.spaces[space_id].ping_pending
                || self.spaces[space_id].immediate_ack_pending;
            if space_id == SpaceId::Data {
                ack_eliciting |= self.can_send_1rtt(frame_space_1rtt);
            }

            pad_datagram_to_mtu |= space_id == SpaceId::Data && self.config.pad_to_mtu;

            // Can we append more data into the current buffer?
            // It is not safe to assume that `buf.len()` is the end of the data,
            // since the last packet might not have been finished.
            let buf_end = if let Some(builder) = &builder_storage {
                buf.len().max(builder.min_size) + builder.tag_len
            } else {
                buf.len()
            };

            let tag_len = if let Some(ref crypto) = self.spaces[space_id].crypto {
                crypto.packet.local.tag_len()
            } else if space_id == SpaceId::Data {
                match self.zero_rtt_crypto.as_ref() {
                    Some(crypto) => crypto.packet.tag_len(),
                    None => {
                        // This should never happen - log and return early
                        error!(
                            "sending packets in the application data space requires known 0-RTT or 1-RTT keys"
                        );
                        return None;
                    }
                }
            } else {
                unreachable!("tried to send {:?} packet without keys", space_id)
            };
            if !coalesce || buf_capacity - buf_end < MIN_PACKET_SPACE + tag_len {
                // We need to send 1 more datagram and extend the buffer for that.

                // Is 1 more datagram allowed?
                if num_datagrams >= max_datagrams {
                    // No more datagrams allowed
                    break;
                }

                // Anti-amplification is only based on `total_sent`, which gets
                // updated at the end of this method. Therefore we pass the amount
                // of bytes for datagrams that are already created, as well as 1 byte
                // for starting another datagram. If there is any anti-amplification
                // budget left, we always allow a full MTU to be sent
                // (see https://github.com/quinn-rs/quinn/issues/1082)
                if self
                    .path
                    .anti_amplification_blocked(segment_size as u64 * (num_datagrams as u64) + 1)
                {
                    trace!("blocked by anti-amplification");
                    break;
                }

                // Congestion control and pacing checks
                // Tail loss probes must not be blocked by congestion, or a deadlock could arise
                if ack_eliciting && self.spaces[space_id].loss_probes == 0 {
                    // Assume the current packet will get padded to fill the segment
                    let untracked_bytes = if let Some(builder) = &builder_storage {
                        buf_capacity - builder.partial_encode.start
                    } else {
                        0
                    } as u64;
                    debug_assert!(untracked_bytes <= segment_size as u64);

                    let bytes_to_send = segment_size as u64 + untracked_bytes;
                    if self.path.in_flight.bytes + bytes_to_send >= self.path.congestion.window() {
                        space_idx += 1;
                        congestion_blocked = true;
                        // We continue instead of breaking here in order to avoid
                        // blocking loss probes queued for higher spaces.
                        trace!("blocked by congestion control");
                        continue;
                    }

                    // Check whether the next datagram is blocked by pacing
                    let smoothed_rtt = self.path.rtt.get();
                    if let Some(delay) = self.path.pacing.delay(
                        smoothed_rtt,
                        bytes_to_send,
                        self.path.current_mtu(),
                        self.path.congestion.window(),
                        now,
                    ) {
                        self.timers.set(Timer::Pacing, delay);
                        congestion_blocked = true;
                        // Loss probes should be subject to pacing, even though
                        // they are not congestion controlled.
                        trace!("blocked by pacing");
                        break;
                    }
                }

                // Finish current packet
                if let Some(mut builder) = builder_storage.take() {
                    if pad_datagram {
                        let min_size = self.pqc_state.min_initial_size();
                        builder.pad_to(min_size);
                    }

                    if num_datagrams > 1 || pad_datagram_to_mtu {
                        // If too many padding bytes would be required to continue the GSO batch
                        // after this packet, end the GSO batch here. Ensures that fixed-size frames
                        // with heterogeneous sizes (e.g. application datagrams) won't inadvertently
                        // waste large amounts of bandwidth. The exact threshold is a bit arbitrary
                        // and might benefit from further tuning, though there's no universally
                        // optimal value.
                        //
                        // Additionally, if this datagram is a loss probe and `segment_size` is
                        // larger than `INITIAL_MTU`, then padding it to `segment_size` to continue
                        // the GSO batch would risk failure to recover from a reduction in path
                        // MTU. Loss probes are the only packets for which we might grow
                        // `buf_capacity` by less than `segment_size`.
                        const MAX_PADDING: usize = 16;
                        let packet_len_unpadded = cmp::max(builder.min_size, buf.len())
                            - datagram_start
                            + builder.tag_len;
                        if (packet_len_unpadded + MAX_PADDING < segment_size
                            && !pad_datagram_to_mtu)
                            || datagram_start + segment_size > buf_capacity
                        {
                            trace!(
                                "GSO truncated by demand for {} padding bytes or loss probe",
                                segment_size - packet_len_unpadded
                            );
                            builder_storage = Some(builder);
                            break;
                        }

                        // Pad the current datagram to GSO segment size so it can be included in the
                        // GSO batch.
                        builder.pad_to(segment_size as u16);
                    }

                    builder.finish_and_track(now, self, sent_frames.take(), buf);

                    if num_datagrams == 1 {
                        // Set the segment size for this GSO batch to the size of the first UDP
                        // datagram in the batch. Larger data that cannot be fragmented
                        // (e.g. application datagrams) will be included in a future batch. When
                        // sending large enough volumes of data for GSO to be useful, we expect
                        // packet sizes to usually be consistent, e.g. populated by max-size STREAM
                        // frames or uniformly sized datagrams.
                        segment_size = buf.len();
                        // Clip the unused capacity out of the buffer so future packets don't
                        // overrun
                        buf_capacity = buf.len();

                        // Check whether the data we planned to send will fit in the reduced segment
                        // size. If not, bail out and leave it for the next GSO batch so we don't
                        // end up trying to send an empty packet. We can't easily compute the right
                        // segment size before the original call to `space_can_send`, because at
                        // that time we haven't determined whether we're going to coalesce with the
                        // first datagram or potentially pad it to `MIN_INITIAL_SIZE`.
                        if space_id == SpaceId::Data {
                            let frame_space_1rtt =
                                segment_size.saturating_sub(self.predict_1rtt_overhead(Some(pn)));
                            if self.space_can_send(space_id, frame_space_1rtt).is_empty() {
                                break;
                            }
                        }
                    }
                }

                // Allocate space for another datagram
                let next_datagram_size_limit = match self.spaces[space_id].loss_probes {
                    0 => segment_size,
                    _ => {
                        self.spaces[space_id].loss_probes -= 1;
                        // Clamp the datagram to at most the minimum MTU to ensure that loss probes
                        // can get through and enable recovery even if the path MTU has shrank
                        // unexpectedly.
                        std::cmp::min(segment_size, usize::from(INITIAL_MTU))
                    }
                };
                buf_capacity += next_datagram_size_limit;
                if buf.capacity() < buf_capacity {
                    // We reserve the maximum space for sending `max_datagrams` upfront
                    // to avoid any reallocations if more datagrams have to be appended later on.
                    // Benchmarks have shown shown a 5-10% throughput improvement
                    // compared to continuously resizing the datagram buffer.
                    // While this will lead to over-allocation for small transmits
                    // (e.g. purely containing ACKs), modern memory allocators
                    // (e.g. mimalloc and jemalloc) will pool certain allocation sizes
                    // and therefore this is still rather efficient.
                    buf.reserve(max_datagrams * segment_size);
                }
                num_datagrams += 1;
                coalesce = true;
                pad_datagram = false;
                datagram_start = buf.len();

                debug_assert_eq!(
                    datagram_start % segment_size,
                    0,
                    "datagrams in a GSO batch must be aligned to the segment size"
                );
            } else {
                // We can append/coalesce the next packet into the current
                // datagram.
                // Finish current packet without adding extra padding
                if let Some(builder) = builder_storage.take() {
                    builder.finish_and_track(now, self, sent_frames.take(), buf);
                }
            }

            debug_assert!(buf_capacity - buf.len() >= MIN_PACKET_SPACE);

            //
            // From here on, we've determined that a packet will definitely be sent.
            //

            if self.spaces[SpaceId::Initial].crypto.is_some()
                && space_id == SpaceId::Handshake
                && self.side.is_client()
            {
                // A client stops both sending and processing Initial packets when it
                // sends its first Handshake packet.
                self.discard_space(now, SpaceId::Initial);
            }
            if let Some(ref mut prev) = self.prev_crypto {
                prev.update_unacked = false;
            }

            debug_assert!(
                builder_storage.is_none() && sent_frames.is_none(),
                "Previous packet must have been finished"
            );

            let builder = builder_storage.insert(PacketBuilder::new(
                now,
                space_id,
                self.rem_cids.active(),
                buf,
                buf_capacity,
                datagram_start,
                ack_eliciting,
                self,
            )?);
            coalesce = coalesce && !builder.short_header;

            // Check if we should adjust coalescing for PQC
            let should_adjust_coalescing = self
                .pqc_state
                .should_adjust_coalescing(buf.len() - datagram_start, space_id);

            if should_adjust_coalescing {
                coalesce = false;
                trace!("Disabling coalescing for PQC handshake in {:?}", space_id);
            }

            // https://tools.ietf.org/html/draft-ietf-quic-transport-34#section-14.1
            pad_datagram |=
                space_id == SpaceId::Initial && (self.side.is_client() || ack_eliciting);

            if close {
                trace!("sending CONNECTION_CLOSE");
                // Encode ACKs before the ConnectionClose message, to give the receiver
                // a better approximate on what data has been processed. This is
                // especially important with ack delay, since the peer might not
                // have gotten any other ACK for the data earlier on.
                if !self.spaces[space_id].pending_acks.ranges().is_empty() {
                    Self::populate_acks(
                        now,
                        self.receiving_ecn,
                        &mut SentFrames::default(),
                        &mut self.spaces[space_id],
                        buf,
                        &mut self.stats,
                    );
                }

                // Since there only 64 ACK frames there will always be enough space
                // to encode the ConnectionClose frame too. However we still have the
                // check here to prevent crashes if something changes.
                debug_assert!(
                    buf.len() + frame::ConnectionClose::SIZE_BOUND < builder.max_size,
                    "ACKs should leave space for ConnectionClose"
                );
                if buf.len() + frame::ConnectionClose::SIZE_BOUND < builder.max_size {
                    let max_frame_size = builder.max_size - buf.len();
                    match self.state {
                        State::Closed(state::Closed { ref reason }) => {
                            if space_id == SpaceId::Data || reason.is_transport_layer() {
                                reason.encode(buf, max_frame_size)
                            } else {
                                frame::ConnectionClose {
                                    error_code: TransportErrorCode::APPLICATION_ERROR,
                                    frame_type: None,
                                    reason: Bytes::new(),
                                }
                                .encode(buf, max_frame_size)
                            }
                        }
                        State::Draining => frame::ConnectionClose {
                            error_code: TransportErrorCode::NO_ERROR,
                            frame_type: None,
                            reason: Bytes::new(),
                        }
                        .encode(buf, max_frame_size),
                        _ => unreachable!(
                            "tried to make a close packet when the connection wasn't closed"
                        ),
                    }
                }
                if space_id == self.highest_space {
                    // Don't send another close packet
                    self.close = false;
                    // `CONNECTION_CLOSE` is the final packet
                    break;
                } else {
                    // Send a close frame in every possible space for robustness, per RFC9000
                    // "Immediate Close during the Handshake". Don't bother trying to send anything
                    // else.
                    space_idx += 1;
                    continue;
                }
            }

            // Send an off-path PATH_RESPONSE. Prioritized over on-path data to ensure that path
            // validation can occur while the link is saturated.
            if space_id == SpaceId::Data && num_datagrams == 1 {
                if let Some((token, remote)) = self.path_responses.pop_off_path(self.path.remote) {
                    // `unwrap` guaranteed to succeed because `builder_storage` was populated just
                    // above.
                    let mut builder = builder_storage.take().unwrap();
                    trace!("PATH_RESPONSE {:08x} (off-path)", token);
                    buf.write(frame::FrameType::PATH_RESPONSE);
                    buf.write(token);
                    self.stats.frame_tx.path_response += 1;
                    let min_size = self.pqc_state.min_initial_size();
                    builder.pad_to(min_size);
                    builder.finish_and_track(
                        now,
                        self,
                        Some(SentFrames {
                            non_retransmits: true,
                            ..SentFrames::default()
                        }),
                        buf,
                    );
                    self.stats.udp_tx.on_sent(1, buf.len());

                    // Trace packet sent
                    #[cfg(feature = "trace")]
                    {
                        use crate::trace_packet_sent;
                        // Tracing imports handled by macros
                        trace_packet_sent!(
                            &self.event_log,
                            self.trace_context.trace_id(),
                            buf.len() as u32,
                            0 // Close packet doesn't have a packet number
                        );
                    }

                    return Some(Transmit {
                        destination: remote,
                        size: buf.len(),
                        ecn: None,
                        segment_size: None,
                        src_ip: self.local_ip,
                    });
                }
            }

            // Check for address observations to send
            if space_id == SpaceId::Data && self.address_discovery_state.is_some() {
                let peer_supports = self.peer_params.address_discovery.is_some();

                if let Some(state) = &mut self.address_discovery_state {
                    if peer_supports {
                        if let Some(frame) = state.queue_observed_address_frame(0, self.path.remote)
                        {
                            self.spaces[space_id]
                                .pending
                                .outbound_observations
                                .push(frame);
                        }
                    }
                }
            }

            let sent =
                self.populate_packet(now, space_id, buf, builder.max_size, builder.exact_number);

            // ACK-only packets should only be sent when explicitly allowed. If we write them due to
            // any other reason, there is a bug which leads to one component announcing write
            // readiness while not writing any data. This degrades performance. The condition is
            // only checked if the full MTU is available and when potentially large fixed-size
            // frames aren't queued, so that lack of space in the datagram isn't the reason for just
            // writing ACKs.
            debug_assert!(
                !(sent.is_ack_only(&self.streams)
                    && !can_send.acks
                    && can_send.other
                    && (buf_capacity - builder.datagram_start) == self.path.current_mtu() as usize
                    && self.datagrams.outgoing.is_empty()),
                "SendableFrames was {can_send:?}, but only ACKs have been written"
            );
            pad_datagram |= sent.requires_padding;

            if sent.largest_acked.is_some() {
                self.spaces[space_id].pending_acks.acks_sent();
                self.timers.stop(Timer::MaxAckDelay);
            }

            // Keep information about the packet around until it gets finalized
            sent_frames = Some(sent);

            // Don't increment space_idx.
            // We stay in the current space and check if there is more data to send.
        }

        // Finish the last packet
        if let Some(mut builder) = builder_storage {
            if pad_datagram {
                let min_size = self.pqc_state.min_initial_size();
                builder.pad_to(min_size);
            }

            // If this datagram is a loss probe and `segment_size` is larger than `INITIAL_MTU`,
            // then padding it to `segment_size` would risk failure to recover from a reduction in
            // path MTU.
            // Loss probes are the only packets for which we might grow `buf_capacity`
            // by less than `segment_size`.
            if pad_datagram_to_mtu && buf_capacity >= datagram_start + segment_size {
                builder.pad_to(segment_size as u16);
            }

            let last_packet_number = builder.exact_number;
            builder.finish_and_track(now, self, sent_frames, buf);
            self.path
                .congestion
                .on_sent(now, buf.len() as u64, last_packet_number);

            #[cfg(feature = "__qlog")]
            self.emit_qlog_recovery_metrics(now);
        }

        self.app_limited = buf.is_empty() && !congestion_blocked;

        // Send MTU probe if necessary
        if buf.is_empty() && self.state.is_established() {
            let space_id = SpaceId::Data;
            let probe_size = self
                .path
                .mtud
                .poll_transmit(now, self.packet_number_filter.peek(&self.spaces[space_id]))?;

            let buf_capacity = probe_size as usize;
            buf.reserve(buf_capacity);

            let mut builder = PacketBuilder::new(
                now,
                space_id,
                self.rem_cids.active(),
                buf,
                buf_capacity,
                0,
                true,
                self,
            )?;

            // We implement MTU probes as ping packets padded up to the probe size
            buf.write(frame::FrameType::PING);
            self.stats.frame_tx.ping += 1;

            // If supported by the peer, we want no delays to the probe's ACK
            if self.peer_supports_ack_frequency() {
                buf.write(frame::FrameType::IMMEDIATE_ACK);
                self.stats.frame_tx.immediate_ack += 1;
            }

            builder.pad_to(probe_size);
            let sent_frames = SentFrames {
                non_retransmits: true,
                ..Default::default()
            };
            builder.finish_and_track(now, self, Some(sent_frames), buf);

            self.stats.path.sent_plpmtud_probes += 1;
            num_datagrams = 1;

            trace!(?probe_size, "writing MTUD probe");
        }

        if buf.is_empty() {
            return None;
        }

        trace!("sending {} bytes in {} datagrams", buf.len(), num_datagrams);
        self.path.total_sent = self.path.total_sent.saturating_add(buf.len() as u64);

        self.stats.udp_tx.on_sent(num_datagrams as u64, buf.len());

        // Trace packets sent
        #[cfg(feature = "trace")]
        {
            use crate::trace_packet_sent;
            // Tracing imports handled by macros
            // Log packet transmission (use highest packet number in transmission)
            let packet_num = self.spaces[SpaceId::Data]
                .next_packet_number
                .saturating_sub(1);
            trace_packet_sent!(
                &self.event_log,
                self.trace_context.trace_id(),
                buf.len() as u32,
                packet_num
            );
        }

        Some(Transmit {
            destination: self.path.remote,
            size: buf.len(),
            ecn: if self.path.sending_ecn {
                Some(EcnCodepoint::Ect0)
            } else {
                None
            },
            segment_size: match num_datagrams {
                1 => None,
                _ => Some(segment_size),
            },
            src_ip: self.local_ip,
        })
    }

    /// Send PUNCH_ME_NOW for coordination if necessary
    fn send_coordination_request(&mut self, _now: Instant, _buf: &mut Vec<u8>) -> Option<Transmit> {
        // Get coordination info without borrowing mutably
        let nat = self.nat_traversal.as_mut()?;
        if !nat.should_send_punch_request() {
            return None;
        }

        let coord = nat.coordination.as_ref()?;
        let round = coord.round;
        if coord.punch_targets.is_empty() {
            return None;
        }

        trace!(
            "queuing PUNCH_ME_NOW round {} with {} targets",
            round,
            coord.punch_targets.len()
        );

        // Enqueue one PunchMeNow frame per target (spec-compliant); normal send loop will encode
        for target in &coord.punch_targets {
            let punch = frame::PunchMeNow {
                round,
                paired_with_sequence_number: target.remote_sequence,
                address: target.remote_addr,
                target_peer_id: None,
            };
            self.spaces[SpaceId::Data].pending.punch_me_now.push(punch);
        }

        // Mark request sent
        nat.mark_punch_request_sent();

        // We don't need to craft a transmit here; frames will be sent by the normal writer
        None
    }

    /// Send coordinated PATH_CHALLENGE for hole punching
    fn send_coordinated_path_challenge(
        &mut self,
        now: Instant,
        buf: &mut Vec<u8>,
    ) -> Option<Transmit> {
        // Check if it's time to start synchronized hole punching
        if let Some(nat_traversal) = &mut self.nat_traversal {
            if nat_traversal.should_start_punching(now) {
                nat_traversal.start_punching_phase(now);
            }
        }

        // Get punch targets if we're in punching phase
        let (target_addr, challenge) = {
            let nat_traversal = self.nat_traversal.as_ref()?;
            match nat_traversal.get_coordination_phase() {
                Some(CoordinationPhase::Punching) => {
                    let targets = nat_traversal.get_punch_targets_from_coordination()?;
                    if targets.is_empty() {
                        return None;
                    }
                    // Send PATH_CHALLENGE to the first target (could be round-robin in future)
                    let target = &targets[0];
                    (target.remote_addr, target.challenge)
                }
                _ => return None,
            }
        };

        debug_assert_eq!(
            self.highest_space,
            SpaceId::Data,
            "PATH_CHALLENGE queued without 1-RTT keys"
        );

        buf.reserve(self.pqc_state.min_initial_size() as usize);
        let buf_capacity = buf.capacity();

        let mut builder = PacketBuilder::new(
            now,
            SpaceId::Data,
            self.rem_cids.active(),
            buf,
            buf_capacity,
            0,
            false,
            self,
        )?;

        trace!(
            "sending coordinated PATH_CHALLENGE {:08x} to {}",
            challenge, target_addr
        );
        buf.write(frame::FrameType::PATH_CHALLENGE);
        buf.write(challenge);
        self.stats.frame_tx.path_challenge += 1;

        let min_size = self.pqc_state.min_initial_size();
        builder.pad_to(min_size);
        builder.finish_and_track(now, self, None, buf);

        // Mark coordination as validating after packet is built
        if let Some(nat_traversal) = &mut self.nat_traversal {
            nat_traversal.mark_coordination_validating();
        }

        Some(Transmit {
            destination: target_addr,
            size: buf.len(),
            ecn: if self.path.sending_ecn {
                Some(EcnCodepoint::Ect0)
            } else {
                None
            },
            segment_size: None,
            src_ip: self.local_ip,
        })
    }

    /// Send PATH_CHALLENGE for NAT traversal candidates if necessary
    fn send_nat_traversal_challenge(
        &mut self,
        now: Instant,
        buf: &mut Vec<u8>,
    ) -> Option<Transmit> {
        // Priority 1: Coordination protocol requests
        if let Some(request) = self.send_coordination_request(now, buf) {
            return Some(request);
        }

        // Priority 2: Coordinated hole punching
        if let Some(punch) = self.send_coordinated_path_challenge(now, buf) {
            return Some(punch);
        }

        // Priority 3: Regular candidate validation (fallback)
        let (remote_addr, remote_sequence) = {
            let nat_traversal = self.nat_traversal.as_ref()?;
            let candidates = nat_traversal.get_validation_candidates();
            if candidates.is_empty() {
                return None;
            }
            // Get the highest priority candidate
            let (sequence, candidate) = candidates[0];
            (candidate.address, sequence)
        };

        let challenge = self.rng.r#gen::<u64>();

        // Start validation for this candidate
        if let Err(e) =
            self.nat_traversal
                .as_mut()?
                .start_validation(remote_sequence, challenge, now)
        {
            warn!("Failed to start NAT traversal validation: {}", e);
            return None;
        }

        debug_assert_eq!(
            self.highest_space,
            SpaceId::Data,
            "PATH_CHALLENGE queued without 1-RTT keys"
        );

        buf.reserve(self.pqc_state.min_initial_size() as usize);
        let buf_capacity = buf.capacity();

        // Use current connection ID for NAT traversal PATH_CHALLENGE
        let mut builder = PacketBuilder::new(
            now,
            SpaceId::Data,
            self.rem_cids.active(),
            buf,
            buf_capacity,
            0,
            false,
            self,
        )?;

        trace!(
            "sending PATH_CHALLENGE {:08x} to NAT candidate {}",
            challenge, remote_addr
        );
        buf.write(frame::FrameType::PATH_CHALLENGE);
        buf.write(challenge);
        self.stats.frame_tx.path_challenge += 1;

        // PATH_CHALLENGE frames must be padded to at least 1200 bytes
        let min_size = self.pqc_state.min_initial_size();
        builder.pad_to(min_size);

        builder.finish_and_track(now, self, None, buf);

        Some(Transmit {
            destination: remote_addr,
            size: buf.len(),
            ecn: if self.path.sending_ecn {
                Some(EcnCodepoint::Ect0)
            } else {
                None
            },
            segment_size: None,
            src_ip: self.local_ip,
        })
    }

    /// Send PATH_CHALLENGE for a previous path if necessary
    fn send_path_challenge(&mut self, now: Instant, buf: &mut Vec<u8>) -> Option<Transmit> {
        let (prev_cid, prev_path) = self.prev_path.as_mut()?;
        if !prev_path.challenge_pending {
            return None;
        }
        prev_path.challenge_pending = false;
        let token = prev_path
            .challenge
            .expect("previous path challenge pending without token");
        let destination = prev_path.remote;
        debug_assert_eq!(
            self.highest_space,
            SpaceId::Data,
            "PATH_CHALLENGE queued without 1-RTT keys"
        );
        buf.reserve(self.pqc_state.min_initial_size() as usize);

        let buf_capacity = buf.capacity();

        // Use the previous CID to avoid linking the new path with the previous path. We
        // don't bother accounting for possible retirement of that prev_cid because this is
        // sent once, immediately after migration, when the CID is known to be valid. Even
        // if a post-migration packet caused the CID to be retired, it's fair to pretend
        // this is sent first.
        let mut builder = PacketBuilder::new(
            now,
            SpaceId::Data,
            *prev_cid,
            buf,
            buf_capacity,
            0,
            false,
            self,
        )?;
        trace!("validating previous path with PATH_CHALLENGE {:08x}", token);
        buf.write(frame::FrameType::PATH_CHALLENGE);
        buf.write(token);
        self.stats.frame_tx.path_challenge += 1;

        // An endpoint MUST expand datagrams that contain a PATH_CHALLENGE frame
        // to at least the smallest allowed maximum datagram size of 1200 bytes,
        // unless the anti-amplification limit for the path does not permit
        // sending a datagram of this size
        let min_size = self.pqc_state.min_initial_size();
        builder.pad_to(min_size);

        builder.finish(self, buf);
        self.stats.udp_tx.on_sent(1, buf.len());

        Some(Transmit {
            destination,
            size: buf.len(),
            ecn: None,
            segment_size: None,
            src_ip: self.local_ip,
        })
    }

    /// Indicate what types of frames are ready to send for the given space
    fn space_can_send(&self, space_id: SpaceId, frame_space_1rtt: usize) -> SendableFrames {
        if self.spaces[space_id].crypto.is_none()
            && (space_id != SpaceId::Data
                || self.zero_rtt_crypto.is_none()
                || self.side.is_server())
        {
            // No keys available for this space
            return SendableFrames::empty();
        }
        let mut can_send = self.spaces[space_id].can_send(&self.streams);
        if space_id == SpaceId::Data {
            can_send.other |= self.can_send_1rtt(frame_space_1rtt);
        }
        can_send
    }

    /// Process `ConnectionEvent`s generated by the associated `Endpoint`
    ///
    /// Will execute protocol logic upon receipt of a connection event, in turn preparing signals
    /// (including application `Event`s, `EndpointEvent`s and outgoing datagrams) that should be
    /// extracted through the relevant methods.
    pub fn handle_event(&mut self, event: ConnectionEvent) {
        use ConnectionEventInner::*;
        match event.0 {
            Datagram(DatagramConnectionEvent {
                now,
                remote,
                ecn,
                first_decode,
                remaining,
            }) => {
                // If this packet could initiate a migration and we're a client or a server that
                // forbids migration, drop the datagram. This could be relaxed to heuristically
                // permit NAT-rebinding-like migration.
                if remote != self.path.remote && !self.side.remote_may_migrate() {
                    trace!("discarding packet from unrecognized peer {}", remote);
                    return;
                }

                let was_anti_amplification_blocked = self.path.anti_amplification_blocked(1);

                self.stats.udp_rx.datagrams += 1;
                self.stats.udp_rx.bytes += first_decode.len() as u64;
                let data_len = first_decode.len();

                self.handle_decode(now, remote, ecn, first_decode);
                // The current `path` might have changed inside `handle_decode`,
                // since the packet could have triggered a migration. Make sure
                // the data received is accounted for the most recent path by accessing
                // `path` after `handle_decode`.
                self.path.total_recvd = self.path.total_recvd.saturating_add(data_len as u64);

                if let Some(data) = remaining {
                    self.stats.udp_rx.bytes += data.len() as u64;
                    self.handle_coalesced(now, remote, ecn, data);
                }

                #[cfg(feature = "__qlog")]
                self.emit_qlog_recovery_metrics(now);

                if was_anti_amplification_blocked {
                    // A prior attempt to set the loss detection timer may have failed due to
                    // anti-amplification, so ensure it's set now. Prevents a handshake deadlock if
                    // the server's first flight is lost.
                    self.set_loss_detection_timer(now);
                }
            }
            NewIdentifiers(ids, now) => {
                self.local_cid_state.new_cids(&ids, now);
                ids.into_iter().rev().for_each(|frame| {
                    self.spaces[SpaceId::Data].pending.new_cids.push(frame);
                });
                // Update Timer::PushNewCid
                if self.timers.get(Timer::PushNewCid).is_none_or(|x| x <= now) {
                    self.reset_cid_retirement();
                }
            }
            QueueAddAddress(add) => {
                // Enqueue AddAddress frame for transmission
                self.spaces[SpaceId::Data].pending.add_addresses.push(add);
            }
            QueuePunchMeNow(punch) => {
                // Enqueue PunchMeNow frame for transmission
                self.spaces[SpaceId::Data].pending.punch_me_now.push(punch);
            }
        }
    }

    /// Process timer expirations
    ///
    /// Executes protocol logic, potentially preparing signals (including application `Event`s,
    /// `EndpointEvent`s and outgoing datagrams) that should be extracted through the relevant
    /// methods.
    ///
    /// It is most efficient to call this immediately after the system clock reaches the latest
    /// `Instant` that was output by `poll_timeout`; however spurious extra calls will simply
    /// no-op and therefore are safe.
    pub fn handle_timeout(&mut self, now: Instant) {
        for &timer in &Timer::VALUES {
            if !self.timers.is_expired(timer, now) {
                continue;
            }
            self.timers.stop(timer);
            trace!(timer = ?timer, "timeout");
            match timer {
                Timer::Close => {
                    self.state = State::Drained;
                    self.endpoint_events.push_back(EndpointEventInner::Drained);
                }
                Timer::Idle => {
                    self.kill(ConnectionError::TimedOut);
                }
                Timer::KeepAlive => {
                    trace!("sending keep-alive");
                    self.ping();
                }
                Timer::LossDetection => {
                    self.on_loss_detection_timeout(now);

                    #[cfg(feature = "__qlog")]
                    self.emit_qlog_recovery_metrics(now);
                }
                Timer::KeyDiscard => {
                    self.zero_rtt_crypto = None;
                    self.prev_crypto = None;
                }
                Timer::PathValidation => {
                    debug!("path validation failed");
                    if let Some((_, prev)) = self.prev_path.take() {
                        self.path = prev;
                    }
                    self.path.challenge = None;
                    self.path.challenge_pending = false;
                }
                Timer::Pacing => trace!("pacing timer expired"),
                Timer::NatTraversal => {
                    self.handle_nat_traversal_timeout(now);
                }
                Timer::PushNewCid => {
                    // Update `retire_prior_to` field in NEW_CONNECTION_ID frame
                    let num_new_cid = self.local_cid_state.on_cid_timeout().into();
                    if !self.state.is_closed() {
                        trace!(
                            "push a new cid to peer RETIRE_PRIOR_TO field {}",
                            self.local_cid_state.retire_prior_to()
                        );
                        self.endpoint_events
                            .push_back(EndpointEventInner::NeedIdentifiers(now, num_new_cid));
                    }
                }
                Timer::MaxAckDelay => {
                    trace!("max ack delay reached");
                    // This timer is only armed in the Data space
                    self.spaces[SpaceId::Data]
                        .pending_acks
                        .on_max_ack_delay_timeout()
                }
            }
        }
    }

    /// Close a connection immediately
    ///
    /// This does not ensure delivery of outstanding data. It is the application's responsibility to
    /// call this only when all important communications have been completed, e.g. by calling
    /// [`SendStream::finish`] on outstanding streams and waiting for the corresponding
    /// [`StreamEvent::Finished`] event.
    ///
    /// If [`Streams::send_streams`] returns 0, all outstanding stream data has been
    /// delivered. There may still be data from the peer that has not been received.
    ///
    /// [`StreamEvent::Finished`]: crate::StreamEvent::Finished
    pub fn close(&mut self, now: Instant, error_code: VarInt, reason: Bytes) {
        self.close_inner(
            now,
            Close::Application(frame::ApplicationClose { error_code, reason }),
        )
    }

    fn close_inner(&mut self, now: Instant, reason: Close) {
        let was_closed = self.state.is_closed();
        if !was_closed {
            self.close_common();
            self.set_close_timer(now);
            self.close = true;
            self.state = State::Closed(state::Closed { reason });
        }
    }

    /// Control datagrams
    pub fn datagrams(&mut self) -> Datagrams<'_> {
        Datagrams { conn: self }
    }

    /// Returns connection statistics
    pub fn stats(&self) -> ConnectionStats {
        let mut stats = self.stats;
        stats.path.rtt = self.path.rtt.get();
        stats.path.cwnd = self.path.congestion.window();
        stats.path.current_mtu = self.path.mtud.current_mtu();

        stats
    }

    /// Set the bound peer identity for token v2 issuance.
    pub fn set_token_binding_peer_id(&mut self, pid: PeerId) {
        self.peer_id_for_tokens = Some(pid);
    }

    /// Control whether NEW_TOKEN frames should be delayed until binding completes.
    pub fn set_delay_new_token_until_binding(&mut self, v: bool) {
        self.delay_new_token_until_binding = v;
    }

    /// Ping the remote endpoint
    ///
    /// Causes an ACK-eliciting packet to be transmitted.
    pub fn ping(&mut self) {
        self.spaces[self.highest_space].ping_pending = true;
    }

    /// Returns true if post-quantum algorithms are in use for this connection.
    pub(crate) fn is_pqc(&self) -> bool {
        self.pqc_state.using_pqc
    }

    /// Update traffic keys spontaneously
    ///
    /// This can be useful for testing key updates, as they otherwise only happen infrequently.
    pub fn force_key_update(&mut self) {
        if !self.state.is_established() {
            debug!("ignoring forced key update in illegal state");
            return;
        }
        if self.prev_crypto.is_some() {
            // We already just updated, or are currently updating, the keys. Concurrent key updates
            // are illegal.
            debug!("ignoring redundant forced key update");
            return;
        }
        self.update_keys(None, false);
    }

    /// Get a session reference
    pub fn crypto_session(&self) -> &dyn crypto::Session {
        &*self.crypto
    }

    /// Whether the connection is in the process of being established
    ///
    /// If this returns `false`, the connection may be either established or closed, signaled by the
    /// emission of a `Connected` or `ConnectionLost` message respectively.
    pub fn is_handshaking(&self) -> bool {
        self.state.is_handshake()
    }

    /// Whether the connection is closed
    ///
    /// Closed connections cannot transport any further data. A connection becomes closed when
    /// either peer application intentionally closes it, or when either transport layer detects an
    /// error such as a time-out or certificate validation failure.
    ///
    /// A `ConnectionLost` event is emitted with details when the connection becomes closed.
    pub fn is_closed(&self) -> bool {
        self.state.is_closed()
    }

    /// Whether there is no longer any need to keep the connection around
    ///
    /// Closed connections become drained after a brief timeout to absorb any remaining in-flight
    /// packets from the peer. All drained connections have been closed.
    pub fn is_drained(&self) -> bool {
        self.state.is_drained()
    }

    /// For clients, if the peer accepted the 0-RTT data packets
    ///
    /// The value is meaningless until after the handshake completes.
    pub fn accepted_0rtt(&self) -> bool {
        self.accepted_0rtt
    }

    /// Whether 0-RTT is/was possible during the handshake
    pub fn has_0rtt(&self) -> bool {
        self.zero_rtt_enabled
    }

    /// Whether there are any pending retransmits
    pub fn has_pending_retransmits(&self) -> bool {
        !self.spaces[SpaceId::Data].pending.is_empty(&self.streams)
    }

    /// Look up whether we're the client or server of this Connection
    pub fn side(&self) -> Side {
        self.side.side()
    }

    /// The latest socket address for this connection's peer
    pub fn remote_address(&self) -> SocketAddr {
        self.path.remote
    }

    /// The local IP address which was used when the peer established
    /// the connection
    ///
    /// This can be different from the address the endpoint is bound to, in case
    /// the endpoint is bound to a wildcard address like `0.0.0.0` or `::`.
    ///
    /// This will return `None` for clients, or when no `local_ip` was passed to
    /// the endpoint's handle method for the datagrams establishing this
    /// connection.
    pub fn local_ip(&self) -> Option<IpAddr> {
        self.local_ip
    }

    /// Current best estimate of this connection's latency (round-trip-time)
    pub fn rtt(&self) -> Duration {
        self.path.rtt.get()
    }

    /// Current state of this connection's congestion controller, for debugging purposes
    pub fn congestion_state(&self) -> &dyn Controller {
        self.path.congestion.as_ref()
    }

    /// Resets path-specific settings.
    ///
    /// This will force-reset several subsystems related to a specific network path.
    /// Currently this is the congestion controller, round-trip estimator, and the MTU
    /// discovery.
    ///
    /// This is useful when it is known the underlying network path has changed and the old
    /// state of these subsystems is no longer valid or optimal. In this case it might be
    /// faster or reduce loss to settle on optimal values by restarting from the initial
    /// configuration in the [`TransportConfig`].
    pub fn path_changed(&mut self, now: Instant) {
        self.path.reset(now, &self.config);
    }

    /// Modify the number of remotely initiated streams that may be concurrently open
    ///
    /// No streams may be opened by the peer unless fewer than `count` are already open. Large
    /// `count`s increase both minimum and worst-case memory consumption.
    pub fn set_max_concurrent_streams(&mut self, dir: Dir, count: VarInt) {
        self.streams.set_max_concurrent(dir, count);
        // If the limit was reduced, then a flow control update previously deemed insignificant may
        // now be significant.
        let pending = &mut self.spaces[SpaceId::Data].pending;
        self.streams.queue_max_stream_id(pending);
    }

    /// Current number of remotely initiated streams that may be concurrently open
    ///
    /// If the target for this limit is reduced using [`set_max_concurrent_streams`](Self::set_max_concurrent_streams),
    /// it will not change immediately, even if fewer streams are open. Instead, it will
    /// decrement by one for each time a remotely initiated stream of matching directionality is closed.
    pub fn max_concurrent_streams(&self, dir: Dir) -> u64 {
        self.streams.max_concurrent(dir)
    }

    /// See [`TransportConfig::receive_window()`]
    pub fn set_receive_window(&mut self, receive_window: VarInt) {
        if self.streams.set_receive_window(receive_window) {
            self.spaces[SpaceId::Data].pending.max_data = true;
        }
    }

    /// Enable or disable address discovery for this connection
    pub fn set_address_discovery_enabled(&mut self, enabled: bool) {
        if let Some(ref mut state) = self.address_discovery_state {
            state.enabled = enabled;
        }
    }

    /// Check if address discovery is enabled for this connection
    pub fn address_discovery_enabled(&self) -> bool {
        self.address_discovery_state
            .as_ref()
            .is_some_and(|state| state.enabled)
    }

    /// Get the observed address for this connection
    ///
    /// Returns the address that the remote peer has observed for this connection,
    /// or None if no OBSERVED_ADDRESS frame has been received yet.
    pub fn observed_address(&self) -> Option<SocketAddr> {
        self.address_discovery_state
            .as_ref()
            .and_then(|state| state.get_observed_address(0)) // Use path ID 0 for primary path
    }

    /// Get the address discovery state (internal use)
    #[allow(dead_code)]
    pub(crate) fn address_discovery_state(&self) -> Option<&AddressDiscoveryState> {
        self.address_discovery_state.as_ref()
    }

    fn on_ack_received(
        &mut self,
        now: Instant,
        space: SpaceId,
        ack: frame::Ack,
    ) -> Result<(), TransportError> {
        if ack.largest >= self.spaces[space].next_packet_number {
            return Err(TransportError::PROTOCOL_VIOLATION("unsent packet acked"));
        }
        let new_largest = {
            let space = &mut self.spaces[space];
            if space.largest_acked_packet.is_none_or(|pn| ack.largest > pn) {
                space.largest_acked_packet = Some(ack.largest);
                if let Some(info) = space.sent_packets.get(&ack.largest) {
                    // This should always succeed, but a misbehaving peer might ACK a packet we
                    // haven't sent. At worst, that will result in us spuriously reducing the
                    // congestion window.
                    space.largest_acked_packet_sent = info.time_sent;
                }
                true
            } else {
                false
            }
        };

        // Avoid DoS from unreasonably huge ack ranges by filtering out just the new acks.
        let mut newly_acked = ArrayRangeSet::new();
        for range in ack.iter() {
            self.packet_number_filter.check_ack(space, range.clone())?;
            for (&pn, _) in self.spaces[space].sent_packets.range(range) {
                newly_acked.insert_one(pn);
            }
        }

        if newly_acked.is_empty() {
            return Ok(());
        }

        let mut ack_eliciting_acked = false;
        for packet in newly_acked.elts() {
            if let Some(info) = self.spaces[space].take(packet) {
                if let Some(acked) = info.largest_acked {
                    // Assume ACKs for all packets below the largest acknowledged in `packet` have
                    // been received. This can cause the peer to spuriously retransmit if some of
                    // our earlier ACKs were lost, but allows for simpler state tracking. See
                    // discussion at
                    // https://www.rfc-editor.org/rfc/rfc9000.html#name-limiting-ranges-by-tracking
                    self.spaces[space].pending_acks.subtract_below(acked);
                }
                ack_eliciting_acked |= info.ack_eliciting;

                // Notify MTU discovery that a packet was acked, because it might be an MTU probe
                let mtu_updated = self.path.mtud.on_acked(space, packet, info.size);
                if mtu_updated {
                    self.path
                        .congestion
                        .on_mtu_update(self.path.mtud.current_mtu());
                }

                // Notify ack frequency that a packet was acked, because it might contain an ACK_FREQUENCY frame
                self.ack_frequency.on_acked(packet);

                self.on_packet_acked(now, packet, info);
            }
        }

        self.path.congestion.on_end_acks(
            now,
            self.path.in_flight.bytes,
            self.app_limited,
            self.spaces[space].largest_acked_packet,
        );

        if new_largest && ack_eliciting_acked {
            let ack_delay = if space != SpaceId::Data {
                Duration::from_micros(0)
            } else {
                cmp::min(
                    self.ack_frequency.peer_max_ack_delay,
                    Duration::from_micros(ack.delay << self.peer_params.ack_delay_exponent.0),
                )
            };
            let rtt = instant_saturating_sub(now, self.spaces[space].largest_acked_packet_sent);
            self.path.rtt.update(ack_delay, rtt);
            if self.path.first_packet_after_rtt_sample.is_none() {
                self.path.first_packet_after_rtt_sample =
                    Some((space, self.spaces[space].next_packet_number));
            }
        }

        // Must be called before crypto/pto_count are clobbered
        self.detect_lost_packets(now, space, true);

        if self.peer_completed_address_validation() {
            self.pto_count = 0;
        }

        // Explicit congestion notification
        if self.path.sending_ecn {
            if let Some(ecn) = ack.ecn {
                // We only examine ECN counters from ACKs that we are certain we received in transmit
                // order, allowing us to compute an increase in ECN counts to compare against the number
                // of newly acked packets that remains well-defined in the presence of arbitrary packet
                // reordering.
                if new_largest {
                    let sent = self.spaces[space].largest_acked_packet_sent;
                    self.process_ecn(now, space, newly_acked.len() as u64, ecn, sent);
                }
            } else {
                // We always start out sending ECN, so any ack that doesn't acknowledge it disables it.
                debug!("ECN not acknowledged by peer");
                self.path.sending_ecn = false;
            }
        }

        self.set_loss_detection_timer(now);
        Ok(())
    }

    /// Process a new ECN block from an in-order ACK
    fn process_ecn(
        &mut self,
        now: Instant,
        space: SpaceId,
        newly_acked: u64,
        ecn: frame::EcnCounts,
        largest_sent_time: Instant,
    ) {
        match self.spaces[space].detect_ecn(newly_acked, ecn) {
            Err(e) => {
                debug!("halting ECN due to verification failure: {}", e);
                self.path.sending_ecn = false;
                // Wipe out the existing value because it might be garbage and could interfere with
                // future attempts to use ECN on new paths.
                self.spaces[space].ecn_feedback = frame::EcnCounts::ZERO;
            }
            Ok(false) => {}
            Ok(true) => {
                self.stats.path.congestion_events += 1;
                self.path
                    .congestion
                    .on_congestion_event(now, largest_sent_time, false, 0);
            }
        }
    }

    // Not timing-aware, so it's safe to call this for inferred acks, such as arise from
    // high-latency handshakes
    fn on_packet_acked(&mut self, now: Instant, pn: u64, info: SentPacket) {
        self.remove_in_flight(pn, &info);
        if info.ack_eliciting && self.path.challenge.is_none() {
            // Only pass ACKs to the congestion controller if we are not validating the current
            // path, so as to ignore any ACKs from older paths still coming in.
            self.path.congestion.on_ack(
                now,
                info.time_sent,
                info.size.into(),
                self.app_limited,
                &self.path.rtt,
            );
        }

        // Update state for confirmed delivery of frames
        if let Some(retransmits) = info.retransmits.get() {
            for (id, _) in retransmits.reset_stream.iter() {
                self.streams.reset_acked(*id);
            }
        }

        for frame in info.stream_frames {
            self.streams.received_ack_of(frame);
        }
    }

    fn set_key_discard_timer(&mut self, now: Instant, space: SpaceId) {
        let start = if self.zero_rtt_crypto.is_some() {
            now
        } else {
            self.prev_crypto
                .as_ref()
                .expect("no previous keys")
                .end_packet
                .as_ref()
                .expect("update not acknowledged yet")
                .1
        };
        self.timers
            .set(Timer::KeyDiscard, start + self.pto(space) * 3);
    }

    fn on_loss_detection_timeout(&mut self, now: Instant) {
        if let Some((_, pn_space)) = self.loss_time_and_space() {
            // Time threshold loss Detection
            self.detect_lost_packets(now, pn_space, false);
            self.set_loss_detection_timer(now);
            return;
        }

        let (_, space) = match self.pto_time_and_space(now) {
            Some(x) => x,
            None => {
                error!("PTO expired while unset");
                return;
            }
        };
        trace!(
            in_flight = self.path.in_flight.bytes,
            count = self.pto_count,
            ?space,
            "PTO fired"
        );

        let count = match self.path.in_flight.ack_eliciting {
            // A PTO when we're not expecting any ACKs must be due to handshake anti-amplification
            // deadlock preventions
            0 => {
                debug_assert!(!self.peer_completed_address_validation());
                1
            }
            // Conventional loss probe
            _ => 2,
        };
        self.spaces[space].loss_probes = self.spaces[space].loss_probes.saturating_add(count);
        self.pto_count = self.pto_count.saturating_add(1);
        self.set_loss_detection_timer(now);
    }

    fn detect_lost_packets(&mut self, now: Instant, pn_space: SpaceId, due_to_ack: bool) {
        let mut lost_packets = Vec::<u64>::new();
        let mut lost_mtu_probe = None;
        let in_flight_mtu_probe = self.path.mtud.in_flight_mtu_probe();
        let rtt = self.path.rtt.conservative();
        let loss_delay = cmp::max(rtt.mul_f32(self.config.time_threshold), TIMER_GRANULARITY);

        // Packets sent before this time are deemed lost.
        let lost_send_time = now.checked_sub(loss_delay).unwrap();
        let largest_acked_packet = self.spaces[pn_space].largest_acked_packet.unwrap();
        let packet_threshold = self.config.packet_threshold as u64;
        let mut size_of_lost_packets = 0u64;

        // InPersistentCongestion: Determine if all packets in the time period before the newest
        // lost packet, including the edges, are marked lost. PTO computation must always
        // include max ACK delay, i.e. operate as if in Data space (see RFC9001 §7.6.1).
        let congestion_period =
            self.pto(SpaceId::Data) * self.config.persistent_congestion_threshold;
        let mut persistent_congestion_start: Option<Instant> = None;
        let mut prev_packet = None;
        let mut in_persistent_congestion = false;

        let space = &mut self.spaces[pn_space];
        space.loss_time = None;

        for (&packet, info) in space.sent_packets.range(0..largest_acked_packet) {
            if prev_packet != Some(packet.wrapping_sub(1)) {
                // An intervening packet was acknowledged
                persistent_congestion_start = None;
            }

            if info.time_sent <= lost_send_time || largest_acked_packet >= packet + packet_threshold
            {
                if Some(packet) == in_flight_mtu_probe {
                    // Lost MTU probes are not included in `lost_packets`, because they should not
                    // trigger a congestion control response
                    lost_mtu_probe = in_flight_mtu_probe;
                } else {
                    lost_packets.push(packet);
                    size_of_lost_packets += info.size as u64;
                    if info.ack_eliciting && due_to_ack {
                        match persistent_congestion_start {
                            // Two ACK-eliciting packets lost more than congestion_period apart, with no
                            // ACKed packets in between
                            Some(start) if info.time_sent - start > congestion_period => {
                                in_persistent_congestion = true;
                            }
                            // Persistent congestion must start after the first RTT sample
                            None if self
                                .path
                                .first_packet_after_rtt_sample
                                .is_some_and(|x| x < (pn_space, packet)) =>
                            {
                                persistent_congestion_start = Some(info.time_sent);
                            }
                            _ => {}
                        }
                    }
                }
            } else {
                let next_loss_time = info.time_sent + loss_delay;
                space.loss_time = Some(
                    space
                        .loss_time
                        .map_or(next_loss_time, |x| cmp::min(x, next_loss_time)),
                );
                persistent_congestion_start = None;
            }

            prev_packet = Some(packet);
        }

        // OnPacketsLost
        if let Some(largest_lost) = lost_packets.last().cloned() {
            let old_bytes_in_flight = self.path.in_flight.bytes;
            let largest_lost_sent = self.spaces[pn_space].sent_packets[&largest_lost].time_sent;
            self.lost_packets += lost_packets.len() as u64;
            self.stats.path.lost_packets += lost_packets.len() as u64;
            self.stats.path.lost_bytes += size_of_lost_packets;
            trace!(
                "packets lost: {:?}, bytes lost: {}",
                lost_packets, size_of_lost_packets
            );

            for &packet in &lost_packets {
                let info = self.spaces[pn_space].take(packet).unwrap(); // safe: lost_packets is populated just above
                self.remove_in_flight(packet, &info);
                for frame in info.stream_frames {
                    self.streams.retransmit(frame);
                }
                self.spaces[pn_space].pending |= info.retransmits;
                self.path.mtud.on_non_probe_lost(packet, info.size);
            }

            if self.path.mtud.black_hole_detected(now) {
                self.stats.path.black_holes_detected += 1;
                self.path
                    .congestion
                    .on_mtu_update(self.path.mtud.current_mtu());
                if let Some(max_datagram_size) = self.datagrams().max_size() {
                    self.datagrams.drop_oversized(max_datagram_size);
                }
            }

            // Don't apply congestion penalty for lost ack-only packets
            let lost_ack_eliciting = old_bytes_in_flight != self.path.in_flight.bytes;

            if lost_ack_eliciting {
                self.stats.path.congestion_events += 1;
                self.path.congestion.on_congestion_event(
                    now,
                    largest_lost_sent,
                    in_persistent_congestion,
                    size_of_lost_packets,
                );
            }
        }

        // Handle a lost MTU probe
        if let Some(packet) = lost_mtu_probe {
            let info = self.spaces[SpaceId::Data].take(packet).unwrap(); // safe: lost_mtu_probe is omitted from lost_packets, and therefore must not have been removed yet
            self.remove_in_flight(packet, &info);
            self.path.mtud.on_probe_lost();
            self.stats.path.lost_plpmtud_probes += 1;
        }
    }

    fn loss_time_and_space(&self) -> Option<(Instant, SpaceId)> {
        SpaceId::iter()
            .filter_map(|id| Some((self.spaces[id].loss_time?, id)))
            .min_by_key(|&(time, _)| time)
    }

    fn pto_time_and_space(&self, now: Instant) -> Option<(Instant, SpaceId)> {
        let backoff = 2u32.pow(self.pto_count.min(MAX_BACKOFF_EXPONENT));
        let mut duration = self.path.rtt.pto_base() * backoff;

        if self.path.in_flight.ack_eliciting == 0 {
            debug_assert!(!self.peer_completed_address_validation());
            let space = match self.highest_space {
                SpaceId::Handshake => SpaceId::Handshake,
                _ => SpaceId::Initial,
            };
            return Some((now + duration, space));
        }

        let mut result = None;
        for space in SpaceId::iter() {
            if self.spaces[space].in_flight == 0 {
                continue;
            }
            if space == SpaceId::Data {
                // Skip ApplicationData until handshake completes.
                if self.is_handshaking() {
                    return result;
                }
                // Include max_ack_delay and backoff for ApplicationData.
                duration += self.ack_frequency.max_ack_delay_for_pto() * backoff;
            }
            let last_ack_eliciting = match self.spaces[space].time_of_last_ack_eliciting_packet {
                Some(time) => time,
                None => continue,
            };
            let pto = last_ack_eliciting + duration;
            if result.is_none_or(|(earliest_pto, _)| pto < earliest_pto) {
                result = Some((pto, space));
            }
        }
        result
    }

    fn peer_completed_address_validation(&self) -> bool {
        if self.side.is_server() || self.state.is_closed() {
            return true;
        }
        // The server is guaranteed to have validated our address if any of our handshake or 1-RTT
        // packets are acknowledged or we've seen HANDSHAKE_DONE and discarded handshake keys.
        self.spaces[SpaceId::Handshake]
            .largest_acked_packet
            .is_some()
            || self.spaces[SpaceId::Data].largest_acked_packet.is_some()
            || (self.spaces[SpaceId::Data].crypto.is_some()
                && self.spaces[SpaceId::Handshake].crypto.is_none())
    }

    fn set_loss_detection_timer(&mut self, now: Instant) {
        if self.state.is_closed() {
            // No loss detection takes place on closed connections, and `close_common` already
            // stopped time timer. Ensure we don't restart it inadvertently, e.g. in response to a
            // reordered packet being handled by state-insensitive code.
            return;
        }

        if let Some((loss_time, _)) = self.loss_time_and_space() {
            // Time threshold loss detection.
            self.timers.set(Timer::LossDetection, loss_time);
            return;
        }

        if self.path.anti_amplification_blocked(1) {
            // We wouldn't be able to send anything, so don't bother.
            self.timers.stop(Timer::LossDetection);
            return;
        }

        if self.path.in_flight.ack_eliciting == 0 && self.peer_completed_address_validation() {
            // There is nothing to detect lost, so no timer is set. However, the client needs to arm
            // the timer if the server might be blocked by the anti-amplification limit.
            self.timers.stop(Timer::LossDetection);
            return;
        }

        // Determine which PN space to arm PTO for.
        // Calculate PTO duration
        if let Some((timeout, _)) = self.pto_time_and_space(now) {
            self.timers.set(Timer::LossDetection, timeout);
        } else {
            self.timers.stop(Timer::LossDetection);
        }
    }

    /// Probe Timeout
    fn pto(&self, space: SpaceId) -> Duration {
        let max_ack_delay = match space {
            SpaceId::Initial | SpaceId::Handshake => Duration::ZERO,
            SpaceId::Data => self.ack_frequency.max_ack_delay_for_pto(),
        };
        self.path.rtt.pto_base() + max_ack_delay
    }

    fn on_packet_authenticated(
        &mut self,
        now: Instant,
        space_id: SpaceId,
        ecn: Option<EcnCodepoint>,
        packet: Option<u64>,
        spin: bool,
        is_1rtt: bool,
    ) {
        self.total_authed_packets += 1;
        self.reset_keep_alive(now);
        self.reset_idle_timeout(now, space_id);
        self.permit_idle_reset = true;
        self.receiving_ecn |= ecn.is_some();
        if let Some(x) = ecn {
            let space = &mut self.spaces[space_id];
            space.ecn_counters += x;

            if x.is_ce() {
                space.pending_acks.set_immediate_ack_required();
            }
        }

        let packet = match packet {
            Some(x) => x,
            None => return,
        };
        if self.side.is_server() {
            if self.spaces[SpaceId::Initial].crypto.is_some() && space_id == SpaceId::Handshake {
                // A server stops sending and processing Initial packets when it receives its first Handshake packet.
                self.discard_space(now, SpaceId::Initial);
            }
            if self.zero_rtt_crypto.is_some() && is_1rtt {
                // Discard 0-RTT keys soon after receiving a 1-RTT packet
                self.set_key_discard_timer(now, space_id)
            }
        }
        let space = &mut self.spaces[space_id];
        space.pending_acks.insert_one(packet, now);
        if packet >= space.rx_packet {
            space.rx_packet = packet;
            // Update outgoing spin bit, inverting iff we're the client
            self.spin = self.side.is_client() ^ spin;
        }
    }

    fn reset_idle_timeout(&mut self, now: Instant, space: SpaceId) {
        let timeout = match self.idle_timeout {
            None => return,
            Some(dur) => dur,
        };
        if self.state.is_closed() {
            self.timers.stop(Timer::Idle);
            return;
        }
        let dt = cmp::max(timeout, 3 * self.pto(space));
        self.timers.set(Timer::Idle, now + dt);
    }

    fn reset_keep_alive(&mut self, now: Instant) {
        let interval = match self.config.keep_alive_interval {
            Some(x) if self.state.is_established() => x,
            _ => return,
        };
        self.timers.set(Timer::KeepAlive, now + interval);
    }

    fn reset_cid_retirement(&mut self) {
        if let Some(t) = self.local_cid_state.next_timeout() {
            self.timers.set(Timer::PushNewCid, t);
        }
    }

    /// Handle the already-decrypted first packet from the client
    ///
    /// Decrypting the first packet in the `Endpoint` allows stateless packet handling to be more
    /// efficient.
    pub(crate) fn handle_first_packet(
        &mut self,
        now: Instant,
        remote: SocketAddr,
        ecn: Option<EcnCodepoint>,
        packet_number: u64,
        packet: InitialPacket,
        remaining: Option<BytesMut>,
    ) -> Result<(), ConnectionError> {
        let span = trace_span!("first recv");
        let _guard = span.enter();
        debug_assert!(self.side.is_server());
        let len = packet.header_data.len() + packet.payload.len();
        self.path.total_recvd = len as u64;

        match self.state {
            State::Handshake(ref mut state) => {
                state.expected_token = packet.header.token.clone();
            }
            _ => unreachable!("first packet must be delivered in Handshake state"),
        }

        self.on_packet_authenticated(
            now,
            SpaceId::Initial,
            ecn,
            Some(packet_number),
            false,
            false,
        );

        self.process_decrypted_packet(now, remote, Some(packet_number), packet.into())?;
        if let Some(data) = remaining {
            self.handle_coalesced(now, remote, ecn, data);
        }

        #[cfg(feature = "__qlog")]
        self.emit_qlog_recovery_metrics(now);

        Ok(())
    }

    fn init_0rtt(&mut self) {
        let (header, packet) = match self.crypto.early_crypto() {
            Some(x) => x,
            None => return,
        };
        if self.side.is_client() {
            match self.crypto.transport_parameters() {
                Ok(params) => {
                    let params = params
                        .expect("crypto layer didn't supply transport parameters with ticket");
                    // Certain values must not be cached
                    let params = TransportParameters {
                        initial_src_cid: None,
                        original_dst_cid: None,
                        preferred_address: None,
                        retry_src_cid: None,
                        stateless_reset_token: None,
                        min_ack_delay: None,
                        ack_delay_exponent: TransportParameters::default().ack_delay_exponent,
                        max_ack_delay: TransportParameters::default().max_ack_delay,
                        ..params
                    };
                    self.set_peer_params(params);
                }
                Err(e) => {
                    error!("session ticket has malformed transport parameters: {}", e);
                    return;
                }
            }
        }
        trace!("0-RTT enabled");
        self.zero_rtt_enabled = true;
        self.zero_rtt_crypto = Some(ZeroRttCrypto { header, packet });
    }

    fn read_crypto(
        &mut self,
        space: SpaceId,
        crypto: &frame::Crypto,
        payload_len: usize,
    ) -> Result<(), TransportError> {
        let expected = if !self.state.is_handshake() {
            SpaceId::Data
        } else if self.highest_space == SpaceId::Initial {
            SpaceId::Initial
        } else {
            // On the server, self.highest_space can be Data after receiving the client's first
            // flight, but we expect Handshake CRYPTO until the handshake is complete.
            SpaceId::Handshake
        };
        // We can't decrypt Handshake packets when highest_space is Initial, CRYPTO frames in 0-RTT
        // packets are illegal, and we don't process 1-RTT packets until the handshake is
        // complete. Therefore, we will never see CRYPTO data from a later-than-expected space.
        debug_assert!(space <= expected, "received out-of-order CRYPTO data");

        let end = crypto.offset + crypto.data.len() as u64;
        if space < expected && end > self.spaces[space].crypto_stream.bytes_read() {
            warn!(
                "received new {:?} CRYPTO data when expecting {:?}",
                space, expected
            );
            return Err(TransportError::PROTOCOL_VIOLATION(
                "new data at unexpected encryption level",
            ));
        }

        // Detect PQC usage from CRYPTO frame data before processing
        self.pqc_state.detect_pqc_from_crypto(&crypto.data, space);

        // Check if we should trigger MTU discovery for PQC
        if self.pqc_state.should_trigger_mtu_discovery() {
            // Request larger MTU for PQC handshakes
            self.path
                .mtud
                .reset(self.pqc_state.min_initial_size(), self.config.min_mtu);
            trace!("Triggered MTU discovery for PQC handshake");
        }

        let space = &mut self.spaces[space];
        let max = end.saturating_sub(space.crypto_stream.bytes_read());
        if max > self.config.crypto_buffer_size as u64 {
            return Err(TransportError::CRYPTO_BUFFER_EXCEEDED(""));
        }

        space
            .crypto_stream
            .insert(crypto.offset, crypto.data.clone(), payload_len);
        while let Some(chunk) = space.crypto_stream.read(usize::MAX, true) {
            trace!("consumed {} CRYPTO bytes", chunk.bytes.len());
            if self.crypto.read_handshake(&chunk.bytes)? {
                self.events.push_back(Event::HandshakeDataReady);
            }
        }

        Ok(())
    }

    fn write_crypto(&mut self) {
        loop {
            let space = self.highest_space;
            let mut outgoing = Vec::new();
            if let Some(crypto) = self.crypto.write_handshake(&mut outgoing) {
                match space {
                    SpaceId::Initial => {
                        self.upgrade_crypto(SpaceId::Handshake, crypto);
                    }
                    SpaceId::Handshake => {
                        self.upgrade_crypto(SpaceId::Data, crypto);
                    }
                    _ => unreachable!("got updated secrets during 1-RTT"),
                }
            }
            if outgoing.is_empty() {
                if space == self.highest_space {
                    break;
                } else {
                    // Keys updated, check for more data to send
                    continue;
                }
            }
            let offset = self.spaces[space].crypto_offset;
            let outgoing = Bytes::from(outgoing);
            if let State::Handshake(ref mut state) = self.state {
                if space == SpaceId::Initial && offset == 0 && self.side.is_client() {
                    state.client_hello = Some(outgoing.clone());
                }
            }
            self.spaces[space].crypto_offset += outgoing.len() as u64;
            trace!("wrote {} {:?} CRYPTO bytes", outgoing.len(), space);

            // Use PQC-aware fragmentation for large CRYPTO data
            let use_pqc_fragmentation = self.pqc_state.using_pqc && outgoing.len() > 1200;

            if use_pqc_fragmentation {
                // Fragment large CRYPTO data for PQC handshakes
                let frames = self.pqc_state.packet_handler.fragment_crypto_data(
                    &outgoing,
                    offset,
                    self.pqc_state.min_initial_size() as usize,
                );
                for frame in frames {
                    self.spaces[space].pending.crypto.push_back(frame);
                }
            } else {
                // Normal CRYPTO frame for non-PQC or small data
                self.spaces[space].pending.crypto.push_back(frame::Crypto {
                    offset,
                    data: outgoing,
                });
            }
        }
    }

    /// Switch to stronger cryptography during handshake
    fn upgrade_crypto(&mut self, space: SpaceId, crypto: Keys) {
        debug_assert!(
            self.spaces[space].crypto.is_none(),
            "already reached packet space {space:?}"
        );
        trace!("{:?} keys ready", space);
        if space == SpaceId::Data {
            // Precompute the first key update
            self.next_crypto = Some(
                self.crypto
                    .next_1rtt_keys()
                    .expect("handshake should be complete"),
            );
        }

        self.spaces[space].crypto = Some(crypto);
        debug_assert!(space as usize > self.highest_space as usize);
        self.highest_space = space;
        if space == SpaceId::Data && self.side.is_client() {
            // Discard 0-RTT keys because 1-RTT keys are available.
            self.zero_rtt_crypto = None;
        }
    }

    fn discard_space(&mut self, now: Instant, space_id: SpaceId) {
        debug_assert!(space_id != SpaceId::Data);
        trace!("discarding {:?} keys", space_id);
        if space_id == SpaceId::Initial {
            // No longer needed
            if let ConnectionSide::Client { token, .. } = &mut self.side {
                *token = Bytes::new();
            }
        }
        let space = &mut self.spaces[space_id];
        space.crypto = None;
        space.time_of_last_ack_eliciting_packet = None;
        space.loss_time = None;
        space.in_flight = 0;
        let sent_packets = mem::take(&mut space.sent_packets);
        for (pn, packet) in sent_packets.into_iter() {
            self.remove_in_flight(pn, &packet);
        }
        self.set_loss_detection_timer(now)
    }

    fn handle_coalesced(
        &mut self,
        now: Instant,
        remote: SocketAddr,
        ecn: Option<EcnCodepoint>,
        data: BytesMut,
    ) {
        self.path.total_recvd = self.path.total_recvd.saturating_add(data.len() as u64);
        let mut remaining = Some(data);
        while let Some(data) = remaining {
            match PartialDecode::new(
                data,
                &FixedLengthConnectionIdParser::new(self.local_cid_state.cid_len()),
                &[self.version],
                self.endpoint_config.grease_quic_bit,
            ) {
                Ok((partial_decode, rest)) => {
                    remaining = rest;
                    self.handle_decode(now, remote, ecn, partial_decode);
                }
                Err(e) => {
                    trace!("malformed header: {}", e);
                    return;
                }
            }
        }
    }

    fn handle_decode(
        &mut self,
        now: Instant,
        remote: SocketAddr,
        ecn: Option<EcnCodepoint>,
        partial_decode: PartialDecode,
    ) {
        if let Some(decoded) = packet_crypto::unprotect_header(
            partial_decode,
            &self.spaces,
            self.zero_rtt_crypto.as_ref(),
            self.peer_params.stateless_reset_token,
        ) {
            self.handle_packet(now, remote, ecn, decoded.packet, decoded.stateless_reset);
        }
    }

    fn handle_packet(
        &mut self,
        now: Instant,
        remote: SocketAddr,
        ecn: Option<EcnCodepoint>,
        packet: Option<Packet>,
        stateless_reset: bool,
    ) {
        self.stats.udp_rx.ios += 1;
        if let Some(ref packet) = packet {
            trace!(
                "got {:?} packet ({} bytes) from {} using id {}",
                packet.header.space(),
                packet.payload.len() + packet.header_data.len(),
                remote,
                packet.header.dst_cid(),
            );

            // Trace packet received
            #[cfg(feature = "trace")]
            {
                use crate::trace_packet_received;
                // Tracing imports handled by macros
                let packet_size = packet.payload.len() + packet.header_data.len();
                trace_packet_received!(
                    &self.event_log,
                    self.trace_context.trace_id(),
                    packet_size as u32,
                    0 // Will be updated when packet number is decoded
                );
            }
        }

        if self.is_handshaking() && remote != self.path.remote {
            debug!("discarding packet with unexpected remote during handshake");
            return;
        }

        let was_closed = self.state.is_closed();
        let was_drained = self.state.is_drained();

        let decrypted = match packet {
            None => Err(None),
            Some(mut packet) => self
                .decrypt_packet(now, &mut packet)
                .map(move |number| (packet, number)),
        };
        let result = match decrypted {
            _ if stateless_reset => {
                debug!("got stateless reset");
                Err(ConnectionError::Reset)
            }
            Err(Some(e)) => {
                warn!("illegal packet: {}", e);
                Err(e.into())
            }
            Err(None) => {
                debug!("failed to authenticate packet");
                self.authentication_failures += 1;
                let integrity_limit = self.spaces[self.highest_space]
                    .crypto
                    .as_ref()
                    .unwrap()
                    .packet
                    .local
                    .integrity_limit();
                if self.authentication_failures > integrity_limit {
                    Err(TransportError::AEAD_LIMIT_REACHED("integrity limit violated").into())
                } else {
                    return;
                }
            }
            Ok((packet, number)) => {
                let span = match number {
                    Some(pn) => trace_span!("recv", space = ?packet.header.space(), pn),
                    None => trace_span!("recv", space = ?packet.header.space()),
                };
                let _guard = span.enter();

                let is_duplicate = |n| self.spaces[packet.header.space()].dedup.insert(n);
                if number.is_some_and(is_duplicate) {
                    debug!("discarding possible duplicate packet");
                    return;
                } else if self.state.is_handshake() && packet.header.is_short() {
                    // TODO: SHOULD buffer these to improve reordering tolerance.
                    trace!("dropping short packet during handshake");
                    return;
                } else {
                    if let Header::Initial(InitialHeader { ref token, .. }) = packet.header {
                        if let State::Handshake(ref hs) = self.state {
                            if self.side.is_server() && token != &hs.expected_token {
                                // Clients must send the same retry token in every Initial. Initial
                                // packets can be spoofed, so we discard rather than killing the
                                // connection.
                                warn!("discarding Initial with invalid retry token");
                                return;
                            }
                        }
                    }

                    if !self.state.is_closed() {
                        let spin = match packet.header {
                            Header::Short { spin, .. } => spin,
                            _ => false,
                        };
                        self.on_packet_authenticated(
                            now,
                            packet.header.space(),
                            ecn,
                            number,
                            spin,
                            packet.header.is_1rtt(),
                        );
                    }

                    self.process_decrypted_packet(now, remote, number, packet)
                }
            }
        };

        // State transitions for error cases
        if let Err(conn_err) = result {
            self.error = Some(conn_err.clone());
            self.state = match conn_err {
                ConnectionError::ApplicationClosed(reason) => State::closed(reason),
                ConnectionError::ConnectionClosed(reason) => State::closed(reason),
                ConnectionError::Reset
                | ConnectionError::TransportError(TransportError {
                    code: TransportErrorCode::AEAD_LIMIT_REACHED,
                    ..
                }) => State::Drained,
                ConnectionError::TimedOut => {
                    unreachable!("timeouts aren't generated by packet processing");
                }
                ConnectionError::TransportError(err) => {
                    debug!("closing connection due to transport error: {}", err);
                    State::closed(err)
                }
                ConnectionError::VersionMismatch => State::Draining,
                ConnectionError::LocallyClosed => {
                    unreachable!("LocallyClosed isn't generated by packet processing");
                }
                ConnectionError::CidsExhausted => {
                    unreachable!("CidsExhausted isn't generated by packet processing");
                }
            };
        }

        if !was_closed && self.state.is_closed() {
            self.close_common();
            if !self.state.is_drained() {
                self.set_close_timer(now);
            }
        }
        if !was_drained && self.state.is_drained() {
            self.endpoint_events.push_back(EndpointEventInner::Drained);
            // Close timer may have been started previously, e.g. if we sent a close and got a
            // stateless reset in response
            self.timers.stop(Timer::Close);
        }

        // Transmit CONNECTION_CLOSE if necessary
        if let State::Closed(_) = self.state {
            self.close = remote == self.path.remote;
        }
    }

    fn process_decrypted_packet(
        &mut self,
        now: Instant,
        remote: SocketAddr,
        number: Option<u64>,
        packet: Packet,
    ) -> Result<(), ConnectionError> {
        let state = match self.state {
            State::Established => {
                match packet.header.space() {
                    SpaceId::Data => self.process_payload(now, remote, number.unwrap(), packet)?,
                    _ if packet.header.has_frames() => self.process_early_payload(now, packet)?,
                    _ => {
                        trace!("discarding unexpected pre-handshake packet");
                    }
                }
                return Ok(());
            }
            State::Closed(_) => {
                for result in frame::Iter::new(packet.payload.freeze())? {
                    let frame = match result {
                        Ok(frame) => frame,
                        Err(err) => {
                            debug!("frame decoding error: {err:?}");
                            continue;
                        }
                    };

                    if let Frame::Padding = frame {
                        continue;
                    };

                    self.stats.frame_rx.record(&frame);

                    if let Frame::Close(_) = frame {
                        trace!("draining");
                        self.state = State::Draining;
                        break;
                    }
                }
                return Ok(());
            }
            State::Draining | State::Drained => return Ok(()),
            State::Handshake(ref mut state) => state,
        };

        match packet.header {
            Header::Retry {
                src_cid: rem_cid, ..
            } => {
                if self.side.is_server() {
                    return Err(TransportError::PROTOCOL_VIOLATION("client sent Retry").into());
                }

                if self.total_authed_packets > 1
                            || packet.payload.len() <= 16 // token + 16 byte tag
                            || !self.crypto.is_valid_retry(
                                &self.rem_cids.active(),
                                &packet.header_data,
                                &packet.payload,
                            )
                {
                    trace!("discarding invalid Retry");
                    // - After the client has received and processed an Initial or Retry
                    //   packet from the server, it MUST discard any subsequent Retry
                    //   packets that it receives.
                    // - A client MUST discard a Retry packet with a zero-length Retry Token
                    //   field.
                    // - Clients MUST discard Retry packets that have a Retry Integrity Tag
                    //   that cannot be validated
                    return Ok(());
                }

                trace!("retrying with CID {}", rem_cid);
                let client_hello = state.client_hello.take().unwrap();
                self.retry_src_cid = Some(rem_cid);
                self.rem_cids.update_initial_cid(rem_cid);
                self.rem_handshake_cid = rem_cid;

                let space = &mut self.spaces[SpaceId::Initial];
                if let Some(info) = space.take(0) {
                    self.on_packet_acked(now, 0, info);
                };

                self.discard_space(now, SpaceId::Initial); // Make sure we clean up after any retransmitted Initials
                self.spaces[SpaceId::Initial] = PacketSpace {
                    crypto: Some(self.crypto.initial_keys(&rem_cid, self.side.side())),
                    next_packet_number: self.spaces[SpaceId::Initial].next_packet_number,
                    crypto_offset: client_hello.len() as u64,
                    ..PacketSpace::new(now)
                };
                self.spaces[SpaceId::Initial]
                    .pending
                    .crypto
                    .push_back(frame::Crypto {
                        offset: 0,
                        data: client_hello,
                    });

                // Retransmit all 0-RTT data
                let zero_rtt = mem::take(&mut self.spaces[SpaceId::Data].sent_packets);
                for (pn, info) in zero_rtt {
                    self.remove_in_flight(pn, &info);
                    self.spaces[SpaceId::Data].pending |= info.retransmits;
                }
                self.streams.retransmit_all_for_0rtt();

                let token_len = packet.payload.len() - 16;
                let ConnectionSide::Client { ref mut token, .. } = self.side else {
                    unreachable!("we already short-circuited if we're server");
                };
                *token = packet.payload.freeze().split_to(token_len);
                self.state = State::Handshake(state::Handshake {
                    expected_token: Bytes::new(),
                    rem_cid_set: false,
                    client_hello: None,
                });
                Ok(())
            }
            Header::Long {
                ty: LongType::Handshake,
                src_cid: rem_cid,
                ..
            } => {
                if rem_cid != self.rem_handshake_cid {
                    debug!(
                        "discarding packet with mismatched remote CID: {} != {}",
                        self.rem_handshake_cid, rem_cid
                    );
                    return Ok(());
                }
                self.on_path_validated();

                self.process_early_payload(now, packet)?;
                if self.state.is_closed() {
                    return Ok(());
                }

                if self.crypto.is_handshaking() {
                    trace!("handshake ongoing");
                    return Ok(());
                }

                if self.side.is_client() {
                    // Client-only because server params were set from the client's Initial
                    let params =
                        self.crypto
                            .transport_parameters()?
                            .ok_or_else(|| TransportError {
                                code: TransportErrorCode::crypto(0x6d),
                                frame: None,
                                reason: "transport parameters missing".into(),
                            })?;

                    if self.has_0rtt() {
                        if !self.crypto.early_data_accepted().unwrap() {
                            debug_assert!(self.side.is_client());
                            debug!("0-RTT rejected");
                            self.accepted_0rtt = false;
                            self.streams.zero_rtt_rejected();

                            // Discard already-queued frames
                            self.spaces[SpaceId::Data].pending = Retransmits::default();

                            // Discard 0-RTT packets
                            let sent_packets =
                                mem::take(&mut self.spaces[SpaceId::Data].sent_packets);
                            for (pn, packet) in sent_packets {
                                self.remove_in_flight(pn, &packet);
                            }
                        } else {
                            self.accepted_0rtt = true;
                            params.validate_resumption_from(&self.peer_params)?;
                        }
                    }
                    if let Some(token) = params.stateless_reset_token {
                        self.endpoint_events
                            .push_back(EndpointEventInner::ResetToken(self.path.remote, token));
                    }
                    self.handle_peer_params(params)?;
                    self.issue_first_cids(now);
                } else {
                    // Server-only
                    self.spaces[SpaceId::Data].pending.handshake_done = true;
                    self.discard_space(now, SpaceId::Handshake);
                }

                self.events.push_back(Event::Connected);
                self.state = State::Established;
                trace!("established");
                Ok(())
            }
            Header::Initial(InitialHeader {
                src_cid: rem_cid, ..
            }) => {
                if !state.rem_cid_set {
                    trace!("switching remote CID to {}", rem_cid);
                    let mut state = state.clone();
                    self.rem_cids.update_initial_cid(rem_cid);
                    self.rem_handshake_cid = rem_cid;
                    self.orig_rem_cid = rem_cid;
                    state.rem_cid_set = true;
                    self.state = State::Handshake(state);
                } else if rem_cid != self.rem_handshake_cid {
                    debug!(
                        "discarding packet with mismatched remote CID: {} != {}",
                        self.rem_handshake_cid, rem_cid
                    );
                    return Ok(());
                }

                let starting_space = self.highest_space;
                self.process_early_payload(now, packet)?;

                if self.side.is_server()
                    && starting_space == SpaceId::Initial
                    && self.highest_space != SpaceId::Initial
                {
                    let params =
                        self.crypto
                            .transport_parameters()?
                            .ok_or_else(|| TransportError {
                                code: TransportErrorCode::crypto(0x6d),
                                frame: None,
                                reason: "transport parameters missing".into(),
                            })?;
                    self.handle_peer_params(params)?;
                    self.issue_first_cids(now);
                    self.init_0rtt();
                }
                Ok(())
            }
            Header::Long {
                ty: LongType::ZeroRtt,
                ..
            } => {
                self.process_payload(now, remote, number.unwrap(), packet)?;
                Ok(())
            }
            Header::VersionNegotiate { .. } => {
                if self.total_authed_packets > 1 {
                    return Ok(());
                }
                let supported = packet
                    .payload
                    .chunks(4)
                    .any(|x| match <[u8; 4]>::try_from(x) {
                        Ok(version) => self.version == u32::from_be_bytes(version),
                        Err(_) => false,
                    });
                if supported {
                    return Ok(());
                }
                debug!("remote doesn't support our version");
                Err(ConnectionError::VersionMismatch)
            }
            Header::Short { .. } => unreachable!(
                "short packets received during handshake are discarded in handle_packet"
            ),
        }
    }

    /// Process an Initial or Handshake packet payload
    fn process_early_payload(
        &mut self,
        now: Instant,
        packet: Packet,
    ) -> Result<(), TransportError> {
        debug_assert_ne!(packet.header.space(), SpaceId::Data);
        let payload_len = packet.payload.len();
        let mut ack_eliciting = false;
        for result in frame::Iter::new(packet.payload.freeze())? {
            let frame = result?;
            let span = match frame {
                Frame::Padding => continue,
                _ => Some(trace_span!("frame", ty = %frame.ty())),
            };

            self.stats.frame_rx.record(&frame);

            let _guard = span.as_ref().map(|x| x.enter());
            ack_eliciting |= frame.is_ack_eliciting();

            // Process frames
            match frame {
                Frame::Padding | Frame::Ping => {}
                Frame::Crypto(frame) => {
                    self.read_crypto(packet.header.space(), &frame, payload_len)?;
                }
                Frame::Ack(ack) => {
                    self.on_ack_received(now, packet.header.space(), ack)?;
                }
                Frame::Close(reason) => {
                    self.error = Some(reason.into());
                    self.state = State::Draining;
                    return Ok(());
                }
                _ => {
                    let mut err =
                        TransportError::PROTOCOL_VIOLATION("illegal frame type in handshake");
                    err.frame = Some(frame.ty());
                    return Err(err);
                }
            }
        }

        if ack_eliciting {
            // In the initial and handshake spaces, ACKs must be sent immediately
            self.spaces[packet.header.space()]
                .pending_acks
                .set_immediate_ack_required();
        }

        self.write_crypto();
        Ok(())
    }

    fn process_payload(
        &mut self,
        now: Instant,
        remote: SocketAddr,
        number: u64,
        packet: Packet,
    ) -> Result<(), TransportError> {
        let payload = packet.payload.freeze();
        let mut is_probing_packet = true;
        let mut close = None;
        let payload_len = payload.len();
        let mut ack_eliciting = false;
        for result in frame::Iter::new(payload)? {
            let frame = result?;
            let span = match frame {
                Frame::Padding => continue,
                _ => Some(trace_span!("frame", ty = %frame.ty())),
            };

            self.stats.frame_rx.record(&frame);
            // Crypto, Stream and Datagram frames are special cased in order no pollute
            // the log with payload data
            match &frame {
                Frame::Crypto(f) => {
                    trace!(offset = f.offset, len = f.data.len(), "got crypto frame");
                }
                Frame::Stream(f) => {
                    trace!(id = %f.id, offset = f.offset, len = f.data.len(), fin = f.fin, "got stream frame");
                }
                Frame::Datagram(f) => {
                    trace!(len = f.data.len(), "got datagram frame");
                }
                f => {
                    trace!("got frame {:?}", f);
                }
            }

            let _guard = span.as_ref().map(|x| x.enter());
            if packet.header.is_0rtt() {
                match frame {
                    Frame::Crypto(_) | Frame::Close(Close::Application(_)) => {
                        return Err(TransportError::PROTOCOL_VIOLATION(
                            "illegal frame type in 0-RTT",
                        ));
                    }
                    _ => {}
                }
            }
            ack_eliciting |= frame.is_ack_eliciting();

            // Check whether this could be a probing packet
            match frame {
                Frame::Padding
                | Frame::PathChallenge(_)
                | Frame::PathResponse(_)
                | Frame::NewConnectionId(_) => {}
                _ => {
                    is_probing_packet = false;
                }
            }
            match frame {
                Frame::Crypto(frame) => {
                    self.read_crypto(SpaceId::Data, &frame, payload_len)?;
                }
                Frame::Stream(frame) => {
                    if self.streams.received(frame, payload_len)?.should_transmit() {
                        self.spaces[SpaceId::Data].pending.max_data = true;
                    }
                }
                Frame::Ack(ack) => {
                    self.on_ack_received(now, SpaceId::Data, ack)?;
                }
                Frame::Padding | Frame::Ping => {}
                Frame::Close(reason) => {
                    close = Some(reason);
                }
                Frame::PathChallenge(token) => {
                    self.path_responses.push(number, token, remote);
                    if remote == self.path.remote {
                        // PATH_CHALLENGE on active path, possible off-path packet forwarding
                        // attack. Send a non-probing packet to recover the active path.
                        match self.peer_supports_ack_frequency() {
                            true => self.immediate_ack(),
                            false => self.ping(),
                        }
                    }
                }
                Frame::PathResponse(token) => {
                    if self.path.challenge == Some(token) && remote == self.path.remote {
                        trace!("new path validated");
                        self.timers.stop(Timer::PathValidation);
                        self.path.challenge = None;
                        self.path.validated = true;
                        if let Some((_, ref mut prev_path)) = self.prev_path {
                            prev_path.challenge = None;
                            prev_path.challenge_pending = false;
                        }
                        self.on_path_validated();
                    } else if let Some(nat_traversal) = &mut self.nat_traversal {
                        // Check if this is a response to NAT traversal PATH_CHALLENGE
                        match nat_traversal.handle_validation_success(remote, token, now) {
                            Ok(sequence) => {
                                trace!(
                                    "NAT traversal candidate {} validated for sequence {}",
                                    remote, sequence
                                );

                                // Check if this was part of a coordination round
                                if nat_traversal.handle_coordination_success(remote, now) {
                                    trace!("Coordination succeeded via {}", remote);

                                    // Check if we should migrate to this better path
                                    let can_migrate = match &self.side {
                                        ConnectionSide::Client { .. } => true, // Clients can always migrate
                                        ConnectionSide::Server { server_config } => {
                                            server_config.migration
                                        }
                                    };

                                    if can_migrate {
                                        // Get the best paths to see if this new one is better
                                        let best_pairs = nat_traversal.get_best_succeeded_pairs();
                                        if let Some(best) = best_pairs.first() {
                                            if best.remote_addr == remote
                                                && best.remote_addr != self.path.remote
                                            {
                                                debug!(
                                                    "NAT traversal found better path, initiating migration"
                                                );
                                                // Trigger migration to the better NAT-traversed path
                                                if let Err(e) =
                                                    self.migrate_to_nat_traversal_path(now)
                                                {
                                                    warn!(
                                                        "Failed to migrate to NAT traversal path: {:?}",
                                                        e
                                                    );
                                                }
                                            }
                                        }
                                    }
                                } else {
                                    // Mark the candidate pair as succeeded for regular validation
                                    if nat_traversal.mark_pair_succeeded(remote) {
                                        trace!("NAT traversal pair succeeded for {}", remote);
                                    }
                                }
                            }
                            Err(NatTraversalError::ChallengeMismatch) => {
                                debug!(
                                    "PATH_RESPONSE challenge mismatch for NAT candidate {}",
                                    remote
                                );
                            }
                            Err(e) => {
                                debug!("NAT traversal validation error: {}", e);
                            }
                        }
                    } else {
                        debug!(token, "ignoring invalid PATH_RESPONSE");
                    }
                }
                Frame::MaxData(bytes) => {
                    self.streams.received_max_data(bytes);
                }
                Frame::MaxStreamData { id, offset } => {
                    self.streams.received_max_stream_data(id, offset)?;
                }
                Frame::MaxStreams { dir, count } => {
                    self.streams.received_max_streams(dir, count)?;
                }
                Frame::ResetStream(frame) => {
                    if self.streams.received_reset(frame)?.should_transmit() {
                        self.spaces[SpaceId::Data].pending.max_data = true;
                    }
                }
                Frame::DataBlocked { offset } => {
                    debug!(offset, "peer claims to be blocked at connection level");
                }
                Frame::StreamDataBlocked { id, offset } => {
                    if id.initiator() == self.side.side() && id.dir() == Dir::Uni {
                        debug!("got STREAM_DATA_BLOCKED on send-only {}", id);
                        return Err(TransportError::STREAM_STATE_ERROR(
                            "STREAM_DATA_BLOCKED on send-only stream",
                        ));
                    }
                    debug!(
                        stream = %id,
                        offset, "peer claims to be blocked at stream level"
                    );
                }
                Frame::StreamsBlocked { dir, limit } => {
                    if limit > MAX_STREAM_COUNT {
                        return Err(TransportError::FRAME_ENCODING_ERROR(
                            "unrepresentable stream limit",
                        ));
                    }
                    debug!(
                        "peer claims to be blocked opening more than {} {} streams",
                        limit, dir
                    );
                }
                Frame::StopSending(frame::StopSending { id, error_code }) => {
                    if id.initiator() != self.side.side() {
                        if id.dir() == Dir::Uni {
                            debug!("got STOP_SENDING on recv-only {}", id);
                            return Err(TransportError::STREAM_STATE_ERROR(
                                "STOP_SENDING on recv-only stream",
                            ));
                        }
                    } else if self.streams.is_local_unopened(id) {
                        return Err(TransportError::STREAM_STATE_ERROR(
                            "STOP_SENDING on unopened stream",
                        ));
                    }
                    self.streams.received_stop_sending(id, error_code);
                }
                Frame::RetireConnectionId { sequence } => {
                    let allow_more_cids = self
                        .local_cid_state
                        .on_cid_retirement(sequence, self.peer_params.issue_cids_limit())?;
                    self.endpoint_events
                        .push_back(EndpointEventInner::RetireConnectionId(
                            now,
                            sequence,
                            allow_more_cids,
                        ));
                }
                Frame::NewConnectionId(frame) => {
                    trace!(
                        sequence = frame.sequence,
                        id = %frame.id,
                        retire_prior_to = frame.retire_prior_to,
                    );
                    if self.rem_cids.active().is_empty() {
                        return Err(TransportError::PROTOCOL_VIOLATION(
                            "NEW_CONNECTION_ID when CIDs aren't in use",
                        ));
                    }
                    if frame.retire_prior_to > frame.sequence {
                        return Err(TransportError::PROTOCOL_VIOLATION(
                            "NEW_CONNECTION_ID retiring unissued CIDs",
                        ));
                    }

                    use crate::cid_queue::InsertError;
                    match self.rem_cids.insert(frame) {
                        Ok(None) => {}
                        Ok(Some((retired, reset_token))) => {
                            let pending_retired =
                                &mut self.spaces[SpaceId::Data].pending.retire_cids;
                            /// Ensure `pending_retired` cannot grow without bound. Limit is
                            /// somewhat arbitrary but very permissive.
                            const MAX_PENDING_RETIRED_CIDS: u64 = CidQueue::LEN as u64 * 10;
                            // We don't bother counting in-flight frames because those are bounded
                            // by congestion control.
                            if (pending_retired.len() as u64)
                                .saturating_add(retired.end.saturating_sub(retired.start))
                                > MAX_PENDING_RETIRED_CIDS
                            {
                                return Err(TransportError::CONNECTION_ID_LIMIT_ERROR(
                                    "queued too many retired CIDs",
                                ));
                            }
                            pending_retired.extend(retired);
                            self.set_reset_token(reset_token);
                        }
                        Err(InsertError::ExceedsLimit) => {
                            return Err(TransportError::CONNECTION_ID_LIMIT_ERROR(""));
                        }
                        Err(InsertError::Retired) => {
                            trace!("discarding already-retired");
                            // RETIRE_CONNECTION_ID might not have been previously sent if e.g. a
                            // range of connection IDs larger than the active connection ID limit
                            // was retired all at once via retire_prior_to.
                            self.spaces[SpaceId::Data]
                                .pending
                                .retire_cids
                                .push(frame.sequence);
                            continue;
                        }
                    };

                    if self.side.is_server() && self.rem_cids.active_seq() == 0 {
                        // We're a server still using the initial remote CID for the client, so
                        // let's switch immediately to enable clientside stateless resets.
                        self.update_rem_cid();
                    }
                }
                Frame::NewToken(NewToken { token }) => {
                    let ConnectionSide::Client {
                        token_store,
                        server_name,
                        ..
                    } = &self.side
                    else {
                        return Err(TransportError::PROTOCOL_VIOLATION("client sent NEW_TOKEN"));
                    };
                    if token.is_empty() {
                        return Err(TransportError::FRAME_ENCODING_ERROR("empty token"));
                    }
                    trace!("got new token");
                    token_store.insert(server_name, token);
                }
                Frame::Datagram(datagram) => {
                    if self
                        .datagrams
                        .received(datagram, &self.config.datagram_receive_buffer_size)?
                    {
                        self.events.push_back(Event::DatagramReceived);
                    }
                }
                Frame::AckFrequency(ack_frequency) => {
                    // This frame can only be sent in the Data space
                    let space = &mut self.spaces[SpaceId::Data];

                    if !self
                        .ack_frequency
                        .ack_frequency_received(&ack_frequency, &mut space.pending_acks)?
                    {
                        // The AckFrequency frame is stale (we have already received a more recent one)
                        continue;
                    }

                    // Our `max_ack_delay` has been updated, so we may need to adjust its associated
                    // timeout
                    if let Some(timeout) = space
                        .pending_acks
                        .max_ack_delay_timeout(self.ack_frequency.max_ack_delay)
                    {
                        self.timers.set(Timer::MaxAckDelay, timeout);
                    }
                }
                Frame::ImmediateAck => {
                    // This frame can only be sent in the Data space
                    self.spaces[SpaceId::Data]
                        .pending_acks
                        .set_immediate_ack_required();
                }
                Frame::HandshakeDone => {
                    if self.side.is_server() {
                        return Err(TransportError::PROTOCOL_VIOLATION(
                            "client sent HANDSHAKE_DONE",
                        ));
                    }
                    if self.spaces[SpaceId::Handshake].crypto.is_some() {
                        self.discard_space(now, SpaceId::Handshake);
                    }
                }
                Frame::AddAddress(add_address) => {
                    self.handle_add_address(&add_address, now)?;
                }
                Frame::PunchMeNow(punch_me_now) => {
                    self.handle_punch_me_now(&punch_me_now, now)?;
                }
                Frame::RemoveAddress(remove_address) => {
                    self.handle_remove_address(&remove_address)?;
                }
                Frame::ObservedAddress(observed_address) => {
                    self.handle_observed_address_frame(&observed_address, now)?;
                }
                Frame::TryConnectTo(try_connect_to) => {
                    self.handle_try_connect_to(&try_connect_to, now)?;
                }
                Frame::TryConnectToResponse(response) => {
                    self.handle_try_connect_to_response(&response)?;
                }
            }
        }

        let space = &mut self.spaces[SpaceId::Data];
        if space
            .pending_acks
            .packet_received(now, number, ack_eliciting, &space.dedup)
        {
            self.timers
                .set(Timer::MaxAckDelay, now + self.ack_frequency.max_ack_delay);
        }

        // Issue stream ID credit due to ACKs of outgoing finish/resets and incoming finish/resets
        // on stopped streams. Incoming finishes/resets on open streams are not handled here as they
        // are only freed, and hence only issue credit, once the application has been notified
        // during a read on the stream.
        let pending = &mut self.spaces[SpaceId::Data].pending;
        self.streams.queue_max_stream_id(pending);

        if let Some(reason) = close {
            self.error = Some(reason.into());
            self.state = State::Draining;
            self.close = true;
        }

        if remote != self.path.remote
            && !is_probing_packet
            && number == self.spaces[SpaceId::Data].rx_packet
        {
            let ConnectionSide::Server { ref server_config } = self.side else {
                return Err(TransportError::PROTOCOL_VIOLATION(
                    "packets from unknown remote should be dropped by clients",
                ));
            };
            debug_assert!(
                server_config.migration,
                "migration-initiating packets should have been dropped immediately"
            );
            self.migrate(now, remote);
            // Break linkability, if possible
            self.update_rem_cid();
            self.spin = false;
        }

        Ok(())
    }

    fn migrate(&mut self, now: Instant, remote: SocketAddr) {
        trace!(%remote, "migration initiated");
        // Reset rtt/congestion state for new path unless it looks like a NAT rebinding.
        // Note that the congestion window will not grow until validation terminates. Helps mitigate
        // amplification attacks performed by spoofing source addresses.
        let mut new_path = if remote.is_ipv4() && remote.ip() == self.path.remote.ip() {
            PathData::from_previous(remote, &self.path, now)
        } else {
            let peer_max_udp_payload_size =
                u16::try_from(self.peer_params.max_udp_payload_size.into_inner())
                    .unwrap_or(u16::MAX);
            PathData::new(
                remote,
                self.allow_mtud,
                Some(peer_max_udp_payload_size),
                now,
                &self.config,
            )
        };
        new_path.challenge = Some(self.rng.r#gen());
        new_path.challenge_pending = true;
        let prev_pto = self.pto(SpaceId::Data);

        let mut prev = mem::replace(&mut self.path, new_path);
        // Don't clobber the original path if the previous one hasn't been validated yet
        if prev.challenge.is_none() {
            prev.challenge = Some(self.rng.r#gen());
            prev.challenge_pending = true;
            // We haven't updated the remote CID yet, this captures the remote CID we were using on
            // the previous path.
            self.prev_path = Some((self.rem_cids.active(), prev));
        }

        self.timers.set(
            Timer::PathValidation,
            now + 3 * cmp::max(self.pto(SpaceId::Data), prev_pto),
        );
    }

    /// Handle a change in the local address, i.e. an active migration
    pub fn local_address_changed(&mut self) {
        self.update_rem_cid();
        self.ping();
    }

    /// Migrate to a better path discovered through NAT traversal
    pub fn migrate_to_nat_traversal_path(&mut self, now: Instant) -> Result<(), TransportError> {
        // Extract necessary data before mutable operations
        let (remote_addr, local_addr) = {
            let nat_state = self
                .nat_traversal
                .as_ref()
                .ok_or_else(|| TransportError::PROTOCOL_VIOLATION("NAT traversal not enabled"))?;

            // Get the best validated NAT traversal path
            let best_pairs = nat_state.get_best_succeeded_pairs();
            if best_pairs.is_empty() {
                return Err(TransportError::PROTOCOL_VIOLATION(
                    "No validated NAT traversal paths",
                ));
            }

            // Select the best path (highest priority that's different from current)
            let best_path = best_pairs
                .iter()
                .find(|pair| pair.remote_addr != self.path.remote)
                .or_else(|| best_pairs.first());

            let best_path = best_path.ok_or_else(|| {
                TransportError::PROTOCOL_VIOLATION("No suitable NAT traversal path")
            })?;

            debug!(
                "Migrating to NAT traversal path: {} -> {} (priority: {})",
                self.path.remote, best_path.remote_addr, best_path.priority
            );

            (best_path.remote_addr, best_path.local_addr)
        };

        // Perform the migration
        self.migrate(now, remote_addr);

        // Update local address if needed
        if local_addr != SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), 0) {
            self.local_ip = Some(local_addr.ip());
        }

        // Queue a PATH_CHALLENGE to confirm the new path
        self.path.challenge_pending = true;

        Ok(())
    }

    /// Switch to a previously unused remote connection ID, if possible
    fn update_rem_cid(&mut self) {
        let (reset_token, retired) = match self.rem_cids.next() {
            Some(x) => x,
            None => return,
        };

        // Retire the current remote CID and any CIDs we had to skip.
        self.spaces[SpaceId::Data]
            .pending
            .retire_cids
            .extend(retired);
        self.set_reset_token(reset_token);
    }

    fn set_reset_token(&mut self, reset_token: ResetToken) {
        self.endpoint_events
            .push_back(EndpointEventInner::ResetToken(
                self.path.remote,
                reset_token,
            ));
        self.peer_params.stateless_reset_token = Some(reset_token);
    }

    /// Issue an initial set of connection IDs to the peer upon connection
    fn issue_first_cids(&mut self, now: Instant) {
        if self.local_cid_state.cid_len() == 0 {
            return;
        }

        // Subtract 1 to account for the CID we supplied while handshaking
        let mut n = self.peer_params.issue_cids_limit() - 1;
        if let ConnectionSide::Server { server_config } = &self.side {
            if server_config.has_preferred_address() {
                // We also sent a CID in the transport parameters
                n -= 1;
            }
        }
        self.endpoint_events
            .push_back(EndpointEventInner::NeedIdentifiers(now, n));
    }

    fn populate_packet(
        &mut self,
        now: Instant,
        space_id: SpaceId,
        buf: &mut Vec<u8>,
        max_size: usize,
        pn: u64,
    ) -> SentFrames {
        let mut sent = SentFrames::default();
        let space = &mut self.spaces[space_id];
        let is_0rtt = space_id == SpaceId::Data && space.crypto.is_none();
        space.pending_acks.maybe_ack_non_eliciting();

        // HANDSHAKE_DONE
        if !is_0rtt && mem::replace(&mut space.pending.handshake_done, false) {
            buf.write(frame::FrameType::HANDSHAKE_DONE);
            sent.retransmits.get_or_create().handshake_done = true;
            // This is just a u8 counter and the frame is typically just sent once
            self.stats.frame_tx.handshake_done =
                self.stats.frame_tx.handshake_done.saturating_add(1);
        }

        // PING
        if mem::replace(&mut space.ping_pending, false) {
            trace!("PING");
            buf.write(frame::FrameType::PING);
            sent.non_retransmits = true;
            self.stats.frame_tx.ping += 1;
        }

        // IMMEDIATE_ACK
        if mem::replace(&mut space.immediate_ack_pending, false) {
            trace!("IMMEDIATE_ACK");
            buf.write(frame::FrameType::IMMEDIATE_ACK);
            sent.non_retransmits = true;
            self.stats.frame_tx.immediate_ack += 1;
        }

        // ACK
        if space.pending_acks.can_send() {
            Self::populate_acks(
                now,
                self.receiving_ecn,
                &mut sent,
                space,
                buf,
                &mut self.stats,
            );
        }

        // ACK_FREQUENCY
        if mem::replace(&mut space.pending.ack_frequency, false) {
            let sequence_number = self.ack_frequency.next_sequence_number();

            // Safe to unwrap because this is always provided when ACK frequency is enabled
            let config = self.config.ack_frequency_config.as_ref().unwrap();

            // Ensure the delay is within bounds to avoid a PROTOCOL_VIOLATION error
            let max_ack_delay = self.ack_frequency.candidate_max_ack_delay(
                self.path.rtt.get(),
                config,
                &self.peer_params,
            );

            trace!(?max_ack_delay, "ACK_FREQUENCY");

            frame::AckFrequency {
                sequence: sequence_number,
                ack_eliciting_threshold: config.ack_eliciting_threshold,
                request_max_ack_delay: max_ack_delay.as_micros().try_into().unwrap_or(VarInt::MAX),
                reordering_threshold: config.reordering_threshold,
            }
            .encode(buf);

            sent.retransmits.get_or_create().ack_frequency = true;

            self.ack_frequency.ack_frequency_sent(pn, max_ack_delay);
            self.stats.frame_tx.ack_frequency += 1;
        }

        // PATH_CHALLENGE
        if buf.len() + 9 < max_size && space_id == SpaceId::Data {
            // Transmit challenges with every outgoing frame on an unvalidated path
            if let Some(token) = self.path.challenge {
                // But only send a packet solely for that purpose at most once
                self.path.challenge_pending = false;
                sent.non_retransmits = true;
                sent.requires_padding = true;
                trace!("PATH_CHALLENGE {:08x}", token);
                buf.write(frame::FrameType::PATH_CHALLENGE);
                buf.write(token);
                self.stats.frame_tx.path_challenge += 1;
            }

            // NAT traversal PATH_CHALLENGE frames are now sent via send_nat_traversal_challenge()
            // which handles multi-destination packet support through the coordination protocol.
        }

        // PATH_RESPONSE
        if buf.len() + 9 < max_size && space_id == SpaceId::Data {
            if let Some(token) = self.path_responses.pop_on_path(self.path.remote) {
                sent.non_retransmits = true;
                sent.requires_padding = true;
                trace!("PATH_RESPONSE {:08x}", token);
                buf.write(frame::FrameType::PATH_RESPONSE);
                buf.write(token);
                self.stats.frame_tx.path_response += 1;
            }
        }

        // CRYPTO
        while buf.len() + frame::Crypto::SIZE_BOUND < max_size && !is_0rtt {
            let mut frame = match space.pending.crypto.pop_front() {
                Some(x) => x,
                None => break,
            };

            // Calculate the maximum amount of crypto data we can store in the buffer.
            // Since the offset is known, we can reserve the exact size required to encode it.
            // For length we reserve 2bytes which allows to encode up to 2^14,
            // which is more than what fits into normally sized QUIC frames.
            let max_crypto_data_size = max_size
                - buf.len()
                - 1 // Frame Type
                - VarInt::size(unsafe { VarInt::from_u64_unchecked(frame.offset) })
                - 2; // Maximum encoded length for frame size, given we send less than 2^14 bytes

            // Use PQC-aware sizing for CRYPTO frames
            let available_space = max_size - buf.len();
            let remaining_data = frame.data.len();
            let optimal_size = self
                .pqc_state
                .calculate_crypto_frame_size(available_space, remaining_data);

            let len = frame
                .data
                .len()
                .min(2usize.pow(14) - 1)
                .min(max_crypto_data_size)
                .min(optimal_size);

            let data = frame.data.split_to(len);
            let truncated = frame::Crypto {
                offset: frame.offset,
                data,
            };
            trace!(
                "CRYPTO: off {} len {}",
                truncated.offset,
                truncated.data.len()
            );
            truncated.encode(buf);
            self.stats.frame_tx.crypto += 1;
            sent.retransmits.get_or_create().crypto.push_back(truncated);
            if !frame.data.is_empty() {
                frame.offset += len as u64;
                space.pending.crypto.push_front(frame);
            }
        }

        if space_id == SpaceId::Data {
            self.streams.write_control_frames(
                buf,
                &mut space.pending,
                &mut sent.retransmits,
                &mut self.stats.frame_tx,
                max_size,
            );
        }

        // NEW_CONNECTION_ID
        while buf.len() + 44 < max_size {
            let issued = match space.pending.new_cids.pop() {
                Some(x) => x,
                None => break,
            };
            trace!(
                sequence = issued.sequence,
                id = %issued.id,
                "NEW_CONNECTION_ID"
            );
            frame::NewConnectionId {
                sequence: issued.sequence,
                retire_prior_to: self.local_cid_state.retire_prior_to(),
                id: issued.id,
                reset_token: issued.reset_token,
            }
            .encode(buf);
            sent.retransmits.get_or_create().new_cids.push(issued);
            self.stats.frame_tx.new_connection_id += 1;
        }

        // RETIRE_CONNECTION_ID
        while buf.len() + frame::RETIRE_CONNECTION_ID_SIZE_BOUND < max_size {
            let seq = match space.pending.retire_cids.pop() {
                Some(x) => x,
                None => break,
            };
            trace!(sequence = seq, "RETIRE_CONNECTION_ID");
            buf.write(frame::FrameType::RETIRE_CONNECTION_ID);
            buf.write_var(seq);
            sent.retransmits.get_or_create().retire_cids.push(seq);
            self.stats.frame_tx.retire_connection_id += 1;
        }

        // DATAGRAM
        let mut sent_datagrams = false;
        while buf.len() + Datagram::SIZE_BOUND < max_size && space_id == SpaceId::Data {
            match self.datagrams.write(buf, max_size) {
                true => {
                    sent_datagrams = true;
                    sent.non_retransmits = true;
                    self.stats.frame_tx.datagram += 1;
                }
                false => break,
            }
        }
        if self.datagrams.send_blocked && sent_datagrams {
            self.events.push_back(Event::DatagramsUnblocked);
            self.datagrams.send_blocked = false;
        }

        // NEW_TOKEN
        while let Some(remote_addr) = space.pending.new_tokens.pop() {
            debug_assert_eq!(space_id, SpaceId::Data);
            let ConnectionSide::Server { server_config } = &self.side else {
                // This should never happen as clients don't enqueue NEW_TOKEN frames
                debug_assert!(false, "NEW_TOKEN frames should not be enqueued by clients");
                continue;
            };

            if remote_addr != self.path.remote {
                // NEW_TOKEN frames contain tokens bound to a client's IP address, and are only
                // useful if used from the same IP address.  Thus, we abandon enqueued NEW_TOKEN
                // frames upon an path change. Instead, when the new path becomes validated,
                // NEW_TOKEN frames may be enqueued for the new path instead.
                continue;
            }

            // If configured to delay until binding and we don't yet have a peer id,
            // postpone NEW_TOKEN issuance.
            if self.delay_new_token_until_binding && self.peer_id_for_tokens.is_none() {
                // Requeue and try again later
                space.pending.new_tokens.push(remote_addr);
                break;
            }

            // Issue token v2 if we have a bound peer id; otherwise fall back to legacy
            let new_token = if let Some(pid) = self.peer_id_for_tokens {
                // Compose token_v2: pt = peer_id[32] || cid_len[1] || cid[..] || nonce16
                // token = pt || nonce12_suffix (last 12 bytes of nonce)
                let nonce_u128: u128 = self.rng.r#gen();
                let nonce = nonce_u128.to_le_bytes();
                let cid = self.rem_cids.active();
                let mut pt = Vec::with_capacity(32 + 1 + cid.len() + 16);
                pt.extend_from_slice(&pid.0);
                pt.push(cid.len() as u8);
                pt.extend_from_slice(&cid[..]);
                pt.extend_from_slice(&nonce);
                let mut tok = pt;
                tok.extend_from_slice(&nonce[..12]);
                NewToken { token: tok.into() }
            } else {
                let token = Token::new(
                    TokenPayload::Validation {
                        ip: remote_addr.ip(),
                        issued: server_config.time_source.now(),
                    },
                    &mut self.rng,
                );
                NewToken {
                    token: token.encode(&*server_config.token_key).into(),
                }
            };

            if buf.len() + new_token.size() >= max_size {
                space.pending.new_tokens.push(remote_addr);
                break;
            }

            new_token.encode(buf);
            sent.retransmits
                .get_or_create()
                .new_tokens
                .push(remote_addr);
            self.stats.frame_tx.new_token += 1;
        }

        // NAT traversal frames - AddAddress
        while buf.len() + frame::AddAddress::SIZE_BOUND < max_size && space_id == SpaceId::Data {
            let add_address = match space.pending.add_addresses.pop() {
                Some(x) => x,
                None => break,
            };
            trace!(
                sequence = %add_address.sequence,
                address = %add_address.address,
                "ADD_ADDRESS"
            );
            // Use the correct encoding format based on negotiated configuration
            if self.nat_traversal_frame_config.use_rfc_format {
                add_address.encode_rfc(buf);
            } else {
                add_address.encode_legacy(buf);
            }
            sent.retransmits
                .get_or_create()
                .add_addresses
                .push(add_address);
            self.stats.frame_tx.add_address += 1;
        }

        // NAT traversal frames - PunchMeNow
        while buf.len() + frame::PunchMeNow::SIZE_BOUND < max_size && space_id == SpaceId::Data {
            let punch_me_now = match space.pending.punch_me_now.pop() {
                Some(x) => x,
                None => break,
            };
            trace!(
                round = %punch_me_now.round,
                paired_with_sequence_number = %punch_me_now.paired_with_sequence_number,
                "PUNCH_ME_NOW"
            );
            // Use the correct encoding format based on negotiated configuration
            if self.nat_traversal_frame_config.use_rfc_format {
                punch_me_now.encode_rfc(buf);
            } else {
                punch_me_now.encode_legacy(buf);
            }
            sent.retransmits
                .get_or_create()
                .punch_me_now
                .push(punch_me_now);
            self.stats.frame_tx.punch_me_now += 1;
        }

        // NAT traversal frames - RemoveAddress
        while buf.len() + frame::RemoveAddress::SIZE_BOUND < max_size && space_id == SpaceId::Data {
            let remove_address = match space.pending.remove_addresses.pop() {
                Some(x) => x,
                None => break,
            };
            trace!(
                sequence = %remove_address.sequence,
                "REMOVE_ADDRESS"
            );
            // RemoveAddress has the same format in both RFC and legacy versions
            remove_address.encode(buf);
            sent.retransmits
                .get_or_create()
                .remove_addresses
                .push(remove_address);
            self.stats.frame_tx.remove_address += 1;
        }

        // OBSERVED_ADDRESS frames
        while buf.len() + frame::ObservedAddress::SIZE_BOUND < max_size && space_id == SpaceId::Data
        {
            let observed_address = match space.pending.outbound_observations.pop() {
                Some(x) => x,
                None => break,
            };
            info!(
                address = %observed_address.address,
                sequence = %observed_address.sequence_number,
                "populate_packet: ENCODING OBSERVED_ADDRESS into packet"
            );
            observed_address.encode(buf);
            sent.retransmits
                .get_or_create()
                .outbound_observations
                .push(observed_address);
            self.stats.frame_tx.observed_address += 1;
        }

        // STREAM
        if space_id == SpaceId::Data {
            sent.stream_frames =
                self.streams
                    .write_stream_frames(buf, max_size, self.config.send_fairness);
            self.stats.frame_tx.stream += sent.stream_frames.len() as u64;
        }

        sent
    }

    /// Write pending ACKs into a buffer
    ///
    /// This method assumes ACKs are pending, and should only be called if
    /// `!PendingAcks::ranges().is_empty()` returns `true`.
    fn populate_acks(
        now: Instant,
        receiving_ecn: bool,
        sent: &mut SentFrames,
        space: &mut PacketSpace,
        buf: &mut Vec<u8>,
        stats: &mut ConnectionStats,
    ) {
        debug_assert!(!space.pending_acks.ranges().is_empty());

        // 0-RTT packets must never carry acks (which would have to be of handshake packets)
        debug_assert!(space.crypto.is_some(), "tried to send ACK in 0-RTT");
        let ecn = if receiving_ecn {
            Some(&space.ecn_counters)
        } else {
            None
        };
        sent.largest_acked = space.pending_acks.ranges().max();

        let delay_micros = space.pending_acks.ack_delay(now).as_micros() as u64;

        // TODO: This should come from `TransportConfig` if that gets configurable.
        let ack_delay_exp = TransportParameters::default().ack_delay_exponent;
        let delay = delay_micros >> ack_delay_exp.into_inner();

        trace!(
            "ACK {:?}, Delay = {}us",
            space.pending_acks.ranges(),
            delay_micros
        );

        frame::Ack::encode(delay as _, space.pending_acks.ranges(), ecn, buf);
        stats.frame_tx.acks += 1;
    }

    fn close_common(&mut self) {
        trace!("connection closed");
        for &timer in &Timer::VALUES {
            self.timers.stop(timer);
        }
    }

    fn set_close_timer(&mut self, now: Instant) {
        self.timers
            .set(Timer::Close, now + 3 * self.pto(self.highest_space));
    }

    /// Handle transport parameters received from the peer
    fn handle_peer_params(&mut self, params: TransportParameters) -> Result<(), TransportError> {
        if Some(self.orig_rem_cid) != params.initial_src_cid
            || (self.side.is_client()
                && (Some(self.initial_dst_cid) != params.original_dst_cid
                    || self.retry_src_cid != params.retry_src_cid))
        {
            return Err(TransportError::TRANSPORT_PARAMETER_ERROR(
                "CID authentication failure",
            ));
        }

        self.set_peer_params(params);

        Ok(())
    }

    fn set_peer_params(&mut self, params: TransportParameters) {
        self.streams.set_params(&params);
        self.idle_timeout =
            negotiate_max_idle_timeout(self.config.max_idle_timeout, Some(params.max_idle_timeout));
        trace!("negotiated max idle timeout {:?}", self.idle_timeout);
        if let Some(ref info) = params.preferred_address {
            self.rem_cids.insert(frame::NewConnectionId {
                sequence: 1,
                id: info.connection_id,
                reset_token: info.stateless_reset_token,
                retire_prior_to: 0,
            }).expect("preferred address CID is the first received, and hence is guaranteed to be legal");
        }
        self.ack_frequency.peer_max_ack_delay = get_max_ack_delay(&params);

        // Handle NAT traversal capability negotiation
        self.negotiate_nat_traversal_capability(&params);

        // Update NAT traversal frame format configuration based on negotiated parameters
        // Check if we have NAT traversal enabled in our config
        let local_has_nat_traversal = self.config.nat_traversal_config.is_some();
        // For now, assume we support RFC if NAT traversal is enabled
        // TODO: Add proper RFC support flag to TransportConfig
        let local_supports_rfc = local_has_nat_traversal;
        self.nat_traversal_frame_config = frame::nat_traversal_unified::NatTraversalFrameConfig {
            // Use RFC format only if both endpoints support it
            use_rfc_format: local_supports_rfc && params.supports_rfc_nat_traversal(),
            // Always accept legacy for backward compatibility
            accept_legacy: true,
        };

        // Handle address discovery negotiation
        self.negotiate_address_discovery(&params);

        // Update PQC state based on peer parameters
        self.pqc_state.update_from_peer_params(&params);

        // If PQC is enabled, adjust MTU discovery configuration
        if self.pqc_state.enabled && self.pqc_state.using_pqc {
            trace!("PQC enabled, adjusting MTU discovery for larger handshake packets");
            // When PQC is enabled, we need to handle larger packets during handshake
            // The actual MTU discovery will probe up to the peer's max_udp_payload_size
            // or the PQC handshake MTU, whichever is smaller
            let current_mtu = self.path.mtud.current_mtu();
            if current_mtu < self.pqc_state.handshake_mtu {
                trace!(
                    "Current MTU {} is less than PQC handshake MTU {}, will rely on MTU discovery",
                    current_mtu, self.pqc_state.handshake_mtu
                );
            }
        }

        self.peer_params = params;
        self.path.mtud.on_peer_max_udp_payload_size_received(
            u16::try_from(self.peer_params.max_udp_payload_size.into_inner()).unwrap_or(u16::MAX),
        );
    }

    /// Negotiate NAT traversal capability between local and peer configurations
    fn negotiate_nat_traversal_capability(&mut self, params: &TransportParameters) {
        // Check if peer supports NAT traversal
        let peer_nat_config = match &params.nat_traversal {
            Some(config) => config,
            None => {
                // Peer doesn't support NAT traversal - handle backward compatibility
                if self.config.nat_traversal_config.is_some() {
                    debug!(
                        "Peer does not support NAT traversal, maintaining backward compatibility"
                    );
                    self.emit_nat_traversal_capability_event(false);

                    // Set connection state to indicate NAT traversal is not available
                    self.set_nat_traversal_compatibility_mode(false);
                }
                return;
            }
        };

        // Check if we support NAT traversal locally
        let local_nat_config = match &self.config.nat_traversal_config {
            Some(config) => config,
            None => {
                debug!("NAT traversal not enabled locally, ignoring peer support");
                self.emit_nat_traversal_capability_event(false);
                self.set_nat_traversal_compatibility_mode(false);
                return;
            }
        };

        // Both peers support NAT traversal - proceed with capability negotiation
        info!("Both peers support NAT traversal, negotiating capabilities");

        // Validate role compatibility and negotiate parameters
        match self.negotiate_nat_traversal_parameters(local_nat_config, peer_nat_config) {
            Ok(negotiated_config) => {
                info!("NAT traversal capability negotiated successfully");
                self.emit_nat_traversal_capability_event(true);

                // Initialize NAT traversal with negotiated parameters
                self.init_nat_traversal_with_negotiated_config(&negotiated_config);

                // Set connection state to indicate NAT traversal is available
                self.set_nat_traversal_compatibility_mode(true);

                // Start NAT traversal process if we're in a client role
                if matches!(
                    negotiated_config,
                    crate::transport_parameters::NatTraversalConfig::ClientSupport
                ) {
                    self.initiate_nat_traversal_process();
                }
            }
            Err(e) => {
                warn!("NAT traversal capability negotiation failed: {}", e);
                self.emit_nat_traversal_capability_event(false);
                self.set_nat_traversal_compatibility_mode(false);
            }
        }
    }

    /// Emit NAT traversal capability negotiation event
    fn emit_nat_traversal_capability_event(&mut self, negotiated: bool) {
        // For now, we'll just log the event
        // In a full implementation, this could emit an event that applications can listen to
        if negotiated {
            info!("NAT traversal capability successfully negotiated");
        } else {
            info!("NAT traversal capability not available (peer or local support missing)");
        }

        // Could add to events queue if needed:
        // self.events.push_back(Event::NatTraversalCapability { negotiated });
    }

    /// Set NAT traversal compatibility mode for backward compatibility
    fn set_nat_traversal_compatibility_mode(&mut self, enabled: bool) {
        if enabled {
            debug!("NAT traversal enabled for this connection");
            // Connection supports NAT traversal - no special handling needed
        } else {
            debug!("NAT traversal disabled for this connection (backward compatibility mode)");
            // Ensure NAT traversal state is cleared if it was partially initialized
            if self.nat_traversal.is_some() {
                warn!("Clearing NAT traversal state due to compatibility mode");
                self.nat_traversal = None;
            }
        }
    }

    /// Negotiate NAT traversal parameters between local and peer configurations
    fn negotiate_nat_traversal_parameters(
        &self,
        local_config: &crate::transport_parameters::NatTraversalConfig,
        peer_config: &crate::transport_parameters::NatTraversalConfig,
    ) -> Result<crate::transport_parameters::NatTraversalConfig, String> {
        // With the new enum-based config, negotiation is simple:
        // - Client/Server roles are determined by who initiated the connection
        // - Concurrency limit is taken from the server's config

        match (local_config, peer_config) {
            // We're client, peer is server - use server's concurrency limit
            (
                crate::transport_parameters::NatTraversalConfig::ClientSupport,
                crate::transport_parameters::NatTraversalConfig::ServerSupport {
                    concurrency_limit,
                },
            ) => Ok(
                crate::transport_parameters::NatTraversalConfig::ServerSupport {
                    concurrency_limit: *concurrency_limit,
                },
            ),
            // We're server, peer is client - use our concurrency limit
            (
                crate::transport_parameters::NatTraversalConfig::ServerSupport {
                    concurrency_limit,
                },
                crate::transport_parameters::NatTraversalConfig::ClientSupport,
            ) => Ok(
                crate::transport_parameters::NatTraversalConfig::ServerSupport {
                    concurrency_limit: *concurrency_limit,
                },
            ),
            // Both are servers (e.g., peer-to-peer) - use minimum concurrency
            (
                crate::transport_parameters::NatTraversalConfig::ServerSupport {
                    concurrency_limit: limit1,
                },
                crate::transport_parameters::NatTraversalConfig::ServerSupport {
                    concurrency_limit: limit2,
                },
            ) => Ok(
                crate::transport_parameters::NatTraversalConfig::ServerSupport {
                    concurrency_limit: (*limit1).min(*limit2),
                },
            ),
            // Both are clients - shouldn't happen in normal operation
            (
                crate::transport_parameters::NatTraversalConfig::ClientSupport,
                crate::transport_parameters::NatTraversalConfig::ClientSupport,
            ) => Err("Both endpoints claim to be NAT traversal clients".to_string()),
        }
    }

    /// Initialize NAT traversal with negotiated configuration
    ///
    /// v0.13.0: All nodes are symmetric P2P nodes - no role distinction.
    /// Every node can observe addresses, discover candidates, and handle coordination.
    fn init_nat_traversal_with_negotiated_config(
        &mut self,
        _config: &crate::transport_parameters::NatTraversalConfig,
    ) {
        // v0.13.0: All nodes are symmetric P2P nodes - no role-based configuration
        // Use sensible defaults for all nodes
        let max_candidates = 50; // Default maximum candidates
        let coordination_timeout = Duration::from_secs(10); // Default 10 second timeout

        // Initialize NAT traversal state (no role parameter - all nodes are symmetric)
        self.nat_traversal = Some(NatTraversalState::new(max_candidates, coordination_timeout));

        trace!("NAT traversal initialized for symmetric P2P node");

        // v0.13.0: All nodes perform all initialization - no role-specific branching
        // All nodes can observe addresses, discover candidates, and coordinate
        self.prepare_address_observation();
        self.schedule_candidate_discovery();
        self.prepare_coordination_handling();
    }

    /// Initiate NAT traversal process for client endpoints
    fn initiate_nat_traversal_process(&mut self) {
        if let Some(nat_state) = &mut self.nat_traversal {
            match nat_state.start_candidate_discovery() {
                Ok(()) => {
                    debug!("NAT traversal process initiated - candidate discovery started");
                    // Schedule the first coordination attempt
                    self.timers.set(
                        Timer::NatTraversal,
                        Instant::now() + Duration::from_millis(100),
                    );
                }
                Err(e) => {
                    warn!("Failed to initiate NAT traversal process: {}", e);
                }
            }
        }
    }

    /// Prepare for address observation (bootstrap nodes)
    fn prepare_address_observation(&mut self) {
        debug!("Preparing for address observation as bootstrap node");
        // Bootstrap nodes are ready to observe peer addresses immediately
        // No additional setup needed - observation happens during connection establishment
    }

    /// Schedule candidate discovery for later execution
    fn schedule_candidate_discovery(&mut self) {
        debug!("Scheduling candidate discovery for client endpoint");
        // Set a timer to start candidate discovery after connection establishment
        self.timers.set(
            Timer::NatTraversal,
            Instant::now() + Duration::from_millis(50),
        );
    }

    /// Prepare to handle coordination requests (server nodes)
    fn prepare_coordination_handling(&mut self) {
        debug!("Preparing to handle coordination requests as server endpoint");
        // Server nodes are ready to handle coordination requests immediately
        // No additional setup needed - coordination happens via frame processing
    }

    /// Handle NAT traversal timeout events
    fn handle_nat_traversal_timeout(&mut self, now: Instant) {
        // First get the actions from nat_state
        let timeout_result = if let Some(nat_state) = &mut self.nat_traversal {
            nat_state.handle_timeout(now)
        } else {
            return;
        };

        // Then handle the actions without holding a mutable borrow to nat_state
        match timeout_result {
            Ok(actions) => {
                for action in actions {
                    match action {
                        nat_traversal::TimeoutAction::RetryDiscovery => {
                            debug!("NAT traversal timeout: retrying candidate discovery");
                            if let Some(nat_state) = &mut self.nat_traversal {
                                if let Err(e) = nat_state.start_candidate_discovery() {
                                    warn!("Failed to retry candidate discovery: {}", e);
                                }
                            }
                        }
                        nat_traversal::TimeoutAction::RetryCoordination => {
                            debug!("NAT traversal timeout: retrying coordination");
                            // Schedule next coordination attempt
                            self.timers
                                .set(Timer::NatTraversal, now + Duration::from_secs(2));
                        }
                        nat_traversal::TimeoutAction::StartValidation => {
                            debug!("NAT traversal timeout: starting path validation");
                            self.start_nat_traversal_validation(now);
                        }
                        nat_traversal::TimeoutAction::Complete => {
                            debug!("NAT traversal completed successfully");
                            // NAT traversal is complete, no more timeouts needed
                            self.timers.stop(Timer::NatTraversal);
                        }
                        nat_traversal::TimeoutAction::Failed => {
                            warn!("NAT traversal failed after timeout");
                            // Consider fallback options or connection failure
                            self.handle_nat_traversal_failure();
                        }
                    }
                }
            }
            Err(e) => {
                warn!("NAT traversal timeout handling failed: {}", e);
                self.handle_nat_traversal_failure();
            }
        }
    }

    /// Start NAT traversal path validation
    fn start_nat_traversal_validation(&mut self, now: Instant) {
        if let Some(nat_state) = &mut self.nat_traversal {
            // Get candidate pairs that need validation
            let pairs = nat_state.get_next_validation_pairs(3);

            for pair in pairs {
                // Send PATH_CHALLENGE to validate the path
                let challenge = self.rng.r#gen();
                self.path.challenge = Some(challenge);
                self.path.challenge_pending = true;

                debug!(
                    "Starting path validation for NAT traversal candidate: {}",
                    pair.remote_addr
                );
            }

            // Set validation timeout
            self.timers
                .set(Timer::PathValidation, now + Duration::from_secs(3));
        }
    }

    /// Handle NAT traversal failure
    fn handle_nat_traversal_failure(&mut self) {
        warn!("NAT traversal failed, considering fallback options");

        // Clear NAT traversal state
        self.nat_traversal = None;
        self.timers.stop(Timer::NatTraversal);

        // In a full implementation, this could:
        // 1. Try relay connections
        // 2. Emit failure events to the application
        // 3. Attempt direct connection as fallback

        // For now, we'll just log the failure
        debug!("NAT traversal disabled for this connection due to failure");
    }

    /// Check if NAT traversal is supported and enabled for this connection
    pub fn nat_traversal_supported(&self) -> bool {
        self.nat_traversal.is_some()
            && self.config.nat_traversal_config.is_some()
            && self.peer_params.nat_traversal.is_some()
    }

    /// Get the negotiated NAT traversal configuration
    pub fn nat_traversal_config(&self) -> Option<&crate::transport_parameters::NatTraversalConfig> {
        self.peer_params.nat_traversal.as_ref()
    }

    /// Check if the connection is ready for NAT traversal operations
    pub fn nat_traversal_ready(&self) -> bool {
        self.nat_traversal_supported() && matches!(self.state, State::Established)
    }

    /// Get NAT traversal statistics for this connection
    ///
    /// This method is preserved for debugging and monitoring purposes.
    /// It may be used in future telemetry or diagnostic features.
    #[allow(dead_code)]
    pub(crate) fn nat_traversal_stats(&self) -> Option<nat_traversal::NatTraversalStats> {
        self.nat_traversal.as_ref().map(|state| state.stats.clone())
    }

    /// Force enable NAT traversal for testing purposes
    ///
    /// v0.13.0: Role parameter removed - all nodes are symmetric P2P nodes.
    #[cfg(test)]
    #[allow(dead_code)]
    pub(crate) fn force_enable_nat_traversal(&mut self) {
        use crate::transport_parameters::NatTraversalConfig;

        // v0.13.0: All nodes use ServerSupport (can coordinate)
        let config = NatTraversalConfig::ServerSupport {
            concurrency_limit: VarInt::from_u32(5),
        };

        self.peer_params.nat_traversal = Some(config.clone());
        self.config = Arc::new({
            let mut transport_config = (*self.config).clone();
            transport_config.nat_traversal_config = Some(config);
            transport_config
        });

        // v0.13.0: No role parameter - all nodes are symmetric
        self.nat_traversal = Some(NatTraversalState::new(8, Duration::from_secs(10)));
    }

    /// Queue an ADD_ADDRESS frame to be sent to the peer
    /// Derive peer ID from connection context
    fn derive_peer_id_from_connection(&self) -> [u8; 32] {
        // Generate a peer ID based on connection IDs
        let mut hasher = std::collections::hash_map::DefaultHasher::new();
        use std::hash::Hasher;
        hasher.write(&self.rem_handshake_cid);
        hasher.write(&self.handshake_cid);
        hasher.write(&self.path.remote.to_string().into_bytes());
        let hash = hasher.finish();
        let mut peer_id = [0u8; 32];
        peer_id[..8].copy_from_slice(&hash.to_be_bytes());
        // Fill remaining bytes with connection ID data
        let cid_bytes = self.rem_handshake_cid.as_ref();
        let copy_len = (cid_bytes.len()).min(24);
        peer_id[8..8 + copy_len].copy_from_slice(&cid_bytes[..copy_len]);
        peer_id
    }

    /// Normalize a socket address by converting IPv4-mapped IPv6 addresses to pure IPv4.
    ///
    /// This is critical for nodes bound to IPv4-only sockets (0.0.0.0:port) that receive
    /// addresses in IPv4-mapped IPv6 format (::ffff:a.b.c.d). Without normalization,
    /// attempting to connect to an IPv4-mapped address from an IPv4-only socket fails
    /// with "Address family not supported by protocol" (EAFNOSUPPORT, error 97).
    fn normalize_socket_addr(addr: SocketAddr) -> SocketAddr {
        match addr {
            SocketAddr::V6(v6_addr) => {
                // Check if this is an IPv4-mapped IPv6 address (::ffff:a.b.c.d)
                if let Some(ipv4) = v6_addr.ip().to_ipv4_mapped() {
                    let normalized = SocketAddr::new(IpAddr::V4(ipv4), v6_addr.port());
                    debug!(
                        "Normalized IPv4-mapped IPv6 address {} to {}",
                        addr, normalized
                    );
                    normalized
                } else {
                    addr
                }
            }
            SocketAddr::V4(_) => addr,
        }
    }

    /// Handle AddAddress frame from peer
    fn handle_add_address(
        &mut self,
        add_address: &crate::frame::AddAddress,
        now: Instant,
    ) -> Result<(), TransportError> {
        let nat_state = self.nat_traversal.as_mut().ok_or_else(|| {
            TransportError::PROTOCOL_VIOLATION("AddAddress frame without NAT traversal negotiation")
        })?;

        // Normalize the address to handle IPv4-mapped IPv6 addresses
        // This is critical for nodes bound to IPv4-only sockets
        let normalized_addr = Self::normalize_socket_addr(add_address.address);

        info!(
            "handle_add_address: RECEIVED ADD_ADDRESS from peer addr={} (normalized={}) seq={} priority={}",
            add_address.address, normalized_addr, add_address.sequence, add_address.priority
        );

        match nat_state.add_remote_candidate(
            add_address.sequence,
            normalized_addr,
            add_address.priority,
            now,
        ) {
            Ok(()) => {
                info!(
                    "Added remote candidate: {} (seq={}, priority={})",
                    normalized_addr, add_address.sequence, add_address.priority
                );

                // Trigger validation of this new candidate
                self.trigger_candidate_validation(normalized_addr, now)?;
                Ok(())
            }
            Err(NatTraversalError::TooManyCandidates) => Err(TransportError::PROTOCOL_VIOLATION(
                "too many NAT traversal candidates",
            )),
            Err(NatTraversalError::DuplicateAddress) => {
                // Silently ignore duplicates (peer may resend)
                Ok(())
            }
            Err(e) => {
                warn!("Failed to add remote candidate: {}", e);
                Ok(()) // Don't terminate connection for non-critical errors
            }
        }
    }

    /// Handle PunchMeNow frame from peer (via coordinator)
    ///
    /// v0.13.0: All nodes can coordinate - no role check needed.
    fn handle_punch_me_now(
        &mut self,
        punch_me_now: &crate::frame::PunchMeNow,
        now: Instant,
    ) -> Result<(), TransportError> {
        trace!(
            "Received PunchMeNow: round={}, target_seq={}, local_addr={}",
            punch_me_now.round, punch_me_now.paired_with_sequence_number, punch_me_now.address
        );

        // v0.13.0: All nodes can coordinate - try coordination first
        if let Some(nat_state) = &self.nat_traversal {
            // All nodes have bootstrap_coordinator now (v0.13.0)
            if nat_state.bootstrap_coordinator.is_some() {
                // Process coordination request
                let from_peer_id = self.derive_peer_id_from_connection();

                // Clone the frame to avoid borrow checker issues
                let punch_me_now_clone = punch_me_now.clone();
                drop(nat_state); // Release the borrow

                match self
                    .nat_traversal
                    .as_mut()
                    .unwrap()
                    .handle_punch_me_now_frame(
                        from_peer_id,
                        self.path.remote,
                        &punch_me_now_clone,
                        now,
                    ) {
                    Ok(Some(coordination_frame)) => {
                        trace!("Node coordinating PUNCH_ME_NOW between peers");

                        // Send coordination frame to target peer via endpoint
                        if let Some(target_peer_id) = punch_me_now.target_peer_id {
                            self.endpoint_events.push_back(
                                crate::shared::EndpointEventInner::RelayPunchMeNow(
                                    target_peer_id,
                                    coordination_frame,
                                ),
                            );
                        }

                        return Ok(());
                    }
                    Ok(None) => {
                        trace!("Coordination completed or no action needed");
                        return Ok(());
                    }
                    Err(e) => {
                        warn!("Coordination failed: {}", e);
                        return Ok(());
                    }
                }
            }
        }

        // We're a regular peer receiving coordination from bootstrap
        let nat_state = self.nat_traversal.as_mut().ok_or_else(|| {
            TransportError::PROTOCOL_VIOLATION("PunchMeNow frame without NAT traversal negotiation")
        })?;

        // Handle peer's coordination request
        if nat_state
            .handle_peer_punch_request(punch_me_now.round, now)
            .map_err(|_e| {
                TransportError::PROTOCOL_VIOLATION("Failed to handle peer punch request")
            })?
        {
            trace!("Coordination synchronized for round {}", punch_me_now.round);

            // Create punch targets based on the received information
            // The peer's address tells us where they'll be listening
            let _local_addr = self
                .local_ip
                .map(|ip| SocketAddr::new(ip, 0))
                .unwrap_or_else(|| {
                    SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), 0)
                });

            let target = nat_traversal::PunchTarget {
                remote_addr: punch_me_now.address,
                remote_sequence: punch_me_now.paired_with_sequence_number,
                challenge: self.rng.r#gen(),
            };

            // Start coordination with this target
            let _ = nat_state.start_coordination_round(vec![target], now);
        } else {
            debug!(
                "Failed to synchronize coordination for round {}",
                punch_me_now.round
            );
        }

        Ok(())
    }

    /// Handle RemoveAddress frame from peer
    fn handle_remove_address(
        &mut self,
        remove_address: &crate::frame::RemoveAddress,
    ) -> Result<(), TransportError> {
        let nat_state = self.nat_traversal.as_mut().ok_or_else(|| {
            TransportError::PROTOCOL_VIOLATION(
                "RemoveAddress frame without NAT traversal negotiation",
            )
        })?;

        if nat_state.remove_candidate(remove_address.sequence) {
            trace!(
                "Removed candidate with sequence {}",
                remove_address.sequence
            );
        } else {
            trace!(
                "Attempted to remove unknown candidate sequence {}",
                remove_address.sequence
            );
        }

        Ok(())
    }

    /// Handle ObservedAddress frame from peer
    fn handle_observed_address_frame(
        &mut self,
        observed_address: &crate::frame::ObservedAddress,
        now: Instant,
    ) -> Result<(), TransportError> {
        tracing::info!(
            address = %observed_address.address,
            sequence = %observed_address.sequence_number,
            "handle_observed_address_frame: RECEIVED OBSERVED_ADDRESS from peer"
        );
        // Get the address discovery state
        let state = self.address_discovery_state.as_mut().ok_or_else(|| {
            TransportError::PROTOCOL_VIOLATION(
                "ObservedAddress frame without address discovery negotiation",
            )
        })?;

        // Check if address discovery is enabled
        if !state.enabled {
            return Err(TransportError::PROTOCOL_VIOLATION(
                "ObservedAddress frame received when address discovery is disabled",
            ));
        }

        // Trace observed address received
        #[cfg(feature = "trace")]
        {
            use crate::trace_observed_address_received;
            // Tracing imports handled by macros
            trace_observed_address_received!(
                &self.event_log,
                self.trace_context.trace_id(),
                observed_address.address,
                0u64 // path_id not part of the frame yet
            );
        }

        // Get the current path ID (0 for primary path in single-path connections)
        let path_id = 0u64; // TODO: Support multi-path scenarios

        // Check sequence number per RFC draft-ietf-quic-address-discovery-00
        // "A peer SHOULD ignore an incoming OBSERVED_ADDRESS frame if it previously
        // received another OBSERVED_ADDRESS frame for the same path with a Sequence
        // Number equal to or higher than the sequence number of the incoming frame."
        if let Some(&last_seq) = state.last_received_sequence.get(&path_id) {
            if observed_address.sequence_number <= last_seq {
                trace!(
                    "Ignoring OBSERVED_ADDRESS frame with stale sequence number {} (last was {})",
                    observed_address.sequence_number, last_seq
                );
                return Ok(());
            }
        }

        // Update the last received sequence number for this path
        state
            .last_received_sequence
            .insert(path_id, observed_address.sequence_number);

        // Normalize the address to handle IPv4-mapped IPv6 addresses
        // This ensures consistent address format for later ADD_ADDRESS advertisements
        let normalized_addr = Self::normalize_socket_addr(observed_address.address);

        // Process the observed address
        state.handle_observed_address(normalized_addr, path_id, now);

        // Update the path's address info
        self.path.update_observed_address(normalized_addr, now);

        // Log the observation
        trace!(
            "Received ObservedAddress frame: address={} for path={}",
            observed_address.address, path_id
        );

        Ok(())
    }

    /// Handle TryConnectTo frame - request from peer to attempt connection to a target
    ///
    /// This is part of the NAT traversal callback mechanism where a peer can request
    /// this node to attempt a connection to verify connectivity.
    fn handle_try_connect_to(
        &mut self,
        try_connect_to: &crate::frame::TryConnectTo,
        now: Instant,
    ) -> Result<(), TransportError> {
        trace!(
            "Received TryConnectTo: request_id={}, target={}, timeout_ms={}",
            try_connect_to.request_id, try_connect_to.target_address, try_connect_to.timeout_ms
        );

        // Validate the target address (basic security checks)
        let target = try_connect_to.target_address;

        // Don't allow requests to loopback addresses from remote peers
        if target.ip().is_loopback() {
            warn!(
                "Rejecting TryConnectTo request to loopback address: {}",
                target
            );
            // Queue error response
            let response = crate::frame::TryConnectToResponse {
                request_id: try_connect_to.request_id,
                success: false,
                error_code: Some(crate::frame::TryConnectError::InvalidAddress),
                source_address: self.path.remote,
            };
            self.spaces[SpaceId::Data]
                .pending
                .try_connect_to_responses
                .push(response);
            return Ok(());
        }

        // Don't allow requests to unspecified addresses
        if target.ip().is_unspecified() {
            warn!(
                "Rejecting TryConnectTo request to unspecified address: {}",
                target
            );
            let response = crate::frame::TryConnectToResponse {
                request_id: try_connect_to.request_id,
                success: false,
                error_code: Some(crate::frame::TryConnectError::InvalidAddress),
                source_address: self.path.remote,
            };
            self.spaces[SpaceId::Data]
                .pending
                .try_connect_to_responses
                .push(response);
            return Ok(());
        }

        // Queue an endpoint event to perform the connection attempt asynchronously
        // The endpoint will handle the actual connection and send back a response
        self.endpoint_events
            .push_back(EndpointEventInner::TryConnectTo {
                request_id: try_connect_to.request_id,
                target_address: try_connect_to.target_address,
                timeout_ms: try_connect_to.timeout_ms,
                requester_connection: self.path.remote,
                requested_at: now,
            });

        trace!(
            "Queued TryConnectTo attempt for request_id={}",
            try_connect_to.request_id
        );

        Ok(())
    }

    /// Handle TryConnectToResponse frame - result of a connection attempt we requested
    fn handle_try_connect_to_response(
        &mut self,
        response: &crate::frame::TryConnectToResponse,
    ) -> Result<(), TransportError> {
        trace!(
            "Received TryConnectToResponse: request_id={}, success={}, error={:?}, source={}",
            response.request_id, response.success, response.error_code, response.source_address
        );

        // If the connection was successful, we've confirmed that the target address
        // can receive connections from the peer that attempted the connection
        if response.success {
            debug!(
                "TryConnectTo succeeded: target can receive connections from {}",
                response.source_address
            );

            // Update NAT traversal state with the successful probe result
            if let Some(nat_state) = &mut self.nat_traversal {
                nat_state
                    .record_successful_callback_probe(response.request_id, response.source_address);
            }
        } else {
            debug!("TryConnectTo failed with error: {:?}", response.error_code);

            // Update NAT traversal state with the failed probe result
            if let Some(nat_state) = &mut self.nat_traversal {
                nat_state.record_failed_callback_probe(response.request_id, response.error_code);
            }
        }

        Ok(())
    }

    /// Queue an AddAddress frame to advertise a new candidate address
    pub fn queue_add_address(&mut self, sequence: VarInt, address: SocketAddr, priority: VarInt) {
        // Queue the AddAddress frame
        let add_address = frame::AddAddress {
            sequence,
            address,
            priority,
        };

        self.spaces[SpaceId::Data]
            .pending
            .add_addresses
            .push(add_address);
        trace!(
            "Queued AddAddress frame: seq={}, addr={}, priority={}",
            sequence, address, priority
        );
    }

    /// Queue a PunchMeNow frame to coordinate NAT traversal
    pub fn queue_punch_me_now(
        &mut self,
        round: VarInt,
        paired_with_sequence_number: VarInt,
        address: SocketAddr,
    ) {
        self.queue_punch_me_now_with_target(round, paired_with_sequence_number, address, None);
    }

    /// Queue a PunchMeNow frame with optional target_peer_id for relay coordination
    ///
    /// When `target_peer_id` is `Some`, the frame is sent to a coordinator who will
    /// relay it to the specified target peer. This enables NAT traversal when neither
    /// peer can directly reach the other.
    ///
    /// # Arguments
    /// * `round` - Coordination round number for synchronization
    /// * `paired_with_sequence_number` - Sequence number of the target candidate address
    /// * `address` - Our address for the hole punching attempt
    /// * `target_peer_id` - Optional target peer ID for relay coordination
    pub fn queue_punch_me_now_with_target(
        &mut self,
        round: VarInt,
        paired_with_sequence_number: VarInt,
        address: SocketAddr,
        target_peer_id: Option<[u8; 32]>,
    ) {
        let punch_me_now = frame::PunchMeNow {
            round,
            paired_with_sequence_number,
            address,
            target_peer_id,
        };

        self.spaces[SpaceId::Data]
            .pending
            .punch_me_now
            .push(punch_me_now);

        if target_peer_id.is_some() {
            trace!(
                "Queued PunchMeNow frame for relay: round={}, target_seq={}, target_peer={:?}",
                round,
                paired_with_sequence_number,
                target_peer_id.map(|p| hex::encode(&p[..8]))
            );
        } else {
            trace!(
                "Queued PunchMeNow frame: round={}, target={}",
                round, paired_with_sequence_number
            );
        }
    }

    /// Queue a RemoveAddress frame to remove a candidate
    pub fn queue_remove_address(&mut self, sequence: VarInt) {
        let remove_address = frame::RemoveAddress { sequence };

        self.spaces[SpaceId::Data]
            .pending
            .remove_addresses
            .push(remove_address);
        trace!("Queued RemoveAddress frame: seq={}", sequence);
    }

    /// Queue an ObservedAddress frame to send to peer
    pub fn queue_observed_address(&mut self, address: SocketAddr) {
        // Get sequence number from address discovery state
        let sequence_number = if let Some(state) = &mut self.address_discovery_state {
            let seq = state.next_sequence_number;
            state.next_sequence_number =
                VarInt::from_u64(state.next_sequence_number.into_inner() + 1)
                    .expect("sequence number overflow");
            seq
        } else {
            // Fallback if no state (shouldn't happen in practice)
            VarInt::from_u32(0)
        };

        let observed_address = frame::ObservedAddress {
            sequence_number,
            address,
        };
        self.spaces[SpaceId::Data]
            .pending
            .outbound_observations
            .push(observed_address);
        trace!("Queued ObservedAddress frame: addr={}", address);
    }

    /// Check if we should send OBSERVED_ADDRESS frames and queue them
    pub fn check_for_address_observations(&mut self, now: Instant) {
        // Only check if we have address discovery state
        let Some(state) = &mut self.address_discovery_state else {
            return;
        };

        // Check if address discovery is enabled
        if !state.enabled {
            return;
        }

        // Get the current path ID (0 for primary path)
        let path_id = 0u64; // TODO: Support multi-path scenarios

        // Get the remote address for this path
        let remote_address = self.path.remote;

        // Check if we should send an observation for this path
        if state.should_send_observation(path_id, now) {
            // Try to queue the observation frame
            if let Some(frame) = state.queue_observed_address_frame(path_id, remote_address) {
                // Queue the frame for sending
                self.spaces[SpaceId::Data]
                    .pending
                    .outbound_observations
                    .push(frame);

                // Record that we sent the observation
                state.record_observation_sent(path_id);

                // Trace observed address sent
                #[cfg(feature = "trace")]
                {
                    use crate::trace_observed_address_sent;
                    // Tracing imports handled by macros
                    trace_observed_address_sent!(
                        &self.event_log,
                        self.trace_context.trace_id(),
                        remote_address,
                        path_id
                    );
                }

                trace!(
                    "Queued OBSERVED_ADDRESS frame for path {} with address {}",
                    path_id, remote_address
                );
            }
        }
    }

    /// Trigger validation of a candidate address using PATH_CHALLENGE
    fn trigger_candidate_validation(
        &mut self,
        candidate_address: SocketAddr,
        now: Instant,
    ) -> Result<(), TransportError> {
        let nat_state = self
            .nat_traversal
            .as_mut()
            .ok_or_else(|| TransportError::PROTOCOL_VIOLATION("NAT traversal not enabled"))?;

        // Check if we already have an active validation for this address
        if nat_state
            .active_validations
            .contains_key(&candidate_address)
        {
            trace!("Validation already in progress for {}", candidate_address);
            return Ok(());
        }

        // Generate a random challenge value
        let challenge = self.rng.r#gen::<u64>();

        // Create path validation state
        let validation_state = nat_traversal::PathValidationState {
            challenge,
            sent_at: now,
            retry_count: 0,
            max_retries: 3,
            coordination_round: None,
            timeout_state: nat_traversal::AdaptiveTimeoutState::new(),
            last_retry_at: None,
        };

        // Store the validation attempt
        nat_state
            .active_validations
            .insert(candidate_address, validation_state);

        // NAT traversal PATH_CHALLENGE frames are sent via send_nat_traversal_challenge()

        // Update statistics
        nat_state.stats.validations_succeeded += 1; // Will be decremented if validation fails

        trace!(
            "Triggered PATH_CHALLENGE validation for {} with challenge {:016x}",
            candidate_address, challenge
        );

        Ok(())
    }

    /// Get current NAT traversal state information
    ///
    /// v0.13.0: Returns (local_candidates, remote_candidates) - role removed since all
    /// nodes are symmetric P2P nodes.
    pub fn nat_traversal_state(&self) -> Option<(usize, usize)> {
        self.nat_traversal
            .as_ref()
            .map(|state| (state.local_candidates.len(), state.remote_candidates.len()))
    }

    /// Initiate NAT traversal coordination through a bootstrap node
    pub fn initiate_nat_traversal_coordination(
        &mut self,
        now: Instant,
    ) -> Result<(), TransportError> {
        let nat_state = self
            .nat_traversal
            .as_mut()
            .ok_or_else(|| TransportError::PROTOCOL_VIOLATION("NAT traversal not enabled"))?;

        // Check if we should send PUNCH_ME_NOW to coordinator
        if nat_state.should_send_punch_request() {
            // Generate candidate pairs for coordination
            nat_state.generate_candidate_pairs(now);

            // Get the best candidate pairs to try
            let pairs = nat_state.get_next_validation_pairs(3);
            if pairs.is_empty() {
                return Err(TransportError::PROTOCOL_VIOLATION(
                    "No candidate pairs for coordination",
                ));
            }

            // Create punch targets from the pairs
            let targets: Vec<_> = pairs
                .into_iter()
                .map(|pair| nat_traversal::PunchTarget {
                    remote_addr: pair.remote_addr,
                    remote_sequence: pair.remote_sequence,
                    challenge: self.rng.r#gen(),
                })
                .collect();

            // Start coordination round
            let round = nat_state
                .start_coordination_round(targets, now)
                .map_err(|_e| {
                    TransportError::PROTOCOL_VIOLATION("Failed to start coordination round")
                })?;

            // Queue PUNCH_ME_NOW frame to be sent to bootstrap node
            // Include our best local address for the peer to target
            let local_addr = self
                .local_ip
                .map(|ip| SocketAddr::new(ip, self.local_ip.map(|_| 0).unwrap_or(0)))
                .unwrap_or_else(|| {
                    SocketAddr::new(std::net::IpAddr::V4(std::net::Ipv4Addr::UNSPECIFIED), 0)
                });

            let punch_me_now = frame::PunchMeNow {
                round,
                paired_with_sequence_number: VarInt::from_u32(0), // Will be filled by bootstrap
                address: local_addr,
                target_peer_id: None, // Direct peer-to-peer communication
            };

            self.spaces[SpaceId::Data]
                .pending
                .punch_me_now
                .push(punch_me_now);
            nat_state.mark_punch_request_sent();

            trace!("Initiated NAT traversal coordination round {}", round);
        }

        Ok(())
    }

    /// Trigger validation of NAT traversal candidates using PATH_CHALLENGE
    pub fn validate_nat_candidates(&mut self, now: Instant) {
        self.generate_nat_traversal_challenges(now);
    }

    // === PUBLIC NAT TRAVERSAL FRAME TRANSMISSION API ===

    /// Send an ADD_ADDRESS frame to advertise a candidate address to the peer
    ///
    /// This is the primary method for sending NAT traversal address advertisements.
    /// The frame will be transmitted in the next outgoing QUIC packet.
    ///
    /// # Arguments
    /// * `address` - The candidate address to advertise
    /// * `priority` - ICE-style priority for this candidate (higher = better)
    ///
    /// # Returns
    /// * `Ok(sequence)` - The sequence number assigned to this candidate
    /// * `Err(ConnectionError)` - If NAT traversal is not enabled or other error
    pub fn send_nat_address_advertisement(
        &mut self,
        address: SocketAddr,
        priority: u32,
    ) -> Result<u64, ConnectionError> {
        // Normalize the address to handle IPv4-mapped IPv6 addresses
        // This ensures consistent address format across all peers
        let normalized_addr = Self::normalize_socket_addr(address);

        // Verify NAT traversal is enabled
        let nat_state = self.nat_traversal.as_mut().ok_or_else(|| {
            ConnectionError::TransportError(TransportError::PROTOCOL_VIOLATION(
                "NAT traversal not enabled on this connection",
            ))
        })?;

        // Generate sequence number and add to local candidates
        let sequence = nat_state.next_sequence;
        nat_state.next_sequence =
            VarInt::from_u64(nat_state.next_sequence.into_inner() + 1).unwrap();

        // Add to local candidates
        let now = Instant::now();
        nat_state.local_candidates.insert(
            sequence,
            nat_traversal::AddressCandidate {
                address: normalized_addr,
                priority,
                source: nat_traversal::CandidateSource::Local,
                discovered_at: now,
                state: nat_traversal::CandidateState::New,
                attempt_count: 0,
                last_attempt: None,
            },
        );

        // Update statistics
        nat_state.stats.local_candidates_sent += 1;

        // Queue the frame for transmission (must be done after releasing nat_state borrow)
        self.queue_add_address(sequence, normalized_addr, VarInt::from_u32(priority));

        debug!(
            "Queued ADD_ADDRESS frame: addr={} (normalized from {}), priority={}, seq={}",
            normalized_addr, address, priority, sequence
        );
        Ok(sequence.into_inner())
    }

    /// Send a PUNCH_ME_NOW frame to coordinate hole punching with a peer
    ///
    /// This triggers synchronized hole punching for NAT traversal.
    ///
    /// # Arguments
    /// * `paired_with_sequence_number` - Sequence number of the target candidate address
    /// * `address` - Our address for the hole punching attempt
    /// * `round` - Coordination round number for synchronization
    ///
    /// # Returns
    /// * `Ok(())` - Frame queued for transmission
    /// * `Err(ConnectionError)` - If NAT traversal is not enabled
    pub fn send_nat_punch_coordination(
        &mut self,
        paired_with_sequence_number: u64,
        address: SocketAddr,
        round: u32,
    ) -> Result<(), ConnectionError> {
        // Verify NAT traversal is enabled
        let _nat_state = self.nat_traversal.as_ref().ok_or_else(|| {
            ConnectionError::TransportError(TransportError::PROTOCOL_VIOLATION(
                "NAT traversal not enabled on this connection",
            ))
        })?;

        // Queue the frame for transmission
        self.queue_punch_me_now(
            VarInt::from_u32(round),
            VarInt::from_u64(paired_with_sequence_number).map_err(|_| {
                ConnectionError::TransportError(TransportError::PROTOCOL_VIOLATION(
                    "Invalid target sequence number",
                ))
            })?,
            address,
        );

        debug!(
            "Queued PUNCH_ME_NOW frame: paired_with_seq={}, addr={}, round={}",
            paired_with_sequence_number, address, round
        );
        Ok(())
    }

    /// Send a PUNCH_ME_NOW frame via a coordinator to reach a target peer behind NAT
    ///
    /// This method sends a PUNCH_ME_NOW frame to the current connection (acting as coordinator)
    /// with the target peer's ID set. The coordinator will relay the frame to the target peer.
    ///
    /// # Arguments
    /// * `target_peer_id` - The 32-byte peer ID of the peer we want to reach
    /// * `our_address` - Our external address where we'll be listening for the punch
    /// * `round` - Coordination round number for synchronization
    ///
    /// # Returns
    /// * `Ok(())` - Frame queued for transmission
    /// * `Err(ConnectionError)` - If NAT traversal is not enabled
    pub fn send_nat_punch_via_relay(
        &mut self,
        target_peer_id: [u8; 32],
        our_address: SocketAddr,
        round: u32,
    ) -> Result<(), ConnectionError> {
        // Verify NAT traversal is enabled
        let _nat_state = self.nat_traversal.as_ref().ok_or_else(|| {
            ConnectionError::TransportError(TransportError::PROTOCOL_VIOLATION(
                "NAT traversal not enabled on this connection",
            ))
        })?;

        // Queue the frame with target_peer_id for relay
        self.queue_punch_me_now_with_target(
            VarInt::from_u32(round),
            VarInt::from_u32(0), // Sequence number 0 for initial coordination
            our_address,
            Some(target_peer_id),
        );

        info!(
            "Queued PUNCH_ME_NOW for relay: target_peer={}, our_addr={}, round={}",
            hex::encode(&target_peer_id[..8]),
            our_address,
            round
        );
        Ok(())
    }

    /// Send a REMOVE_ADDRESS frame to remove a previously advertised candidate
    ///
    /// This removes a candidate address that is no longer valid or available.
    ///
    /// # Arguments
    /// * `sequence` - Sequence number of the candidate to remove
    ///
    /// # Returns
    /// * `Ok(())` - Frame queued for transmission
    /// * `Err(ConnectionError)` - If NAT traversal is not enabled
    pub fn send_nat_address_removal(&mut self, sequence: u64) -> Result<(), ConnectionError> {
        // Verify NAT traversal is enabled
        let nat_state = self.nat_traversal.as_mut().ok_or_else(|| {
            ConnectionError::TransportError(TransportError::PROTOCOL_VIOLATION(
                "NAT traversal not enabled on this connection",
            ))
        })?;

        let sequence_varint = VarInt::from_u64(sequence).map_err(|_| {
            ConnectionError::TransportError(TransportError::PROTOCOL_VIOLATION(
                "Invalid sequence number",
            ))
        })?;

        // Remove from local candidates
        nat_state.local_candidates.remove(&sequence_varint);

        // Queue the frame for transmission
        self.queue_remove_address(sequence_varint);

        debug!("Queued REMOVE_ADDRESS frame: seq={}", sequence);
        Ok(())
    }

    /// Get statistics about NAT traversal activity on this connection
    ///
    /// # Returns
    /// * `Some(stats)` - Current NAT traversal statistics
    /// * `None` - If NAT traversal is not enabled
    ///
    /// This method is preserved for debugging and monitoring purposes.
    /// It may be used in future telemetry or diagnostic features.
    #[allow(dead_code)]
    pub(crate) fn get_nat_traversal_stats(&self) -> Option<&nat_traversal::NatTraversalStats> {
        self.nat_traversal.as_ref().map(|state| &state.stats)
    }

    /// Check if NAT traversal is enabled and active on this connection
    pub fn is_nat_traversal_enabled(&self) -> bool {
        self.nat_traversal.is_some()
    }

    // v0.13.0: get_nat_traversal_role() removed - all nodes are symmetric P2P nodes

    /// Negotiate address discovery parameters with peer
    fn negotiate_address_discovery(&mut self, peer_params: &TransportParameters) {
        let now = Instant::now();

        info!(
            "negotiate_address_discovery: peer_params.address_discovery = {:?}",
            peer_params.address_discovery
        );

        // Check if peer supports address discovery
        match &peer_params.address_discovery {
            Some(peer_config) => {
                // Peer supports address discovery
                info!("Peer supports address discovery: {:?}", peer_config);
                if let Some(state) = &mut self.address_discovery_state {
                    if state.enabled {
                        // Both support - no additional negotiation needed with enum-based config
                        // Rate limiting and path observation use fixed defaults from state creation
                        info!(
                            "Address discovery negotiated successfully: rate={}, all_paths={}",
                            state.max_observation_rate, state.observe_all_paths
                        );
                    } else {
                        // We don't support it but peer does
                        info!("Address discovery disabled locally, ignoring peer support");
                    }
                } else {
                    // Initialize state based on peer config if we don't have one
                    self.address_discovery_state =
                        Some(AddressDiscoveryState::new(peer_config, now));
                    info!("Address discovery initialized from peer config");
                }
            }
            _ => {
                // Peer doesn't support address discovery
                warn!("Peer does NOT support address discovery (transport parameter not present)");
                if let Some(state) = &mut self.address_discovery_state {
                    state.enabled = false;
                }
            }
        }

        // Update paths with negotiated observation rate if enabled
        if let Some(state) = &self.address_discovery_state {
            if state.enabled {
                self.path.set_observation_rate(state.max_observation_rate);
            }
        }
    }

    fn decrypt_packet(
        &mut self,
        now: Instant,
        packet: &mut Packet,
    ) -> Result<Option<u64>, Option<TransportError>> {
        let result = packet_crypto::decrypt_packet_body(
            packet,
            &self.spaces,
            self.zero_rtt_crypto.as_ref(),
            self.key_phase,
            self.prev_crypto.as_ref(),
            self.next_crypto.as_ref(),
        )?;

        let result = match result {
            Some(r) => r,
            None => return Ok(None),
        };

        if result.outgoing_key_update_acked {
            if let Some(prev) = self.prev_crypto.as_mut() {
                prev.end_packet = Some((result.number, now));
                self.set_key_discard_timer(now, packet.header.space());
            }
        }

        if result.incoming_key_update {
            trace!("key update authenticated");
            self.update_keys(Some((result.number, now)), true);
            self.set_key_discard_timer(now, packet.header.space());
        }

        Ok(Some(result.number))
    }

    fn update_keys(&mut self, end_packet: Option<(u64, Instant)>, remote: bool) {
        trace!("executing key update");
        // Generate keys for the key phase after the one we're switching to, store them in
        // `next_crypto`, make the contents of `next_crypto` current, and move the current keys into
        // `prev_crypto`.
        let new = self
            .crypto
            .next_1rtt_keys()
            .expect("only called for `Data` packets");
        self.key_phase_size = new
            .local
            .confidentiality_limit()
            .saturating_sub(KEY_UPDATE_MARGIN);
        let old = mem::replace(
            &mut self.spaces[SpaceId::Data]
                .crypto
                .as_mut()
                .unwrap() // safe because update_keys() can only be triggered by short packets
                .packet,
            mem::replace(self.next_crypto.as_mut().unwrap(), new),
        );
        self.spaces[SpaceId::Data].sent_with_keys = 0;
        self.prev_crypto = Some(PrevCrypto {
            crypto: old,
            end_packet,
            update_unacked: remote,
        });
        self.key_phase = !self.key_phase;
    }

    fn peer_supports_ack_frequency(&self) -> bool {
        self.peer_params.min_ack_delay.is_some()
    }

    /// Send an IMMEDIATE_ACK frame to the remote endpoint
    ///
    /// According to the spec, this will result in an error if the remote endpoint does not support
    /// the Acknowledgement Frequency extension
    pub(crate) fn immediate_ack(&mut self) {
        self.spaces[self.highest_space].immediate_ack_pending = true;
    }

    /// Decodes a packet, returning its decrypted payload, so it can be inspected in tests
    #[cfg(test)]
    #[allow(dead_code)]
    pub(crate) fn decode_packet(&self, event: &ConnectionEvent) -> Option<Vec<u8>> {
        let (first_decode, remaining) = match &event.0 {
            ConnectionEventInner::Datagram(DatagramConnectionEvent {
                first_decode,
                remaining,
                ..
            }) => (first_decode, remaining),
            _ => return None,
        };

        if remaining.is_some() {
            panic!("Packets should never be coalesced in tests");
        }

        let decrypted_header = packet_crypto::unprotect_header(
            first_decode.clone(),
            &self.spaces,
            self.zero_rtt_crypto.as_ref(),
            self.peer_params.stateless_reset_token,
        )?;

        let mut packet = decrypted_header.packet?;
        packet_crypto::decrypt_packet_body(
            &mut packet,
            &self.spaces,
            self.zero_rtt_crypto.as_ref(),
            self.key_phase,
            self.prev_crypto.as_ref(),
            self.next_crypto.as_ref(),
        )
        .ok()?;

        Some(packet.payload.to_vec())
    }

    /// The number of bytes of packets containing retransmittable frames that have not been
    /// acknowledged or declared lost.
    #[cfg(test)]
    #[allow(dead_code)]
    pub(crate) fn bytes_in_flight(&self) -> u64 {
        self.path.in_flight.bytes
    }

    /// Number of bytes worth of non-ack-only packets that may be sent
    #[cfg(test)]
    #[allow(dead_code)]
    pub(crate) fn congestion_window(&self) -> u64 {
        self.path
            .congestion
            .window()
            .saturating_sub(self.path.in_flight.bytes)
    }

    /// Whether no timers but keepalive, idle, rtt, pushnewcid, and key discard are running
    #[cfg(test)]
    #[allow(dead_code)]
    pub(crate) fn is_idle(&self) -> bool {
        Timer::VALUES
            .iter()
            .filter(|&&t| !matches!(t, Timer::KeepAlive | Timer::PushNewCid | Timer::KeyDiscard))
            .filter_map(|&t| Some((t, self.timers.get(t)?)))
            .min_by_key(|&(_, time)| time)
            .is_none_or(|(timer, _)| timer == Timer::Idle)
    }

    /// Total number of outgoing packets that have been deemed lost
    #[cfg(test)]
    #[allow(dead_code)]
    pub(crate) fn lost_packets(&self) -> u64 {
        self.lost_packets
    }

    /// Whether explicit congestion notification is in use on outgoing packets.
    #[cfg(test)]
    #[allow(dead_code)]
    pub(crate) fn using_ecn(&self) -> bool {
        self.path.sending_ecn
    }

    /// The number of received bytes in the current path
    #[cfg(test)]
    #[allow(dead_code)]
    pub(crate) fn total_recvd(&self) -> u64 {
        self.path.total_recvd
    }

    #[cfg(test)]
    #[allow(dead_code)]
    pub(crate) fn active_local_cid_seq(&self) -> (u64, u64) {
        self.local_cid_state.active_seq()
    }

    /// Instruct the peer to replace previously issued CIDs by sending a NEW_CONNECTION_ID frame
    /// with updated `retire_prior_to` field set to `v`
    #[cfg(test)]
    #[allow(dead_code)]
    pub(crate) fn rotate_local_cid(&mut self, v: u64, now: Instant) {
        let n = self.local_cid_state.assign_retire_seq(v);
        self.endpoint_events
            .push_back(EndpointEventInner::NeedIdentifiers(now, n));
    }

    /// Check the current active remote CID sequence
    #[cfg(test)]
    #[allow(dead_code)]
    pub(crate) fn active_rem_cid_seq(&self) -> u64 {
        self.rem_cids.active_seq()
    }

    /// Returns the detected maximum udp payload size for the current path
    #[cfg(test)]
    #[cfg(test)]
    #[allow(dead_code)]
    pub(crate) fn path_mtu(&self) -> u16 {
        self.path.current_mtu()
    }

    /// Whether we have 1-RTT data to send
    ///
    /// See also `self.space(SpaceId::Data).can_send()`
    fn can_send_1rtt(&self, max_size: usize) -> bool {
        self.streams.can_send_stream_data()
            || self.path.challenge_pending
            || self
                .prev_path
                .as_ref()
                .is_some_and(|(_, x)| x.challenge_pending)
            || !self.path_responses.is_empty()
            || self
                .datagrams
                .outgoing
                .front()
                .is_some_and(|x| x.size(true) <= max_size)
    }

    /// Update counters to account for a packet becoming acknowledged, lost, or abandoned
    fn remove_in_flight(&mut self, pn: u64, packet: &SentPacket) {
        // Visit known paths from newest to oldest to find the one `pn` was sent on
        for path in [&mut self.path]
            .into_iter()
            .chain(self.prev_path.as_mut().map(|(_, data)| data))
        {
            if path.remove_in_flight(pn, packet) {
                return;
            }
        }
    }

    /// Terminate the connection instantly, without sending a close packet
    fn kill(&mut self, reason: ConnectionError) {
        self.close_common();
        self.error = Some(reason);
        self.state = State::Drained;
        self.endpoint_events.push_back(EndpointEventInner::Drained);
    }

    /// Generate PATH_CHALLENGE frames for NAT traversal candidate validation
    fn generate_nat_traversal_challenges(&mut self, now: Instant) {
        // Get candidates ready for validation first
        let candidates: Vec<(VarInt, SocketAddr)> = if let Some(nat_state) = &self.nat_traversal {
            nat_state
                .get_validation_candidates()
                .into_iter()
                .take(3) // Validate up to 3 candidates in parallel
                .map(|(seq, candidate)| (seq, candidate.address))
                .collect()
        } else {
            return;
        };

        if candidates.is_empty() {
            return;
        }

        // Now process candidates with mutable access
        if let Some(nat_state) = &mut self.nat_traversal {
            for (seq, address) in candidates {
                // Generate a random challenge token
                let challenge: u64 = self.rng.r#gen();

                // Start validation for this candidate
                if let Err(e) = nat_state.start_validation(seq, challenge, now) {
                    debug!("Failed to start validation for candidate {}: {}", seq, e);
                    continue;
                }

                // NAT traversal PATH_CHALLENGE frames are sent via send_nat_traversal_challenge()
                trace!(
                    "Started NAT validation for {} with token {:08x}",
                    address, challenge
                );
            }
        }
    }

    /// Storage size required for the largest packet known to be supported by the current path
    ///
    /// Buffers passed to [`Connection::poll_transmit`] should be at least this large.
    pub fn current_mtu(&self) -> u16 {
        self.path.current_mtu()
    }

    /// Size of non-frame data for a 1-RTT packet
    ///
    /// Quantifies space consumed by the QUIC header and AEAD tag. All other bytes in a packet are
    /// frames. Changes if the length of the remote connection ID changes, which is expected to be
    /// rare. If `pn` is specified, may additionally change unpredictably due to variations in
    /// latency and packet loss.
    fn predict_1rtt_overhead(&self, pn: Option<u64>) -> usize {
        let pn_len = match pn {
            Some(pn) => PacketNumber::new(
                pn,
                self.spaces[SpaceId::Data].largest_acked_packet.unwrap_or(0),
            )
            .len(),
            // Upper bound
            None => 4,
        };

        // 1 byte for flags
        1 + self.rem_cids.active().len() + pn_len + self.tag_len_1rtt()
    }

    fn tag_len_1rtt(&self) -> usize {
        let key = match self.spaces[SpaceId::Data].crypto.as_ref() {
            Some(crypto) => Some(&*crypto.packet.local),
            None => self.zero_rtt_crypto.as_ref().map(|x| &*x.packet),
        };
        // If neither Data nor 0-RTT keys are available, make a reasonable tag length guess. As of
        // this writing, all QUIC cipher suites use 16-byte tags. We could return `None` instead,
        // but that would needlessly prevent sending datagrams during 0-RTT.
        key.map_or(16, |x| x.tag_len())
    }

    /// Mark the path as validated, and enqueue NEW_TOKEN frames to be sent as appropriate
    fn on_path_validated(&mut self) {
        self.path.validated = true;
        let ConnectionSide::Server { server_config } = &self.side else {
            return;
        };
        let new_tokens = &mut self.spaces[SpaceId::Data as usize].pending.new_tokens;
        new_tokens.clear();
        for _ in 0..server_config.validation_token.sent {
            new_tokens.push(self.path.remote);
        }
    }
}

impl fmt::Debug for Connection {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.debug_struct("Connection")
            .field("handshake_cid", &self.handshake_cid)
            .finish()
    }
}

/// Fields of `Connection` specific to it being client-side or server-side
enum ConnectionSide {
    Client {
        /// Sent in every outgoing Initial packet. Always empty after Initial keys are discarded
        token: Bytes,
        token_store: Arc<dyn TokenStore>,
        server_name: String,
    },
    Server {
        server_config: Arc<ServerConfig>,
    },
}

impl ConnectionSide {
    fn remote_may_migrate(&self) -> bool {
        match self {
            Self::Server { server_config } => server_config.migration,
            Self::Client { .. } => false,
        }
    }

    fn is_client(&self) -> bool {
        self.side().is_client()
    }

    fn is_server(&self) -> bool {
        self.side().is_server()
    }

    fn side(&self) -> Side {
        match *self {
            Self::Client { .. } => Side::Client,
            Self::Server { .. } => Side::Server,
        }
    }
}

impl From<SideArgs> for ConnectionSide {
    fn from(side: SideArgs) -> Self {
        match side {
            SideArgs::Client {
                token_store,
                server_name,
            } => Self::Client {
                token: token_store.take(&server_name).unwrap_or_default(),
                token_store,
                server_name,
            },
            SideArgs::Server {
                server_config,
                pref_addr_cid: _,
                path_validated: _,
            } => Self::Server { server_config },
        }
    }
}

/// Parameters to `Connection::new` specific to it being client-side or server-side
pub(crate) enum SideArgs {
    Client {
        token_store: Arc<dyn TokenStore>,
        server_name: String,
    },
    Server {
        server_config: Arc<ServerConfig>,
        pref_addr_cid: Option<ConnectionId>,
        path_validated: bool,
    },
}

impl SideArgs {
    pub(crate) fn pref_addr_cid(&self) -> Option<ConnectionId> {
        match *self {
            Self::Client { .. } => None,
            Self::Server { pref_addr_cid, .. } => pref_addr_cid,
        }
    }

    pub(crate) fn path_validated(&self) -> bool {
        match *self {
            Self::Client { .. } => true,
            Self::Server { path_validated, .. } => path_validated,
        }
    }

    pub(crate) fn side(&self) -> Side {
        match *self {
            Self::Client { .. } => Side::Client,
            Self::Server { .. } => Side::Server,
        }
    }
}

/// Reasons why a connection might be lost
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum ConnectionError {
    /// The peer doesn't implement any supported version
    #[error("peer doesn't implement any supported version")]
    VersionMismatch,
    /// The peer violated the QUIC specification as understood by this implementation
    #[error(transparent)]
    TransportError(#[from] TransportError),
    /// The peer's QUIC stack aborted the connection automatically
    #[error("aborted by peer: {0}")]
    ConnectionClosed(frame::ConnectionClose),
    /// The peer closed the connection
    #[error("closed by peer: {0}")]
    ApplicationClosed(frame::ApplicationClose),
    /// The peer is unable to continue processing this connection, usually due to having restarted
    #[error("reset by peer")]
    Reset,
    /// Communication with the peer has lapsed for longer than the negotiated idle timeout
    ///
    /// If neither side is sending keep-alives, a connection will time out after a long enough idle
    /// period even if the peer is still reachable. See also [`TransportConfig::max_idle_timeout()`]
    /// and [`TransportConfig::keep_alive_interval()`].
    #[error("timed out")]
    TimedOut,
    /// The local application closed the connection
    #[error("closed")]
    LocallyClosed,
    /// The connection could not be created because not enough of the CID space is available
    ///
    /// Try using longer connection IDs.
    #[error("CIDs exhausted")]
    CidsExhausted,
}

impl From<Close> for ConnectionError {
    fn from(x: Close) -> Self {
        match x {
            Close::Connection(reason) => Self::ConnectionClosed(reason),
            Close::Application(reason) => Self::ApplicationClosed(reason),
        }
    }
}

// For compatibility with API consumers
impl From<ConnectionError> for io::Error {
    fn from(x: ConnectionError) -> Self {
        use ConnectionError::*;
        let kind = match x {
            TimedOut => io::ErrorKind::TimedOut,
            Reset => io::ErrorKind::ConnectionReset,
            ApplicationClosed(_) | ConnectionClosed(_) => io::ErrorKind::ConnectionAborted,
            TransportError(_) | VersionMismatch | LocallyClosed | CidsExhausted => {
                io::ErrorKind::Other
            }
        };
        Self::new(kind, x)
    }
}

#[derive(Clone, Debug)]
/// Connection state machine states
pub enum State {
    /// Connection is in handshake phase
    Handshake(state::Handshake),
    /// Connection is established and ready for data transfer
    Established,
    /// Connection is closed with a reason
    Closed(state::Closed),
    /// Connection is draining (waiting for peer acknowledgment)
    Draining,
    /// Waiting for application to call close so we can dispose of the resources
    Drained,
}

impl State {
    fn closed<R: Into<Close>>(reason: R) -> Self {
        Self::Closed(state::Closed {
            reason: reason.into(),
        })
    }

    fn is_handshake(&self) -> bool {
        matches!(*self, Self::Handshake(_))
    }

    fn is_established(&self) -> bool {
        matches!(*self, Self::Established)
    }

    fn is_closed(&self) -> bool {
        matches!(*self, Self::Closed(_) | Self::Draining | Self::Drained)
    }

    fn is_drained(&self) -> bool {
        matches!(*self, Self::Drained)
    }
}

mod state {
    use super::*;

    #[derive(Clone, Debug)]
    pub struct Handshake {
        /// Whether the remote CID has been set by the peer yet
        ///
        /// Always set for servers
        pub(super) rem_cid_set: bool,
        /// Stateless retry token received in the first Initial by a server.
        ///
        /// Must be present in every Initial. Always empty for clients.
        pub(super) expected_token: Bytes,
        /// First cryptographic message
        ///
        /// Only set for clients
        pub(super) client_hello: Option<Bytes>,
    }

    #[derive(Clone, Debug)]
    pub struct Closed {
        pub(super) reason: Close,
    }
}

/// Events of interest to the application
#[derive(Debug)]
pub enum Event {
    /// The connection's handshake data is ready
    HandshakeDataReady,
    /// The connection was successfully established
    Connected,
    /// The connection was lost
    ///
    /// Emitted if the peer closes the connection or an error is encountered.
    ConnectionLost {
        /// Reason that the connection was closed
        reason: ConnectionError,
    },
    /// Stream events
    Stream(StreamEvent),
    /// One or more application datagrams have been received
    DatagramReceived,
    /// One or more application datagrams have been sent after blocking
    DatagramsUnblocked,
}

fn instant_saturating_sub(x: Instant, y: Instant) -> Duration {
    if x > y { x - y } else { Duration::ZERO }
}

fn get_max_ack_delay(params: &TransportParameters) -> Duration {
    Duration::from_micros(params.max_ack_delay.0 * 1000)
}

// Prevents overflow and improves behavior in extreme circumstances
const MAX_BACKOFF_EXPONENT: u32 = 16;

/// Minimal remaining size to allow packet coalescing, excluding cryptographic tag
///
/// This must be at least as large as the header for a well-formed empty packet to be coalesced,
/// plus some space for frames. We only care about handshake headers because short header packets
/// necessarily have smaller headers, and initial packets are only ever the first packet in a
/// datagram (because we coalesce in ascending packet space order and the only reason to split a
/// packet is when packet space changes).
const MIN_PACKET_SPACE: usize = MAX_HANDSHAKE_OR_0RTT_HEADER_SIZE + 32;

/// Largest amount of space that could be occupied by a Handshake or 0-RTT packet's header
///
/// Excludes packet-type-specific fields such as packet number or Initial token
// https://www.rfc-editor.org/rfc/rfc9000.html#name-0-rtt: flags + version + dcid len + dcid +
// scid len + scid + length + pn
const MAX_HANDSHAKE_OR_0RTT_HEADER_SIZE: usize =
    1 + 4 + 1 + MAX_CID_SIZE + 1 + MAX_CID_SIZE + VarInt::from_u32(u16::MAX as u32).size() + 4;

/// Perform key updates this many packets before the AEAD confidentiality limit.
///
/// Chosen arbitrarily, intended to be large enough to prevent spurious connection loss.
const KEY_UPDATE_MARGIN: u64 = 10_000;

#[derive(Default)]
struct SentFrames {
    retransmits: ThinRetransmits,
    largest_acked: Option<u64>,
    stream_frames: StreamMetaVec,
    /// Whether the packet contains non-retransmittable frames (like datagrams)
    non_retransmits: bool,
    requires_padding: bool,
}

impl SentFrames {
    /// Returns whether the packet contains only ACKs
    fn is_ack_only(&self, streams: &StreamsState) -> bool {
        self.largest_acked.is_some()
            && !self.non_retransmits
            && self.stream_frames.is_empty()
            && self.retransmits.is_empty(streams)
    }
}

/// Compute the negotiated idle timeout based on local and remote max_idle_timeout transport parameters.
///
/// According to the definition of max_idle_timeout, a value of `0` means the timeout is disabled; see <https://www.rfc-editor.org/rfc/rfc9000#section-18.2-4.4.1.>
///
/// According to the negotiation procedure, either the minimum of the timeouts or one specified is used as the negotiated value; see <https://www.rfc-editor.org/rfc/rfc9000#section-10.1-2.>
///
/// Returns the negotiated idle timeout as a `Duration`, or `None` when both endpoints have opted out of idle timeout.
fn negotiate_max_idle_timeout(x: Option<VarInt>, y: Option<VarInt>) -> Option<Duration> {
    match (x, y) {
        (Some(VarInt(0)) | None, Some(VarInt(0)) | None) => None,
        (Some(VarInt(0)) | None, Some(y)) => Some(Duration::from_millis(y.0)),
        (Some(x), Some(VarInt(0)) | None) => Some(Duration::from_millis(x.0)),
        (Some(x), Some(y)) => Some(Duration::from_millis(cmp::min(x, y).0)),
    }
}

/// State for tracking PQC support in the connection
#[derive(Debug, Clone)]
pub(crate) struct PqcState {
    /// Whether the peer supports PQC algorithms
    enabled: bool,
    /// Supported PQC algorithms advertised by peer
    #[allow(dead_code)]
    algorithms: Option<crate::transport_parameters::PqcAlgorithms>,
    /// Target MTU for PQC handshakes
    handshake_mtu: u16,
    /// Whether we're currently using PQC algorithms
    using_pqc: bool,
    /// PQC packet handler for managing larger handshakes
    packet_handler: crate::crypto::pqc::packet_handler::PqcPacketHandler,
}

#[allow(dead_code)]
impl PqcState {
    fn new() -> Self {
        Self {
            enabled: false,
            algorithms: None,
            handshake_mtu: MIN_INITIAL_SIZE,
            using_pqc: false,
            packet_handler: crate::crypto::pqc::packet_handler::PqcPacketHandler::new(),
        }
    }

    /// Get the minimum initial packet size based on PQC state
    fn min_initial_size(&self) -> u16 {
        if self.enabled && self.using_pqc {
            // Use larger initial packet size for PQC handshakes
            std::cmp::max(self.handshake_mtu, 4096)
        } else {
            MIN_INITIAL_SIZE
        }
    }

    /// Update PQC state based on peer's transport parameters
    fn update_from_peer_params(&mut self, params: &TransportParameters) {
        if let Some(ref algorithms) = params.pqc_algorithms {
            self.enabled = true;
            self.algorithms = Some(algorithms.clone());
            // v0.2: Pure PQC - if any algorithm is supported, prepare for larger packets
            if algorithms.ml_kem_768 || algorithms.ml_dsa_65 {
                self.using_pqc = true;
                self.handshake_mtu = 4096; // Default PQC handshake MTU
            }
        }
    }

    /// Detect PQC from CRYPTO frame data
    fn detect_pqc_from_crypto(&mut self, crypto_data: &[u8], space: SpaceId) {
        if !self.enabled {
            return;
        }
        if self.packet_handler.detect_pqc_handshake(crypto_data, space) {
            self.using_pqc = true;
            // Update handshake MTU based on PQC detection
            self.handshake_mtu = self.packet_handler.get_min_packet_size(space);
        }
    }

    /// Check if MTU discovery should be triggered for PQC
    fn should_trigger_mtu_discovery(&mut self) -> bool {
        self.packet_handler.should_trigger_mtu_discovery()
    }

    /// Get PQC-aware MTU configuration
    fn get_mtu_config(&self) -> MtuDiscoveryConfig {
        self.packet_handler.get_pqc_mtu_config()
    }

    /// Calculate optimal CRYPTO frame size
    fn calculate_crypto_frame_size(&self, available_space: usize, remaining_data: usize) -> usize {
        self.packet_handler
            .calculate_crypto_frame_size(available_space, remaining_data)
    }

    /// Check if packet coalescing should be adjusted
    fn should_adjust_coalescing(&self, current_size: usize, space: SpaceId) -> bool {
        self.packet_handler
            .adjust_coalescing_for_pqc(current_size, space)
    }

    /// Handle packet sent event
    fn on_packet_sent(&mut self, space: SpaceId, size: u16) {
        self.packet_handler.on_packet_sent(space, size);
    }

    /// Reset PQC state (e.g., on retry)
    fn reset(&mut self) {
        self.enabled = false;
        self.algorithms = None;
        self.handshake_mtu = MIN_INITIAL_SIZE;
        self.using_pqc = false;
        self.packet_handler.reset();
    }
}

impl Default for PqcState {
    fn default() -> Self {
        Self::new()
    }
}

/// State for tracking address discovery via OBSERVED_ADDRESS frames
#[derive(Debug, Clone)]
pub(crate) struct AddressDiscoveryState {
    /// Whether address discovery is enabled for this connection
    enabled: bool,
    /// Maximum rate of OBSERVED_ADDRESS frames per path (per second)
    max_observation_rate: u8,
    /// Whether to observe addresses for all paths or just primary
    observe_all_paths: bool,
    /// Per-path local observations (what we saw the peer at, for sending)
    sent_observations: std::collections::HashMap<u64, paths::PathAddressInfo>,
    /// Per-path remote observations (what the peer saw us at, for our info)
    received_observations: std::collections::HashMap<u64, paths::PathAddressInfo>,
    /// Rate limiter for sending observations
    rate_limiter: AddressObservationRateLimiter,
    /// Historical record of observations received
    received_history: Vec<ObservedAddressEvent>,
    /// Whether this connection is in bootstrap mode (aggressive observation)
    bootstrap_mode: bool,
    /// Next sequence number for OBSERVED_ADDRESS frames
    next_sequence_number: VarInt,
    /// Map of path_id to last received sequence number
    last_received_sequence: std::collections::HashMap<u64, VarInt>,
    /// Total number of observations sent
    frames_sent: u64,
}

/// Event for when we receive an OBSERVED_ADDRESS frame
#[derive(Debug, Clone, PartialEq, Eq)]
struct ObservedAddressEvent {
    /// The address the peer observed
    address: SocketAddr,
    /// When we received this observation
    received_at: Instant,
    /// Which path this was received on
    path_id: u64,
}

/// Rate limiter for address observations
#[derive(Debug, Clone)]
struct AddressObservationRateLimiter {
    /// Tokens available for sending observations
    tokens: f64,
    /// Maximum tokens (burst capacity)
    max_tokens: f64,
    /// Rate of token replenishment (tokens per second)
    rate: f64,
    /// Last time tokens were updated
    last_update: Instant,
}

#[allow(dead_code)]
impl AddressDiscoveryState {
    /// Create a new address discovery state
    fn new(config: &crate::transport_parameters::AddressDiscoveryConfig, now: Instant) -> Self {
        use crate::transport_parameters::AddressDiscoveryConfig::*;

        // Set defaults based on the config variant
        let (enabled, _can_send, _can_receive) = match config {
            SendOnly => (true, true, false),
            ReceiveOnly => (true, false, true),
            SendAndReceive => (true, true, true),
        };

        // For now, use fixed defaults for rate limiting
        // TODO: These could be made configurable via a separate mechanism
        let max_observation_rate = 10u8; // Default rate
        let observe_all_paths = false; // Default to primary path only

        Self {
            enabled,
            max_observation_rate,
            observe_all_paths,
            sent_observations: std::collections::HashMap::new(),
            received_observations: std::collections::HashMap::new(),
            rate_limiter: AddressObservationRateLimiter::new(max_observation_rate, now),
            received_history: Vec::new(),
            bootstrap_mode: false,
            next_sequence_number: VarInt::from_u32(0),
            last_received_sequence: std::collections::HashMap::new(),
            frames_sent: 0,
        }
    }

    /// Check if we should send an observation for the given path
    fn should_send_observation(&mut self, path_id: u64, now: Instant) -> bool {
        // Use the new should_observe_path method which considers bootstrap mode
        if !self.should_observe_path(path_id) {
            return false;
        }

        // Check if this is a new path or if the address has changed
        let needs_observation = match self.sent_observations.get(&path_id) {
            Some(info) => info.observed_address.is_none() || !info.notified,
            None => true,
        };

        if !needs_observation {
            return false;
        }

        // Check rate limit
        self.rate_limiter.try_consume(1.0, now)
    }

    /// Record that we sent an observation for a path
    fn record_observation_sent(&mut self, path_id: u64) {
        if let Some(info) = self.sent_observations.get_mut(&path_id) {
            info.mark_notified();
        }
    }

    /// Handle receiving an OBSERVED_ADDRESS frame
    fn handle_observed_address(&mut self, address: SocketAddr, path_id: u64, now: Instant) {
        if !self.enabled {
            return;
        }

        self.received_history.push(ObservedAddressEvent {
            address,
            received_at: now,
            path_id,
        });

        // Update or create path info for received observations
        let info = self
            .received_observations
            .entry(path_id)
            .or_insert_with(paths::PathAddressInfo::new);
        info.update_observed_address(address, now);
    }

    /// Get the most recently observed address for a path
    pub(crate) fn get_observed_address(&self, path_id: u64) -> Option<SocketAddr> {
        self.received_observations
            .get(&path_id)
            .and_then(|info| info.observed_address)
    }

    /// Get all observed addresses across all paths
    pub(crate) fn get_all_received_history(&self) -> Vec<SocketAddr> {
        self.received_observations
            .values()
            .filter_map(|info| info.observed_address)
            .collect()
    }

    /// Get statistics for address discovery
    pub(crate) fn stats(&self) -> AddressDiscoveryStats {
        AddressDiscoveryStats {
            frames_sent: self.frames_sent,
            frames_received: self.received_history.len() as u64,
            addresses_discovered: self
                .received_observations
                .values()
                .filter(|info| info.observed_address.is_some())
                .count() as u64,
            address_changes_detected: 0, // TODO: Track address changes properly
        }
    }

    /// Check if we have any unnotified address changes
    ///
    /// This checks both:
    /// - `sent_observations`: addresses we've observed about peers that need to be sent
    /// - `received_observations`: addresses peers observed about us that need app notification
    fn has_unnotified_changes(&self) -> bool {
        // Check if we have observations to send to peers
        let has_unsent = self
            .sent_observations
            .values()
            .any(|info| info.observed_address.is_some() && !info.notified);

        // Check if we have received observations to notify the app about
        let has_unreceived = self
            .received_observations
            .values()
            .any(|info| info.observed_address.is_some() && !info.notified);

        has_unsent || has_unreceived
    }

    /// Queue an OBSERVED_ADDRESS frame for sending if conditions are met
    fn queue_observed_address_frame(
        &mut self,
        path_id: u64,
        address: SocketAddr,
    ) -> Option<frame::ObservedAddress> {
        // Check if address discovery is enabled
        if !self.enabled {
            tracing::debug!("queue_observed_address_frame: BLOCKED - address discovery disabled");
            return None;
        }

        // Check path restrictions
        if !self.observe_all_paths && path_id != 0 {
            tracing::debug!(
                "queue_observed_address_frame: BLOCKED - path {} not allowed (observe_all_paths={})",
                path_id,
                self.observe_all_paths
            );
            return None;
        }

        // Check if this path has already been notified
        if let Some(info) = self.sent_observations.get(&path_id) {
            if info.notified {
                tracing::trace!(
                    "queue_observed_address_frame: BLOCKED - path {} already notified",
                    path_id
                );
                return None;
            }
        }

        // Check rate limiting
        if self.rate_limiter.tokens < 1.0 {
            tracing::debug!(
                "queue_observed_address_frame: BLOCKED - rate limited (tokens={})",
                self.rate_limiter.tokens
            );
            return None;
        }

        tracing::info!(
            "queue_observed_address_frame: SENDING OBSERVED_ADDRESS to {} for path {}",
            address,
            path_id
        );

        // Consume a token and update path info
        self.rate_limiter.tokens -= 1.0;

        // Update or create path info
        let info = self
            .sent_observations
            .entry(path_id)
            .or_insert_with(paths::PathAddressInfo::new);
        info.observed_address = Some(address);
        info.notified = true;

        tracing::trace!(
            path_id = ?path_id,
            address = %address,
            "queue_observed_address_frame: queuing frame"
        );

        // Create and return the frame with sequence number
        let sequence_number = self.next_sequence_number;
        self.next_sequence_number = VarInt::from_u64(self.next_sequence_number.into_inner() + 1)
            .expect("sequence number overflow");

        Some(frame::ObservedAddress {
            sequence_number,
            address,
        })
    }

    /// Check for address observations that need to be sent
    fn check_for_address_observations(
        &mut self,
        _current_path: u64,
        peer_supports_address_discovery: bool,
        now: Instant,
    ) -> Vec<frame::ObservedAddress> {
        let mut frames = Vec::new();

        // Check if we should send observations
        if !self.enabled || !peer_supports_address_discovery {
            return frames;
        }

        // Update rate limiter tokens
        self.rate_limiter.update_tokens(now);

        // Collect all paths that need observation frames
        let paths_to_notify: Vec<u64> = self
            .sent_observations
            .iter()
            .filter_map(|(&path_id, info)| {
                if info.observed_address.is_some() && !info.notified {
                    Some(path_id)
                } else {
                    None
                }
            })
            .collect();

        // Send frames for each path that needs notification
        for path_id in paths_to_notify {
            // Check path restrictions (considers bootstrap mode)
            if !self.should_observe_path(path_id) {
                continue;
            }

            // Check rate limiting (bootstrap nodes get more lenient limits)
            if !self.bootstrap_mode && self.rate_limiter.tokens < 1.0 {
                break; // No more tokens available for non-bootstrap nodes
            }

            // Get the address
            if let Some(info) = self.sent_observations.get_mut(&path_id) {
                if let Some(address) = info.observed_address {
                    // Consume a token (bootstrap nodes consume at reduced rate)
                    if self.bootstrap_mode {
                        self.rate_limiter.tokens -= 0.2; // Bootstrap nodes consume 1/5th token
                    } else {
                        self.rate_limiter.tokens -= 1.0;
                    }

                    // Mark as notified
                    info.notified = true;

                    // Create frame with sequence number
                    let sequence_number = self.next_sequence_number;
                    self.next_sequence_number =
                        VarInt::from_u64(self.next_sequence_number.into_inner() + 1)
                            .expect("sequence number overflow");

                    self.frames_sent += 1;

                    frames.push(frame::ObservedAddress {
                        sequence_number,
                        address,
                    });
                }
            }
        }

        frames
    }

    /// Update the rate limit configuration
    fn update_rate_limit(&mut self, new_rate: f64) {
        self.max_observation_rate = new_rate as u8;
        self.rate_limiter.set_rate(new_rate as u8);
    }

    /// Create from transport parameters
    fn from_transport_params(params: &TransportParameters) -> Option<Self> {
        params
            .address_discovery
            .as_ref()
            .map(|config| Self::new(config, Instant::now()))
    }

    /// Alternative constructor for tests - creates with simplified parameters
    #[cfg(test)]
    fn new_with_params(enabled: bool, max_rate: f64, observe_all_paths: bool) -> Self {
        // For tests, use SendAndReceive if enabled, otherwise create a disabled state
        if !enabled {
            // Create disabled state manually since we don't have a "disabled" variant
            return Self {
                enabled: false,
                max_observation_rate: max_rate as u8,
                observe_all_paths,
                sent_observations: std::collections::HashMap::new(),
                received_observations: std::collections::HashMap::new(),
                rate_limiter: AddressObservationRateLimiter::new(max_rate as u8, Instant::now()),
                received_history: Vec::new(),
                bootstrap_mode: false,
                next_sequence_number: VarInt::from_u32(0),
                last_received_sequence: std::collections::HashMap::new(),
                frames_sent: 0,
            };
        }

        // Create using the config, then override specific fields for test purposes
        let config = crate::transport_parameters::AddressDiscoveryConfig::SendAndReceive;
        let mut state = Self::new(&config, Instant::now());
        state.max_observation_rate = max_rate as u8;
        state.observe_all_paths = observe_all_paths;
        state.rate_limiter = AddressObservationRateLimiter::new(max_rate as u8, Instant::now());
        state
    }

    /// Enable or disable bootstrap mode (aggressive observation)
    fn set_bootstrap_mode(&mut self, enabled: bool) {
        self.bootstrap_mode = enabled;
        // If enabling bootstrap mode, update rate limiter to allow higher rates
        if enabled {
            let bootstrap_rate = self.get_effective_rate_limit();
            self.rate_limiter.rate = bootstrap_rate;
            self.rate_limiter.max_tokens = bootstrap_rate * 2.0; // Allow burst of 2 seconds
            // Also fill tokens to max for immediate use
            self.rate_limiter.tokens = self.rate_limiter.max_tokens;
        }
    }

    /// Check if bootstrap mode is enabled
    fn is_bootstrap_mode(&self) -> bool {
        self.bootstrap_mode
    }

    /// Get the effective rate limit (considering bootstrap mode)
    fn get_effective_rate_limit(&self) -> f64 {
        if self.bootstrap_mode {
            // Bootstrap nodes get 5x the configured rate
            (self.max_observation_rate as f64) * 5.0
        } else {
            self.max_observation_rate as f64
        }
    }

    /// Check if we should observe this path (considering bootstrap mode)
    fn should_observe_path(&self, path_id: u64) -> bool {
        if !self.enabled {
            return false;
        }

        // Bootstrap nodes observe all paths regardless of configuration
        if self.bootstrap_mode {
            return true;
        }

        // Normal mode respects the configuration
        self.observe_all_paths || path_id == 0
    }

    /// Check if we should send observation immediately (for bootstrap nodes)
    fn should_send_observation_immediately(&self, is_new_connection: bool) -> bool {
        self.bootstrap_mode && is_new_connection
    }
}

#[allow(dead_code)]
impl AddressObservationRateLimiter {
    /// Create a new rate limiter
    fn new(rate: u8, now: Instant) -> Self {
        let rate_f64 = rate as f64;
        Self {
            tokens: rate_f64,
            max_tokens: rate_f64,
            rate: rate_f64,
            last_update: now,
        }
    }

    /// Try to consume tokens, returns true if successful
    fn try_consume(&mut self, tokens: f64, now: Instant) -> bool {
        self.update_tokens(now);

        if self.tokens >= tokens {
            self.tokens -= tokens;
            true
        } else {
            false
        }
    }

    /// Update available tokens based on elapsed time
    fn update_tokens(&mut self, now: Instant) {
        let elapsed = now.saturating_duration_since(self.last_update);
        let new_tokens = elapsed.as_secs_f64() * self.rate;
        self.tokens = (self.tokens + new_tokens).min(self.max_tokens);
        self.last_update = now;
    }

    /// Update the rate
    fn set_rate(&mut self, rate: u8) {
        let rate_f64 = rate as f64;
        self.rate = rate_f64;
        self.max_tokens = rate_f64;
        // Don't change current tokens, just cap at new max
        if self.tokens > self.max_tokens {
            self.tokens = self.max_tokens;
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::transport_parameters::AddressDiscoveryConfig;
    use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};

    #[test]
    fn address_discovery_state_new() {
        let config = crate::transport_parameters::AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let state = AddressDiscoveryState::new(&config, now);

        assert!(state.enabled);
        assert_eq!(state.max_observation_rate, 10);
        assert!(!state.observe_all_paths);
        assert!(state.sent_observations.is_empty());
        assert!(state.received_observations.is_empty());
        assert!(state.received_history.is_empty());
        assert_eq!(state.rate_limiter.tokens, 10.0);
    }

    #[test]
    fn address_discovery_state_disabled() {
        let config = crate::transport_parameters::AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);

        // Disable the state
        state.enabled = false;

        // Should not send observations when disabled
        assert!(!state.should_send_observation(0, now));
    }

    #[test]
    fn address_discovery_state_should_send_observation() {
        let config = crate::transport_parameters::AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);

        // Should send for new path
        assert!(state.should_send_observation(0, now));

        // Add path info
        let mut path_info = paths::PathAddressInfo::new();
        path_info.update_observed_address(
            SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 8080),
            now,
        );
        path_info.mark_notified();
        state.sent_observations.insert(0, path_info);

        // Should not send if already notified
        assert!(!state.should_send_observation(0, now));

        // Path 1 is not observed by default (only path 0 is)
        assert!(!state.should_send_observation(1, now));
    }

    #[test]
    fn address_discovery_state_rate_limiting() {
        let config = crate::transport_parameters::AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);

        // Configure to observe all paths for this test
        state.observe_all_paths = true;

        // Should allow first observation on path 0
        assert!(state.should_send_observation(0, now));

        // Consume some tokens to test rate limiting
        state.rate_limiter.try_consume(9.0, now); // Consume 9 tokens (leaving ~1)

        // Next observation should be rate limited
        assert!(!state.should_send_observation(0, now));

        // After 1 second, should have replenished tokens (10 per second)
        let later = now + Duration::from_secs(1);
        state.rate_limiter.update_tokens(later);
        assert!(state.should_send_observation(0, later));
    }

    #[test]
    fn address_discovery_state_handle_observed_address() {
        let config = crate::transport_parameters::AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);

        let addr1 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 443);
        let addr2 = SocketAddr::new(
            IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)),
            8080,
        );

        // Handle first observation
        state.handle_observed_address(addr1, 0, now);
        assert_eq!(state.received_history.len(), 1);
        assert_eq!(state.received_history[0].address, addr1);
        assert_eq!(state.received_history[0].path_id, 0);

        // Handle second observation
        let later = now + Duration::from_millis(100);
        state.handle_observed_address(addr2, 1, later);
        assert_eq!(state.received_history.len(), 2);
        assert_eq!(state.received_history[1].address, addr2);
        assert_eq!(state.received_history[1].path_id, 1);
    }

    #[test]
    fn address_discovery_state_get_observed_address() {
        let config = crate::transport_parameters::AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);

        // No address initially
        assert_eq!(state.get_observed_address(0), None);

        // Add path info
        let mut path_info = paths::PathAddressInfo::new();
        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 80);
        path_info.update_observed_address(addr, now);
        state.received_observations.insert(0, path_info);

        // Should return the address
        assert_eq!(state.get_observed_address(0), Some(addr));
        assert_eq!(state.get_observed_address(1), None);
    }

    #[test]
    fn address_discovery_state_unnotified_changes() {
        let config = crate::transport_parameters::AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);

        // No changes initially
        assert!(!state.has_unnotified_changes());

        // Add unnotified path
        let mut path_info = paths::PathAddressInfo::new();
        path_info.update_observed_address(
            SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 8080),
            now,
        );
        state.sent_observations.insert(0, path_info);

        // Should have unnotified changes
        assert!(state.has_unnotified_changes());

        // Mark as notified
        state.record_observation_sent(0);
        assert!(!state.has_unnotified_changes());
    }

    #[test]
    fn address_observation_rate_limiter_token_bucket() {
        let now = Instant::now();
        let mut limiter = AddressObservationRateLimiter::new(5, now); // 5 tokens/sec

        // Initial state
        assert_eq!(limiter.tokens, 5.0);
        assert_eq!(limiter.max_tokens, 5.0);
        assert_eq!(limiter.rate, 5.0);

        // Consume 3 tokens
        assert!(limiter.try_consume(3.0, now));
        assert_eq!(limiter.tokens, 2.0);

        // Try to consume more than available
        assert!(!limiter.try_consume(3.0, now));
        assert_eq!(limiter.tokens, 2.0);

        // After 1 second, should have 5 more tokens (capped at max)
        let later = now + Duration::from_secs(1);
        limiter.update_tokens(later);
        assert_eq!(limiter.tokens, 5.0); // 2 + 5 = 7, but capped at 5

        // After 0.5 seconds from original, should have 2.5 more tokens
        let half_sec = now + Duration::from_millis(500);
        let mut limiter2 = AddressObservationRateLimiter::new(5, now);
        limiter2.try_consume(3.0, now);
        limiter2.update_tokens(half_sec);
        assert_eq!(limiter2.tokens, 4.5); // 2 + 2.5
    }

    // Tests for address_discovery_state field in Connection
    #[test]
    fn connection_initializes_address_discovery_state_default() {
        // Test that Connection initializes with default address discovery state
        // For now, just test that AddressDiscoveryState can be created with default config
        let config = crate::transport_parameters::AddressDiscoveryConfig::default();
        let state = AddressDiscoveryState::new(&config, Instant::now());
        assert!(state.enabled); // Default is now enabled
        assert_eq!(state.max_observation_rate, 10); // Default is 10
        assert!(!state.observe_all_paths);
    }

    #[test]
    fn connection_initializes_with_address_discovery_enabled() {
        // Test that AddressDiscoveryState can be created with enabled config
        let config = crate::transport_parameters::AddressDiscoveryConfig::SendAndReceive;
        let state = AddressDiscoveryState::new(&config, Instant::now());
        assert!(state.enabled);
        assert_eq!(state.max_observation_rate, 10);
        assert!(!state.observe_all_paths);
    }

    #[test]
    fn connection_address_discovery_enabled_by_default() {
        // Test that AddressDiscoveryState is enabled with default config
        let config = crate::transport_parameters::AddressDiscoveryConfig::default();
        let state = AddressDiscoveryState::new(&config, Instant::now());
        assert!(state.enabled); // Default is now enabled
    }

    #[test]
    fn negotiate_max_idle_timeout_commutative() {
        let test_params = [
            (None, None, None),
            (None, Some(VarInt(0)), None),
            (None, Some(VarInt(2)), Some(Duration::from_millis(2))),
            (Some(VarInt(0)), Some(VarInt(0)), None),
            (
                Some(VarInt(2)),
                Some(VarInt(0)),
                Some(Duration::from_millis(2)),
            ),
            (
                Some(VarInt(1)),
                Some(VarInt(4)),
                Some(Duration::from_millis(1)),
            ),
        ];

        for (left, right, result) in test_params {
            assert_eq!(negotiate_max_idle_timeout(left, right), result);
            assert_eq!(negotiate_max_idle_timeout(right, left), result);
        }
    }

    #[test]
    fn path_creation_initializes_address_discovery() {
        let config = TransportConfig::default();
        let remote = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 8080);
        let now = Instant::now();

        // Test initial path creation
        let path = paths::PathData::new(remote, false, None, now, &config);

        // Should have address info initialized
        assert!(path.address_info.observed_address.is_none());
        assert!(path.address_info.last_observed.is_none());
        assert_eq!(path.address_info.observation_count, 0);
        assert!(!path.address_info.notified);

        // Should have rate limiter initialized
        assert_eq!(path.observation_rate_limiter.rate, 10.0);
        assert_eq!(path.observation_rate_limiter.max_tokens, 10.0);
        assert_eq!(path.observation_rate_limiter.tokens, 10.0);
    }

    #[test]
    fn path_migration_resets_address_discovery() {
        let config = TransportConfig::default();
        let remote1 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 8080);
        let remote2 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 443);
        let now = Instant::now();

        // Create initial path with some address discovery state
        let mut path1 = paths::PathData::new(remote1, false, None, now, &config);
        path1.update_observed_address(remote1, now);
        path1.mark_address_notified();
        path1.consume_observation_token(now);
        path1.set_observation_rate(20);

        // Migrate to new path
        let path2 = paths::PathData::from_previous(remote2, &path1, now);

        // Address info should be reset
        assert!(path2.address_info.observed_address.is_none());
        assert!(path2.address_info.last_observed.is_none());
        assert_eq!(path2.address_info.observation_count, 0);
        assert!(!path2.address_info.notified);

        // Rate limiter should have same rate but full tokens
        assert_eq!(path2.observation_rate_limiter.rate, 20.0);
        assert_eq!(path2.observation_rate_limiter.tokens, 20.0);
    }

    #[test]
    fn connection_path_updates_observation_rate() {
        let config = TransportConfig::default();
        let remote = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 42);
        let now = Instant::now();

        let mut path = paths::PathData::new(remote, false, None, now, &config);

        // Initial rate should be default
        assert_eq!(path.observation_rate_limiter.rate, 10.0);

        // Update rate based on negotiated config
        path.set_observation_rate(25);
        assert_eq!(path.observation_rate_limiter.rate, 25.0);
        assert_eq!(path.observation_rate_limiter.max_tokens, 25.0);

        // Tokens should be capped at new max if needed
        path.observation_rate_limiter.tokens = 30.0; // Set higher than max
        path.set_observation_rate(20);
        assert_eq!(path.observation_rate_limiter.tokens, 20.0); // Capped at new max
    }

    #[test]
    fn path_validation_preserves_discovery_state() {
        let config = TransportConfig::default();
        let remote = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 8080);
        let now = Instant::now();

        let mut path = paths::PathData::new(remote, false, None, now, &config);

        // Set up some discovery state
        let observed = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 5678);
        path.update_observed_address(observed, now);
        path.set_observation_rate(15);

        // Simulate path validation
        path.validated = true;

        // Discovery state should be preserved
        assert_eq!(path.address_info.observed_address, Some(observed));
        assert_eq!(path.observation_rate_limiter.rate, 15.0);
    }

    #[test]
    fn address_discovery_state_initialization() {
        // Use the test constructor that allows setting specific values
        let state = AddressDiscoveryState::new_with_params(true, 30.0, true);

        assert!(state.enabled);
        assert_eq!(state.max_observation_rate, 30);
        assert!(state.observe_all_paths);
        assert!(state.sent_observations.is_empty());
        assert!(state.received_observations.is_empty());
        assert!(state.received_history.is_empty());
    }

    // Tests for Task 2.3: Frame Processing Pipeline
    #[test]
    fn handle_observed_address_frame_basic() {
        let config = AddressDiscoveryConfig::SendAndReceive;
        let mut state = AddressDiscoveryState::new(&config, Instant::now());
        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 8080);
        let now = Instant::now();
        let path_id = 0;

        // Handle an observed address frame
        state.handle_observed_address(addr, path_id, now);

        // Should have recorded the observation
        assert_eq!(state.received_history.len(), 1);
        assert_eq!(state.received_history[0].address, addr);
        assert_eq!(state.received_history[0].path_id, path_id);
        assert_eq!(state.received_history[0].received_at, now);

        // Should have updated path state
        assert!(state.received_observations.contains_key(&path_id));
        let path_info = &state.received_observations[&path_id];
        assert_eq!(path_info.observed_address, Some(addr));
        assert_eq!(path_info.last_observed, Some(now));
        assert_eq!(path_info.observation_count, 1);
    }

    #[test]
    fn handle_observed_address_frame_multiple_observations() {
        let config = AddressDiscoveryConfig::SendAndReceive;
        let mut state = AddressDiscoveryState::new(&config, Instant::now());
        let addr1 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 8080);
        let addr2 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)), 443);
        let now = Instant::now();
        let path_id = 0;

        // Handle multiple observations
        state.handle_observed_address(addr1, path_id, now);
        state.handle_observed_address(addr1, path_id, now + Duration::from_secs(1));
        state.handle_observed_address(addr2, path_id, now + Duration::from_secs(2));

        // Should have all observations in the event list
        assert_eq!(state.received_history.len(), 3);

        // Path info should reflect the latest observation
        let path_info = &state.received_observations[&path_id];
        assert_eq!(path_info.observed_address, Some(addr2));
        assert_eq!(path_info.observation_count, 1); // Reset for new address
    }

    #[test]
    fn handle_observed_address_frame_disabled() {
        let config = AddressDiscoveryConfig::SendAndReceive;
        let mut state = AddressDiscoveryState::new(&config, Instant::now());
        state.enabled = false; // Disable after creation
        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 8080);
        let now = Instant::now();

        // Should not handle when disabled
        state.handle_observed_address(addr, 0, now);

        // Should not record anything
        assert!(state.received_history.is_empty());
        assert!(state.sent_observations.is_empty());
        assert!(state.received_observations.is_empty());
    }

    #[test]
    fn should_send_observation_basic() {
        let config = AddressDiscoveryConfig::SendAndReceive;
        let mut state = AddressDiscoveryState::new(&config, Instant::now());
        state.max_observation_rate = 10;
        let now = Instant::now();
        let path_id = 0;

        // Should be able to send initially
        assert!(state.should_send_observation(path_id, now));

        // Record that we sent one
        state.record_observation_sent(path_id);

        // Should still be able to send (have tokens)
        assert!(state.should_send_observation(path_id, now));
    }

    #[test]
    fn should_send_observation_rate_limiting() {
        let config = AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);
        state.max_observation_rate = 2; // Very low rate
        state.update_rate_limit(2.0);
        let path_id = 0;

        // Consume all tokens
        assert!(state.should_send_observation(path_id, now));
        state.record_observation_sent(path_id);
        assert!(state.should_send_observation(path_id, now));
        state.record_observation_sent(path_id);

        // Should be rate limited now
        assert!(!state.should_send_observation(path_id, now));

        // Wait for token replenishment
        let later = now + Duration::from_secs(1);
        assert!(state.should_send_observation(path_id, later));
    }

    #[test]
    fn should_send_observation_disabled() {
        let config = AddressDiscoveryConfig::SendAndReceive;
        let mut state = AddressDiscoveryState::new(&config, Instant::now());
        state.enabled = false;

        // Should never send when disabled
        assert!(!state.should_send_observation(0, Instant::now()));
    }

    #[test]
    fn should_send_observation_per_path() {
        let config = AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);
        state.max_observation_rate = 2; // Allow 2 observations per second
        state.observe_all_paths = true;
        state.update_rate_limit(2.0);

        // Path 0 uses a token from the shared rate limiter
        assert!(state.should_send_observation(0, now));
        state.record_observation_sent(0);

        // Path 1 can still send because we have 2 tokens per second
        assert!(state.should_send_observation(1, now));
        state.record_observation_sent(1);

        // Now both paths should be rate limited (no more tokens)
        assert!(!state.should_send_observation(0, now));
        assert!(!state.should_send_observation(1, now));

        // After 1 second, we should have new tokens
        let later = now + Duration::from_secs(1);
        assert!(state.should_send_observation(0, later));
    }

    #[test]
    fn has_unnotified_changes_test() {
        let config = AddressDiscoveryConfig::SendAndReceive;
        let mut state = AddressDiscoveryState::new(&config, Instant::now());
        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 8080);
        let now = Instant::now();

        // Initially no changes
        assert!(!state.has_unnotified_changes());

        // After receiving an observation
        state.handle_observed_address(addr, 0, now);
        assert!(state.has_unnotified_changes());

        // After marking as notified
        state.received_observations.get_mut(&0).unwrap().notified = true;
        assert!(!state.has_unnotified_changes());
    }

    #[test]
    fn get_observed_address_test() {
        let config = AddressDiscoveryConfig::SendAndReceive;
        let mut state = AddressDiscoveryState::new(&config, Instant::now());
        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 8080);
        let now = Instant::now();
        let path_id = 0;

        // Initially no address
        assert_eq!(state.get_observed_address(path_id), None);

        // After observation
        state.handle_observed_address(addr, path_id, now);
        assert_eq!(state.get_observed_address(path_id), Some(addr));

        // Non-existent path
        assert_eq!(state.get_observed_address(999), None);
    }

    // Tests for Task 2.4: Rate Limiting Implementation
    #[test]
    fn rate_limiter_token_bucket_basic() {
        let now = Instant::now();
        let mut limiter = AddressObservationRateLimiter::new(10, now); // 10 tokens per second

        // Should be able to consume tokens up to the limit
        assert!(limiter.try_consume(5.0, now));
        assert!(limiter.try_consume(5.0, now));

        // Should not be able to consume more tokens
        assert!(!limiter.try_consume(1.0, now));
    }

    #[test]
    fn rate_limiter_token_replenishment() {
        let now = Instant::now();
        let mut limiter = AddressObservationRateLimiter::new(10, now); // 10 tokens per second

        // Consume all tokens
        assert!(limiter.try_consume(10.0, now));
        assert!(!limiter.try_consume(0.1, now)); // Should be empty

        // After 1 second, should have new tokens
        let later = now + Duration::from_secs(1);
        assert!(limiter.try_consume(10.0, later)); // Should work after replenishment

        // After 0.5 seconds, should have 5 new tokens
        assert!(!limiter.try_consume(0.1, later)); // Empty again
        let later = later + Duration::from_millis(500);
        assert!(limiter.try_consume(5.0, later)); // Should have ~5 tokens
        assert!(!limiter.try_consume(0.1, later)); // But not more
    }

    #[test]
    fn rate_limiter_max_tokens_cap() {
        let now = Instant::now();
        let mut limiter = AddressObservationRateLimiter::new(10, now);

        // After 2 seconds, should still be capped at max_tokens
        let later = now + Duration::from_secs(2);
        // Try to consume more than max - should fail
        assert!(limiter.try_consume(10.0, later));
        assert!(!limiter.try_consume(10.1, later)); // Can't consume more than max even after time

        // Consume some tokens
        let later2 = later + Duration::from_secs(1);
        assert!(limiter.try_consume(3.0, later2));

        // After another 2 seconds, should be back at max
        let much_later = later2 + Duration::from_secs(2);
        assert!(limiter.try_consume(10.0, much_later)); // Can consume full amount
        assert!(!limiter.try_consume(0.1, much_later)); // But not more
    }

    #[test]
    fn rate_limiter_fractional_consumption() {
        let now = Instant::now();
        let mut limiter = AddressObservationRateLimiter::new(10, now);

        // Should handle fractional token consumption
        assert!(limiter.try_consume(0.5, now));
        assert!(limiter.try_consume(2.3, now));
        assert!(limiter.try_consume(7.2, now)); // Total: 10.0
        assert!(!limiter.try_consume(0.1, now)); // Should be empty

        // Should handle fractional replenishment
        let later = now + Duration::from_millis(100); // 0.1 seconds = 1 token
        assert!(limiter.try_consume(1.0, later));
        assert!(!limiter.try_consume(0.1, later));
    }

    #[test]
    fn rate_limiter_zero_rate() {
        let now = Instant::now();
        let mut limiter = AddressObservationRateLimiter::new(0, now); // 0 tokens per second

        // Should never be able to consume tokens
        assert!(!limiter.try_consume(1.0, now));
        assert!(!limiter.try_consume(0.1, now));
        assert!(!limiter.try_consume(0.001, now));

        // Even after time passes, no tokens
        let later = now + Duration::from_secs(10);
        assert!(!limiter.try_consume(0.001, later));
    }

    #[test]
    fn rate_limiter_high_rate() {
        let now = Instant::now();
        let mut limiter = AddressObservationRateLimiter::new(63, now); // Max allowed rate

        // Consume many tokens
        assert!(limiter.try_consume(60.0, now));
        assert!(limiter.try_consume(3.0, now));
        assert!(!limiter.try_consume(0.1, now)); // Should be empty

        // After 1 second, should have replenished
        let later = now + Duration::from_secs(1);
        assert!(limiter.try_consume(63.0, later)); // Full amount available
        assert!(!limiter.try_consume(0.1, later)); // But not more
    }

    #[test]
    fn rate_limiter_time_precision() {
        let now = Instant::now();
        let mut limiter = AddressObservationRateLimiter::new(100, now); // 100 tokens per second (max for u8)

        // Consume all tokens
        assert!(limiter.try_consume(100.0, now));
        assert!(!limiter.try_consume(0.1, now));

        // After 10 milliseconds, should have ~1 token
        let later = now + Duration::from_millis(10);
        assert!(limiter.try_consume(0.8, later)); // Should have ~1 token (allowing for precision)
        assert!(!limiter.try_consume(0.5, later)); // But not much more

        // Reset for next test by waiting longer
        let much_later = later + Duration::from_millis(100); // 100ms = 10 tokens
        assert!(limiter.try_consume(5.0, much_later)); // Should have some tokens

        // Consume remaining to have a clean state
        limiter.tokens = 0.0; // Force empty state

        // After 1 millisecond from empty state
        let final_time = much_later + Duration::from_millis(1);
        // With 100 tokens/sec, 1 millisecond = 0.1 tokens
        limiter.update_tokens(final_time); // Update tokens manually

        // Check we have approximately 0.1 tokens (allow for floating point error)
        assert!(limiter.tokens >= 0.09 && limiter.tokens <= 0.11);
    }

    #[test]
    fn per_path_rate_limiting_independent() {
        let config = crate::transport_parameters::AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);

        // Enable all paths observation
        state.observe_all_paths = true;

        // Set a lower rate limit for this test (5 tokens)
        state.update_rate_limit(5.0);

        // Set up path addresses so should_send_observation returns true
        state
            .sent_observations
            .insert(0, paths::PathAddressInfo::new());
        state
            .sent_observations
            .insert(1, paths::PathAddressInfo::new());
        state
            .sent_observations
            .insert(2, paths::PathAddressInfo::new());

        // Set observed addresses so paths need observation
        state
            .sent_observations
            .get_mut(&0)
            .unwrap()
            .observed_address = Some(SocketAddr::new(
            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)),
            8080,
        ));
        state
            .sent_observations
            .get_mut(&1)
            .unwrap()
            .observed_address = Some(SocketAddr::new(
            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2)),
            8081,
        ));
        state
            .sent_observations
            .get_mut(&2)
            .unwrap()
            .observed_address = Some(SocketAddr::new(
            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 3)),
            8082,
        ));

        // Path 0: consume 3 tokens
        for _ in 0..3 {
            assert!(state.should_send_observation(0, now));
            state.record_observation_sent(0);
            // Reset notified flag for next check
            state.sent_observations.get_mut(&0).unwrap().notified = false;
        }

        // Path 1: consume 2 tokens
        for _ in 0..2 {
            assert!(state.should_send_observation(1, now));
            state.record_observation_sent(1);
            // Reset notified flag for next check
            state.sent_observations.get_mut(&1).unwrap().notified = false;
        }

        // Global limit should be hit (5 total)
        assert!(!state.should_send_observation(2, now));

        // After 1 second, should have 5 more tokens
        let later = now + Duration::from_secs(1);

        // All paths should be able to send again
        assert!(state.should_send_observation(0, later));
        assert!(state.should_send_observation(1, later));
        assert!(state.should_send_observation(2, later));
    }

    #[test]
    fn per_path_rate_limiting_with_path_specific_limits() {
        let now = Instant::now();
        let remote1 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 8080);
        let remote2 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2)), 8081);
        let config = TransportConfig::default();

        // Create paths with different rate limits
        let mut path1 = paths::PathData::new(remote1, false, None, now, &config);
        let mut path2 = paths::PathData::new(remote2, false, None, now, &config);

        // Set different rate limits
        path1.observation_rate_limiter = paths::PathObservationRateLimiter::new(10, now); // 10/sec
        path2.observation_rate_limiter = paths::PathObservationRateLimiter::new(5, now); // 5/sec

        // Path 1 should allow 10 observations
        for _ in 0..10 {
            assert!(path1.observation_rate_limiter.can_send(now));
            path1.observation_rate_limiter.consume_token(now);
        }
        assert!(!path1.observation_rate_limiter.can_send(now));

        // Path 2 should allow 5 observations
        for _ in 0..5 {
            assert!(path2.observation_rate_limiter.can_send(now));
            path2.observation_rate_limiter.consume_token(now);
        }
        assert!(!path2.observation_rate_limiter.can_send(now));
    }

    #[test]
    fn per_path_rate_limiting_address_change_detection() {
        let config = crate::transport_parameters::AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);

        // Setup initial path with address
        let path_id = 0;
        let addr1 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 8080);
        let addr2 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2)), 8080);

        // First observation should be allowed
        assert!(state.should_send_observation(path_id, now));

        // Queue the frame (this also marks it as notified in sent_observations)
        let frame = state.queue_observed_address_frame(path_id, addr1);
        assert!(frame.is_some());

        // Same path, should not send again (already notified)
        assert!(!state.should_send_observation(path_id, now));

        // Simulate address change detection by marking as not notified
        if let Some(info) = state.sent_observations.get_mut(&path_id) {
            info.notified = false;
            info.observed_address = Some(addr2);
        }

        // Should now allow sending for the address change
        assert!(state.should_send_observation(path_id, now));
    }

    #[test]
    fn per_path_rate_limiting_migration() {
        let now = Instant::now();
        let remote1 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 8080);
        let remote2 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 2)), 8081);
        let config = TransportConfig::default();

        // Create initial path and consume tokens
        let mut path = paths::PathData::new(remote1, false, None, now, &config);
        path.observation_rate_limiter = paths::PathObservationRateLimiter::new(10, now);

        // Consume some tokens
        for _ in 0..5 {
            assert!(path.observation_rate_limiter.can_send(now));
            path.observation_rate_limiter.consume_token(now);
        }

        // Create new path (simulates connection migration)
        let mut new_path = paths::PathData::new(remote2, false, None, now, &config);

        // New path should have fresh rate limiter (migration resets limits)
        // Since default observation rate is 0, set it manually
        new_path.observation_rate_limiter = paths::PathObservationRateLimiter::new(10, now);

        // Should have full tokens available
        for _ in 0..10 {
            assert!(new_path.observation_rate_limiter.can_send(now));
            new_path.observation_rate_limiter.consume_token(now);
        }
        assert!(!new_path.observation_rate_limiter.can_send(now));
    }

    #[test]
    fn per_path_rate_limiting_disabled_paths() {
        let config = crate::transport_parameters::AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);

        // Primary path (id 0) should be allowed
        assert!(state.should_send_observation(0, now));

        // Non-primary paths should not be allowed when observe_all_paths is false
        assert!(!state.should_send_observation(1, now));
        assert!(!state.should_send_observation(2, now));

        // Even with rate limit available
        let later = now + Duration::from_secs(1);
        assert!(!state.should_send_observation(1, later));
    }

    #[test]
    fn respecting_negotiated_max_observation_rate_basic() {
        let config = crate::transport_parameters::AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);

        // Simulate negotiated rate from peer (lower than ours)
        state.max_observation_rate = 10; // Peer only allows 10/sec
        state.rate_limiter = AddressObservationRateLimiter::new(10, now);

        // Should respect the negotiated rate (10, not 20)
        for _ in 0..10 {
            assert!(state.should_send_observation(0, now));
        }
        // 11th should fail
        assert!(!state.should_send_observation(0, now));
    }

    #[test]
    fn respecting_negotiated_max_observation_rate_zero() {
        let config = crate::transport_parameters::AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);

        // Peer negotiated rate of 0 (disabled)
        state.max_observation_rate = 0;
        state.rate_limiter = AddressObservationRateLimiter::new(0, now);

        // Should not send any observations
        assert!(!state.should_send_observation(0, now));
        assert!(!state.should_send_observation(1, now));

        // Even after time passes
        let later = now + Duration::from_secs(10);
        assert!(!state.should_send_observation(0, later));
    }

    #[test]
    fn respecting_negotiated_max_observation_rate_higher() {
        let config = crate::transport_parameters::AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);

        // Set up a path with an address to observe
        state
            .sent_observations
            .insert(0, paths::PathAddressInfo::new());
        state
            .sent_observations
            .get_mut(&0)
            .unwrap()
            .observed_address = Some(SocketAddr::new(
            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)),
            8080,
        ));

        // Set our local rate to 5
        state.update_rate_limit(5.0);

        // Simulate negotiated rate from peer (higher than ours)
        state.max_observation_rate = 20; // Peer allows 20/sec

        // Should respect our local rate (5, not 20)
        for _ in 0..5 {
            assert!(state.should_send_observation(0, now));
            state.record_observation_sent(0);
            // Reset notified flag for next iteration
            state.sent_observations.get_mut(&0).unwrap().notified = false;
        }
        // 6th should fail (out of tokens)
        assert!(!state.should_send_observation(0, now));
    }

    #[test]
    fn respecting_negotiated_max_observation_rate_dynamic_update() {
        let config = crate::transport_parameters::AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);

        // Set up initial path
        state
            .sent_observations
            .insert(0, paths::PathAddressInfo::new());
        state
            .sent_observations
            .get_mut(&0)
            .unwrap()
            .observed_address = Some(SocketAddr::new(
            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)),
            8080,
        ));

        // Use initial rate - consume 5 tokens
        for _ in 0..5 {
            assert!(state.should_send_observation(0, now));
            state.record_observation_sent(0);
            // Reset notified flag for next iteration
            state.sent_observations.get_mut(&0).unwrap().notified = false;
        }

        // We have 5 tokens remaining

        // Simulate rate renegotiation (e.g., from transport parameter update)
        state.max_observation_rate = 3;
        state.rate_limiter.set_rate(3);

        // Can still use remaining tokens from before (5 tokens)
        // But they're capped at new max (3), so we'll have 3 tokens
        for _ in 0..3 {
            assert!(state.should_send_observation(0, now));
            state.record_observation_sent(0);
            // Reset notified flag for next iteration
            state.sent_observations.get_mut(&0).unwrap().notified = false;
        }

        // Should be out of tokens now
        assert!(!state.should_send_observation(0, now));

        // After 1 second, should only have 3 new tokens
        let later = now + Duration::from_secs(1);
        for _ in 0..3 {
            assert!(state.should_send_observation(0, later));
            state.record_observation_sent(0);
            // Reset notified flag for next iteration
            state.sent_observations.get_mut(&0).unwrap().notified = false;
        }

        // Should be out of tokens again
        assert!(!state.should_send_observation(0, later));
    }

    #[test]
    fn respecting_negotiated_max_observation_rate_with_paths() {
        let config = crate::transport_parameters::AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);

        // Enable all paths observation
        state.observe_all_paths = true;

        // Set up multiple paths with addresses
        for i in 0..3 {
            state
                .sent_observations
                .insert(i, paths::PathAddressInfo::new());
            state
                .sent_observations
                .get_mut(&i)
                .unwrap()
                .observed_address = Some(SocketAddr::new(
                IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100 + i as u8)),
                5000,
            ));
        }

        // Consume tokens by sending observations
        // We start with 10 tokens
        for _ in 0..3 {
            // Each iteration sends one observation per path
            for i in 0..3 {
                if state.should_send_observation(i, now) {
                    state.record_observation_sent(i);
                    // Reset notified flag for next iteration
                    state.sent_observations.get_mut(&i).unwrap().notified = false;
                }
            }
        }

        // We've sent 9 observations (3 iterations × 3 paths), have 1 token left
        // One more observation should succeed
        assert!(state.should_send_observation(0, now));
        state.record_observation_sent(0);

        // All paths should be rate limited now (no tokens left)
        assert!(!state.should_send_observation(0, now));
        assert!(!state.should_send_observation(1, now));
        assert!(!state.should_send_observation(2, now));
    }

    #[test]
    fn queue_observed_address_frame_basic() {
        let config = crate::transport_parameters::AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);

        // Queue a frame for path 0
        let address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), 5000);
        let frame = state.queue_observed_address_frame(0, address);

        // Should return Some(frame) since this is the first observation
        assert!(frame.is_some());
        let frame = frame.unwrap();
        assert_eq!(frame.address, address);

        // Should mark path as notified
        assert!(state.sent_observations.contains_key(&0));
        assert!(state.sent_observations.get(&0).unwrap().notified);
    }

    #[test]
    fn queue_observed_address_frame_rate_limited() {
        let config = crate::transport_parameters::AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);

        // Enable all paths for this test
        state.observe_all_paths = true;

        // With 10 tokens initially, we should be able to send 10 frames
        let mut addresses = Vec::new();
        for i in 0..10 {
            let addr = SocketAddr::new(
                IpAddr::V4(Ipv4Addr::new(192, 168, 1, i as u8)),
                5000 + i as u16,
            );
            addresses.push(addr);
            assert!(
                state.queue_observed_address_frame(i as u64, addr).is_some(),
                "Frame {} should be allowed",
                i + 1
            );
        }

        // 11th should be rate limited
        let addr11 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 11)), 5011);
        assert!(
            state.queue_observed_address_frame(10, addr11).is_none(),
            "11th frame should be rate limited"
        );
    }

    #[test]
    fn queue_observed_address_frame_disabled() {
        let config = crate::transport_parameters::AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);

        // Disable address discovery
        state.enabled = false;

        let address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), 5000);

        // Should return None when disabled
        assert!(state.queue_observed_address_frame(0, address).is_none());
    }

    #[test]
    fn queue_observed_address_frame_already_notified() {
        let config = crate::transport_parameters::AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);

        let address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), 5000);

        // First observation should succeed
        assert!(state.queue_observed_address_frame(0, address).is_some());

        // Second observation for same address should return None
        assert!(state.queue_observed_address_frame(0, address).is_none());

        // Even with different address, if already notified, should return None
        let new_address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 101)), 5001);
        assert!(state.queue_observed_address_frame(0, new_address).is_none());
    }

    #[test]
    fn queue_observed_address_frame_primary_path_only() {
        let config = crate::transport_parameters::AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);

        let address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), 5000);

        // Primary path should work
        assert!(state.queue_observed_address_frame(0, address).is_some());

        // Non-primary paths should not work
        assert!(state.queue_observed_address_frame(1, address).is_none());
        assert!(state.queue_observed_address_frame(2, address).is_none());
    }

    #[test]
    fn queue_observed_address_frame_updates_path_info() {
        let config = crate::transport_parameters::AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);

        let address = SocketAddr::new(
            IpAddr::V6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 1)),
            5000,
        );

        // Queue frame
        let frame = state.queue_observed_address_frame(0, address);
        assert!(frame.is_some());

        // Check path info was updated
        let path_info = state.sent_observations.get(&0).unwrap();
        assert_eq!(path_info.observed_address, Some(address));
        assert!(path_info.notified);

        // Note: received_history list is NOT updated by queue_observed_address_frame
        // That list is for addresses we've received from peers, not ones we're sending
        assert_eq!(state.received_history.len(), 0);
    }

    #[test]
    fn retransmits_includes_outbound_observations() {
        use crate::connection::spaces::Retransmits;

        // Create a retransmits struct
        let mut retransmits = Retransmits::default();

        // Initially should be empty
        assert!(retransmits.outbound_observations.is_empty());

        // Add an observed address frame
        let address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), 5000);
        let frame = frame::ObservedAddress {
            sequence_number: VarInt::from_u32(1),
            address,
        };
        retransmits.outbound_observations.push(frame);

        // Should now have one frame
        assert_eq!(retransmits.outbound_observations.len(), 1);
        assert_eq!(retransmits.outbound_observations[0].address, address);
    }

    #[test]
    fn check_for_address_observations_no_peer_support() {
        let config = crate::transport_parameters::AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);

        // Simulate address change on path 0
        state
            .sent_observations
            .insert(0, paths::PathAddressInfo::new());
        state
            .sent_observations
            .get_mut(&0)
            .unwrap()
            .observed_address = Some(SocketAddr::new(
            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)),
            5000,
        ));

        // Check for observations with no peer support
        let frames = state.check_for_address_observations(0, false, now);

        // Should return empty vec when peer doesn't support
        assert!(frames.is_empty());
    }

    #[test]
    fn check_for_address_observations_with_peer_support() {
        let config = crate::transport_parameters::AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);

        // Simulate address change on path 0
        let address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), 5000);
        state
            .sent_observations
            .insert(0, paths::PathAddressInfo::new());
        state
            .sent_observations
            .get_mut(&0)
            .unwrap()
            .observed_address = Some(address);

        // Check for observations with peer support
        let frames = state.check_for_address_observations(0, true, now);

        // Should return frame for unnotified address
        assert_eq!(frames.len(), 1);
        assert_eq!(frames[0].address, address);

        // Path should now be marked as notified
        assert!(state.sent_observations.get(&0).unwrap().notified);
    }

    #[test]
    fn check_for_address_observations_rate_limited() {
        let config = crate::transport_parameters::AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);

        // Set up a single path with observed address
        let address = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), 5000);
        state
            .sent_observations
            .insert(0, paths::PathAddressInfo::new());
        state
            .sent_observations
            .get_mut(&0)
            .unwrap()
            .observed_address = Some(address);

        // Consume all initial tokens (starts with 10)
        for _ in 0..10 {
            let frames = state.check_for_address_observations(0, true, now);
            if frames.is_empty() {
                break;
            }
            // Mark path as unnotified again for next iteration
            state.sent_observations.get_mut(&0).unwrap().notified = false;
        }

        // Verify we've consumed all tokens
        assert_eq!(state.rate_limiter.tokens, 0.0);

        // Mark path as unnotified again to test rate limiting
        state.sent_observations.get_mut(&0).unwrap().notified = false;

        // Now check should be rate limited (no tokens left)
        let frames2 = state.check_for_address_observations(0, true, now);
        assert_eq!(frames2.len(), 0);

        // Mark path as unnotified again
        state.sent_observations.get_mut(&0).unwrap().notified = false;

        // After time passes, should be able to send again
        let later = now + Duration::from_millis(200); // 0.2 seconds = 2 tokens at 10/sec
        let frames3 = state.check_for_address_observations(0, true, later);
        assert_eq!(frames3.len(), 1);
    }

    #[test]
    fn check_for_address_observations_multiple_paths() {
        let config = crate::transport_parameters::AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);

        // Enable observation on all paths for this test
        state.observe_all_paths = true;

        // Set up two paths with observed addresses
        let addr1 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 100)), 5000);
        let addr2 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 101)), 5001);

        state
            .sent_observations
            .insert(0, paths::PathAddressInfo::new());
        state
            .sent_observations
            .get_mut(&0)
            .unwrap()
            .observed_address = Some(addr1);

        state
            .sent_observations
            .insert(1, paths::PathAddressInfo::new());
        state
            .sent_observations
            .get_mut(&1)
            .unwrap()
            .observed_address = Some(addr2);

        // Check for observations - should get both since we have tokens
        let frames = state.check_for_address_observations(0, true, now);

        // Should get frames for both paths
        assert_eq!(frames.len(), 2);

        // Verify both addresses are included
        let addresses: Vec<_> = frames.iter().map(|f| f.address).collect();
        assert!(addresses.contains(&addr1));
        assert!(addresses.contains(&addr2));

        // Both paths should be marked as notified
        assert!(state.sent_observations.get(&0).unwrap().notified);
        assert!(state.sent_observations.get(&1).unwrap().notified);
    }

    // Tests for Task 2.4: Rate Limiter Configuration
    #[test]
    fn test_rate_limiter_configuration() {
        // Test different rate configurations
        let state = AddressDiscoveryState::new_with_params(true, 10.0, false);
        assert_eq!(state.rate_limiter.rate, 10.0);
        assert_eq!(state.rate_limiter.max_tokens, 10.0);
        assert_eq!(state.rate_limiter.tokens, 10.0);

        let state = AddressDiscoveryState::new_with_params(true, 63.0, false);
        assert_eq!(state.rate_limiter.rate, 63.0);
        assert_eq!(state.rate_limiter.max_tokens, 63.0);
    }

    #[test]
    fn test_rate_limiter_update_configuration() {
        let mut state = AddressDiscoveryState::new_with_params(true, 5.0, false);

        // Initial configuration
        assert_eq!(state.rate_limiter.rate, 5.0);

        // Update configuration
        state.update_rate_limit(10.0);
        assert_eq!(state.rate_limiter.rate, 10.0);
        assert_eq!(state.rate_limiter.max_tokens, 10.0);

        // Tokens should not exceed new max
        state.rate_limiter.tokens = 15.0;
        state.update_rate_limit(8.0);
        assert_eq!(state.rate_limiter.tokens, 8.0);
    }

    #[test]
    fn test_rate_limiter_from_transport_params() {
        let mut params = TransportParameters::default();
        params.address_discovery = Some(AddressDiscoveryConfig::SendAndReceive);

        let state = AddressDiscoveryState::from_transport_params(&params);
        assert!(state.is_some());
        let state = state.unwrap();
        assert_eq!(state.rate_limiter.rate, 10.0); // Default rate is 10
        assert!(!state.observe_all_paths); // Default is false
    }

    #[test]
    fn test_rate_limiter_zero_rate() {
        let state = AddressDiscoveryState::new_with_params(true, 0.0, false);
        assert_eq!(state.rate_limiter.rate, 0.0);
        assert_eq!(state.rate_limiter.tokens, 0.0);

        // Should never allow sending with zero rate
        let address = "192.168.1.1:443".parse().unwrap();
        let mut state = AddressDiscoveryState::new_with_params(true, 0.0, false);
        let frame = state.queue_observed_address_frame(0, address);
        assert!(frame.is_none());
    }

    #[test]
    fn test_rate_limiter_configuration_edge_cases() {
        // Test maximum allowed rate (63)
        let state = AddressDiscoveryState::new_with_params(true, 63.0, false);
        assert_eq!(state.rate_limiter.rate, 63.0);

        // Test rates > 63 get converted to u8 then back to f64
        let state = AddressDiscoveryState::new_with_params(true, 100.0, false);
        // 100 as u8 is 100
        assert_eq!(state.rate_limiter.rate, 100.0);

        // Test fractional rates get truncated due to u8 storage
        let state = AddressDiscoveryState::new_with_params(true, 2.5, false);
        // 2.5 as u8 is 2, then back to f64 is 2.0
        assert_eq!(state.rate_limiter.rate, 2.0);
    }

    #[test]
    fn test_rate_limiter_runtime_update() {
        let mut state = AddressDiscoveryState::new_with_params(true, 10.0, false);
        let now = Instant::now();

        // Consume some tokens
        state.rate_limiter.tokens = 5.0;

        // Update rate while tokens are partially consumed
        state.update_rate_limit(3.0);

        // Tokens should be capped at new max
        assert_eq!(state.rate_limiter.tokens, 3.0);
        assert_eq!(state.rate_limiter.rate, 3.0);
        assert_eq!(state.rate_limiter.max_tokens, 3.0);

        // Wait for replenishment
        let later = now + Duration::from_secs(1);
        state.rate_limiter.update_tokens(later);

        // Should be capped at new max
        assert_eq!(state.rate_limiter.tokens, 3.0);
    }

    // Tests for Task 2.5: Connection Tests
    #[test]
    fn test_address_discovery_state_initialization_default() {
        // Test that connection initializes with default address discovery state
        let now = Instant::now();
        let default_config = crate::transport_parameters::AddressDiscoveryConfig::default();

        // Create a connection (simplified test setup)
        // In reality, this happens in Connection::new()
        let address_discovery_state = Some(AddressDiscoveryState::new(&default_config, now));

        assert!(address_discovery_state.is_some());
        let state = address_discovery_state.unwrap();

        // Default config should have address discovery disabled
        assert!(state.enabled); // Default is now enabled
        assert_eq!(state.max_observation_rate, 10); // Default rate
        assert!(!state.observe_all_paths);
    }

    #[test]
    fn test_address_discovery_state_initialization_on_handshake() {
        // Test that address discovery state is updated when transport parameters are received
        let now = Instant::now();

        // Simulate initial state (as in Connection::new)
        let mut address_discovery_state = Some(AddressDiscoveryState::new(
            &crate::transport_parameters::AddressDiscoveryConfig::default(),
            now,
        ));

        // Simulate receiving peer's transport parameters with address discovery enabled
        let peer_params = TransportParameters {
            address_discovery: Some(AddressDiscoveryConfig::SendAndReceive),
            ..TransportParameters::default()
        };

        // Update address discovery state based on peer params
        if let Some(peer_config) = &peer_params.address_discovery {
            // Any variant means address discovery is supported
            address_discovery_state = Some(AddressDiscoveryState::new(peer_config, now));
        }

        // Verify state was updated
        assert!(address_discovery_state.is_some());
        let state = address_discovery_state.unwrap();
        assert!(state.enabled);
        // Default values from new state creation
        assert_eq!(state.max_observation_rate, 10); // Default rate
        assert!(!state.observe_all_paths); // Default is primary path only
    }

    #[test]
    fn test_address_discovery_negotiation_disabled_peer() {
        // Test when peer doesn't support address discovery
        let now = Instant::now();

        // Start with our config enabling address discovery
        let our_config = AddressDiscoveryConfig::SendAndReceive;
        let mut address_discovery_state = Some(AddressDiscoveryState::new(&our_config, now));

        // Peer's transport parameters without address discovery
        let peer_params = TransportParameters {
            address_discovery: None,
            ..TransportParameters::default()
        };

        // If peer doesn't advertise address discovery, we should disable it
        if peer_params.address_discovery.is_none() {
            if let Some(state) = &mut address_discovery_state {
                state.enabled = false;
            }
        }

        // Verify it's disabled
        let state = address_discovery_state.unwrap();
        assert!(!state.enabled); // Should be disabled when peer doesn't support it
    }

    #[test]
    fn test_address_discovery_negotiation_rate_limiting() {
        // Test rate limit negotiation - should use minimum of local and peer rates
        let now = Instant::now();

        // Our config with rate 30
        let our_config = AddressDiscoveryConfig::SendAndReceive;
        let mut address_discovery_state = Some(AddressDiscoveryState::new(&our_config, now));

        // Set a custom rate for testing
        if let Some(state) = &mut address_discovery_state {
            state.max_observation_rate = 30;
            state.update_rate_limit(30.0);
        }

        // Peer config with rate 15
        let peer_params = TransportParameters {
            address_discovery: Some(AddressDiscoveryConfig::SendAndReceive),
            ..TransportParameters::default()
        };

        // Negotiate - should use minimum rate
        // Since the enum doesn't contain rate info, this test simulates negotiation
        if let (Some(state), Some(_peer_config)) =
            (&mut address_discovery_state, &peer_params.address_discovery)
        {
            // In a real scenario, rate would be extracted from connection parameters
            // For this test, we simulate peer having rate 15
            let peer_rate = 15u8;
            let negotiated_rate = state.max_observation_rate.min(peer_rate);
            state.update_rate_limit(negotiated_rate as f64);
        }

        // Verify negotiated rate
        let state = address_discovery_state.unwrap();
        assert_eq!(state.rate_limiter.rate, 15.0); // Min of 30 and 15
    }

    #[test]
    fn test_address_discovery_path_initialization() {
        // Test that paths are initialized with address discovery support
        let now = Instant::now();
        let config = AddressDiscoveryConfig::SendAndReceive;
        let mut state = AddressDiscoveryState::new(&config, now);

        // Simulate path creation (path_id = 0)
        assert!(state.sent_observations.is_empty());
        assert!(state.received_observations.is_empty());

        // When we first check if we should send observation, it should create path entry
        let should_send = state.should_send_observation(0, now);
        assert!(should_send); // Should allow first observation

        // Path entry should now exist (created on demand)
        // Note: In the actual implementation, path entries are created when needed
    }

    #[test]
    fn test_address_discovery_multiple_path_initialization() {
        // Test initialization with multiple paths
        let now = Instant::now();
        let config = AddressDiscoveryConfig::SendAndReceive;
        let mut state = AddressDiscoveryState::new(&config, now);

        // By default, only primary path is observed
        assert!(state.should_send_observation(0, now)); // Primary path
        assert!(!state.should_send_observation(1, now)); // Secondary path not observed by default
        assert!(!state.should_send_observation(2, now)); // Additional path not observed by default

        // Enable all paths
        state.observe_all_paths = true;
        assert!(state.should_send_observation(1, now)); // Now secondary path is observed
        assert!(state.should_send_observation(2, now)); // Now additional path is observed

        // With observe_all_paths = false, only primary path should be allowed
        let config_primary_only = AddressDiscoveryConfig::SendAndReceive;
        let mut state_primary = AddressDiscoveryState::new(&config_primary_only, now);

        assert!(state_primary.should_send_observation(0, now)); // Primary path allowed
        assert!(!state_primary.should_send_observation(1, now)); // Secondary path not allowed
    }

    #[test]
    fn test_handle_observed_address_frame_valid() {
        // Test processing a valid OBSERVED_ADDRESS frame
        let now = Instant::now();
        let config = AddressDiscoveryConfig::SendAndReceive;
        let mut state = AddressDiscoveryState::new(&config, now);

        // Simulate receiving an OBSERVED_ADDRESS frame
        let observed_addr = SocketAddr::from(([192, 168, 1, 100], 5000));
        state.handle_observed_address(observed_addr, 0, now);

        // Verify the address was recorded
        assert_eq!(state.received_history.len(), 1);
        assert_eq!(state.received_history[0].address, observed_addr);
        assert_eq!(state.received_history[0].path_id, 0);
        assert_eq!(state.received_history[0].received_at, now);

        // Path should also have the observed address
        let path_info = state.received_observations.get(&0).unwrap();
        assert_eq!(path_info.observed_address, Some(observed_addr));
        assert_eq!(path_info.last_observed, Some(now));
        assert_eq!(path_info.observation_count, 1);
    }

    #[test]
    fn test_handle_multiple_received_history() {
        // Test processing multiple OBSERVED_ADDRESS frames from different paths
        let now = Instant::now();
        let config = AddressDiscoveryConfig::SendAndReceive;
        let mut state = AddressDiscoveryState::new(&config, now);

        // Receive addresses from multiple paths
        let addr1 = SocketAddr::from(([192, 168, 1, 100], 5000));
        let addr2 = SocketAddr::from(([10, 0, 0, 50], 6000));
        let addr3 = SocketAddr::from(([192, 168, 1, 100], 7000)); // Same IP, different port

        state.handle_observed_address(addr1, 0, now);
        state.handle_observed_address(addr2, 1, now);
        state.handle_observed_address(addr3, 0, now + Duration::from_millis(100));

        // Verify all addresses were recorded
        assert_eq!(state.received_history.len(), 3);

        // Path 0 should have the most recent address (addr3)
        let path0_info = state.received_observations.get(&0).unwrap();
        assert_eq!(path0_info.observed_address, Some(addr3));
        assert_eq!(path0_info.observation_count, 1); // Reset to 1 for new address

        // Path 1 should have addr2
        let path1_info = state.received_observations.get(&1).unwrap();
        assert_eq!(path1_info.observed_address, Some(addr2));
        assert_eq!(path1_info.observation_count, 1);
    }

    #[test]
    fn test_get_observed_address() {
        // Test retrieving observed addresses for specific paths
        let now = Instant::now();
        let config = AddressDiscoveryConfig::SendAndReceive;
        let mut state = AddressDiscoveryState::new(&config, now);

        // Initially no address
        assert_eq!(state.get_observed_address(0), None);

        // Add an address
        let addr = SocketAddr::from(([192, 168, 1, 100], 5000));
        state.handle_observed_address(addr, 0, now);

        // Should return the most recent address for the path
        assert_eq!(state.get_observed_address(0), Some(addr));

        // Non-existent path should return None
        assert_eq!(state.get_observed_address(999), None);
    }

    #[test]
    fn test_has_unnotified_changes() {
        // Test detection of unnotified address changes
        let now = Instant::now();
        let config = AddressDiscoveryConfig::SendAndReceive;
        let mut state = AddressDiscoveryState::new(&config, now);

        // Initially no changes
        assert!(!state.has_unnotified_changes());

        // Add an address - should have unnotified change
        let addr = SocketAddr::from(([192, 168, 1, 100], 5000));
        state.handle_observed_address(addr, 0, now);
        assert!(state.has_unnotified_changes());

        // Mark as notified
        if let Some(path_info) = state.received_observations.get_mut(&0) {
            path_info.notified = true;
        }
        assert!(!state.has_unnotified_changes());

        // Add another address - should have change again
        let addr2 = SocketAddr::from(([192, 168, 1, 100], 6000));
        state.handle_observed_address(addr2, 0, now + Duration::from_secs(1));
        assert!(state.has_unnotified_changes());
    }

    #[test]
    fn test_address_discovery_disabled() {
        // Test that frames are not processed when address discovery is disabled
        let now = Instant::now();
        let config = AddressDiscoveryConfig::SendAndReceive;
        let mut state = AddressDiscoveryState::new(&config, now);

        // Disable address discovery after creation
        state.enabled = false;

        // Try to process a frame
        let addr = SocketAddr::from(([192, 168, 1, 100], 5000));
        state.handle_observed_address(addr, 0, now);

        // When disabled, addresses are not recorded
        assert_eq!(state.received_history.len(), 0);

        // Should not send observations when disabled
        assert!(!state.should_send_observation(0, now));
    }

    #[test]
    fn test_rate_limiting_basic() {
        // Test basic rate limiting functionality
        let now = Instant::now();
        let config = AddressDiscoveryConfig::SendAndReceive;
        let mut state = AddressDiscoveryState::new(&config, now);

        // Enable all paths for this test and set a low rate
        state.observe_all_paths = true;
        state.rate_limiter.set_rate(2); // 2 per second

        // First observation should be allowed and consumes a token
        assert!(state.should_send_observation(0, now));
        // Need to mark path 0 as notified so subsequent checks will pass
        state.record_observation_sent(0);

        // Need a different path since path 0 is already notified
        assert!(state.should_send_observation(1, now));
        state.record_observation_sent(1);

        // Third observation should be rate limited (no more tokens)
        assert!(!state.should_send_observation(2, now));

        // After 500ms, we should have 1 token available
        let later = now + Duration::from_millis(500);
        assert!(state.should_send_observation(3, later));
        state.record_observation_sent(3);

        // But not a second one (all tokens consumed)
        assert!(!state.should_send_observation(4, later));

        // After 1 second from start, we've consumed 3 tokens total
        // With rate 2/sec, after 1 second we've generated 2 new tokens
        // So we should have 0 tokens available (consumed 3, generated 2 = -1, but capped at 0)
        let _one_sec_later = now + Duration::from_secs(1);
        // Actually we need to wait longer to accumulate more tokens
        // After 1.5 seconds, we've generated 3 tokens total, consumed 3, so we can send 0 more
        // After 2 seconds, we've generated 4 tokens total, consumed 3, so we can send 1 more
        let two_sec_later = now + Duration::from_secs(2);
        assert!(state.should_send_observation(5, two_sec_later));
        state.record_observation_sent(5);

        // At exactly 2 seconds, we have:
        // - Generated: 4 tokens (2 per second × 2 seconds)
        // - Consumed: 4 tokens (paths 0, 1, 3, 5)
        // - Remaining: 0 tokens
        // But since the rate limiter is continuous and tokens accumulate over time,
        // by the time we check, we might have accumulated a tiny fraction more.
        // The test shows we have exactly 1 token, which makes sense - we're checking
        // slightly after consuming for path 5, so we've accumulated a bit more.

        // So path 6 CAN send one more time, consuming that 1 token
        assert!(state.should_send_observation(6, two_sec_later));
        state.record_observation_sent(6);

        // NOW we should be out of tokens
        assert!(
            !state.should_send_observation(7, two_sec_later),
            "Expected no tokens available"
        );
    }

    #[test]
    fn test_rate_limiting_per_path() {
        // Test that rate limiting is shared across paths (not per-path)
        let now = Instant::now();
        let config = AddressDiscoveryConfig::SendAndReceive;
        let mut state = AddressDiscoveryState::new(&config, now);

        // Set up path 0 with an address to observe
        state
            .sent_observations
            .insert(0, paths::PathAddressInfo::new());
        state
            .sent_observations
            .get_mut(&0)
            .unwrap()
            .observed_address = Some(SocketAddr::new(
            IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)),
            8080,
        ));

        // Use up all initial tokens (we start with 10)
        for _ in 0..10 {
            assert!(state.should_send_observation(0, now));
            state.record_observation_sent(0);
            // Reset notified flag for next iteration
            state.sent_observations.get_mut(&0).unwrap().notified = false;
        }

        // Now we're out of tokens, so path 0 should be rate limited
        assert!(!state.should_send_observation(0, now));

        // After 100ms, we get 1 token back (10 tokens/sec = 1 token/100ms)
        let later = now + Duration::from_millis(100);
        assert!(state.should_send_observation(0, later));
        state.record_observation_sent(0);

        // Reset notified flag to test again
        state.sent_observations.get_mut(&0).unwrap().notified = false;

        // And it's consumed again
        assert!(!state.should_send_observation(0, later));
    }

    #[test]
    fn test_rate_limiting_zero_rate() {
        // Test that rate of 0 means no observations
        let now = Instant::now();
        let config = AddressDiscoveryConfig::SendAndReceive;
        let mut state = AddressDiscoveryState::new(&config, now);

        // Set rate to 0
        state.rate_limiter.set_rate(0);
        state.rate_limiter.tokens = 0.0;
        state.rate_limiter.max_tokens = 0.0;

        // Should never allow observations
        assert!(!state.should_send_observation(0, now));
        assert!(!state.should_send_observation(0, now + Duration::from_secs(10)));
        assert!(!state.should_send_observation(0, now + Duration::from_secs(100)));
    }

    #[test]
    fn test_rate_limiting_update() {
        // Test updating rate limit during connection
        let now = Instant::now();
        let config = AddressDiscoveryConfig::SendAndReceive;
        let mut state = AddressDiscoveryState::new(&config, now);

        // Enable all paths observation
        state.observe_all_paths = true;

        // Set up multiple paths with addresses to observe
        for i in 0..12 {
            state
                .sent_observations
                .insert(i, paths::PathAddressInfo::new());
            state
                .sent_observations
                .get_mut(&i)
                .unwrap()
                .observed_address = Some(SocketAddr::new(
                IpAddr::V4(Ipv4Addr::new(192, 168, 1, (i + 1) as u8)),
                8080,
            ));
        }

        // Initially we have 10 tokens (rate is 10/sec)
        // Use up all the initial tokens
        for i in 0..10 {
            assert!(state.should_send_observation(i, now));
            state.record_observation_sent(i);
        }
        // Now we should be out of tokens
        assert!(!state.should_send_observation(10, now));

        // Update rate limit to 20 per second (double the original)
        state.update_rate_limit(20.0);

        // Tokens don't immediately increase, need to wait for replenishment
        // After 50ms with rate 20/sec, we should get 1 token
        let later = now + Duration::from_millis(50);
        assert!(state.should_send_observation(10, later));
        state.record_observation_sent(10);

        // And we can continue sending at the new rate
        let later2 = now + Duration::from_millis(100);
        assert!(state.should_send_observation(11, later2));
    }

    #[test]
    fn test_rate_limiting_burst() {
        // Test that rate limiter allows burst up to bucket capacity
        let now = Instant::now();
        let config = AddressDiscoveryConfig::SendAndReceive;
        let mut state = AddressDiscoveryState::new(&config, now);

        // Should allow up to 10 observations in burst
        for _ in 0..10 {
            assert!(state.should_send_observation(0, now));
            state.record_observation_sent(0);
        }

        // 11th should be rate limited
        assert!(!state.should_send_observation(0, now));

        // After 100ms, we should have 1 more token
        let later = now + Duration::from_millis(100);
        assert!(state.should_send_observation(0, later));
        state.record_observation_sent(0);
        assert!(!state.should_send_observation(0, later));
    }

    #[test]
    fn test_connection_rate_limiting_with_check_observations() {
        // Test rate limiting through check_for_address_observations
        let now = Instant::now();
        let config = AddressDiscoveryConfig::SendAndReceive;
        let mut state = AddressDiscoveryState::new(&config, now);

        // Set up a path with an address
        let mut path_info = paths::PathAddressInfo::new();
        path_info.update_observed_address(
            SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1)), 8080),
            now,
        );
        state.sent_observations.insert(0, path_info);

        // First observation should succeed
        let frame1 =
            state.queue_observed_address_frame(0, SocketAddr::from(([192, 168, 1, 1], 8080)));
        assert!(frame1.is_some());
        state.record_observation_sent(0);

        // Reset notified flag to test rate limiting (simulate address change or new observation opportunity)
        if let Some(info) = state.sent_observations.get_mut(&0) {
            info.notified = false;
        }

        // We start with 10 tokens, use them all up (minus the 1 we already used)
        for _ in 1..10 {
            // Reset notified flag to allow testing rate limiting
            if let Some(info) = state.sent_observations.get_mut(&0) {
                info.notified = false;
            }
            let frame =
                state.queue_observed_address_frame(0, SocketAddr::from(([192, 168, 1, 1], 8080)));
            assert!(frame.is_some());
            state.record_observation_sent(0);
        }

        // Now we should be out of tokens
        if let Some(info) = state.sent_observations.get_mut(&0) {
            info.notified = false;
        }
        let frame3 =
            state.queue_observed_address_frame(0, SocketAddr::from(([192, 168, 1, 1], 8080)));
        assert!(frame3.is_none()); // Should fail due to rate limiting

        // After 100ms, should allow 1 more (rate is 10/sec, so 0.1s = 1 token)
        let later = now + Duration::from_millis(100);
        state.rate_limiter.update_tokens(later); // Update tokens based on elapsed time

        // Reset notified flag to test token replenishment
        if let Some(info) = state.sent_observations.get_mut(&0) {
            info.notified = false;
        }

        let frame4 =
            state.queue_observed_address_frame(0, SocketAddr::from(([192, 168, 1, 1], 8080)));
        assert!(frame4.is_some()); // Should succeed due to token replenishment
    }

    #[test]
    fn test_queue_observed_address_frame() {
        // Test queueing OBSERVED_ADDRESS frames with rate limiting
        let now = Instant::now();
        let config = AddressDiscoveryConfig::SendAndReceive;
        let mut state = AddressDiscoveryState::new(&config, now);

        let addr = SocketAddr::from(([192, 168, 1, 100], 5000));

        // Should queue frame when allowed
        let frame = state.queue_observed_address_frame(0, addr);
        assert!(frame.is_some());
        assert_eq!(frame.unwrap().address, addr);

        // Record that we sent it
        state.record_observation_sent(0);

        // Should respect rate limiting - we start with 10 tokens
        for i in 0..9 {
            // Reset notified flag to test rate limiting
            if let Some(info) = state.sent_observations.get_mut(&0) {
                info.notified = false;
            }

            let frame = state.queue_observed_address_frame(0, addr);
            assert!(frame.is_some(), "Frame {} should be allowed", i + 2);
            state.record_observation_sent(0);
        }

        // Reset notified flag one more time
        if let Some(info) = state.sent_observations.get_mut(&0) {
            info.notified = false;
        }

        // 11th should be rate limited (we've used all 10 tokens)
        let frame = state.queue_observed_address_frame(0, addr);
        assert!(frame.is_none(), "11th frame should be rate limited");
    }

    #[test]
    fn test_multi_path_basic() {
        // Test basic multi-path functionality
        let now = Instant::now();
        let config = AddressDiscoveryConfig::SendAndReceive;
        let mut state = AddressDiscoveryState::new(&config, now);

        let addr1 = SocketAddr::from(([192, 168, 1, 1], 5000));
        let addr2 = SocketAddr::from(([10, 0, 0, 1], 6000));
        let addr3 = SocketAddr::from(([172, 16, 0, 1], 7000));

        // Handle observations for different paths
        state.handle_observed_address(addr1, 0, now);
        state.handle_observed_address(addr2, 1, now);
        state.handle_observed_address(addr3, 2, now);

        // Each path should have its own observed address
        assert_eq!(state.get_observed_address(0), Some(addr1));
        assert_eq!(state.get_observed_address(1), Some(addr2));
        assert_eq!(state.get_observed_address(2), Some(addr3));

        // All paths should have unnotified changes
        assert!(state.has_unnotified_changes());

        // Check that we have 3 observation events
        assert_eq!(state.received_history.len(), 3);
    }

    #[test]
    fn test_multi_path_observe_primary_only() {
        // Test that when observe_all_paths is false, only primary path is observed
        let now = Instant::now();
        let config = AddressDiscoveryConfig::SendAndReceive;
        let mut state = AddressDiscoveryState::new(&config, now);

        // Primary path (0) should be observable
        assert!(state.should_send_observation(0, now));
        state.record_observation_sent(0);

        // Non-primary paths should not be observable
        assert!(!state.should_send_observation(1, now));
        assert!(!state.should_send_observation(2, now));

        // Can't queue frames for non-primary paths
        let addr = SocketAddr::from(([192, 168, 1, 1], 5000));
        assert!(state.queue_observed_address_frame(0, addr).is_some());
        assert!(state.queue_observed_address_frame(1, addr).is_none());
        assert!(state.queue_observed_address_frame(2, addr).is_none());
    }

    #[test]
    fn test_multi_path_rate_limiting() {
        // Test that rate limiting is shared across all paths
        let now = Instant::now();
        let config = AddressDiscoveryConfig::SendAndReceive;
        let mut state = AddressDiscoveryState::new(&config, now);

        // Enable all paths observation
        state.observe_all_paths = true;

        // Set up multiple paths with addresses to observe
        for i in 0..21 {
            state
                .sent_observations
                .insert(i, paths::PathAddressInfo::new());
            state
                .sent_observations
                .get_mut(&i)
                .unwrap()
                .observed_address = Some(SocketAddr::new(
                IpAddr::V4(Ipv4Addr::new(192, 168, 1, (i + 1) as u8)),
                8080,
            ));
        }

        // Use all 10 initial tokens across different paths
        for i in 0..10 {
            assert!(state.should_send_observation(i, now));
            state.record_observation_sent(i);
        }

        // All tokens consumed, no path can send
        assert!(!state.should_send_observation(10, now));

        // Reset path 0 to test if it can send again (it shouldn't)
        state.sent_observations.get_mut(&0).unwrap().notified = false;
        assert!(!state.should_send_observation(0, now)); // Even path 0 can't send again

        // After 1 second, we get 10 more tokens (rate is 10/sec)
        let later = now + Duration::from_secs(1);
        for i in 10..20 {
            assert!(state.should_send_observation(i, later));
            state.record_observation_sent(i);
        }
        // And we're out again
        assert!(!state.should_send_observation(20, later));
    }

    #[test]
    fn test_multi_path_address_changes() {
        // Test handling address changes on different paths
        let now = Instant::now();
        let config = AddressDiscoveryConfig::SendAndReceive;
        let mut state = AddressDiscoveryState::new(&config, now);

        let addr1a = SocketAddr::from(([192, 168, 1, 1], 5000));
        let addr1b = SocketAddr::from(([192, 168, 1, 2], 5000));
        let addr2a = SocketAddr::from(([10, 0, 0, 1], 6000));
        let addr2b = SocketAddr::from(([10, 0, 0, 2], 6000));

        // Initial addresses
        state.handle_observed_address(addr1a, 0, now);
        state.handle_observed_address(addr2a, 1, now);

        // Mark received observations as notified
        if let Some(info) = state.received_observations.get_mut(&0) {
            info.notified = true;
        }
        if let Some(info) = state.received_observations.get_mut(&1) {
            info.notified = true;
        }
        assert!(!state.has_unnotified_changes());

        // Change address on path 0
        state.handle_observed_address(addr1b, 0, now + Duration::from_secs(1));
        assert!(state.has_unnotified_changes());

        // Path 0 should have new address, path 1 unchanged
        assert_eq!(state.get_observed_address(0), Some(addr1b));
        assert_eq!(state.get_observed_address(1), Some(addr2a));

        // Mark path 0 as notified
        if let Some(info) = state.received_observations.get_mut(&0) {
            info.notified = true;
        }
        assert!(!state.has_unnotified_changes());

        // Change address on path 1
        state.handle_observed_address(addr2b, 1, now + Duration::from_secs(2));
        assert!(state.has_unnotified_changes());
    }

    #[test]
    fn test_multi_path_migration() {
        // Test path migration scenario
        let now = Instant::now();
        let config = AddressDiscoveryConfig::SendAndReceive;
        let mut state = AddressDiscoveryState::new(&config, now);

        let addr_old = SocketAddr::from(([192, 168, 1, 1], 5000));
        let addr_new = SocketAddr::from(([10, 0, 0, 1], 6000));

        // Establish observation on path 0
        state.handle_observed_address(addr_old, 0, now);
        assert_eq!(state.get_observed_address(0), Some(addr_old));

        // Simulate path migration - new path gets different ID
        state.handle_observed_address(addr_new, 1, now + Duration::from_secs(1));

        // Both paths should have their addresses
        assert_eq!(state.get_observed_address(0), Some(addr_old));
        assert_eq!(state.get_observed_address(1), Some(addr_new));

        // In real implementation, old path would be cleaned up eventually
        // For now, we just track both in received_observations
        assert_eq!(state.received_observations.len(), 2);
    }

    #[test]
    fn test_check_for_address_observations_multi_path() {
        // Test the check_for_address_observations method with multiple paths
        let now = Instant::now();
        let config = AddressDiscoveryConfig::SendAndReceive;
        let mut state = AddressDiscoveryState::new(&config, now);

        // Enable observation of all paths
        state.observe_all_paths = true;

        // Set up multiple paths with addresses to send (sent_observations)
        let addr1 = SocketAddr::from(([192, 168, 1, 1], 5000));
        let addr2 = SocketAddr::from(([10, 0, 0, 1], 6000));
        let addr3 = SocketAddr::from(([172, 16, 0, 1], 7000));

        // Set up sent_observations for testing check_for_address_observations
        state
            .sent_observations
            .insert(0, paths::PathAddressInfo::new());
        state
            .sent_observations
            .get_mut(&0)
            .unwrap()
            .observed_address = Some(addr1);
        state
            .sent_observations
            .insert(1, paths::PathAddressInfo::new());
        state
            .sent_observations
            .get_mut(&1)
            .unwrap()
            .observed_address = Some(addr2);
        state
            .sent_observations
            .insert(2, paths::PathAddressInfo::new());
        state
            .sent_observations
            .get_mut(&2)
            .unwrap()
            .observed_address = Some(addr3);

        // Check for observations - should return frames for all unnotified paths
        let frames = state.check_for_address_observations(0, true, now);

        // Should get frames for all 3 paths
        assert_eq!(frames.len(), 3);

        // Verify all addresses are present in frames (order doesn't matter)
        let frame_addrs: Vec<_> = frames.iter().map(|f| f.address).collect();
        assert!(frame_addrs.contains(&addr1), "addr1 should be in frames");
        assert!(frame_addrs.contains(&addr2), "addr2 should be in frames");
        assert!(frame_addrs.contains(&addr3), "addr3 should be in frames");

        // All paths should now be marked as notified
        assert!(!state.has_unnotified_changes());
    }

    #[test]
    fn test_multi_path_with_peer_not_supporting() {
        // Test behavior when peer doesn't support address discovery
        let now = Instant::now();
        let config = AddressDiscoveryConfig::SendAndReceive;
        let mut state = AddressDiscoveryState::new(&config, now);

        // Set up paths
        state.handle_observed_address(SocketAddr::from(([192, 168, 1, 1], 5000)), 0, now);
        state.handle_observed_address(SocketAddr::from(([10, 0, 0, 1], 6000)), 1, now);

        // Check with peer not supporting - should return empty
        let frames = state.check_for_address_observations(0, false, now);
        assert_eq!(frames.len(), 0);

        // Paths should still have unnotified changes
        assert!(state.has_unnotified_changes());
    }

    // Tests for Phase 3.2: Bootstrap Node Behavior
    #[test]
    fn test_bootstrap_node_aggressive_observation_mode() {
        // Test that bootstrap nodes use more aggressive observation settings
        let config = AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);

        // Initially not in bootstrap mode
        assert!(!state.is_bootstrap_mode());

        // Enable bootstrap mode
        state.set_bootstrap_mode(true);
        assert!(state.is_bootstrap_mode());

        // Bootstrap mode should observe all paths regardless of config
        assert!(state.should_observe_path(0)); // Primary path
        assert!(state.should_observe_path(1)); // Secondary paths
        assert!(state.should_observe_path(2));

        // Bootstrap mode should have higher rate limit
        let bootstrap_rate = state.get_effective_rate_limit();
        assert!(bootstrap_rate > 10.0); // Should be higher than configured
    }

    #[test]
    fn test_bootstrap_node_immediate_observation() {
        // Test that bootstrap nodes send observations immediately on new connections
        let config = AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);
        state.set_bootstrap_mode(true);

        // Add an observed address
        let addr = SocketAddr::from(([192, 168, 1, 100], 5000));
        state.handle_observed_address(addr, 0, now);

        // Bootstrap nodes should want to send immediately on new connections
        assert!(state.should_send_observation_immediately(true));

        // Should bypass normal rate limiting for first observation
        assert!(state.should_send_observation(0, now));

        // Queue the frame
        let frame = state.queue_observed_address_frame(0, addr);
        assert!(frame.is_some());
    }

    #[test]
    fn test_bootstrap_node_multiple_path_observations() {
        // Test bootstrap nodes observe all paths aggressively
        let config = AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);
        state.set_bootstrap_mode(true);

        // Add addresses to sent_observations for testing check_for_address_observations
        let addrs = vec![
            (0u64, SocketAddr::from(([192, 168, 1, 1], 5000))),
            (1u64, SocketAddr::from(([10, 0, 0, 1], 6000))),
            (2u64, SocketAddr::from(([172, 16, 0, 1], 7000))),
        ];

        for (path_id, addr) in &addrs {
            state
                .sent_observations
                .insert(*path_id, paths::PathAddressInfo::new());
            state
                .sent_observations
                .get_mut(path_id)
                .unwrap()
                .observed_address = Some(*addr);
        }

        // Bootstrap nodes should observe all paths despite config
        let frames = state.check_for_address_observations(0, true, now);
        assert_eq!(frames.len(), 3);

        // Verify all addresses are included
        for (_, addr) in &addrs {
            assert!(frames.iter().any(|f| f.address == *addr));
        }
    }

    #[test]
    fn test_bootstrap_node_rate_limit_override() {
        // Test that bootstrap nodes have higher rate limits
        let config = AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);
        state.set_bootstrap_mode(true);

        // Bootstrap nodes should be able to send more than configured rate
        let addr = SocketAddr::from(([192, 168, 1, 1], 5000));

        // Send multiple observations rapidly
        for i in 0..10 {
            state.handle_observed_address(addr, i, now);
            let can_send = state.should_send_observation(i, now);
            assert!(can_send, "Bootstrap node should send observation {i}");
            state.record_observation_sent(i);
        }
    }

    #[test]
    fn test_bootstrap_node_configuration() {
        // Test bootstrap-specific configuration
        let config = AddressDiscoveryConfig::SendAndReceive;
        let mut state = AddressDiscoveryState::new(&config, Instant::now());

        // Apply bootstrap mode
        state.set_bootstrap_mode(true);

        // Bootstrap mode should enable aggressive observation
        assert!(state.bootstrap_mode);
        assert!(state.enabled);

        // Rate limiter should be updated for bootstrap mode
        let effective_rate = state.get_effective_rate_limit();
        assert!(effective_rate > state.max_observation_rate as f64);
    }

    #[test]
    fn test_bootstrap_node_persistent_observation() {
        // Test that bootstrap nodes continue observing throughout connection lifetime
        let config = AddressDiscoveryConfig::SendAndReceive;
        let mut now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);
        state.set_bootstrap_mode(true);

        let addr1 = SocketAddr::from(([192, 168, 1, 1], 5000));
        let addr2 = SocketAddr::from(([192, 168, 1, 2], 5000));

        // Initial observation
        state.handle_observed_address(addr1, 0, now);
        assert!(state.should_send_observation(0, now));
        state.record_observation_sent(0);

        // After some time, address changes
        now += Duration::from_secs(60);
        state.handle_observed_address(addr2, 0, now);

        // Bootstrap nodes should still be observing actively
        assert!(state.should_send_observation(0, now));
    }

    #[test]
    fn test_bootstrap_node_multi_peer_support() {
        // Test that bootstrap nodes can handle observations for multiple peers
        // This is more of an integration test concept, but we can test the state management
        let config = AddressDiscoveryConfig::SendAndReceive;
        let now = Instant::now();
        let mut state = AddressDiscoveryState::new(&config, now);
        state.set_bootstrap_mode(true);

        // Simulate multiple peer connections (using different path IDs)
        let peer_addresses: Vec<(u64, SocketAddr)> = vec![
            (0, SocketAddr::from(([192, 168, 1, 1], 5000))), // Peer 1
            (1, SocketAddr::from(([10, 0, 0, 1], 6000))),    // Peer 2
            (2, SocketAddr::from(([172, 16, 0, 1], 7000))),  // Peer 3
            (3, SocketAddr::from(([192, 168, 2, 1], 8000))), // Peer 4
        ];

        // Add all peer addresses to sent_observations
        for (path_id, addr) in &peer_addresses {
            state
                .sent_observations
                .insert(*path_id, paths::PathAddressInfo::new());
            state
                .sent_observations
                .get_mut(path_id)
                .unwrap()
                .observed_address = Some(*addr);
        }

        // Bootstrap should observe all peers
        let frames = state.check_for_address_observations(0, true, now);
        assert_eq!(frames.len(), peer_addresses.len());

        // Verify all addresses are observed
        for (_, addr) in &peer_addresses {
            assert!(frames.iter().any(|f| f.address == *addr));
        }
    }

    // Include comprehensive address discovery tests
    mod address_discovery_tests {
        include!("address_discovery_tests.rs");
    }
}
