//! TCP Transport Implementation
//!
//! Provides TCP-based transport for FIPS peer communication. TCP enables
//! firewall traversal (many networks allow TCP on port 443 but block UDP)
//! and serves as the foundation for the future Tor transport.
//!
//! FIPS protocols (FMP, FSP, MMP) are all unreliable datagrams. This
//! transport carries those datagrams over TCP — the main pathology is
//! head-of-line blocking, which adds latency jitter that MMP correctly
//! measures and cost-based parent selection correctly penalizes.
//!
//! ## Architecture
//!
//! Unlike UDP (one socket serves all peers), TCP requires one `TcpStream`
//! per peer. The transport maintains a connection pool mapping
//! `TransportAddr` to per-connection state, plus an optional `TcpListener`
//! for inbound connections.
//!
//! ## Framing
//!
//! Uses the existing 4-byte FMP common prefix to recover packet boundaries.
//! No additional framing overhead — packets are written directly to the
//! TCP stream and the receiver uses phase-dependent size computation.

pub mod stats;
pub mod stream;

use super::resolve_socket_addrs;
use super::{
    ConnectionState, DiscoveredPeer, PacketTx, ReceivedPacket, Transport, TransportAddr,
    TransportError, TransportId, TransportState, TransportType,
};
use crate::config::TcpConfig;
use stats::TcpStats;
use stream::read_fmp_packet;

use futures::FutureExt;
use socket2::TcpKeepalive;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use tokio::io::AsyncWriteExt;
use tokio::net::tcp::OwnedWriteHalf;
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::Mutex;
use tokio::task::JoinHandle;
use tokio::time::{Instant, timeout};
use tracing::{debug, info, trace, warn};

// ============================================================================
// Connection Pool
// ============================================================================

/// State for a single TCP connection to a peer.
struct TcpConnection {
    /// Write half of the split stream.
    writer: Arc<Mutex<OwnedWriteHalf>>,
    /// Receive task for this connection.
    recv_task: JoinHandle<()>,
    /// MSS-derived MTU for this connection (used for dynamic MTU re-reading).
    #[allow(dead_code)]
    mtu: u16,
    /// When the connection was established.
    #[allow(dead_code)]
    established_at: Instant,
}

/// Shared connection pool.
type ConnectionPool = Arc<Mutex<HashMap<TransportAddr, TcpConnection>>>;

/// A pending background connection attempt.
///
/// Holds the JoinHandle for a spawned TCP connect task. The task
/// produces a configured `TcpStream` and MSS-derived MTU on success.
struct ConnectingEntry {
    /// Background task performing TCP connect + socket configuration.
    task: JoinHandle<Result<(TcpStream, u16), TransportError>>,
}

/// Map of addresses with background connection attempts in progress.
type ConnectingPool = Arc<Mutex<HashMap<TransportAddr, ConnectingEntry>>>;

// ============================================================================
// TCP Transport
// ============================================================================

/// TCP transport for FIPS.
///
/// Provides connection-oriented, reliable byte stream delivery over TCP/IP.
/// Each peer has its own TCP connection; links are managed per-connection
/// with a connection pool keyed by `TransportAddr`.
pub struct TcpTransport {
    /// Unique transport identifier.
    transport_id: TransportId,
    /// Optional instance name (for named instances in config).
    name: Option<String>,
    /// Configuration.
    config: TcpConfig,
    /// Current state.
    state: TransportState,
    /// Connection pool: addr -> established connections.
    pool: ConnectionPool,
    /// Pending connection attempts: addr -> background connect task.
    connecting: ConnectingPool,
    /// Channel for delivering received packets to Node.
    packet_tx: PacketTx,
    /// Accept loop task handle (if listener bound).
    accept_task: Option<JoinHandle<()>>,
    /// Local listener address (after start, if bind_addr configured).
    local_addr: Option<SocketAddr>,
    /// Transport statistics.
    stats: Arc<TcpStats>,
}

impl TcpTransport {
    /// Create a new TCP transport.
    pub fn new(
        transport_id: TransportId,
        name: Option<String>,
        config: TcpConfig,
        packet_tx: PacketTx,
    ) -> Self {
        Self {
            transport_id,
            name,
            config,
            state: TransportState::Configured,
            pool: Arc::new(Mutex::new(HashMap::new())),
            connecting: Arc::new(Mutex::new(HashMap::new())),
            packet_tx,
            accept_task: None,
            local_addr: None,
            stats: Arc::new(TcpStats::new()),
        }
    }

    /// Get the instance name (if configured as a named instance).
    pub fn name(&self) -> Option<&str> {
        self.name.as_deref()
    }

    /// Get the local listener address (only valid after start with bind_addr).
    pub fn local_addr(&self) -> Option<SocketAddr> {
        self.local_addr
    }

    /// Get the transport statistics.
    pub fn stats(&self) -> &Arc<TcpStats> {
        &self.stats
    }

    /// Start the transport asynchronously.
    ///
    /// If `bind_addr` is configured, binds a TCP listener and spawns
    /// the accept loop. Otherwise, operates in outbound-only mode.
    pub async fn start_async(&mut self) -> Result<(), TransportError> {
        if !self.state.can_start() {
            return Err(TransportError::AlreadyStarted);
        }

        self.state = TransportState::Starting;

        // Bind listener if configured
        if let Some(ref bind_addr) = self.config.bind_addr {
            let addr: SocketAddr = bind_addr
                .parse()
                .map_err(|e| TransportError::StartFailed(format!("invalid bind address: {}", e)))?;

            let listener = TcpListener::bind(addr)
                .await
                .map_err(|e| TransportError::StartFailed(format!("bind failed: {}", e)))?;

            self.local_addr = Some(
                listener
                    .local_addr()
                    .map_err(|e| TransportError::StartFailed(format!("get local addr: {}", e)))?,
            );

            // Spawn accept loop
            let transport_id = self.transport_id;
            let packet_tx = self.packet_tx.clone();
            let pool = self.pool.clone();
            let stats = self.stats.clone();
            let cfg = AcceptConfig {
                mtu: self.config.mtu(),
                max_inbound: self.config.max_inbound_connections(),
                nodelay: self.config.nodelay(),
                keepalive_secs: self.config.keepalive_secs(),
                recv_buf: self.config.recv_buf_size(),
                send_buf: self.config.send_buf_size(),
                first_frame_timeout_ms: self.config.first_frame_timeout_ms(),
            };

            let accept_task = tokio::spawn(async move {
                accept_loop(listener, transport_id, packet_tx, pool, cfg, stats).await;
            });
            self.accept_task = Some(accept_task);
        }

        self.state = TransportState::Up;

        if let Some(ref name) = self.name {
            info!(
                name = %name,
                local_addr = ?self.local_addr,
                mtu = self.config.mtu(),
                "TCP transport started"
            );
        } else {
            info!(
                local_addr = ?self.local_addr,
                mtu = self.config.mtu(),
                "TCP transport started"
            );
        }

        Ok(())
    }

    /// Stop the transport asynchronously.
    pub async fn stop_async(&mut self) -> Result<(), TransportError> {
        if !self.state.is_operational() {
            return Err(TransportError::NotStarted);
        }

        // Abort accept loop
        if let Some(task) = self.accept_task.take() {
            task.abort();
            let _ = task.await;
        }

        // Abort pending connection attempts
        let mut connecting = self.connecting.lock().await;
        for (addr, entry) in connecting.drain() {
            entry.task.abort();
            debug!(
                transport_id = %self.transport_id,
                remote_addr = %addr,
                "TCP connect aborted (transport stopping)"
            );
        }
        drop(connecting);

        // Close all established connections
        let mut pool = self.pool.lock().await;
        for (addr, conn) in pool.drain() {
            conn.recv_task.abort();
            let _ = conn.recv_task.await;
            debug!(
                transport_id = %self.transport_id,
                remote_addr = %addr,
                "TCP connection closed (transport stopping)"
            );
        }
        drop(pool);

        self.local_addr = None;
        self.state = TransportState::Down;

        info!(
            transport_id = %self.transport_id,
            "TCP transport stopped"
        );

        Ok(())
    }

    /// Send a packet asynchronously.
    ///
    /// If no connection exists to the given address, performs connect-on-send:
    /// establishes a new TCP connection, configures socket options, splits the
    /// stream, spawns a receive task, and stores the connection in the pool.
    pub async fn send_async(
        &self,
        addr: &TransportAddr,
        data: &[u8],
    ) -> Result<usize, TransportError> {
        if !self.state.is_operational() {
            return Err(TransportError::NotStarted);
        }

        // Pre-send MTU check: reject oversize packets before writing them
        // to the TCP stream. Without this, the receiver's FMP stream reader
        // would see payload_len > max and close the connection, causing a
        // disruptive reset-reconnect cycle.
        let mtu = self.config.mtu() as usize;
        if data.len() > mtu {
            self.stats.record_mtu_exceeded();
            return Err(TransportError::MtuExceeded {
                packet_size: data.len(),
                mtu: self.config.mtu(),
            });
        }

        // Get or create connection
        let writer = {
            let pool = self.pool.lock().await;
            pool.get(addr).map(|c| c.writer.clone())
        };

        let writer = match writer {
            Some(w) => w,
            None => {
                // Connect-on-send
                self.connect(addr).await?
            }
        };

        // Write packet directly (no framing transformation needed)
        let mut w = writer.lock().await;
        match w.write_all(data).await {
            Ok(()) => {
                self.stats.record_send(data.len());
                trace!(
                    transport_id = %self.transport_id,
                    remote_addr = %addr,
                    bytes = data.len(),
                    "TCP packet sent"
                );
                Ok(data.len())
            }
            Err(e) => {
                self.stats.record_send_error();
                drop(w);
                // Remove failed connection from pool
                let mut pool = self.pool.lock().await;
                if let Some(conn) = pool.remove(addr) {
                    conn.recv_task.abort();
                }
                Err(TransportError::SendFailed(format!("{}", e)))
            }
        }
    }

    /// Establish a new TCP connection to the given address.
    ///
    /// Configures socket options, reads TCP_MAXSEG for MTU, splits the
    /// stream, spawns a receive task, and stores in the pool.
    async fn connect(
        &self,
        addr: &TransportAddr,
    ) -> Result<Arc<Mutex<OwnedWriteHalf>>, TransportError> {
        let socket_addrs = resolve_socket_addrs(addr).await?;
        let timeout_ms = self.config.connect_timeout_ms();

        let stream = match connect_to_any_addr(&socket_addrs, timeout_ms).await {
            Ok(stream) => stream,
            Err(error @ TransportError::Timeout) => {
                self.stats.record_connect_timeout();
                return Err(error);
            }
            Err(error @ TransportError::ConnectionRefused) => {
                self.stats.record_connect_refused();
                return Err(error);
            }
            Err(error) => return Err(error),
        };

        // Configure socket options via socket2
        let std_stream = stream
            .into_std()
            .map_err(|e| TransportError::StartFailed(format!("into_std: {}", e)))?;
        configure_socket(&std_stream, &self.config)?;

        // Read TCP_MAXSEG for per-connection MTU
        let mss_mtu = read_mss_mtu(&std_stream, self.config.mtu());

        // Convert back to tokio
        let stream = TcpStream::from_std(std_stream)
            .map_err(|e| TransportError::StartFailed(format!("from_std: {}", e)))?;

        // Split and spawn receive task
        let (read_half, write_half) = stream.into_split();
        let writer = Arc::new(Mutex::new(write_half));

        let transport_id = self.transport_id;
        let packet_tx = self.packet_tx.clone();
        let pool = self.pool.clone();
        let recv_stats = self.stats.clone();
        let remote_addr = addr.clone();
        let mtu = mss_mtu;

        let recv_task = tokio::spawn(async move {
            tcp_receive_loop(
                read_half,
                TcpReceiveContext {
                    transport_id,
                    remote_addr,
                    packet_tx,
                    pool,
                    mtu,
                    stats: recv_stats,
                    first_frame_timeout: None,
                },
            )
            .await;
        });

        let conn = TcpConnection {
            writer: writer.clone(),
            recv_task,
            mtu: mss_mtu,
            established_at: Instant::now(),
        };

        let mut pool = self.pool.lock().await;
        pool.insert(addr.clone(), conn);

        self.stats.record_connection_established();

        debug!(
            transport_id = %self.transport_id,
            remote_addr = %addr,
            mtu = mss_mtu,
            "TCP connection established (connect-on-send)"
        );

        Ok(writer)
    }

    /// Close a specific connection asynchronously.
    ///
    /// Removes the connection from the pool, aborts its receive task,
    /// and drops the write half (sends FIN to remote).
    pub async fn close_connection_async(&self, addr: &TransportAddr) {
        let mut pool = self.pool.lock().await;
        if let Some(conn) = pool.remove(addr) {
            conn.recv_task.abort();
            debug!(
                transport_id = %self.transport_id,
                remote_addr = %addr,
                "TCP connection closed (close_connection)"
            );
        }
    }

    /// Initiate a non-blocking connection to a remote address.
    ///
    /// Spawns a background task that performs TCP connect with timeout,
    /// configures socket options, and reads MSS. The connection becomes
    /// available for `send_async()` once the task completes successfully.
    ///
    /// Poll `connection_state_sync()` to check progress.
    pub async fn connect_async(&self, addr: &TransportAddr) -> Result<(), TransportError> {
        if !self.state.is_operational() {
            return Err(TransportError::NotStarted);
        }

        // Already established?
        {
            let pool = self.pool.lock().await;
            if pool.contains_key(addr) {
                return Ok(());
            }
        }

        // Already connecting?
        {
            let connecting = self.connecting.lock().await;
            if connecting.contains_key(addr) {
                return Ok(());
            }
        }

        let socket_addrs = resolve_socket_addrs(addr).await?;
        let timeout_ms = self.config.connect_timeout_ms();
        let config = self.config.clone();
        let transport_id = self.transport_id;
        let remote_addr = addr.clone();

        debug!(
            transport_id = %transport_id,
            remote_addr = %remote_addr,
            timeout_ms,
            "Initiating background TCP connect"
        );

        let task = tokio::spawn(async move {
            let stream = match connect_to_any_addr(&socket_addrs, timeout_ms).await {
                Ok(stream) => stream,
                Err(error @ TransportError::ConnectionRefused) => {
                    debug!(
                        transport_id = %transport_id,
                        remote_addr = %remote_addr,
                        error = %error,
                        "Background TCP connect refused"
                    );
                    return Err(error);
                }
                Err(error @ TransportError::Timeout) => {
                    debug!(
                        transport_id = %transport_id,
                        remote_addr = %remote_addr,
                        "Background TCP connect timed out"
                    );
                    return Err(error);
                }
                Err(error) => return Err(error),
            };

            // Configure socket options via socket2
            let std_stream = stream
                .into_std()
                .map_err(|e| TransportError::StartFailed(format!("into_std: {}", e)))?;
            configure_socket(&std_stream, &config)?;

            // Read TCP_MAXSEG for per-connection MTU
            let mss_mtu = read_mss_mtu(&std_stream, config.mtu());

            // Convert back to tokio
            let stream = TcpStream::from_std(std_stream)
                .map_err(|e| TransportError::StartFailed(format!("from_std: {}", e)))?;

            Ok((stream, mss_mtu))
        });

        let mut connecting = self.connecting.lock().await;
        connecting.insert(addr.clone(), ConnectingEntry { task });

        Ok(())
    }

    /// Query the state of a connection to a remote address.
    ///
    /// Checks both established and connecting pools. If a background
    /// connect task has completed, promotes it to the established pool
    /// (spawning a receive loop) or reports the failure.
    ///
    /// This method is synchronous but uses `try_lock` internally.
    /// Returns `ConnectionState::Connecting` if locks can't be acquired.
    pub fn connection_state_sync(&self, addr: &TransportAddr) -> ConnectionState {
        // Check established pool first
        if let Ok(pool) = self.pool.try_lock() {
            if pool.contains_key(addr) {
                return ConnectionState::Connected;
            }
        } else {
            return ConnectionState::Connecting; // can't tell, assume still going
        }

        // Check connecting pool
        let mut connecting = match self.connecting.try_lock() {
            Ok(c) => c,
            Err(_) => return ConnectionState::Connecting,
        };

        let entry = match connecting.get_mut(addr) {
            Some(e) => e,
            None => return ConnectionState::None,
        };

        // Check if the background task has completed
        if !entry.task.is_finished() {
            return ConnectionState::Connecting;
        }

        // Task is done — take the result and remove from connecting pool.
        // We need to poll the finished task. Since it's finished, we use
        // now_or_never to get the result without blocking.
        let addr_clone = addr.clone();
        let task = connecting.remove(&addr_clone).unwrap().task;

        // Use futures::FutureExt::now_or_never or block_on for the finished task.
        // Since the task is finished, we can safely poll it.
        match task.now_or_never() {
            Some(Ok(Ok((stream, mss_mtu)))) => {
                // Promote to established pool
                self.promote_connection(addr, stream, mss_mtu);
                ConnectionState::Connected
            }
            Some(Ok(Err(e))) => ConnectionState::Failed(format!("{}", e)),
            Some(Err(e)) => {
                // JoinError (panic or cancel)
                ConnectionState::Failed(format!("task failed: {}", e))
            }
            None => {
                // Shouldn't happen since is_finished() was true
                ConnectionState::Connecting
            }
        }
    }

    /// Promote a completed background connection to the established pool.
    ///
    /// Splits the stream, spawns a receive loop, and inserts into the pool.
    /// Called from `connection_state_sync()` when a background task completes.
    fn promote_connection(&self, addr: &TransportAddr, stream: TcpStream, mss_mtu: u16) {
        let (read_half, write_half) = stream.into_split();
        let writer = Arc::new(Mutex::new(write_half));

        let transport_id = self.transport_id;
        let packet_tx = self.packet_tx.clone();
        let pool = self.pool.clone();
        let recv_stats = self.stats.clone();
        let remote_addr = addr.clone();

        let recv_task = tokio::spawn(async move {
            tcp_receive_loop(
                read_half,
                TcpReceiveContext {
                    transport_id,
                    remote_addr,
                    packet_tx,
                    pool,
                    mtu: mss_mtu,
                    stats: recv_stats,
                    first_frame_timeout: None,
                },
            )
            .await;
        });

        let conn = TcpConnection {
            writer,
            recv_task,
            mtu: mss_mtu,
            established_at: Instant::now(),
        };

        // Use try_lock since we're in a sync context and the pool
        // should be available (connection_state_sync already checked it)
        if let Ok(mut pool) = self.pool.try_lock() {
            pool.insert(addr.clone(), conn);
            self.stats.record_connection_established();
            debug!(
                transport_id = %self.transport_id,
                remote_addr = %addr,
                mtu = mss_mtu,
                "TCP connection established (background connect)"
            );
        } else {
            // Pool locked — abort the recv task, connection will be retried
            conn.recv_task.abort();
            warn!(
                transport_id = %self.transport_id,
                remote_addr = %addr,
                "Failed to promote connection (pool locked)"
            );
        }
    }
}

impl Transport for TcpTransport {
    fn transport_id(&self) -> TransportId {
        self.transport_id
    }

    fn transport_type(&self) -> &TransportType {
        &TransportType::TCP
    }

    fn state(&self) -> TransportState {
        self.state
    }

    fn mtu(&self) -> u16 {
        self.config.mtu()
    }

    fn link_mtu(&self, _addr: &TransportAddr) -> u16 {
        // Per-link MTU would require synchronous pool access.
        // For now, return the configured default. The async send path
        // uses the per-connection MSS-derived MTU for validation.
        self.config.mtu()
    }

    fn start(&mut self) -> Result<(), TransportError> {
        Err(TransportError::NotSupported(
            "use start_async() for TCP transport".into(),
        ))
    }

    fn stop(&mut self) -> Result<(), TransportError> {
        Err(TransportError::NotSupported(
            "use stop_async() for TCP transport".into(),
        ))
    }

    fn send(&self, _addr: &TransportAddr, _data: &[u8]) -> Result<(), TransportError> {
        Err(TransportError::NotSupported(
            "use send_async() for TCP transport".into(),
        ))
    }

    fn discover(&self) -> Result<Vec<DiscoveredPeer>, TransportError> {
        // TCP has no discovery mechanism
        Ok(Vec::new())
    }

    fn accept_connections(&self) -> bool {
        // If bind_addr is configured, we accept inbound connections
        self.config.bind_addr.is_some()
    }
}

// ============================================================================
// Accept Loop
// ============================================================================

/// Socket configuration parameters passed to the accept loop.
struct AcceptConfig {
    mtu: u16,
    max_inbound: usize,
    nodelay: bool,
    keepalive_secs: u64,
    recv_buf: usize,
    send_buf: usize,
    first_frame_timeout_ms: u64,
}

/// TCP accept loop — runs as a spawned task when bind_addr is configured.
#[allow(clippy::too_many_arguments)]
async fn accept_loop(
    listener: TcpListener,
    transport_id: TransportId,
    packet_tx: PacketTx,
    pool: ConnectionPool,
    cfg: AcceptConfig,
    stats: Arc<TcpStats>,
) {
    let AcceptConfig {
        mtu,
        max_inbound,
        nodelay,
        keepalive_secs,
        recv_buf,
        send_buf,
        first_frame_timeout_ms,
    } = cfg;
    debug!(transport_id = %transport_id, "TCP accept loop starting");

    loop {
        match listener.accept().await {
            Ok((stream, peer_addr)) => {
                // Check connection limit
                {
                    let pool_guard = pool.lock().await;
                    if pool_guard.len() >= max_inbound {
                        stats.record_connection_rejected();
                        warn!(
                            transport_id = %transport_id,
                            peer_addr = %peer_addr,
                            max = max_inbound,
                            "Rejecting inbound TCP connection (max_inbound_connections reached)"
                        );
                        continue;
                    }
                }

                // Configure socket options
                let std_stream = match stream.into_std() {
                    Ok(s) => s,
                    Err(e) => {
                        warn!(
                            transport_id = %transport_id,
                            error = %e,
                            "Failed to convert accepted stream to std"
                        );
                        continue;
                    }
                };

                if let Err(e) = configure_accepted_socket(
                    &std_stream,
                    nodelay,
                    keepalive_secs,
                    recv_buf,
                    send_buf,
                ) {
                    warn!(
                        transport_id = %transport_id,
                        peer_addr = %peer_addr,
                        error = %e,
                        "Failed to configure accepted socket"
                    );
                    continue;
                }

                // Read MSS for per-connection MTU
                let conn_mtu = read_mss_mtu(&std_stream, mtu);

                let stream = match TcpStream::from_std(std_stream) {
                    Ok(s) => s,
                    Err(e) => {
                        warn!(
                            transport_id = %transport_id,
                            error = %e,
                            "Failed to convert accepted stream back to tokio"
                        );
                        continue;
                    }
                };

                let remote_addr = TransportAddr::from_string(&peer_addr.to_string());

                // Split and spawn receive task
                let (read_half, write_half) = stream.into_split();
                let writer = Arc::new(Mutex::new(write_half));

                let recv_pool = pool.clone();
                let recv_packet_tx = packet_tx.clone();
                let recv_stats = stats.clone();
                let recv_addr = remote_addr.clone();

                let recv_task = tokio::spawn(async move {
                    tcp_receive_loop(
                        read_half,
                        TcpReceiveContext {
                            transport_id,
                            remote_addr: recv_addr,
                            packet_tx: recv_packet_tx,
                            pool: recv_pool,
                            mtu: conn_mtu,
                            stats: recv_stats,
                            first_frame_timeout: first_frame_timeout(first_frame_timeout_ms),
                        },
                    )
                    .await;
                });

                let conn = TcpConnection {
                    writer,
                    recv_task,
                    mtu: conn_mtu,
                    established_at: Instant::now(),
                };

                let mut pool_guard = pool.lock().await;
                pool_guard.insert(remote_addr.clone(), conn);

                stats.record_connection_accepted();

                debug!(
                    transport_id = %transport_id,
                    remote_addr = %remote_addr,
                    mtu = conn_mtu,
                    "Accepted inbound TCP connection"
                );
            }
            Err(e) => {
                warn!(
                    transport_id = %transport_id,
                    error = %e,
                    "TCP accept error"
                );
            }
        }
    }
}

// ============================================================================
// Receive Loop (per-connection)
// ============================================================================

/// Per-connection TCP receive loop.
///
/// Reads complete FMP packets using the stream reader, delivers them to
/// the node via the packet channel. On error or EOF, removes the
/// connection from the pool and exits.
struct TcpReceiveContext {
    transport_id: TransportId,
    remote_addr: TransportAddr,
    packet_tx: PacketTx,
    pool: ConnectionPool,
    mtu: u16,
    stats: Arc<TcpStats>,
    first_frame_timeout: Option<Duration>,
}

async fn tcp_receive_loop(mut reader: tokio::net::tcp::OwnedReadHalf, ctx: TcpReceiveContext) {
    let TcpReceiveContext {
        transport_id,
        remote_addr,
        packet_tx,
        pool,
        mtu,
        stats,
        first_frame_timeout,
    } = ctx;

    debug!(
        transport_id = %transport_id,
        remote_addr = %remote_addr,
        "TCP receive loop starting"
    );

    let mut first_frame = true;
    loop {
        let read_result = if first_frame {
            match first_frame_timeout {
                Some(limit) => match timeout(limit, read_fmp_packet(&mut reader, mtu)).await {
                    Ok(result) => result,
                    Err(_) => {
                        stats.record_first_frame_timeout();
                        debug!(
                            transport_id = %transport_id,
                            remote_addr = %remote_addr,
                            timeout_ms = limit.as_millis(),
                            "TCP inbound connection timed out before first complete FMP frame"
                        );
                        break;
                    }
                },
                None => read_fmp_packet(&mut reader, mtu).await,
            }
        } else {
            read_fmp_packet(&mut reader, mtu).await
        };

        match read_result {
            Ok(data) => {
                first_frame = false;
                stats.record_recv(data.len());

                trace!(
                    transport_id = %transport_id,
                    remote_addr = %remote_addr,
                    bytes = data.len(),
                    "TCP packet received"
                );

                let packet = ReceivedPacket::new(transport_id, remote_addr.clone(), data);

                if packet_tx.send(packet).is_err() {
                    debug!(
                        transport_id = %transport_id,
                        "Packet channel closed, stopping TCP receive loop"
                    );
                    break;
                }
            }
            Err(e) => {
                stats.record_recv_error();
                // EOF or protocol error — remove connection from pool
                debug!(
                    transport_id = %transport_id,
                    remote_addr = %remote_addr,
                    error = %e,
                    "TCP receive error, removing connection"
                );
                break;
            }
        }
    }

    // Clean up: remove ourselves from the pool
    let mut pool_guard = pool.lock().await;
    pool_guard.remove(&remote_addr);

    debug!(
        transport_id = %transport_id,
        remote_addr = %remote_addr,
        "TCP receive loop stopped"
    );
}

fn first_frame_timeout(timeout_ms: u64) -> Option<Duration> {
    (timeout_ms > 0).then(|| Duration::from_millis(timeout_ms))
}

// ============================================================================
// Socket Configuration Helpers
// ============================================================================

async fn connect_to_any_addr(
    socket_addrs: &[SocketAddr],
    timeout_ms: u64,
) -> Result<TcpStream, TransportError> {
    let mut last_error = None;
    for socket_addr in socket_addrs {
        match tokio::time::timeout(
            Duration::from_millis(timeout_ms),
            TcpStream::connect(socket_addr),
        )
        .await
        {
            Ok(Ok(stream)) => return Ok(stream),
            Ok(Err(error)) => {
                trace!(
                    remote_addr = %socket_addr,
                    error = %error,
                    "TCP connect candidate failed"
                );
                last_error = Some(TransportError::ConnectionRefused);
            }
            Err(_) => {
                trace!(
                    remote_addr = %socket_addr,
                    timeout_ms,
                    "TCP connect candidate timed out"
                );
                last_error = Some(TransportError::Timeout);
            }
        }
    }
    Err(last_error
        .unwrap_or_else(|| TransportError::InvalidAddress("no TCP addresses to dial".to_string())))
}

/// Configure a TCP socket with the transport's settings.
fn configure_socket(
    stream: &std::net::TcpStream,
    config: &TcpConfig,
) -> Result<(), TransportError> {
    let socket = socket2::SockRef::from(stream)
        .try_clone()
        .map_err(|e| TransportError::StartFailed(format!("clone socket: {}", e)))?;

    // TCP_NODELAY
    socket
        .set_tcp_nodelay(config.nodelay())
        .map_err(|e| TransportError::StartFailed(format!("set nodelay: {}", e)))?;

    // Keepalive
    let keepalive_secs = config.keepalive_secs();
    if keepalive_secs > 0 {
        let keepalive = TcpKeepalive::new().with_time(Duration::from_secs(keepalive_secs));
        socket
            .set_tcp_keepalive(&keepalive)
            .map_err(|e| TransportError::StartFailed(format!("set keepalive: {}", e)))?;
    }

    // Buffer sizes
    socket
        .set_recv_buffer_size(config.recv_buf_size())
        .map_err(|e| TransportError::StartFailed(format!("set recv buffer: {}", e)))?;
    socket
        .set_send_buffer_size(config.send_buf_size())
        .map_err(|e| TransportError::StartFailed(format!("set send buffer: {}", e)))?;

    Ok(())
}

/// Configure an accepted TCP socket (without TcpConfig reference).
fn configure_accepted_socket(
    stream: &std::net::TcpStream,
    nodelay: bool,
    keepalive_secs: u64,
    recv_buf: usize,
    send_buf: usize,
) -> Result<(), TransportError> {
    let socket = socket2::SockRef::from(stream)
        .try_clone()
        .map_err(|e| TransportError::StartFailed(format!("clone socket: {}", e)))?;

    socket
        .set_tcp_nodelay(nodelay)
        .map_err(|e| TransportError::StartFailed(format!("set nodelay: {}", e)))?;

    if keepalive_secs > 0 {
        let keepalive = TcpKeepalive::new().with_time(Duration::from_secs(keepalive_secs));
        socket
            .set_tcp_keepalive(&keepalive)
            .map_err(|e| TransportError::StartFailed(format!("set keepalive: {}", e)))?;
    }

    socket
        .set_recv_buffer_size(recv_buf)
        .map_err(|e| TransportError::StartFailed(format!("set recv buffer: {}", e)))?;
    socket
        .set_send_buffer_size(send_buf)
        .map_err(|e| TransportError::StartFailed(format!("set send buffer: {}", e)))?;

    Ok(())
}

/// Read TCP_MAXSEG and derive per-connection MTU, falling back to default.
fn read_mss_mtu(stream: &std::net::TcpStream, default_mtu: u16) -> u16 {
    // Try to read TCP_MAXSEG. Not all platforms support this.
    #[cfg(target_os = "linux")]
    {
        use std::os::unix::io::AsRawFd;
        unsafe {
            let mut mss: libc::c_int = 0;
            let mut len: libc::socklen_t = std::mem::size_of::<libc::c_int>() as libc::socklen_t;
            let fd = stream.as_raw_fd();
            let ret = libc::getsockopt(
                fd,
                libc::IPPROTO_TCP,
                libc::TCP_MAXSEG,
                &mut mss as *mut libc::c_int as *mut libc::c_void,
                &mut len,
            );
            if ret == 0 && mss > 0 {
                let mss_mtu = (mss as u32).min(u16::MAX as u32) as u16;
                // Use the smaller of MSS and configured default
                return mss_mtu.min(default_mtu);
            }
        }
    }

    #[cfg(not(target_os = "linux"))]
    let _ = stream;

    // Fallback: use configured default MTU
    default_mtu
}

// ============================================================================
// Tests
// ============================================================================

#[cfg(test)]
mod tests {
    use super::*;
    use crate::transport::packet_channel;
    use tokio::time::{Duration, timeout};

    fn make_config() -> TcpConfig {
        TcpConfig {
            bind_addr: Some("127.0.0.1:0".to_string()),
            mtu: Some(1400),
            ..Default::default()
        }
    }

    fn make_outbound_config() -> TcpConfig {
        TcpConfig {
            bind_addr: None,
            mtu: Some(1400),
            ..Default::default()
        }
    }

    async fn unused_loopback_addr(except_port: u16) -> SocketAddr {
        for port in 49152..65535 {
            if port == except_port {
                continue;
            }
            let addr = SocketAddr::from(([127, 0, 0, 1], port));
            if TcpStream::connect(addr).await.is_err() {
                return addr;
            }
        }
        panic!("no unused loopback port found");
    }

    #[tokio::test]
    async fn test_start_stop() {
        let (tx, _rx) = packet_channel(100);
        let mut transport = TcpTransport::new(TransportId::new(1), None, make_config(), tx);

        assert_eq!(transport.state(), TransportState::Configured);

        transport.start_async().await.unwrap();
        assert_eq!(transport.state(), TransportState::Up);
        assert!(transport.local_addr().is_some());

        transport.stop_async().await.unwrap();
        assert_eq!(transport.state(), TransportState::Down);
    }

    #[tokio::test]
    async fn connect_to_any_addr_tries_later_candidates() {
        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
        let good_addr = listener.local_addr().unwrap();
        let bad_addr = unused_loopback_addr(good_addr.port()).await;
        let accept = tokio::spawn(async move { listener.accept().await });

        let stream = connect_to_any_addr(&[bad_addr, good_addr], 1_000)
            .await
            .expect("second TCP candidate should connect");
        drop(stream);

        timeout(Duration::from_secs(1), accept)
            .await
            .expect("listener should accept")
            .expect("accept task should not panic")
            .expect("accept should succeed");
    }

    #[tokio::test]
    async fn test_start_outbound_only() {
        let (tx, _rx) = packet_channel(100);
        let mut transport =
            TcpTransport::new(TransportId::new(1), None, make_outbound_config(), tx);

        transport.start_async().await.unwrap();
        assert_eq!(transport.state(), TransportState::Up);
        // No listener, so no local_addr
        assert!(transport.local_addr().is_none());

        transport.stop_async().await.unwrap();
    }

    #[tokio::test]
    async fn test_double_start_fails() {
        let (tx, _rx) = packet_channel(100);
        let mut transport = TcpTransport::new(TransportId::new(1), None, make_config(), tx);

        transport.start_async().await.unwrap();

        let result = transport.start_async().await;
        assert!(matches!(result, Err(TransportError::AlreadyStarted)));

        transport.stop_async().await.unwrap();
    }

    #[tokio::test]
    async fn test_stop_not_started_fails() {
        let (tx, _rx) = packet_channel(100);
        let mut transport = TcpTransport::new(TransportId::new(1), None, make_config(), tx);

        let result = transport.stop_async().await;
        assert!(matches!(result, Err(TransportError::NotStarted)));
    }

    #[tokio::test]
    async fn test_send_not_started() {
        let (tx, _rx) = packet_channel(100);
        let transport = TcpTransport::new(TransportId::new(1), None, make_config(), tx);

        let result = transport
            .send_async(&TransportAddr::from_string("127.0.0.1:9999"), b"test")
            .await;

        assert!(matches!(result, Err(TransportError::NotStarted)));
    }

    #[tokio::test]
    async fn test_send_recv() {
        let (tx1, _rx1) = packet_channel(100);
        let (tx2, mut rx2) = packet_channel(100);

        let mut t1 = TcpTransport::new(TransportId::new(1), None, make_outbound_config(), tx1);
        let mut t2 = TcpTransport::new(TransportId::new(2), None, make_config(), tx2);

        t1.start_async().await.unwrap();
        t2.start_async().await.unwrap();

        let addr2 = t2.local_addr().unwrap();

        // Build a valid FMP established frame to send
        // [ver+phase:1][flags:1][payload_len:2 LE][12 bytes header][payload bytes][16 bytes tag]
        let payload_len = 4u16;
        let total = 4 + 12 + payload_len as usize + 16;
        let mut frame = vec![0u8; total];
        frame[0] = 0x00; // ver=0, phase=0 (established)
        frame[1] = 0x00; // flags
        frame[2..4].copy_from_slice(&payload_len.to_le_bytes());
        // Fill the rest with a recognizable pattern
        for (i, byte) in frame[4..total].iter_mut().enumerate() {
            *byte = ((4 + i) & 0xFF) as u8;
        }

        let bytes_sent = t1
            .send_async(&TransportAddr::from_string(&addr2.to_string()), &frame)
            .await
            .unwrap();
        assert_eq!(bytes_sent, frame.len());

        // Receive on t2
        let packet = timeout(Duration::from_secs(2), rx2.recv())
            .await
            .expect("timeout")
            .expect("channel closed");

        assert_eq!(packet.data, frame);

        t1.stop_async().await.unwrap();
        t2.stop_async().await.unwrap();
    }

    #[tokio::test]
    async fn inbound_first_frame_timeout_closes_slowloris_connection() {
        use tokio::io::AsyncWriteExt as _;

        let (tx, _rx) = packet_channel(100);
        let config = TcpConfig {
            bind_addr: Some("127.0.0.1:0".to_string()),
            first_frame_timeout_ms: Some(50),
            max_inbound_connections: Some(1),
            ..Default::default()
        };
        let mut transport = TcpTransport::new(TransportId::new(1), None, config, tx);
        transport.start_async().await.unwrap();

        let addr = transport.local_addr().unwrap();
        let mut stream = TcpStream::connect(addr).await.unwrap();
        stream.write_all(b"\x01").await.unwrap();

        timeout(Duration::from_secs(1), async {
            loop {
                if transport.stats.snapshot().first_frame_timeouts >= 1 {
                    break;
                }
                tokio::time::sleep(Duration::from_millis(10)).await;
            }
        })
        .await
        .expect("slowloris connection should hit first-frame timeout");

        let pool = transport.pool.lock().await;
        assert_eq!(pool.len(), 0);
        drop(pool);

        transport.stop_async().await.unwrap();
    }

    #[tokio::test]
    async fn test_bidirectional() {
        let (tx1, mut rx1) = packet_channel(100);
        let (tx2, mut rx2) = packet_channel(100);

        let mut t1 = TcpTransport::new(TransportId::new(1), None, make_config(), tx1);
        let mut t2 = TcpTransport::new(TransportId::new(2), None, make_config(), tx2);

        t1.start_async().await.unwrap();
        t2.start_async().await.unwrap();

        let addr1 = t1.local_addr().unwrap();
        let addr2 = t2.local_addr().unwrap();

        // Build valid FMP msg1 frame (114 bytes)
        let mut msg1_frame = vec![0xAA; 114];
        msg1_frame[0] = 0x01; // phase=msg1
        msg1_frame[1] = 0x00;
        msg1_frame[2..4].copy_from_slice(&110u16.to_le_bytes()); // payload_len = 110

        // Send from t1 to t2
        t1.send_async(&TransportAddr::from_string(&addr2.to_string()), &msg1_frame)
            .await
            .unwrap();

        let packet = timeout(Duration::from_secs(2), rx2.recv())
            .await
            .expect("timeout")
            .expect("channel closed");
        assert_eq!(packet.data, msg1_frame);

        // Build valid FMP msg2 frame (69 bytes)
        let mut msg2_frame = vec![0xBB; 69];
        msg2_frame[0] = 0x02; // phase=msg2
        msg2_frame[1] = 0x00;
        msg2_frame[2..4].copy_from_slice(&65u16.to_le_bytes()); // payload_len = 65

        // Send from t2 to t1
        t2.send_async(&TransportAddr::from_string(&addr1.to_string()), &msg2_frame)
            .await
            .unwrap();

        let packet = timeout(Duration::from_secs(2), rx1.recv())
            .await
            .expect("timeout")
            .expect("channel closed");
        assert_eq!(packet.data, msg2_frame);

        t1.stop_async().await.unwrap();
        t2.stop_async().await.unwrap();
    }

    #[tokio::test]
    async fn test_connect_timeout() {
        let (tx, _rx) = packet_channel(100);
        let config = TcpConfig {
            bind_addr: None,
            connect_timeout_ms: Some(100), // Very short timeout
            ..Default::default()
        };
        let mut transport = TcpTransport::new(TransportId::new(1), None, config, tx);
        transport.start_async().await.unwrap();

        // Try to connect to a non-routable address (should timeout)
        let result = transport
            .send_async(
                &TransportAddr::from_string("192.0.2.1:2121"),
                b"\x00\x00\x04\x00test1234567890123456789012345678",
            )
            .await;

        assert!(result.is_err());

        transport.stop_async().await.unwrap();
    }

    #[tokio::test]
    async fn test_close_connection() {
        let (tx1, _rx1) = packet_channel(100);
        let (tx2, _rx2) = packet_channel(100);

        let mut t1 = TcpTransport::new(TransportId::new(1), None, make_outbound_config(), tx1);
        let mut t2 = TcpTransport::new(TransportId::new(2), None, make_config(), tx2);

        t1.start_async().await.unwrap();
        t2.start_async().await.unwrap();

        let addr2 = t2.local_addr().unwrap();
        let remote = TransportAddr::from_string(&addr2.to_string());

        // Build valid msg1 frame to establish connection
        let mut msg1 = vec![0xAA; 114];
        msg1[0] = 0x01;
        msg1[1] = 0x00;
        msg1[2..4].copy_from_slice(&110u16.to_le_bytes());

        t1.send_async(&remote, &msg1).await.unwrap();

        // Connection should exist
        {
            let pool = t1.pool.lock().await;
            assert!(pool.contains_key(&remote));
        }

        // Close it
        t1.close_connection_async(&remote).await;

        // Connection should be gone
        {
            let pool = t1.pool.lock().await;
            assert!(!pool.contains_key(&remote));
        }

        t1.stop_async().await.unwrap();
        t2.stop_async().await.unwrap();
    }

    #[tokio::test]
    async fn test_discover_returns_empty() {
        let (tx, _rx) = packet_channel(100);
        let transport = TcpTransport::new(TransportId::new(1), None, make_config(), tx);

        let peers = transport.discover().unwrap();
        assert!(peers.is_empty());
    }

    #[test]
    fn test_transport_type() {
        let (tx, _rx) = packet_channel(100);
        let transport = TcpTransport::new(TransportId::new(1), None, make_config(), tx);

        assert_eq!(transport.transport_type().name, "tcp");
        assert!(transport.transport_type().connection_oriented);
        assert!(transport.transport_type().reliable);
    }

    #[test]
    fn test_sync_methods_return_not_supported() {
        let (tx, _rx) = packet_channel(100);
        let mut transport = TcpTransport::new(TransportId::new(1), None, make_config(), tx);

        assert!(matches!(
            transport.start(),
            Err(TransportError::NotSupported(_))
        ));
        assert!(matches!(
            transport.stop(),
            Err(TransportError::NotSupported(_))
        ));
        assert!(matches!(
            transport.send(&TransportAddr::from_string("test"), b"data"),
            Err(TransportError::NotSupported(_))
        ));
    }

    #[test]
    fn test_accept_connections_with_bind() {
        let (tx, _rx) = packet_channel(100);
        let config = TcpConfig {
            bind_addr: Some("0.0.0.0:0".to_string()),
            ..Default::default()
        };
        let transport = TcpTransport::new(TransportId::new(1), None, config, tx);
        assert!(transport.accept_connections());
    }

    #[test]
    fn test_accept_connections_without_bind() {
        let (tx, _rx) = packet_channel(100);
        let config = TcpConfig {
            bind_addr: None,
            ..Default::default()
        };
        let transport = TcpTransport::new(TransportId::new(1), None, config, tx);
        assert!(!transport.accept_connections());
    }

    #[tokio::test]
    async fn test_connection_drop_and_reconnect() {
        let (tx1, _rx1) = packet_channel(100);
        let (tx2, mut rx2) = packet_channel(100);

        let mut t1 = TcpTransport::new(TransportId::new(1), None, make_outbound_config(), tx1);
        let mut t2 = TcpTransport::new(TransportId::new(2), None, make_config(), tx2);

        t1.start_async().await.unwrap();
        t2.start_async().await.unwrap();

        let addr2 = t2.local_addr().unwrap();
        let remote = TransportAddr::from_string(&addr2.to_string());

        // Build valid msg1 frame
        let mut msg1 = vec![0xAA; 114];
        msg1[0] = 0x01;
        msg1[1] = 0x00;
        msg1[2..4].copy_from_slice(&110u16.to_le_bytes());

        // First send establishes connection
        t1.send_async(&remote, &msg1).await.unwrap();
        let _ = timeout(Duration::from_secs(1), rx2.recv()).await;

        // Force-close the connection
        t1.close_connection_async(&remote).await;

        // Second send should reconnect (connect-on-send)
        t1.send_async(&remote, &msg1).await.unwrap();

        let packet = timeout(Duration::from_secs(2), rx2.recv())
            .await
            .expect("timeout")
            .expect("channel closed");
        assert_eq!(packet.data, msg1);

        t1.stop_async().await.unwrap();
        t2.stop_async().await.unwrap();
    }

    #[tokio::test]
    async fn test_connect_async_success() {
        let (tx1, mut rx1) = packet_channel(100);
        let (tx2, _rx2) = packet_channel(100);

        let mut t1 = TcpTransport::new(TransportId::new(1), None, make_outbound_config(), tx1);
        let mut t2 = TcpTransport::new(TransportId::new(2), None, make_config(), tx2);

        t1.start_async().await.unwrap();
        t2.start_async().await.unwrap();

        let addr2 = t2.local_addr().unwrap();
        let remote = TransportAddr::from_string(&addr2.to_string());

        // State should be None before connect
        assert_eq!(t1.connection_state_sync(&remote), ConnectionState::None);

        // Initiate non-blocking connect
        t1.connect_async(&remote).await.unwrap();

        // Wait for the background connect to complete
        tokio::time::sleep(Duration::from_millis(200)).await;

        // Poll state — should be Connected now
        let state = t1.connection_state_sync(&remote);
        assert_eq!(state, ConnectionState::Connected);

        // Now send should work (connection already established)
        let mut msg1 = vec![0xAA; 114];
        msg1[0] = 0x01;
        msg1[1] = 0x00;
        msg1[2..4].copy_from_slice(&110u16.to_le_bytes());

        t1.send_async(&remote, &msg1).await.unwrap();

        let packet = timeout(Duration::from_secs(2), rx1.recv()).await;
        // We receive on rx1 but that's the wrong receiver — t2's rx gets the packet
        // Just verify send didn't error
        drop(packet);

        t1.stop_async().await.unwrap();
        t2.stop_async().await.unwrap();
    }

    #[tokio::test]
    async fn test_connect_async_timeout() {
        let (tx, _rx) = packet_channel(100);
        let config = TcpConfig {
            bind_addr: None,
            connect_timeout_ms: Some(100), // Very short timeout
            ..Default::default()
        };
        let mut transport = TcpTransport::new(TransportId::new(1), None, config, tx);
        transport.start_async().await.unwrap();

        let remote = TransportAddr::from_string("192.0.2.1:2121");
        transport.connect_async(&remote).await.unwrap();

        // Wait for timeout
        tokio::time::sleep(Duration::from_millis(500)).await;

        let state = transport.connection_state_sync(&remote);
        assert!(matches!(state, ConnectionState::Failed(_)));

        transport.stop_async().await.unwrap();
    }

    #[tokio::test]
    async fn test_connect_async_not_started() {
        let (tx, _rx) = packet_channel(100);
        let transport = TcpTransport::new(TransportId::new(1), None, make_config(), tx);

        let result = transport
            .connect_async(&TransportAddr::from_string("127.0.0.1:9999"))
            .await;

        assert!(matches!(result, Err(TransportError::NotStarted)));
    }

    #[tokio::test]
    async fn test_connect_async_already_connected() {
        let (tx1, _rx1) = packet_channel(100);
        let (tx2, _rx2) = packet_channel(100);

        let mut t1 = TcpTransport::new(TransportId::new(1), None, make_outbound_config(), tx1);
        let mut t2 = TcpTransport::new(TransportId::new(2), None, make_config(), tx2);

        t1.start_async().await.unwrap();
        t2.start_async().await.unwrap();

        let addr2 = t2.local_addr().unwrap();
        let remote = TransportAddr::from_string(&addr2.to_string());

        // Connect first time
        t1.connect_async(&remote).await.unwrap();
        tokio::time::sleep(Duration::from_millis(200)).await;
        assert_eq!(
            t1.connection_state_sync(&remote),
            ConnectionState::Connected
        );

        // Second connect should be a no-op (already connected)
        t1.connect_async(&remote).await.unwrap();

        t1.stop_async().await.unwrap();
        t2.stop_async().await.unwrap();
    }

    #[tokio::test]
    async fn test_connect_async_then_send_recv() {
        let (tx1, _rx1) = packet_channel(100);
        let (tx2, mut rx2) = packet_channel(100);

        let mut t1 = TcpTransport::new(TransportId::new(1), None, make_outbound_config(), tx1);
        let mut t2 = TcpTransport::new(TransportId::new(2), None, make_config(), tx2);

        t1.start_async().await.unwrap();
        t2.start_async().await.unwrap();

        let addr2 = t2.local_addr().unwrap();
        let remote = TransportAddr::from_string(&addr2.to_string());

        // Connect first, then send
        t1.connect_async(&remote).await.unwrap();
        tokio::time::sleep(Duration::from_millis(200)).await;
        assert_eq!(
            t1.connection_state_sync(&remote),
            ConnectionState::Connected
        );

        // Build valid FMP msg1 frame
        let mut msg1 = vec![0xAA; 114];
        msg1[0] = 0x01;
        msg1[1] = 0x00;
        msg1[2..4].copy_from_slice(&110u16.to_le_bytes());

        // Send using the pre-established connection
        t1.send_async(&remote, &msg1).await.unwrap();

        let packet = timeout(Duration::from_secs(2), rx2.recv())
            .await
            .expect("timeout")
            .expect("channel closed");
        assert_eq!(packet.data, msg1);

        t1.stop_async().await.unwrap();
        t2.stop_async().await.unwrap();
    }

    #[test]
    fn test_connection_state_none_for_unknown() {
        let (tx, _rx) = packet_channel(100);
        let transport = TcpTransport::new(TransportId::new(1), None, make_config(), tx);

        let state = transport.connection_state_sync(&TransportAddr::from_string("unknown:1234"));
        assert_eq!(state, ConnectionState::None);
    }

    #[tokio::test]
    async fn test_connect_ip_string() {
        let (tx1, _rx1) = packet_channel(100);
        let (tx2, mut rx2) = packet_channel(100);

        let mut t1 = TcpTransport::new(TransportId::new(1), None, make_config(), tx1);
        let mut t2 = TcpTransport::new(
            TransportId::new(2),
            None,
            TcpConfig {
                bind_addr: Some("127.0.0.1:0".to_string()),
                ..Default::default()
            },
            tx2,
        );

        t1.start_async().await.unwrap();
        t2.start_async().await.unwrap();

        let port2 = t2.local_addr().unwrap().port();

        // Connect using IP string — build a valid FMP frame (114 bytes)
        let addr = TransportAddr::from_string(&format!("127.0.0.1:{}", port2));
        let mut frame = vec![0xAA; 114];
        frame[0] = 0x01; // ver=0, phase=1
        frame[1] = 0x00; // flags
        frame[2..4].copy_from_slice(&110u16.to_le_bytes()); // payload_len
        t1.send_async(&addr, &frame).await.unwrap();

        // Receive on t2
        let packet = tokio::time::timeout(Duration::from_secs(5), rx2.recv())
            .await
            .expect("timeout")
            .expect("channel closed");

        assert_eq!(packet.data, frame);

        t1.stop_async().await.unwrap();
        t2.stop_async().await.unwrap();
    }

    #[tokio::test]
    async fn test_connect_async_ip_string() {
        let (tx1, _rx1) = packet_channel(100);
        let (tx2, _rx2) = packet_channel(100);

        let mut t1 = TcpTransport::new(TransportId::new(1), None, make_config(), tx1);
        let mut t2 = TcpTransport::new(
            TransportId::new(2),
            None,
            TcpConfig {
                bind_addr: Some("127.0.0.1:0".to_string()),
                ..Default::default()
            },
            tx2,
        );

        t1.start_async().await.unwrap();
        t2.start_async().await.unwrap();

        let port2 = t2.local_addr().unwrap().port();
        let addr = TransportAddr::from_string(&format!("127.0.0.1:{}", port2));

        // Non-blocking connect via IP string
        t1.connect_async(&addr).await.unwrap();

        // Poll until connected
        for _ in 0..50 {
            let state = t1.connection_state_sync(&addr);
            if state == ConnectionState::Connected {
                break;
            }
            tokio::time::sleep(Duration::from_millis(100)).await;
        }

        assert_eq!(t1.connection_state_sync(&addr), ConnectionState::Connected,);

        t1.stop_async().await.unwrap();
        t2.stop_async().await.unwrap();
    }
}
