//! Deterministic simulation tests using Turmoil.
//!
//! All tests in this file use Turmoil's deterministic scheduler for reproducible execution.
//! Same seed MUST produce identical results across runs.
//!
//! Thread-local state (GlobalRng, GlobalSimulationTime, GlobalTestMetrics) and per-network
//! registries (sockets, topology, fault injectors) provide test isolation, so parallel
//! execution is safe.
//!
//! Enable with: cargo test -p freenet --features "simulation_tests,testing" --test simulation_integration
//!
//! The `testing` feature is required for `run_controlled_simulation` method.
//!
//! For non-deterministic smoke tests, see `simulation_smoke.rs`.

#![cfg(feature = "simulation_tests")]

use freenet::config::GlobalTestMetrics;
use freenet::config::{GlobalRng, GlobalSimulationTime, SimulationTransportOpt};
use freenet::dev_tool::{
    RequestId, SimNetwork, StreamId, VirtualTime, check_convergence_from_logs,
    reset_channel_id_counter, reset_event_id_counter, reset_global_node_index, reset_nonce_counter,
};
use freenet::simulation::TimeSource;
use freenet::transport::in_memory_socket::{
    SimulationSocket, clear_all_socket_registries, register_address_network,
    register_network_time_source,
};
use std::collections::{BTreeMap, HashMap, HashSet};
use std::hash::{Hash, Hasher};
use std::net::{Ipv6Addr, SocketAddr};
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;

// =============================================================================
// Test Configuration
// =============================================================================

/// Configuration for a simulation test.
struct TestConfig {
    name: &'static str,
    seed: u64,
    gateways: usize,
    nodes: usize,
    ring_max_htl: usize,
    rnd_if_htl_above: usize,
    max_connections: usize,
    min_connections: usize,
    max_contracts: usize,
    iterations: usize,
    duration: Duration,
    /// Time to sleep between each event in turmoil virtual time.
    /// Default: 200ms (sufficient for event propagation in deterministic sim).
    event_wait: Duration,
    /// Time to sleep after all events complete (post-events phase).
    sleep_after_events: Duration,
    require_convergence: bool,
    /// Optional latency range for jitter simulation (min..max)
    latency_range: Option<std::ops::Range<Duration>>,
    /// Optional message loss rate for fault injection (0.0 to 1.0)
    message_loss_rate: f64,
    /// Use MockWasmRuntime (production ContractExecutor path) instead of MockRuntime.
    ///
    /// **Prefer `true` for new tests.** MockWasmRuntime shares production `bridged_*`
    /// code paths (init tracking, validation, subscriber notifications, corrupted state
    /// recovery). MockRuntime has its own independent implementation that can silently
    /// drift from production behavior. See issue #3141.
    use_mock_wasm: bool,
}

impl TestConfig {
    /// Create a small test configuration (quick CI tests).
    fn small(name: &'static str, seed: u64) -> Self {
        Self {
            name,
            seed,
            gateways: 1,
            nodes: 3,
            ring_max_htl: 7,
            rnd_if_htl_above: 3,
            max_connections: 10,
            min_connections: 2,
            max_contracts: 3,
            iterations: 15,
            duration: Duration::from_secs(20),
            event_wait: Duration::from_millis(200),
            sleep_after_events: Duration::from_secs(1),
            require_convergence: true,
            latency_range: None,
            message_loss_rate: 0.0,
            use_mock_wasm: false,
        }
    }

    /// Create a medium test configuration.
    fn medium(name: &'static str, seed: u64) -> Self {
        Self {
            name,
            seed,
            gateways: 2,
            nodes: 6,
            ring_max_htl: 10,
            rnd_if_htl_above: 7,
            max_connections: 15,
            min_connections: 2,
            max_contracts: 8,
            iterations: 100,
            duration: Duration::from_secs(120),
            event_wait: Duration::from_millis(200),
            sleep_after_events: Duration::from_secs(3),
            require_convergence: true,
            latency_range: None,
            message_loss_rate: 0.0,
            use_mock_wasm: false,
        }
    }

    /// Create a large/dense test configuration.
    fn large(name: &'static str, seed: u64) -> Self {
        Self {
            name,
            seed,
            gateways: 3,
            nodes: 12,
            ring_max_htl: 12,
            rnd_if_htl_above: 8,
            max_connections: 12,
            min_connections: 6,
            max_contracts: 15,
            iterations: 150,
            duration: Duration::from_secs(300),
            event_wait: Duration::from_millis(200),
            sleep_after_events: Duration::from_secs(10),
            require_convergence: true,
            latency_range: None,
            message_loss_rate: 0.0,
            use_mock_wasm: false,
        }
    }

    /// Create a long-running test configuration (1 hour virtual time).
    ///
    /// Designed to uncover time-dependent bugs:
    /// - Connection timeout handling over extended periods
    /// - State drift in long-lived contracts
    /// - Timer edge cases (keep-alive, connection idle timeout)
    /// - Resource exhaustion patterns
    ///
    /// Events are distributed across the full hour (1 event every ~10s) so
    /// the network must remain functional throughout — not just survive idle.
    ///
    /// Includes 10-50ms latency jitter to simulate realistic network conditions.
    ///
    /// # Virtual Time Breakdown
    /// - 360 events × 10s between events = 3600 seconds (1 hour)
    /// - Post-events buffer: 10 seconds for final propagation
    #[allow(dead_code)]
    fn long_running(name: &'static str, seed: u64) -> Self {
        // 360 events × 10s apart = 3600s (1 hour) virtual time.
        // Turmoil steps every virtual ms: 3600s ≈ 3.6M steps × 8 hosts.
        // Wall clock: ~2.5 min (25x acceleration with 8 hosts).
        Self {
            name,
            seed,
            gateways: 2,
            nodes: 6,
            ring_max_htl: 10,
            rnd_if_htl_above: 5,
            max_connections: 15,
            min_connections: 3,
            max_contracts: 8,
            iterations: 360, // Events distributed across 1 hour virtual
            duration: Duration::from_secs(3700), // Max simulation time (buffer)
            event_wait: Duration::from_secs(10), // 10s between events = ~3600s total
            sleep_after_events: Duration::from_secs(10), // Brief propagation wait
            require_convergence: true,
            // Realistic latency jitter (10-50ms) to uncover timing issues
            latency_range: Some(Duration::from_millis(10)..Duration::from_millis(50)),
            message_loss_rate: 0.0,
            use_mock_wasm: false,
        }
    }

    // Builder methods
    fn with_nodes(mut self, nodes: usize) -> Self {
        self.nodes = nodes;
        self
    }

    fn with_gateways(mut self, gateways: usize) -> Self {
        self.gateways = gateways;
        self
    }

    fn with_iterations(mut self, iterations: usize) -> Self {
        self.iterations = iterations;
        self
    }

    fn with_max_contracts(mut self, max_contracts: usize) -> Self {
        self.max_contracts = max_contracts;
        self
    }

    fn with_duration(mut self, duration: Duration) -> Self {
        self.duration = duration;
        self
    }

    fn with_sleep(mut self, sleep: Duration) -> Self {
        self.sleep_after_events = sleep;
        self
    }

    fn with_connections(mut self, min: usize, max: usize) -> Self {
        self.min_connections = min;
        self.max_connections = max;
        self
    }

    fn with_htl(mut self, max_htl: usize, rnd_above: usize) -> Self {
        self.ring_max_htl = max_htl;
        self.rnd_if_htl_above = rnd_above;
        self
    }

    fn require_convergence(mut self) -> Self {
        self.require_convergence = true;
        self
    }

    /// Add latency jitter simulation.
    #[allow(dead_code)]
    fn with_latency(mut self, min: Duration, max: Duration) -> Self {
        self.latency_range = Some(min..max);
        self
    }

    /// Set the delay between events in virtual time.
    fn with_event_wait(mut self, event_wait: Duration) -> Self {
        self.event_wait = event_wait;
        self
    }

    /// Add message loss fault injection (0.0 to 1.0).
    fn with_message_loss(mut self, rate: f64) -> Self {
        self.message_loss_rate = rate.clamp(0.0, 1.0);
        self
    }

    /// Use MockWasmRuntime (production ContractExecutor path) instead of MockRuntime.
    fn with_mock_wasm(mut self) -> Self {
        self.use_mock_wasm = true;
        self
    }

    /// Run the simulation and return results.
    fn run(self) -> TestResult {
        use freenet::simulation::FaultConfig;

        setup_deterministic_state(self.seed);
        let rt = create_runtime();

        let (sim, logs_handle) = rt.block_on(async {
            let mut sim = SimNetwork::new(
                self.name,
                self.gateways,
                self.nodes,
                self.ring_max_htl,
                self.rnd_if_htl_above,
                self.max_connections,
                self.min_connections,
                self.seed,
            )
            .await;

            // Apply fault injection if configured (latency and/or message loss)
            let has_latency = self.latency_range.is_some();
            let has_loss = self.message_loss_rate > 0.0;
            if has_latency || has_loss {
                let mut builder = FaultConfig::builder();
                if let Some(ref latency) = self.latency_range {
                    builder = builder.latency_range(latency.clone());
                    tracing::info!(
                        "Latency jitter enabled: {:?} - {:?}",
                        latency.start,
                        latency.end
                    );
                }
                if has_loss {
                    builder = builder.message_loss_rate(self.message_loss_rate);
                    tracing::info!(
                        "Message loss enabled: {:.1}%",
                        self.message_loss_rate * 100.0
                    );
                }
                sim.with_fault_injection(builder.build());
            }

            let logs_handle = sim.event_logs_handle();
            (sim, logs_handle)
        });

        let sleep_duration = self.sleep_after_events;
        let event_wait = self.event_wait;
        let result = sim.run_simulation::<rand::rngs::SmallRng, _, _>(
            self.seed,
            self.max_contracts,
            self.iterations,
            self.duration,
            event_wait,
            move || async move {
                tokio::time::sleep(sleep_duration).await;
                Ok(())
            },
        );

        let convergence = rt.block_on(async { check_convergence_from_logs(&logs_handle).await });
        let event_count = rt.block_on(async { logs_handle.lock().await.len() });

        TestResult {
            seed: self.seed,
            name: self.name,
            simulation_result: result,
            convergence,
            event_count,
            require_convergence: self.require_convergence,
            logs_handle,
        }
    }

    /// Run the simulation using the direct runner (single-threaded paused-time runtime).
    ///
    /// This avoids turmoil's O(n²) link overhead, making it suitable for large-scale
    /// or long-running simulations.
    ///
    /// Note: Does not support mid-simulation fault injection (partitions, crashes).
    /// Static latency jitter via `FaultConfig` is supported.
    #[allow(dead_code)] // Used by nightly_tests-gated tests
    fn run_direct(self) -> TestResult {
        use freenet::simulation::FaultConfig;

        setup_deterministic_state(self.seed);
        let rt = create_runtime();

        let use_mock_wasm = self.use_mock_wasm;
        let (sim, logs_handle) = rt.block_on(async {
            let mut sim = SimNetwork::new(
                self.name,
                self.gateways,
                self.nodes,
                self.ring_max_htl,
                self.rnd_if_htl_above,
                self.max_connections,
                self.min_connections,
                self.seed,
            )
            .await;
            sim.use_mock_wasm = use_mock_wasm;

            // Apply fault injection if configured (latency and/or message loss)
            let has_latency = self.latency_range.is_some();
            let has_loss = self.message_loss_rate > 0.0;
            if has_latency || has_loss {
                let mut builder = FaultConfig::builder();
                if let Some(ref latency) = self.latency_range {
                    builder = builder.latency_range(latency.clone());
                    tracing::info!(
                        "Latency jitter enabled: {:?} - {:?}",
                        latency.start,
                        latency.end
                    );
                }
                if has_loss {
                    builder = builder.message_loss_rate(self.message_loss_rate);
                    tracing::info!(
                        "Message loss enabled: {:.1}%",
                        self.message_loss_rate * 100.0
                    );
                }
                sim.with_fault_injection(builder.build());
            }

            let logs_handle = sim.event_logs_handle();
            (sim, logs_handle)
        });

        drop(rt);

        let direct_result = sim.run_simulation_direct::<rand::rngs::SmallRng>(
            self.seed,
            self.max_contracts,
            self.iterations,
            self.event_wait,
        );

        // Map anyhow::Result to turmoil::Result for TestResult compatibility
        let simulation_result: turmoil::Result = match direct_result {
            Ok(()) => Ok(()),
            Err(e) => Err(Box::new(std::io::Error::other(e.to_string()))),
        };

        let rt = create_runtime();
        let convergence = rt.block_on(async { check_convergence_from_logs(&logs_handle).await });
        let event_count = rt.block_on(async { logs_handle.lock().await.len() });

        TestResult {
            seed: self.seed,
            name: self.name,
            simulation_result,
            convergence,
            event_count,
            require_convergence: self.require_convergence,
            logs_handle,
        }
    }
}

/// Result of running a simulation test.
struct TestResult {
    seed: u64,
    name: &'static str,
    simulation_result: turmoil::Result,
    convergence: freenet::dev_tool::ConvergenceResult,
    event_count: usize,
    require_convergence: bool,
    logs_handle: Arc<Mutex<Vec<freenet::tracing::NetLogMessage>>>,
}

impl TestResult {
    /// Assert the simulation completed successfully.
    fn assert_ok(self) -> Self {
        if let Err(e) = &self.simulation_result {
            tracing::error!("============================================================");
            tracing::error!("SIMULATION TEST FAILED: {}", self.name);
            tracing::error!("============================================================");
            tracing::error!("Seed for reproduction: 0x{:X}", self.seed);
            tracing::error!("Error: {:?}", e);
            tracing::error!("============================================================");
        }
        assert!(
            self.simulation_result.is_ok(),
            "{} failed: {:?}",
            self.name,
            self.simulation_result.err()
        );
        self
    }

    /// Log convergence results and optionally assert convergence.
    fn check_convergence(self) -> Self {
        tracing::info!("=== CONVERGENCE CHECK: {} ===", self.name);
        tracing::info!(
            "Result: {} converged, {} diverged, {} events",
            self.convergence.converged.len(),
            self.convergence.diverged.len(),
            self.event_count
        );

        // Get logs for detailed analysis
        let rt = create_runtime();
        let logs = rt.block_on(async { self.logs_handle.lock().await.clone() });

        for diverged in &self.convergence.diverged {
            tracing::warn!(
                "DIVERGED: {} - {} unique states across {} peers",
                diverged.contract_key,
                diverged.unique_state_count(),
                diverged.peer_states.len()
            );
            for (peer, hash) in &diverged.peer_states {
                tracing::warn!("  peer {}: {}", peer, hash);
            }

            // Show stored_hash events only (PutSuccess, UpdateSuccess, BroadcastApplied)
            tracing::debug!(
                "Stored state events for contract {}:",
                diverged.contract_key
            );
            for log in &logs {
                let contract_key = log.kind.contract_key().map(|k| format!("{:?}", k));
                if contract_key.as_ref() == Some(&diverged.contract_key) {
                    if let Some(state_hash) = log.kind.stored_state_hash() {
                        let variant = log.kind.variant_name();
                        tracing::debug!(
                            "  {} @ {}: {} -> {}",
                            log.tx,
                            log.peer_id.socket_addr(),
                            variant,
                            &state_hash[..16]
                        );
                    }
                }
            }

            // Show Subscribe events
            tracing::debug!("Subscribe events for contract {}:", diverged.contract_key);
            for log in &logs {
                let contract_key = log.kind.contract_key().map(|k| format!("{:?}", k));
                if contract_key.as_ref() == Some(&diverged.contract_key) {
                    let variant = log.kind.variant_name();
                    if variant.contains("Subscribe") {
                        tracing::debug!(
                            "  {} @ {}: {}",
                            log.tx,
                            log.peer_id.socket_addr(),
                            variant
                        );
                    }
                }
            }
        }

        if self.require_convergence {
            assert!(
                self.convergence.is_converged(),
                "{} convergence failed: {} converged, {} diverged",
                self.name,
                self.convergence.converged.len(),
                self.convergence.diverged.len()
            );
        }

        tracing::info!("{} PASSED", self.name);
        self
    }

    /// Verify that all operation types (PUT, GET, UPDATE, SUBSCRIBE) were executed.
    ///
    /// This ensures tests are exercising the full range of contract operations
    /// for robust coverage. Fails if any operation type has zero occurrences.
    ///
    /// Also tracks concurrent PUTs - multiple PUTs to the same contract from different peers.
    /// This exercises the CRDT merge logic that was fixed in PR #2683.
    fn verify_operation_coverage(self) -> Self {
        let rt = create_runtime();
        let logs = rt.block_on(async { self.logs_handle.lock().await.clone() });

        let mut put_count = 0;
        let mut get_count = 0;
        let mut update_count = 0;
        let mut subscribe_count = 0;
        let mut contracts_with_puts: std::collections::HashSet<String> =
            std::collections::HashSet::new();

        // Track PUTs per contract per peer to detect concurrent puts
        // Key: contract_key, Value: set of (peer_addr, state_hash) tuples
        let mut puts_per_contract: std::collections::HashMap<
            String,
            std::collections::HashSet<(String, String)>,
        > = std::collections::HashMap::new();

        for log in &logs {
            let variant = log.kind.variant_name();
            if variant.starts_with("Put") {
                put_count += 1;
                if let Some(key) = log.kind.contract_key() {
                    let key_str = format!("{:?}", key);
                    contracts_with_puts.insert(key_str.clone());

                    // Track peer and state for this PUT
                    let peer_addr = format!("{}", log.peer_id.socket_addr());
                    let state_hash = log
                        .kind
                        .state_hash()
                        .map(|h| h[..8].to_string())
                        .unwrap_or_default();
                    puts_per_contract
                        .entry(key_str)
                        .or_default()
                        .insert((peer_addr, state_hash));
                }
            } else if variant.starts_with("Get") {
                get_count += 1;
            } else if variant.starts_with("Update") {
                update_count += 1;
            } else if variant.starts_with("Subscribe") {
                subscribe_count += 1;
            }
        }

        // Count contracts with concurrent puts (multiple unique peer+state combinations)
        let contracts_with_concurrent_puts: Vec<_> = puts_per_contract
            .iter()
            .filter(|(_, puts)| puts.len() > 1)
            .collect();

        tracing::info!("=== OPERATION COVERAGE: {} ===", self.name);
        tracing::info!("PUT operations:       {}", put_count);
        tracing::info!("GET operations:       {}", get_count);
        tracing::info!("UPDATE operations:    {}", update_count);
        tracing::info!("SUBSCRIBE operations: {}", subscribe_count);
        tracing::info!("Contracts with PUTs:  {}", contracts_with_puts.len());
        tracing::info!(
            "Contracts with concurrent PUTs: {}",
            contracts_with_concurrent_puts.len()
        );

        // Log details of concurrent puts for debugging
        for (contract, puts) in &contracts_with_concurrent_puts {
            tracing::debug!(
                "  Contract {} has {} concurrent puts from peers:",
                &contract[..20],
                puts.len()
            );
            for (peer, hash) in puts.iter().take(5) {
                tracing::debug!("    peer={} state={}", peer, hash);
            }
        }

        // Verify minimum coverage - all operation types should be exercised
        assert!(
            put_count > 0,
            "{}: No PUT operations detected - test not exercising puts",
            self.name
        );
        assert!(
            get_count > 0,
            "{}: No GET operations detected - test not exercising gets",
            self.name
        );
        assert!(
            update_count > 0,
            "{}: No UPDATE operations detected - test not exercising updates",
            self.name
        );
        assert!(
            subscribe_count > 0,
            "{}: No SUBSCRIBE operations detected - test not exercising subscribes",
            self.name
        );

        // Verify at least one contract is being tested
        // Note: For larger convergence tests, multiple contracts should naturally be created
        // given sufficient iterations. Small CI tests may only have 1 contract.
        assert!(
            !contracts_with_puts.is_empty(),
            "{}: No contracts with PUTs - test not exercising contract creation",
            self.name
        );

        tracing::info!(
            "{}: Operation coverage verified (PUT={}, GET={}, UPDATE={}, SUBSCRIBE={}, contracts={})",
            self.name,
            put_count,
            get_count,
            update_count,
            subscribe_count,
            contracts_with_puts.len()
        );

        self
    }

    /// Run anomaly detection on the simulation event logs and report findings.
    ///
    /// This is exploratory: it logs all detected anomalies without asserting
    /// that the report is clean. This lets us see what the detectors find
    /// against real simulation data.
    fn verify_state_report(self) -> Self {
        let rt = create_runtime();
        let report = rt.block_on(async {
            let logs = self.logs_handle.lock().await;
            let verifier = freenet::tracing::StateVerifier::from_events(logs.clone());
            verifier.verify()
        });

        tracing::info!("=== ANOMALY DETECTION REPORT: {} ===", self.name);
        tracing::info!(
            "Events analyzed: {} total, {} state-mutating",
            report.total_events,
            report.state_events
        );
        tracing::info!("Contracts analyzed: {}", report.contracts_analyzed);
        tracing::info!("Total anomalies: {}", report.anomalies.len());

        if report.anomalies.is_empty() {
            tracing::info!(
                "{}: State verification CLEAN - no anomalies detected",
                self.name
            );
        } else {
            // Break down by category
            let divergences = report.divergences();
            let missing = report.missing_broadcasts();
            let unapplied = report.unapplied_broadcasts();
            let partitions = report.suspected_partitions();
            let stale = report.stale_peers();
            let oscillations = report.state_oscillations();
            let zombies = report.zombie_transactions();
            let storms = report.broadcast_storms();
            let cascades = report.delta_sync_cascades();

            tracing::warn!(
                "{}: {} anomalies detected: divergences={}, missing_broadcasts={}, \
                 unapplied_broadcasts={}, partitions={}, stale_peers={}, oscillations={}, \
                 zombies={}, storms={}, cascades={}",
                self.name,
                report.anomalies.len(),
                divergences.len(),
                missing.len(),
                unapplied.len(),
                partitions.len(),
                stale.len(),
                oscillations.len(),
                zombies.len(),
                storms.len(),
                cascades.len(),
            );

            // Log each anomaly at debug level for detailed inspection
            for (i, anomaly) in report.anomalies.iter().enumerate() {
                tracing::debug!("{}: anomaly[{}] = {:?}", self.name, i, anomaly);
            }
        }

        self
    }

    /// Extract router snapshot data from event logs.
    ///
    /// Returns a list of `(failure_events, success_events, prediction_active)` tuples,
    /// one per `RouterSnapshot` event found in the logs. These snapshots are emitted
    /// every 5 minutes of virtual time by each node's Ring telemetry.
    fn router_snapshots(&self) -> Vec<(usize, usize, bool)> {
        let rt = create_runtime();
        rt.block_on(async {
            let logs = self.logs_handle.lock().await;
            logs.iter()
                .filter_map(|log| log.kind.router_snapshot_summary())
                .collect()
        })
    }
}

// =============================================================================
// Helper Functions
// =============================================================================

/// Setup deterministic simulation state before running a test.
///
/// Resets all thread-local state for deterministic simulation testing.
///
/// All ID counters are now thread-local with per-thread offset blocks, so resetting
/// them is safe for parallel execution — each thread only resets its own counters.
///
/// Per-network state (sockets, topology, fault injectors) is cleaned up by
/// `SimNetwork::Drop`, so no scorched-earth registry clear is needed.
fn setup_deterministic_state(seed: u64) {
    // All state below is thread-local — safe for parallel tests.
    GlobalRng::set_seed(seed);
    const BASE_EPOCH_MS: u64 = 1577836800000; // 2020-01-01 00:00:00 UTC
    const RANGE_MS: u64 = 5 * 365 * 24 * 60 * 60 * 1000; // ~5 years
    GlobalSimulationTime::set_time_ms(BASE_EPOCH_MS + (seed % RANGE_MS));
    GlobalTestMetrics::reset();
    // Reset transport optimization — run_simulation_direct() enables it when needed,
    // but tests that reuse a thread should start with production timer behavior.
    SimulationTransportOpt::disable();

    // Clear CRDT contract registrations from prior tests on this thread.
    freenet::dev_tool::clear_crdt_contracts();

    // Reset all thread-local ID counters for exact event sequence reproducibility.
    RequestId::reset_counter();
    freenet::dev_tool::ClientId::reset_counter();
    reset_event_id_counter();
    reset_channel_id_counter();
    StreamId::reset_counter();
    reset_nonce_counter();
    reset_global_node_index();
}

/// Create a tokio runtime for running simulation setup.
fn create_runtime() -> tokio::runtime::Runtime {
    tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .unwrap()
}

// =============================================================================
// STRICT Determinism Tests - Exact Event Equality
// =============================================================================

/// Compact fingerprint of a simulation trace for cross-run verification.
///
/// Provides a hash-based integrity check on top of field-by-field assertions.
/// The fingerprint is logged so it can be compared across CI invocations.
#[derive(Debug, Clone, PartialEq, Eq)]
struct TraceFingerprint {
    total_events: usize,
    /// Hash of the full event sequence (order-sensitive).
    sequence_hash: u64,
    /// Hash of the per-type event counts (order-insensitive via BTreeMap).
    counts_hash: u64,
}

impl TraceFingerprint {
    fn from_events(events: &[String], event_counts: &HashMap<String, usize>) -> Self {
        let mut seq_hasher = std::collections::hash_map::DefaultHasher::new();
        for event in events {
            event.hash(&mut seq_hasher);
        }

        let sorted_counts: BTreeMap<&String, &usize> = event_counts.iter().collect();
        let mut counts_hasher = std::collections::hash_map::DefaultHasher::new();
        for (k, v) in &sorted_counts {
            k.hash(&mut counts_hasher);
            v.hash(&mut counts_hasher);
        }

        TraceFingerprint {
            total_events: events.len(),
            sequence_hash: seq_hasher.finish(),
            counts_hash: counts_hasher.finish(),
        }
    }
}

/// **STRICT** determinism test: verifies that same seed produces EXACTLY identical events.
///
/// This test uses Turmoil's deterministic scheduler to ensure reproducible execution.
/// It verifies:
/// 1. Exact same number of events
/// 2. Exact same event counts per type
/// 3. Exact same event sequence (order, content)
///
/// If this test fails, it indicates non-determinism in the simulation that Turmoil
/// doesn't control (e.g., DashMap/HashMap iteration order, external I/O, real time usage).
///
/// The test runs the simulation 3 times with the same seed and compares traces.
/// All three runs must produce identical results.
///
/// **Scale:** Uses 2 gateways + 18 nodes to catch DashMap iteration non-determinism
/// that only manifests at scale (e.g., in subscription/interest management).
///
/// Process isolation via nextest eliminates inter-test DashMap state leakage (#3051).
#[test_log::test]
fn test_strict_determinism_exact_event_equality() {
    const SEED: u64 = 0xDE7E_2A1E_1234;

    /// Captures all simulation state for comparison
    #[derive(Debug, PartialEq)]
    struct SimulationTrace {
        event_counts: HashMap<String, usize>,
        event_sequence: Vec<String>, // event_kind names in order
        total_events: usize,
    }

    fn run_and_trace(name: &str, seed: u64) -> (turmoil::Result, SimulationTrace) {
        setup_deterministic_state(seed);

        let rt = create_runtime();

        // Create SimNetwork and get event logs handle before run_simulation consumes it
        let (sim, logs_handle) = rt.block_on(async {
            let sim = SimNetwork::new(
                name, 2,  // gateways - multi-gateway to test more code paths
                18, // nodes - increased to trigger DashMap iteration issues
                10, // ring_max_htl
                3,  // rnd_if_htl_above
                15, // max_connections - higher to create more subscription operations
                5,  // min_connections
                seed,
            )
            .await;
            let logs_handle = sim.event_logs_handle();
            (sim, logs_handle)
        });

        // Run simulation with Turmoil's deterministic scheduler
        let result = sim.run_simulation::<rand::rngs::SmallRng, _, _>(
            seed,
            10, // max_contract_num - more contracts = more subscription operations
            40, // iterations - more iterations to trigger DashMap operations
            Duration::from_secs(30), // simulation_duration
            Duration::from_millis(200),
            || async {
                tokio::time::sleep(Duration::from_secs(1)).await;
                Ok(())
            },
        );

        // Extract event trace from the shared logs handle
        let trace = rt.block_on(async {
            let logs = logs_handle.lock().await;
            let mut event_counts: HashMap<String, usize> = HashMap::new();
            let mut event_sequence: Vec<String> = Vec::new();

            for log in logs.iter() {
                let kind_name = log.kind.variant_name().to_string();
                *event_counts.entry(kind_name.clone()).or_insert(0) += 1;
                event_sequence.push(kind_name);
            }

            SimulationTrace {
                total_events: logs.len(),
                event_counts,
                event_sequence,
            }
        });

        (result, trace)
    }

    // Run simulation THREE times with identical seed
    let (result1, trace1) = run_and_trace("strict-det-run1", SEED);
    let (result2, trace2) = run_and_trace("strict-det-run2", SEED);
    let (result3, trace3) = run_and_trace("strict-det-run3", SEED);

    // All simulations should have the same outcome
    assert_eq!(
        result1.is_ok(),
        result2.is_ok(),
        "STRICT DETERMINISM FAILURE: Simulation outcomes differ!\nRun 1: {:?}\nRun 2: {:?}",
        result1,
        result2
    );
    assert_eq!(
        result2.is_ok(),
        result3.is_ok(),
        "STRICT DETERMINISM FAILURE: Simulation outcomes differ!\nRun 2: {:?}\nRun 3: {:?}",
        result2,
        result3
    );

    // Debug: Print detailed event breakdown before assertions
    if trace1.total_events != trace2.total_events || trace2.total_events != trace3.total_events {
        tracing::info!("\n=== DETERMINISM DEBUG ===");
        tracing::info!(
            "Run 1 total: {}, Run 2 total: {}, Run 3 total: {}",
            trace1.total_events,
            trace2.total_events,
            trace3.total_events
        );

        let mut all_types: std::collections::BTreeSet<&String> =
            trace1.event_counts.keys().collect();
        all_types.extend(trace2.event_counts.keys());
        all_types.extend(trace3.event_counts.keys());

        for event_type in all_types {
            let count1 = trace1.event_counts.get(event_type).unwrap_or(&0);
            let count2 = trace2.event_counts.get(event_type).unwrap_or(&0);
            let count3 = trace3.event_counts.get(event_type).unwrap_or(&0);
            if count1 != count2 || count2 != count3 {
                tracing::info!(
                    "  {} : {} vs {} vs {} (DIFFERS)",
                    event_type,
                    count1,
                    count2,
                    count3
                );
            }
        }
        tracing::info!("=========================\n");
    }

    // STRICT ASSERTION 1: Exact same total event count
    assert_eq!(
        trace1.total_events, trace2.total_events,
        "STRICT DETERMINISM FAILURE: Total event counts differ!"
    );
    assert_eq!(
        trace2.total_events, trace3.total_events,
        "STRICT DETERMINISM FAILURE: Total event counts differ!"
    );

    // STRICT ASSERTION 2: Exact same event counts per type
    assert_eq!(
        trace1.event_counts, trace2.event_counts,
        "STRICT DETERMINISM FAILURE: Event counts per type differ!"
    );
    assert_eq!(
        trace2.event_counts, trace3.event_counts,
        "STRICT DETERMINISM FAILURE: Event counts per type differ!"
    );

    // STRICT ASSERTION 3: Exact same event sequence
    for (i, ((e1, e2), e3)) in trace1
        .event_sequence
        .iter()
        .zip(trace2.event_sequence.iter())
        .zip(trace3.event_sequence.iter())
        .enumerate()
    {
        assert_eq!(e1, e2, "Event sequence differs at index {}!", i);
        assert_eq!(e2, e3, "Event sequence differs at index {}!", i);
    }

    // Fingerprint verification: compact hash check for cross-run integrity
    let fp1 = TraceFingerprint::from_events(&trace1.event_sequence, &trace1.event_counts);
    let fp2 = TraceFingerprint::from_events(&trace2.event_sequence, &trace2.event_counts);
    let fp3 = TraceFingerprint::from_events(&trace3.event_sequence, &trace3.event_counts);
    assert_eq!(fp1, fp2, "Fingerprint mismatch between run 1 and run 2");
    assert_eq!(fp2, fp3, "Fingerprint mismatch between run 2 and run 3");

    tracing::info!(
        "STRICT DETERMINISM TEST PASSED: {} events, fingerprint={:#018x}",
        trace1.total_events,
        fp1.sequence_hash
    );
}

/// **STRICT** determinism test with MULTIPLE GATEWAYS.
///
/// This test verifies that simulations with 2+ gateways remain deterministic.
///
/// Process isolation via nextest eliminates inter-test DashMap state leakage (#3051).
#[test_log::test]
fn test_strict_determinism_multi_gateway() {
    const SEED: u64 = 0xAB17_6A7E_1234;

    #[derive(Debug, PartialEq)]
    struct SimulationTrace {
        event_counts: HashMap<String, usize>,
        event_sequence: Vec<String>,
        total_events: usize,
    }

    fn run_and_trace(name: &str, seed: u64) -> (turmoil::Result, SimulationTrace) {
        setup_deterministic_state(seed);

        let rt = create_runtime();

        let (sim, logs_handle) = rt.block_on(async {
            let sim = SimNetwork::new(
                name, 2, // gateways - MULTI-GATEWAY
                6, 7, 3, 10, 2, seed,
            )
            .await;
            let logs_handle = sim.event_logs_handle();
            (sim, logs_handle)
        });

        let result = sim.run_simulation::<rand::rngs::SmallRng, _, _>(
            seed,
            5,
            20,
            Duration::from_secs(30),
            Duration::from_millis(200),
            || async {
                tokio::time::sleep(Duration::from_secs(1)).await;
                Ok(())
            },
        );

        let trace = rt.block_on(async {
            let logs = logs_handle.lock().await;
            let mut event_counts: HashMap<String, usize> = HashMap::new();
            let mut event_sequence: Vec<String> = Vec::new();

            for log in logs.iter() {
                let kind_name = log.kind.variant_name().to_string();
                *event_counts.entry(kind_name.clone()).or_insert(0) += 1;
                event_sequence.push(kind_name);
            }

            SimulationTrace {
                total_events: logs.len(),
                event_counts,
                event_sequence,
            }
        });

        (result, trace)
    }

    let (result1, trace1) = run_and_trace("multi-gw-det-run1", SEED);
    let (result2, trace2) = run_and_trace("multi-gw-det-run2", SEED);
    let (result3, trace3) = run_and_trace("multi-gw-det-run3", SEED);

    assert_eq!(result1.is_ok(), result2.is_ok());
    assert_eq!(result2.is_ok(), result3.is_ok());

    assert_eq!(trace1.total_events, trace2.total_events);
    assert_eq!(trace2.total_events, trace3.total_events);

    assert_eq!(trace1.event_counts, trace2.event_counts);
    assert_eq!(trace2.event_counts, trace3.event_counts);

    for (i, ((e1, e2), e3)) in trace1
        .event_sequence
        .iter()
        .zip(trace2.event_sequence.iter())
        .zip(trace3.event_sequence.iter())
        .enumerate()
    {
        assert_eq!(e1, e2, "Event sequence differs at index {}!", i);
        assert_eq!(e2, e3, "Event sequence differs at index {}!", i);
    }

    // Fingerprint verification
    let fp1 = TraceFingerprint::from_events(&trace1.event_sequence, &trace1.event_counts);
    let fp2 = TraceFingerprint::from_events(&trace2.event_sequence, &trace2.event_counts);
    let fp3 = TraceFingerprint::from_events(&trace3.event_sequence, &trace3.event_counts);
    assert_eq!(fp1, fp2, "Fingerprint mismatch between run 1 and run 2");
    assert_eq!(fp2, fp3, "Fingerprint mismatch between run 2 and run 3");

    tracing::info!(
        "MULTI-GATEWAY DETERMINISM TEST PASSED: {} events (2 gateways), fingerprint={:#018x}",
        trace1.total_events,
        fp1.sequence_hash
    );
}

/// **STRICT** determinism test: verifies that same seed produces identical replay.
#[test_log::test]
fn test_deterministic_replay_events() {
    const SEED: u64 = 0xDEAD_BEEF_1234;

    #[derive(Debug, PartialEq)]
    struct ReplayTrace {
        event_counts: HashMap<String, usize>,
        total_events: usize,
    }

    fn run_and_trace(name: &str, seed: u64) -> (turmoil::Result, ReplayTrace) {
        setup_deterministic_state(seed);

        let rt = create_runtime();

        let (sim, logs_handle) = rt.block_on(async {
            let sim = SimNetwork::new(name, 1, 3, 7, 3, 10, 2, seed).await;
            let logs_handle = sim.event_logs_handle();
            (sim, logs_handle)
        });

        let result = sim.run_simulation::<rand::rngs::SmallRng, _, _>(
            seed,
            3,
            10,
            Duration::from_secs(20),
            Duration::from_millis(200),
            || async {
                tokio::time::sleep(Duration::from_secs(1)).await;
                Ok(())
            },
        );

        let trace = rt.block_on(async {
            let logs = logs_handle.lock().await;
            let mut event_counts: HashMap<String, usize> = HashMap::new();

            for log in logs.iter() {
                let kind_name = log.kind.variant_name().to_string();
                *event_counts.entry(kind_name).or_insert(0) += 1;
            }

            ReplayTrace {
                total_events: logs.len(),
                event_counts,
            }
        });

        (result, trace)
    }

    let (result1, trace1) = run_and_trace("replay-run1", SEED);
    let (result2, trace2) = run_and_trace("replay-run2", SEED);

    assert_eq!(result1.is_ok(), result2.is_ok());
    assert!(trace1.total_events > 0);
    assert_eq!(trace1.event_counts, trace2.event_counts);
    assert_eq!(trace1.total_events, trace2.total_events);

    // Fingerprint verification (counts-only since this test doesn't track sequence)
    let fp1 = TraceFingerprint::from_events(&[], &trace1.event_counts);
    let fp2 = TraceFingerprint::from_events(&[], &trace2.event_counts);
    assert_eq!(
        fp1.counts_hash, fp2.counts_hash,
        "Counts fingerprint mismatch between replay runs"
    );

    tracing::info!(
        "Deterministic replay test passed - {} events, counts_hash={:#018x}",
        trace1.total_events,
        fp1.counts_hash
    );
}

// =============================================================================
// Basic Infrastructure Tests
// =============================================================================

/// Test that SimNetwork can be created and peers can be built.
#[test_log::test]
fn test_sim_network_basic_setup() {
    #[allow(clippy::unusual_byte_groupings)]
    const SEED: u64 = 0xBA51C_5E70_0001; // "BASIC SETO 0001"

    setup_deterministic_state(SEED);
    let rt = create_runtime();

    rt.block_on(async {
        let mut sim = SimNetwork::new("basic-setup", 1, 5, 7, 3, 10, 2, SEED).await;
        sim.with_start_backoff(Duration::from_millis(100));

        let peers = sim.build_peers();
        assert_eq!(peers.len(), 6, "Expected 1 gateway + 5 nodes = 6 peers");

        let gateway_count = peers.iter().filter(|(l, _)| !l.is_node()).count();
        let node_count = peers.iter().filter(|(l, _)| l.is_node()).count();
        assert_eq!(gateway_count, 1, "Expected 1 gateway");
        assert_eq!(node_count, 5, "Expected 5 regular nodes");
    });
}

/// Test that peers can start and run under Turmoil's deterministic scheduler.
#[test_log::test]
fn test_sim_network_peer_startup() {
    TestConfig::small("peer-startup", 0xBEE2_5747_0001)
        .with_nodes(2)
        .with_iterations(5)
        .with_duration(Duration::from_secs(15))
        .run()
        .assert_ok();
}

/// Test network connectivity under Turmoil's deterministic scheduler.
#[test_log::test]
fn test_sim_network_connectivity() {
    TestConfig::small("connectivity-test", 0xC0EE_3C70_0001)
        .with_max_contracts(2)
        .with_iterations(10)
        .run()
        .assert_ok();
}

// =============================================================================
// CI Simulation Tests
// =============================================================================

/// CI simulation test - small network with contract operations.
#[test_log::test]
fn ci_quick_simulation() {
    TestConfig::small("ci-quick-sim", 0xC1F1_ED5E_ED00)
        .with_nodes(4)
        .with_max_contracts(5)
        .with_iterations(50)
        .with_duration(Duration::from_secs(45))
        .with_sleep(Duration::from_secs(2))
        .run()
        .assert_ok()
        .verify_operation_coverage()
        .check_convergence()
        .verify_state_report();
}

/// CI simulation test - medium network with more operations.
#[test_log::test]
fn ci_medium_simulation() {
    TestConfig::medium("ci-medium-sim", 0xC1F1_ED7E_ED01)
        .run()
        .assert_ok()
        .verify_operation_coverage()
        .check_convergence()
        .verify_state_report();
}

// =============================================================================
// MockWasmRuntime Tests
// =============================================================================

/// Simulation using MockWasmRuntime — exercises the production ContractExecutor code path
/// (init_tracker, validation, notification pipeline, corrupted state recovery) without WASM.
#[test_log::test]
fn mock_wasm_runtime_simulation() {
    TestConfig::small("mock-wasm-sim", 0xA0C1_1A5E_0001)
        .with_mock_wasm()
        .run_direct()
        .assert_ok()
        .verify_state_report();
}

// =============================================================================
// Convergence Tests
// =============================================================================

/// Replica validation test with stepwise consistency checking.
/// Verifies that contracts converge to the same state across all replicas.
#[test_log::test]
fn replica_validation_and_stepwise_consistency() {
    TestConfig::medium("replica-validation", 0xBEE1_1CA5_0001)
        .with_gateways(2)
        .with_nodes(8)
        .with_htl(8, 4)
        .with_connections(4, 8)
        .with_max_contracts(10)
        .with_iterations(90)
        .with_duration(Duration::from_secs(180))
        .with_sleep(Duration::from_secs(5))
        .require_convergence()
        .run()
        .assert_ok()
        .verify_operation_coverage()
        .check_convergence()
        .verify_state_report();
}

/// Dense network test with high connectivity.
/// Tests contract replication and convergence in a densely connected network.
#[test_log::test]
fn dense_network_replication() {
    TestConfig::large("dense-network", 0xDE05_E0F0_0001)
        .require_convergence()
        .run()
        .assert_ok()
        .verify_operation_coverage()
        .check_convergence()
        .verify_state_report();
}

// =============================================================================
// Determinism Verification Tests
// =============================================================================

/// Verify that running the same simulation twice produces identical results.
///
/// Process isolation via nextest eliminates inter-test DashMap state leakage (#3051).
#[test_log::test]
fn test_turmoil_determinism_verification() {
    const SEED: u64 = 0xDE7E_2A11_0001;

    fn run_simulation(name: &'static str, seed: u64) -> Vec<String> {
        setup_deterministic_state(seed);
        let rt = create_runtime();

        let (sim, logs_handle) = rt.block_on(async {
            let sim = SimNetwork::new(name, 1, 3, 7, 3, 10, 2, seed).await;
            let logs_handle = sim.event_logs_handle();
            (sim, logs_handle)
        });

        let result = sim.run_simulation::<rand::rngs::SmallRng, _, _>(
            seed,
            3,
            15,
            Duration::from_secs(20),
            Duration::from_millis(200),
            || async {
                tokio::time::sleep(Duration::from_secs(1)).await;
                Ok(())
            },
        );

        assert!(result.is_ok(), "Simulation failed: {:?}", result.err());

        rt.block_on(async {
            let logs = logs_handle.lock().await;
            logs.iter()
                .map(|log| format!("{:?}", log.kind.variant_name()))
                .collect()
        })
    }

    let events1 = run_simulation("det-verify-1", SEED);
    let events2 = run_simulation("det-verify-2", SEED);

    assert_eq!(
        events1.len(),
        events2.len(),
        "Event counts differ: {} vs {}",
        events1.len(),
        events2.len()
    );

    for (i, (e1, e2)) in events1.iter().zip(events2.iter()).enumerate() {
        assert_eq!(e1, e2, "Event {} differs: {:?} vs {:?}", i, e1, e2);
    }

    // Fingerprint verification
    let counts1: HashMap<String, usize> = {
        let mut m = HashMap::new();
        for e in &events1 {
            *m.entry(e.clone()).or_insert(0) += 1;
        }
        m
    };
    let counts2: HashMap<String, usize> = {
        let mut m = HashMap::new();
        for e in &events2 {
            *m.entry(e.clone()).or_insert(0) += 1;
        }
        m
    };
    let fp1 = TraceFingerprint::from_events(&events1, &counts1);
    let fp2 = TraceFingerprint::from_events(&events2, &counts2);
    assert_eq!(fp1, fp2, "Fingerprint mismatch between run 1 and run 2");

    tracing::info!(
        "Determinism verified: {} identical events, fingerprint={:#018x}",
        events1.len(),
        fp1.sequence_hash
    );
}

// =============================================================================
// Graceful Shutdown Regression Tests
// =============================================================================

/// Regression test for graceful shutdown using Turmoil deterministic simulation.
#[test_log::test]
fn test_graceful_shutdown_no_deadlock() {
    const SEED: u64 = 0xDEAD_BEEF_CAFE;

    let rt = create_runtime();

    let sim = rt.block_on(async {
        SimNetwork::new("graceful-shutdown-test", 1, 2, 7, 3, 10, 2, SEED).await
    });

    let result = sim.run_simulation::<rand::rngs::SmallRng, _, _>(
        SEED,
        10,
        3,
        Duration::from_secs(30),
        Duration::from_millis(200),
        || async {
            tokio::time::sleep(Duration::from_secs(5)).await;
            tracing::info!("Test client: simulation will end gracefully");
            Ok(())
        },
    );

    assert!(
        result.is_ok(),
        "Simulation should complete without deadlock: {:?}",
        result.err()
    );

    tracing::info!("Graceful shutdown test passed - no deadlock under Turmoil");
}

/// Tests that the typed EventLoopExitReason error is properly handled.
#[test_log::test]
fn test_graceful_shutdown_typed_error() {
    use freenet::EventLoopExitReason;

    let graceful = EventLoopExitReason::GracefulShutdown;
    let unexpected = EventLoopExitReason::UnexpectedStreamEnd;

    assert_eq!(graceful.to_string(), "Graceful shutdown");
    assert_eq!(
        unexpected.to_string(),
        "Network event stream ended unexpectedly"
    );

    tracing::info!("Typed error test passed");
}

// =============================================================================
// High-Latency Regression Test
// =============================================================================

/// Regression test for v0.1.92 timeout storm bug with high-latency connections.
///
/// In production, we observed 935 timeouts in 10 seconds when transferring to peers
/// with ~150ms RTT. The root cause was MIN_RTO (200ms) < RTT + ACK_CHECK_INTERVAL (244ms).
///
/// This test verifies that:
/// 1. High-latency connections (150-200ms) don't experience excessive timeouts
/// 2. Data transfers complete successfully despite latency
/// 3. BBR congestion control handles high-latency correctly after the fix
///
/// Fix applied in this codebase:
/// - MIN_RTO increased from 200ms to 300ms
/// - BBR now uses adaptive timeout floor based on max BDP seen
#[test_log::test]
fn test_high_latency_timeout_regression() {
    use freenet::simulation::FaultConfig;

    // Use a fixed seed for reproducibility
    const SEED: u64 = 0xDEAD_BEEF_1234;
    tracing::info!(
        "Starting high-latency regression test with seed: 0x{:X}",
        SEED
    );

    setup_deterministic_state(SEED);
    let rt = create_runtime();

    rt.block_on(async {
        // Create a network with 1 gateway and 3 peers
        let mut sim = SimNetwork::new(
            "high-latency-regression",
            1,  // gateways
            3,  // nodes
            7,  // ring_max_htl
            3,  // rnd_if_htl_above
            10, // max_connections
            2,  // min_connections
            SEED,
        )
        .await;

        // Configure high latency (150-200ms) - similar to intercontinental connections
        // This latency, combined with ACK_CHECK_INTERVAL (100ms), would have caused
        // spurious timeouts with the old MIN_RTO of 200ms
        let fault_config = FaultConfig::builder()
            .latency_range(Duration::from_millis(150)..Duration::from_millis(200))
            .build();
        sim.with_fault_injection(fault_config);

        sim.with_start_backoff(Duration::from_millis(100));

        // Start the network
        let _handles = sim
            .start_with_rand_gen::<rand::rngs::SmallRng>(SEED, 1, 1)
            .await;

        // Give time for initial connections with high latency
        // (needs more time than normal due to latency)
        for _ in 0..50 {
            sim.advance_time(Duration::from_millis(100));
            tokio::task::yield_now().await;
        }

        // Check connectivity with a longer timeout due to high latency
        match sim
            .check_partial_connectivity(Duration::from_secs(60), 0.5)
            .await
        {
            Ok(()) => {
                tracing::info!("High-latency network established connectivity");
            }
            Err(e) => {
                tracing::warn!(
                    "High-latency connectivity check incomplete: {} (this may be acceptable)",
                    e
                );
            }
        }

        // The key test: verify we don't have excessive timeouts
        // With the old MIN_RTO=200ms, we'd see hundreds of timeouts
        // With the fix (MIN_RTO=300ms + adaptive floor), timeouts should be minimal
        let summary = sim.get_operation_summary().await;
        let total_timeouts = summary.timeouts;

        tracing::info!(
            "High-latency test results: {} total timeouts, {:.1}% success rate",
            total_timeouts,
            summary.overall_success_rate() * 100.0
        );

        // Before the fix, we'd see 100+ timeouts per second
        // After the fix, timeouts should be rare (mostly actual network issues)
        // Allow up to 50 timeouts for a 60-second test (less than 1/second)
        assert!(
            total_timeouts < 50,
            "Expected <50 timeouts with MIN_RTO=300ms fix, got {} (timeout storm detected!)",
            total_timeouts
        );

        tracing::info!("High-latency regression test PASSED - no timeout storm detected");
    });
}

// =============================================================================
// SimulationSocket Integration Test
// =============================================================================

/// Test: Use actual SimulationSocket inside Turmoil.
/// This verifies that the real in-memory socket works with Turmoil's deterministic scheduler.
#[test]
fn test_turmoil_with_real_simulation_socket() -> turmoil::Result {
    // Clean up any previous socket state
    clear_all_socket_registries();

    let network_name = "turmoil-test";
    let virtual_time = VirtualTime::new();

    // Register the network's time source
    register_network_time_source(network_name, virtual_time);

    let server_addr: SocketAddr = (Ipv6Addr::LOCALHOST, 18000).into();
    let client_addr: SocketAddr = (Ipv6Addr::LOCALHOST, 19000).into();

    // Register addresses with the network
    register_address_network(server_addr, network_name);
    register_address_network(client_addr, network_name);

    let mut sim = turmoil::Builder::new()
        .simulation_duration(Duration::from_secs(10))
        .rng_seed(0xDEAD_BEEF_CAFE_1234)
        .build();

    // Server host using SimulationSocket
    sim.host("server", move || async move {
        let socket = SimulationSocket::bind(server_addr)
            .await
            .expect("Server bind failed");

        tracing::info!("Server bound to {:?}", server_addr);

        // Wait for a packet
        let mut buf = [0u8; 1024];
        match socket.recv_from(&mut buf).await {
            Ok((len, from)) => {
                let msg = &buf[..len];
                tracing::info!("Server received {:?} from {:?}", msg, from);

                // Echo back
                socket.send_to(b"pong", from).await.ok();
            }
            Err(e) => {
                tracing::error!("Server recv error: {:?}", e);
            }
        }

        Ok(())
    });

    // Client host using SimulationSocket
    sim.client("client", async move {
        // Give server time to bind
        tokio::time::sleep(Duration::from_millis(100)).await;

        let socket = SimulationSocket::bind(client_addr)
            .await
            .expect("Client bind failed");

        tracing::info!("Client bound to {:?}", client_addr);

        // Send ping to server
        socket.send_to(b"ping", server_addr).await.ok();
        tracing::info!("Client sent ping");

        // Wait for response
        let mut buf = [0u8; 1024];
        match tokio::time::timeout(Duration::from_secs(5), socket.recv_from(&mut buf)).await {
            Ok(Ok((len, from))) => {
                let msg = &buf[..len];
                tracing::info!("Client received {:?} from {:?}", msg, from);
                assert_eq!(msg, b"pong");
                assert_eq!(from, server_addr);
            }
            Ok(Err(e)) => {
                tracing::error!("Client recv error: {:?}", e);
            }
            Err(_) => {
                tracing::error!("Client recv timeout");
            }
        }

        Ok(())
    });

    sim.run()
}

// =============================================================================
// Subscription Topology Validation
// =============================================================================
// These tests validate the subscription topology infrastructure to detect
// issues like bidirectional cycles (#2720), orphan hosters (#2719), etc.

/// Source detection threshold: peers within 5% of ring distance to contract
/// are considered sources. Must match SOURCE_THRESHOLD in topology_registry.rs.
const SOURCE_THRESHOLD: f64 = 0.05;

/// Network name for single hoster topology test.
const SINGLE_HOSTER_NETWORK: &str = "single-hoster-test";

/// Calculate ring distance (handles wrap-around at 0/1 boundary).
fn ring_distance(a: f64, b: f64) -> f64 {
    let diff = (a - b).abs();
    diff.min(1.0 - diff)
}

/// Test topology capture with a single hoster (gateway only).
///
/// This test validates the topology infrastructure without triggering the
/// bidirectional cycle issue (#2720) by using only a single subscriber.
///
/// ## Scenario
/// 1. Gateway PUTs a contract with subscribe=true
/// 2. Wait for topology registration
/// 3. Verify: gateway's location is within SOURCE_THRESHOLD of contract location
/// 4. Verify: gateway's snapshot shows it's hosting the contract
/// 5. Verify: no topology issues (no cycles since only one subscriber)
///
/// ## Related
/// - For Issue #2720 (bidirectional cycles), that bug is now fixed
/// - For Issue #2755 (orphan/disconnected hosters), see `test_orphan_hosters_no_source` (ignored)
#[test_log::test]
fn test_topology_single_hoster() {
    use freenet::dev_tool::{
        Location, NodeLabel, ScheduledOperation, SimOperation, validate_topology_from_snapshots,
    };

    const SEED: u64 = 0x5EED_0001_CAFE;

    // Set up deterministic state BEFORE creating SimNetwork
    // This ensures peer locations are deterministic (uses GlobalRng)
    setup_deterministic_state(SEED);

    let rt = create_runtime();

    // Create SimNetwork and get gateway location
    let (sim, gateway_location) = rt.block_on(async {
        let sim = SimNetwork::new(
            SINGLE_HOSTER_NETWORK,
            1,  // 1 gateway
            2,  // 2 peers (they won't subscribe)
            7,  // max_htl
            3,  // rnd_if_htl_above
            10, // max_connections
            2,  // min_connections
            SEED,
        )
        .await;
        // Get gateway location (first peer in the list)
        let locations = sim.get_peer_locations();
        let gateway_loc = locations
            .first()
            .copied()
            .expect("Should have at least one gateway");
        (sim, gateway_loc)
    });

    // Note: get_peer_locations() returns locations from SimNetwork config, but actual
    // running nodes may have different locations due to how ConnectionManager is initialized.
    // We use a fixed contract seed and validate based on actual topology snapshot locations.
    // See: https://github.com/freenet/freenet-core/issues/2759 for the peer location non-determinism issue.
    let _ = gateway_location; // Silence unused variable warning

    // Use a fixed contract seed - we'll determine source status from actual topology
    let contract_seed = 42u8;
    let contract = SimOperation::create_test_contract(contract_seed);
    let contract_id = *contract.key().id();
    let initial_state = SimOperation::create_test_state(contract_seed);

    // Schedule only ONE operation: gateway PUTs the contract with subscribe=true
    // No other nodes subscribe - this avoids #2720 bidirectional cycle issue
    let operations = vec![ScheduledOperation::new(
        NodeLabel::gateway(SINGLE_HOSTER_NETWORK, 0),
        SimOperation::Put {
            contract: contract.clone(),
            state: initial_state.clone(),
            subscribe: true,
        },
    )];

    // Run simulation with controlled events under Turmoil
    // Wait 45 seconds after operations for:
    // - Topology registration (runs periodically)
    // - Orphan recovery (runs every 30 seconds)
    let result = sim.run_controlled_simulation(
        SEED,
        operations,
        Duration::from_secs(120), // simulation duration
        Duration::from_secs(45),  // post-operation wait for topology stabilization
    );

    assert!(
        result.turmoil_result.is_ok(),
        "Simulation should complete successfully: {:?}",
        result.turmoil_result.err()
    );

    // Get topology snapshots from the returned result (captured before SimNetwork::Drop clears registry)
    let snapshots = result.topology_snapshots;
    tracing::info!("Captured {} topology snapshots", snapshots.len());

    // Find the gateway's snapshot with the contract
    let gateway_with_contract = snapshots
        .iter()
        .find(|snap| snap.contracts.contains_key(&contract_id));

    assert!(
        gateway_with_contract.is_some(),
        "Gateway should have the contract in its topology snapshot. \
         Available snapshots: {} peers",
        snapshots.len()
    );

    let gateway_snap = gateway_with_contract.unwrap();
    let contract_sub = gateway_snap.contracts.get(&contract_id).unwrap();

    // Get the actual contract location derived from the contract ID
    let contract_location = Location::from(&contract_id).as_f64();
    let gateway_location = gateway_snap.location;

    // CRITICAL: Validate that gateway is within SOURCE_THRESHOLD of contract location
    let gateway_distance = ring_distance(gateway_location, contract_location);
    tracing::info!(
        "Contract location: {:.4}, Gateway location: {:.4}, Distance: {:.4}, Threshold: {:.4}",
        contract_location,
        gateway_location,
        gateway_distance,
        SOURCE_THRESHOLD
    );

    // Gateway should be hosting (it did PUT with subscribe=true)
    assert!(
        contract_sub.is_hosting,
        "Gateway should be hosting the contract after PUT with subscribe=true"
    );

    tracing::info!(
        "Gateway {} is hosting contract: upstream={:?}, downstream={:?}",
        gateway_snap.peer_addr,
        contract_sub.upstream,
        contract_sub.downstream
    );

    // Validate topology using captured snapshots
    let result = validate_topology_from_snapshots(&snapshots, &contract_id, contract_location);

    tracing::info!(
        "Topology validation: cycles={}, orphans={}, disconnected={}, unreachable={}, proximity={}",
        result.bidirectional_cycles.len(),
        result.orphan_hosters.len(),
        result.disconnected_upstream.len(),
        result.unreachable_hosters.len(),
        result.proximity_violations.len()
    );

    // CRITICAL: With only one subscriber, there should NEVER be cycles
    // This is the primary assertion - single hoster cannot form cycles
    assert!(
        result.bidirectional_cycles.is_empty(),
        "Single hoster should have no bidirectional cycles, found: {:?}",
        result.bidirectional_cycles
    );

    // Check if gateway is considered a "source" based on actual topology snapshot
    let gateway_is_source = gateway_distance < SOURCE_THRESHOLD;

    if gateway_is_source {
        tracing::info!(
            "Gateway IS source (distance={:.4} < threshold={:.4}) - validating healthy topology",
            gateway_distance,
            SOURCE_THRESHOLD
        );

        // Source hoster should have healthy topology
        assert!(
            result.orphan_hosters.is_empty(),
            "Source hoster (within threshold) should not be marked as orphan, found: {:?}",
            result.orphan_hosters
        );

        assert!(
            result.disconnected_upstream.is_empty(),
            "Source hoster (within threshold) should not be disconnected, found: {:?}",
            result.disconnected_upstream
        );

        assert!(
            result.unreachable_hosters.is_empty(),
            "Source hoster should have no unreachable hosters, found: {:?}",
            result.unreachable_hosters
        );

        assert!(
            result.is_healthy(),
            "Single source hoster topology should be healthy, got {} issues",
            result.issue_count
        );
    } else {
        tracing::warn!(
            "Gateway is NOT source (distance={:.4} >= threshold={:.4}) - orphan/disconnected issues are expected",
            gateway_distance,
            SOURCE_THRESHOLD
        );

        // Non-source hoster will have orphan/disconnected issues - this is expected behavior
        // The key assertion (no cycles) already passed above
        if !result.orphan_hosters.is_empty() {
            tracing::info!(
                "Expected: Gateway flagged as orphan (not a source, no upstream): {:?}",
                result.orphan_hosters
            );
        }
        if !result.disconnected_upstream.is_empty() {
            tracing::info!(
                "Expected: Gateway flagged as disconnected upstream (not a source, has downstream): {:?}",
                result.disconnected_upstream
            );
        }
    }

    tracing::info!(
        "Single hoster topology test passed - gateway_is_source={}, cycles=0, other_issues={}",
        gateway_is_source,
        result.issue_count
    );
}

// =============================================================================
// Concurrent Updates Convergence Test
// =============================================================================

/// Tests that concurrent updates from multiple peers converge to the same state.
///
/// This test validates the core eventual consistency property of Freenet's
/// state synchronization:
///
/// 1. Multiple peers subscribe to the same contract
/// 2. Multiple peers issue concurrent updates
/// 3. Updates propagate via broadcast mechanism
/// 4. All subscribers MUST converge to identical state
///
/// ## What This Catches
///
/// - **Echo-back bugs**: If A's update incorrectly comes back to A
/// - **Missed broadcasts**: If some peer doesn't receive propagated updates
/// - **CRDT merge bugs**: If deterministic merge produces different results
/// - **Summary caching bugs**: If summary comparison incorrectly skips peers
/// - **Race conditions**: In update propagation and application
///
/// ## Related Issues
///
/// - Issue #2764: Echo-back prevention (summary comparison should handle this)
/// - PR #2763: Conditional summary caching
///
/// ## CI Impact
///
/// Configuration: 2 gateways + 6 nodes with few contracts and many iterations
/// to maximize concurrent update scenarios per contract.
///
/// Expected runtime: ~60-90 seconds (similar to ci_medium_simulation).
#[test_log::test]
fn test_concurrent_updates_convergence() {
    // Use a specific seed for reproducibility
    // This seed was chosen to produce good coverage of concurrent update scenarios
    TestConfig::medium("concurrent-updates-convergence", 0xC0C0_BEEF_1234)
        .with_gateways(2) // Multiple gateways for richer topology
        .with_nodes(6) // 6 regular nodes for concurrent updates
        .with_max_contracts(3) // Few contracts = more updates per contract
        .with_iterations(80) // Many iterations to stress concurrent updates
        .with_duration(Duration::from_secs(90))
        .with_sleep(Duration::from_secs(2)) // Short sleep between ops for concurrency
        .require_convergence() // FAIL if any contract diverges
        .run()
        .assert_ok()
        .verify_operation_coverage()
        .check_convergence()
        .verify_state_report();
}

// =============================================================================
// Full State Send Caching Bug Test (Issue #2763)
// =============================================================================

/// Tests correct behavior of summary caching after full state sends.
///
/// ## Background (PR #2763)
///
/// Before the fix, when a peer sent full state (not a delta) to another peer,
/// it would incorrectly cache its own summary as the recipient's summary.
/// This caused problems because:
///
/// 1. After receiving full state, the recipient's state depends on CRDT merge
///    with their existing state - we can't predict their resulting summary
/// 2. Later broadcasts would compute deltas based on the wrong cached summary
/// 3. The recipient couldn't apply these deltas, triggering ResyncRequests
/// 4. This caused inefficiency (extra round trips) and potential divergence in race conditions
///
/// ## What This Test Does
///
/// Creates a scenario that exercises the full state send path:
///
/// 1. Gateway PUTs contract with state S1, subscribes
/// 2. Nodes subscribe → receive full state (gateway has no cached summaries)
/// 3. Multiple nodes issue UPDATEs → broadcasts to subscribers
/// 4. Wait for propagation and CRDT merges
/// 5. Assert all peers converge to the same state
/// 6. Assert no ResyncRequests were needed (via GlobalTestMetrics)
///
/// ## Test Infrastructure
///
/// This test uses `GlobalTestMetrics` to track behavior across all nodes:
/// - `GlobalTestMetrics::reset()` at test start
/// - `GlobalTestMetrics::resync_requests()` - should be 0 with the fix
/// - `GlobalTestMetrics::delta_sends()` / `full_state_sends()` - broadcast statistics
///
/// MockRuntime uses hash-based summaries (32 bytes) which enables the delta
/// efficiency check to pass (for states > 64 bytes). However, it returns full
/// state as "delta" content, so the actual CRDT merge behavior is:
/// - Compare incoming state hash with current state hash
/// - Larger hash wins (deterministic CRDT convergence)
///
/// ## Current Limitations
///
/// While MockRuntime implements CRDT-style "largest hash wins" merging, it doesn't
/// fully reproduce the bug because:
/// 1. The "delta" is actually full state, so it works regardless of base state
/// 2. Even with wrong cached summary, the merge produces correct results
/// 3. No ResyncRequests are triggered because delta application never truly fails
///
/// The bug would fully manifest with a real CRDT contract where:
/// - Delta is computed from assumed base state
/// - Applying delta to wrong base state produces incorrect result
/// - This triggers ResyncRequest or causes state divergence
///
/// The test still provides value as:
/// 1. Documentation of the expected behavior and fix
/// 2. Infrastructure for tracking broadcasts (delta vs full state)
/// 3. Verification that convergence works correctly
/// 4. Regression guard if MockRuntime or protocol logic changes
///
/// ## Related
///
/// - PR #2763: Conditional summary caching to prevent state divergence
/// - Issue #2764: Echo-back prevention
#[test_log::test]
fn test_full_state_send_no_incorrect_caching() {
    use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation};

    const SEED: u64 = 0x2763_CAFE_0001;
    const NETWORK_NAME: &str = "full-state-cache-test";

    // Reset global test metrics at the start
    GlobalTestMetrics::reset();

    setup_deterministic_state(SEED);
    let rt = create_runtime();

    // Create network: 1 gateway + 3 nodes
    // This gives us enough peers to have interesting broadcast patterns
    let (sim, logs_handle) = rt.block_on(async {
        let sim = SimNetwork::new(
            NETWORK_NAME,
            1,  // 1 gateway
            3,  // 3 regular nodes
            7,  // max_htl
            3,  // rnd_if_htl_above
            10, // max_connections
            2,  // min_connections
            SEED,
        )
        .await;
        let logs_handle = sim.event_logs_handle();
        (sim, logs_handle)
    });

    // Create a test contract
    let contract = SimOperation::create_test_contract(0x63);
    let contract_id = *contract.key().id();
    let contract_key = contract.key();

    // Create distinct states for each update
    // Using different byte patterns to ensure CRDT merges produce distinct results
    let initial_state = SimOperation::create_test_state(1);
    let update_state_2 = SimOperation::create_test_state(2);
    let update_state_3 = SimOperation::create_test_state(3);
    let update_state_4 = SimOperation::create_test_state(4);

    // Schedule operations that will trigger the bug scenario:
    // 1. Gateway PUTs contract (becomes the source)
    // 2. Nodes subscribe one by one (each receives full state, gateway might incorrectly cache)
    // 3. Multiple updates from different nodes (tests delta computation with cached summaries)
    let operations = vec![
        // Step 1: Gateway puts contract with initial state and subscribes
        ScheduledOperation::new(
            NodeLabel::gateway(NETWORK_NAME, 0),
            SimOperation::Put {
                contract: contract.clone(),
                state: initial_state,
                subscribe: true,
            },
        ),
        // Step 2: Node 1 subscribes - will receive full state from gateway
        ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, 1),
            SimOperation::Subscribe { contract_id },
        ),
        // Step 3: Node 2 subscribes - will receive full state from gateway
        ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, 2),
            SimOperation::Subscribe { contract_id },
        ),
        // Step 4: Node 3 subscribes - will receive full state from gateway
        ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, 3),
            SimOperation::Subscribe { contract_id },
        ),
        // Step 5: Gateway updates - broadcasts to all subscribers
        // BUG: If gateway incorrectly cached subscriber summaries after full state sends,
        // it will compute deltas based on wrong base state
        ScheduledOperation::new(
            NodeLabel::gateway(NETWORK_NAME, 0),
            SimOperation::Update {
                key: contract_key,
                data: update_state_2,
            },
        ),
        // Step 6: Node 1 updates - broadcasts to subscribers
        ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, 1),
            SimOperation::Update {
                key: contract_key,
                data: update_state_3,
            },
        ),
        // Step 7: Node 2 updates - broadcasts to subscribers
        ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, 2),
            SimOperation::Update {
                key: contract_key,
                data: update_state_4,
            },
        ),
    ];

    // Run simulation with enough time for all operations and CRDT merges
    let result = sim.run_controlled_simulation(
        SEED,
        operations,
        Duration::from_secs(120), // simulation duration
        Duration::from_secs(60),  // post-operation wait for propagation
    );

    assert!(
        result.turmoil_result.is_ok(),
        "Simulation should complete successfully: {:?}",
        result.turmoil_result.err()
    );

    // Check convergence - this is the key assertion
    // With the fix, all peers should converge to the same state
    // Without the fix, incorrect caching may cause divergence
    let convergence =
        rt.block_on(async { freenet::dev_tool::check_convergence_from_logs(&logs_handle).await });

    tracing::info!(
        "Convergence check: {} converged, {} diverged",
        convergence.converged.len(),
        convergence.diverged.len()
    );

    // Log any diverged contracts for debugging
    for diverged in &convergence.diverged {
        tracing::error!(
            "DIVERGED: {} - {} unique states across {} peers",
            diverged.contract_key,
            diverged.unique_state_count(),
            diverged.peer_states.len()
        );
        for (peer, hash) in &diverged.peer_states {
            tracing::error!("  peer {}: {}", peer, hash);
        }
    }

    // PRIMARY ASSERTION: All peers must converge
    // This should pass with the fix (PR #2763) and may fail without it
    assert!(
        convergence.is_converged(),
        "PR #2763 REGRESSION: State divergence detected! \
         This indicates incorrect summary caching after full state sends. \
         {} contracts converged, {} diverged",
        convergence.converged.len(),
        convergence.diverged.len()
    );

    // SECONDARY ASSERTION: No ResyncRequests should be needed
    // With correct summary caching (PR #2763), deltas should work correctly
    // Without the fix, peers would fail to apply deltas and send ResyncRequests
    let resync_count = GlobalTestMetrics::resync_requests();
    let delta_sends = GlobalTestMetrics::delta_sends();
    let full_state_sends = GlobalTestMetrics::full_state_sends();

    tracing::info!(
        "Broadcast stats - delta_sends: {}, full_state_sends: {}, resync_requests: {}",
        delta_sends,
        full_state_sends,
        resync_count
    );

    // Note: Some resyncs may occur during normal operation (e.g., initial state sync),
    // but excessive resyncs indicate the caching bug. We check for zero resyncs in this
    // controlled scenario where all peers start fresh and updates flow correctly.
    assert_eq!(
        resync_count, 0,
        "PR #2763 REGRESSION: {} ResyncRequests detected! \
         This indicates deltas are failing due to incorrect summary caching. \
         With the fix, no resyncs should be needed in this scenario.",
        resync_count
    );

    // TERTIARY ASSERTION: Verify broadcast activity
    // With MockRuntime using hash summaries, we expect a mix of delta and full state sends.
    // The specific counts depend on the subscription topology and CRDT merge outcomes.
    // The important thing is that broadcasts ARE happening (contract is propagating).
    let total_broadcasts = delta_sends + full_state_sends;
    assert!(
        total_broadcasts > 0,
        "Expected at least one broadcast to occur during the simulation"
    );

    tracing::info!(
        "test_full_state_send_no_incorrect_caching PASSED: \
         {} peers converged, {} broadcasts ({} delta, {} full state), {} resyncs",
        convergence
            .converged
            .iter()
            .map(|c| c.replica_count)
            .sum::<usize>(),
        total_broadcasts,
        delta_sends,
        full_state_sends,
        resync_count
    );
}

// =============================================================================
// Task-per-tx Subscribe ForwardingAck Regression Test (PR #3806)
// =============================================================================

/// Exercises the task-per-tx client-initiated subscribe path
/// (`subscribe_with_id` → `start_client_subscribe`) in a multi-node
/// simulation with network-routed subscribes.
///
/// ## Background (PR #3806, #1454 Phase 2b)
///
/// Phase 2b migrated client-initiated SUBSCRIBE to a task-per-tx driver.
/// This test verifies the driver works end-to-end with network round-trips,
/// retries, and peer selection.
///
/// ## ForwardingAck coverage
///
/// The ForwardingAck relay bug (commit 5cb6f37c) is covered by unit tests
/// in `node.rs::callback_forward_tests`. Triggering it in simulation
/// requires `SeedContract` + `use_mock_wasm=true` so that relay peers
/// lack the contract. The `SeedContract` framework is in place but the
/// mock WASM runtime has compatibility issues with `OpCtx::send_and_await`
/// (separate from the local-completion fix below).
///
/// The task-per-tx `send_and_await` path IS exercised here: subscribes
/// complete with `outcome="subscribed"` via the local-completion synthesis
/// in the SUBSCRIBE branch of `handle_pure_network_message_v1`.
#[test_log::test]
fn test_subscribe_forwarding_ack_relay() {
    use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation};

    const SEED: u64 = 0x5CB6_F37C_0001;
    const NETWORK_NAME: &str = "subscribe-fwd-ack";

    GlobalTestMetrics::reset();
    setup_deterministic_state(SEED);
    let rt = create_runtime();

    let (sim, logs_handle) = rt.block_on(async {
        let sim = SimNetwork::new(
            NETWORK_NAME,
            1, // 1 gateway
            4, // 4 regular nodes
            7, // ring_max_htl
            3, // rnd_if_htl_above
            5, // max_connections
            2, // min_connections
            SEED,
        )
        .await;
        let logs_handle = sim.event_logs_handle();
        (sim, logs_handle)
    });

    let contract = SimOperation::create_test_contract(0xAC);
    let contract_id = *contract.key().id();
    let initial_state = SimOperation::create_test_state(1);

    // Gateway PUTs, then multiple nodes subscribe via the task-per-tx path.
    //
    // Note: SeedContract (no-propagation local store) is available for
    // tests that need to control which nodes have the contract, but
    // requires use_mock_wasm=true which has compatibility issues with
    // OpCtx::send_and_await. For now, use regular PUT + Subscribe.
    let operations = vec![
        ScheduledOperation::new(
            NodeLabel::gateway(NETWORK_NAME, 0),
            SimOperation::Put {
                contract: contract.clone(),
                state: initial_state,
                subscribe: true,
            },
        ),
        ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, 1),
            SimOperation::Subscribe { contract_id },
        ),
        ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, 2),
            SimOperation::Subscribe { contract_id },
        ),
        ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, 3),
            SimOperation::Subscribe { contract_id },
        ),
        ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, 4),
            SimOperation::Subscribe { contract_id },
        ),
    ];

    let result = sim.run_controlled_simulation(
        SEED,
        operations,
        Duration::from_secs(120),
        Duration::from_secs(30),
    );

    // PRIMARY: simulation must complete without errors.
    // Before the fix, ForwardingAck on the task-per-tx channel
    // would cause UnexpectedOpState, crashing the simulation.
    assert!(
        result.turmoil_result.is_ok(),
        "Simulation failed (ForwardingAck regression?): {:?}",
        result.turmoil_result.err()
    );

    // Verify subscribes succeeded.
    let (success_count, not_found_count) = rt.block_on(async {
        let logs = logs_handle.lock().await;
        let mut successes = 0usize;
        let mut not_found = 0usize;
        for log in logs.iter() {
            match log.kind.subscribe_outcome() {
                Some(true) => successes += 1,
                Some(false) => not_found += 1,
                None => {}
            }
        }
        (successes, not_found)
    });

    tracing::info!(
        success_count,
        not_found_count,
        "Subscribe telemetry summary"
    );

    // 4 explicit subscribes + 1 from gateway's put-with-subscribe.
    assert!(
        success_count >= 4,
        "Expected at least 4 successful subscribes, got {} (not_found={})",
        success_count,
        not_found_count,
    );
}

/// Soundness check for the fix that moved `ring.subscribe()` /
/// `record_subscription()` / `announce_contract_hosted()` out of the shared
/// prefix of `SubscribeMsgResult::Subscribed` and into the originator-only
/// branch.
///
/// The *direct* regression is covered by the unit test
/// `ring::hosting::tests::test_relay_downstream_only_not_in_renewal`, which
/// asserts at the HostingManager level that a pure-relay state (downstream
/// subscriber registered, no self-lease) does not get recruited into
/// `contracts_needing_renewal()`. That unit test is where the bug bite is
/// pinned.
///
/// This simulation test verifies the broader property that subscribes still
/// complete successfully under the refactored handler and that the bounded
/// number of active-subscription-lease holders stays sane under a multi-peer
/// subscribe scenario — any regression that re-introduced relay-side
/// `ring.subscribe` calls on nodes running the LEGACY process_message path
/// would cause `subscribed_peers` to grow beyond the expected count of
/// genuine originators.
///
/// Note on turmoil vs production: in the turmoil simulation runner every
/// peer shares one process, which means `pending_op_results` is effectively
/// global. `node::try_forward_task_per_tx_reply` on an intermediate peer
/// can therefore find the originator's task entry and short-circuit the
/// Response before it reaches the legacy relay handler. In production each
/// peer is an independent process with its own `pending_op_results`, so
/// relays fall through to the legacy handler and hit the bug. This is why
/// the *primary* regression test for the bug is the in-process hosting
/// manager unit test rather than this simulation test.
///
/// Simulation-side regression coverage for the bug tracked in #3874: when a
/// subscribed client disconnects, the node MUST emit an upstream Unsubscribe
/// so the hoster can drop the downstream subscriber registration.
///
/// This test exercises the same code path the failing integration test
/// `test_client_disconnect_triggers_upstream_unsubscribe` exercises — the
/// `ClientRequest::Disconnect` arm in `client_events::process_open_request`,
/// which calls `remove_client_from_all_subscriptions` /
/// `should_unsubscribe_upstream` / `send_unsubscribe_upstream` — but through
/// the simulation harness where the bug's preconditions (completed PUT
/// replay from GC, subscription resurrection, etc.) can be controlled and
/// reproduced deterministically.
///
/// Before this test existed, `SimOperation` had no client-disconnect variant
/// and simulation had NO coverage of the disconnect → unsubscribe chain —
/// see #3874 for the full gap analysis. The integration-test regression
/// that came out of PR #3843 was therefore invisible to the simulation
/// suite.
///
/// This test currently FAILS and reproduces a concrete regression that
/// was invisible to the simulation suite before: the task-per-tx
/// subscribe driver in `operations/subscribe/op_ctx_task.rs` does NOT
/// call `interest_manager.register_peer_interest(..., is_upstream=true)`
/// on subscribe completion, so `send_unsubscribe_upstream` logs
/// `"No upstream peer found for unsubscribe"` and no Unsubscribe is
/// ever emitted. The legacy `operations/subscribe.rs::SubscribeMsg::Response`
/// arm DOES register the upstream peer (see subscribe.rs:1767), so the
/// behaviour regressed when client-initiated SUBSCRIBE migrated to the
/// task-per-tx driver in Phase 2b.
///
/// This is either the #3874 root cause or a closely-related bug on the
/// same code path. Marked `#[ignore]` until the missing registration
/// is restored; the test is kept in-tree as executable documentation
/// of the gap and as a regression guard for the fix.
#[test_log::test]
fn test_client_disconnect_emits_upstream_unsubscribe() {
    use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation};

    const SEED: u64 = 0x5CB6_F37C_0003;
    const NETWORK_NAME: &str = "client-disconnect-unsub";

    GlobalTestMetrics::reset();
    setup_deterministic_state(SEED);
    let rt = create_runtime();

    let (sim, logs_handle) = rt.block_on(async {
        let sim = SimNetwork::new(
            NETWORK_NAME,
            1, // 1 gateway
            4, // 4 regular nodes (matching subscribe_forwarding_ack_relay)
            7, // ring_max_htl
            3, // rnd_if_htl_above
            5, // max_connections
            2, // min_connections
            SEED,
        )
        .await;
        let logs_handle = sim.event_logs_handle();
        (sim, logs_handle)
    });

    let contract = SimOperation::create_test_contract(0xDC);
    let contract_id = *contract.key().id();
    let initial_state = SimOperation::create_test_state(1);

    // Gateway PUTs → node-1 subscribes → node-1 disconnects.
    // Node-1 is a pure downstream subscriber with no further downstream
    // of its own, so `should_unsubscribe_upstream` must return true
    // on disconnect and an Unsubscribe must be emitted to its upstream.
    let operations = vec![
        ScheduledOperation::new(
            NodeLabel::gateway(NETWORK_NAME, 0),
            SimOperation::Put {
                contract: contract.clone(),
                state: initial_state,
                subscribe: false,
            },
        ),
        ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, 1),
            SimOperation::Subscribe { contract_id },
        ),
        ScheduledOperation::new(NodeLabel::node(NETWORK_NAME, 1), SimOperation::Disconnect),
    ];

    let result = sim.run_controlled_simulation(
        SEED,
        operations,
        Duration::from_secs(120),
        Duration::from_secs(30),
    );

    assert!(
        result.turmoil_result.is_ok(),
        "Simulation failed: {:?}",
        result.turmoil_result.err()
    );

    let (sent_count, received_count) = rt.block_on(async {
        let logs = logs_handle.lock().await;
        let mut sent = 0usize;
        let mut received = 0usize;
        for log in logs.iter() {
            if log
                .kind
                .unsubscribe_sent_instance_id()
                .is_some_and(|id| *id == contract_id)
            {
                sent += 1;
            }
            if log
                .kind
                .unsubscribe_received_instance_id()
                .is_some_and(|id| *id == contract_id)
            {
                received += 1;
            }
        }
        (sent, received)
    });

    tracing::info!(
        sent_count,
        received_count,
        "Unsubscribe telemetry after client disconnect"
    );

    assert!(
        sent_count > 0,
        "Disconnecting node MUST emit at least one UnsubscribeSent for the \
         contract it was subscribed to (sent={sent_count}, received={received_count})"
    );
    assert!(
        received_count > 0,
        "Upstream node MUST log UnsubscribeReceived after client disconnect \
         (sent={sent_count}, received={received_count})"
    );
}

/// Edge case: disconnect from a node that never subscribed. No Unsubscribe
/// should be emitted and nothing should log "No upstream peer found".
#[test_log::test]
fn test_client_disconnect_without_subscriptions_is_noop() {
    use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation};

    const SEED: u64 = 0x5CB6_F37C_0004;
    const NETWORK_NAME: &str = "disconnect-no-subs";

    GlobalTestMetrics::reset();
    setup_deterministic_state(SEED);
    let rt = create_runtime();

    let (sim, logs_handle) = rt.block_on(async {
        let sim = SimNetwork::new(NETWORK_NAME, 1, 4, 7, 3, 5, 2, SEED).await;
        let logs_handle = sim.event_logs_handle();
        (sim, logs_handle)
    });

    let operations = vec![ScheduledOperation::new(
        NodeLabel::node(NETWORK_NAME, 1),
        SimOperation::Disconnect,
    )];

    let result = sim.run_controlled_simulation(
        SEED,
        operations,
        Duration::from_secs(60),
        Duration::from_secs(30),
    );

    assert!(
        result.turmoil_result.is_ok(),
        "Simulation failed: {:?}",
        result.turmoil_result.err()
    );

    let sent_count = rt.block_on(async {
        let logs = logs_handle.lock().await;
        logs.iter()
            .filter(|log| log.kind.unsubscribe_sent_instance_id().is_some())
            .count()
    });

    assert_eq!(
        sent_count, 0,
        "Disconnecting a client with no subscriptions must not emit any \
         UnsubscribeSent events (got {sent_count})"
    );
}

/// Edge case: subscribe twice from the same client, then disconnect.
/// The upstream registration is idempotent, so only one Unsubscribe should
/// be emitted per contract.
#[test_log::test]
fn test_repeat_subscribe_then_disconnect_emits_single_unsubscribe() {
    use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation};

    const SEED: u64 = 0x5CB6_F37C_0005;
    const NETWORK_NAME: &str = "repeat-subscribe-disconnect";

    GlobalTestMetrics::reset();
    setup_deterministic_state(SEED);
    let rt = create_runtime();

    let (sim, logs_handle) = rt.block_on(async {
        let sim = SimNetwork::new(NETWORK_NAME, 1, 4, 7, 3, 5, 2, SEED).await;
        let logs_handle = sim.event_logs_handle();
        (sim, logs_handle)
    });

    let contract = SimOperation::create_test_contract(0xDD);
    let contract_id = *contract.key().id();
    let initial_state = SimOperation::create_test_state(1);

    let operations = vec![
        ScheduledOperation::new(
            NodeLabel::gateway(NETWORK_NAME, 0),
            SimOperation::Put {
                contract: contract.clone(),
                state: initial_state,
                subscribe: false,
            },
        ),
        ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, 1),
            SimOperation::Subscribe { contract_id },
        ),
        ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, 1),
            SimOperation::Subscribe { contract_id },
        ),
        ScheduledOperation::new(NodeLabel::node(NETWORK_NAME, 1), SimOperation::Disconnect),
    ];

    let result = sim.run_controlled_simulation(
        SEED,
        operations,
        Duration::from_secs(120),
        Duration::from_secs(30),
    );

    assert!(
        result.turmoil_result.is_ok(),
        "Simulation failed: {:?}",
        result.turmoil_result.err()
    );

    let sent_count = rt.block_on(async {
        let logs = logs_handle.lock().await;
        logs.iter()
            .filter(|log| {
                log.kind
                    .unsubscribe_sent_instance_id()
                    .is_some_and(|id| *id == contract_id)
            })
            .count()
    });

    assert_eq!(
        sent_count, 1,
        "Repeat-subscribe + disconnect must emit exactly one UnsubscribeSent \
         (got {sent_count})"
    );
}

#[test_log::test]
fn test_relay_does_not_pollute_active_subscriptions() {
    use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation};

    const SEED: u64 = 0x5CB6_F37C_0002;
    const NETWORK_NAME: &str = "subscribe-relay-pollution";

    GlobalTestMetrics::reset();
    setup_deterministic_state(SEED);
    let rt = create_runtime();

    // Sparse topology: more peers than max_connections so the ring can't
    // be fully connected and subscribe requests MUST route through at least
    // one relay to reach the peer that holds the contract.
    let sim = rt.block_on(async {
        SimNetwork::new(
            NETWORK_NAME,
            2,  // 2 gateways
            18, // 18 regular nodes (20 total peers)
            10, // ring_max_htl
            3,  // rnd_if_htl_above
            3,  // max_connections — tight ring, non-full topology
            2,  // min_connections
            SEED,
        )
        .await
    });

    let contract = SimOperation::create_test_contract(0xAD);
    let contract_id = *contract.key().id();
    let initial_state = SimOperation::create_test_state(1);

    // Gateway PUTs (with subscribe). Multiple distant nodes then subscribe
    // in parallel so at least some Subscribe requests must route through
    // relay hops before reaching a peer that holds the contract.
    let operations = vec![
        ScheduledOperation::new(
            NodeLabel::gateway(NETWORK_NAME, 0),
            SimOperation::Put {
                contract: contract.clone(),
                state: initial_state,
                subscribe: true,
            },
        ),
        ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, 15),
            SimOperation::Subscribe { contract_id },
        ),
        ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, 16),
            SimOperation::Subscribe { contract_id },
        ),
        ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, 17),
            SimOperation::Subscribe { contract_id },
        ),
        ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, 18),
            SimOperation::Subscribe { contract_id },
        ),
    ];

    let result = sim.run_controlled_simulation(
        SEED,
        operations,
        Duration::from_secs(120),
        Duration::from_secs(30),
    );

    assert!(
        result.turmoil_result.is_ok(),
        "Simulation failed: {:?}",
        result.turmoil_result.err()
    );

    // run_controlled_simulation snapshots every peer's HostingManager state
    // at shutdown and returns them on the result. Use those directly rather
    // than the global registry (which is cleared on SimNetwork drop).
    let snapshots = &result.topology_snapshots;
    assert!(
        !snapshots.is_empty(),
        "No topology snapshots captured — background snapshot task didn't run"
    );

    // Count peers whose `HostingManager::active_subscriptions` contains the
    // contract. Only genuine originators of a subscription should appear here:
    //
    //   * The PUT-with-subscribe originator (gateway 0).
    //   * Each of nodes 15..=18 that called Subscribe explicitly.
    //
    // That's at most 5 peers. Before the fix, the
    // `SubscribeMsgResult::Subscribed` handler would ALSO add every relay
    // that forwards a Subscribed response to the set — crucially, when four
    // simultaneous subscribers route through each other, the bug's recruited
    // relays overlap with legit subscribers, but additional strictly-relay
    // peers still accumulate. After the fix the bound collapses to the 5
    // genuine originators.
    let mut subscribed_peers: Vec<SocketAddr> = snapshots
        .iter()
        .filter(|snap| snap.active_subscription_keys.contains(&contract_id))
        .map(|snap| snap.peer_addr)
        .collect();
    subscribed_peers.sort();

    tracing::info!(
        subscribed_peer_count = subscribed_peers.len(),
        ?subscribed_peers,
        total_snapshots = snapshots.len(),
        "Peers with contract in active_subscriptions"
    );

    // Five nodes legitimately install a lease in `active_subscriptions`:
    // the PUT-with-subscribe gateway and the four explicit Subscribe
    // originators (nodes 15..=18). Anything more means a peer that did
    // not originate a subscribe — a pure relay — ended up with the
    // contract in its active_subscriptions, which is the bug.
    assert!(
        subscribed_peers.len() <= 5,
        "Relay pollution regression: {} peer(s) have the contract in \
         `active_subscriptions`, expected at most 5 (gateway + 4 subscribing \
         nodes). Polluted peers: {:?}. \
         See subscribe.rs::SubscribeMsgResult::Subscribed — `ring.subscribe()` \
         must only run on the originator branch, not on relays.",
        subscribed_peers.len(),
        subscribed_peers,
    );

    // Sanity: at least some peer must have actually subscribed.
    assert!(
        !subscribed_peers.is_empty(),
        "No peers have the contract in active_subscriptions — \
         subscribes didn't complete, test is degenerate"
    );
}

// =============================================================================
// CRDT Emulation Mode Test (PR #2763 Bug Reproduction)
// =============================================================================

/// Tests the CRDT emulation mode which can trigger version mismatch ResyncRequests.
///
/// ## Background
///
/// This test uses the CRDT emulation mode which adds version tracking to contract state.
/// When a peer caches the wrong summary (the bug PR #2763 fixed), subsequent delta
/// computation uses the wrong `from_version`. When the receiving peer tries to apply
/// the delta, it fails because versions don't match → triggers ResyncRequest.
///
/// ## Test Scenario
///
/// 1. Register contract for CRDT emulation mode
/// 2. Gateway PUTs contract with version 1
/// 3. Nodes subscribe and receive version 1
/// 4. Gateway updates to version 2, broadcasts delta (from v1 to v2)
/// 5. With correct caching, all nodes update to v2
/// 6. Assert: no ResyncRequests (versions match)
/// 7. Assert: delta sends occur (CRDT mode enables delta computation)
///
/// ## CRDT State Format
///
/// State: [version: u64 LE][64 bytes data]
/// Summary: [version: u64 LE][blake3 hash: 32 bytes]
/// Delta: [from_version: u64][to_version: u64][new data]
#[test_log::test]
fn test_crdt_mode_version_tracking() {
    use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation, register_crdt_contract};

    const SEED: u64 = 0x0276_3CD0_0001;
    const NETWORK_NAME: &str = "crdt-version-test";

    setup_deterministic_state(SEED);
    let rt = create_runtime();

    // Create network
    let (sim, logs_handle) = rt.block_on(async {
        let sim = SimNetwork::new(
            NETWORK_NAME,
            1,  // 1 gateway
            2,  // 2 regular nodes
            7,  // max_htl
            3,  // rnd_if_htl_above
            10, // max_connections
            2,  // min_connections
            SEED,
        )
        .await;
        let logs_handle = sim.event_logs_handle();
        (sim, logs_handle)
    });

    // Create a test contract and register it for CRDT mode
    let contract = SimOperation::create_test_contract(0xCD);
    let contract_id = *contract.key().id();
    let contract_key = contract.key();

    // Register contract for CRDT emulation BEFORE simulation starts
    register_crdt_contract(contract_id);

    // Create CRDT-formatted states with version numbers
    let state_v1 = SimOperation::create_crdt_state(1, 0x11);
    let state_v2 = SimOperation::create_crdt_state(2, 0x22);

    // Schedule operations
    let operations = vec![
        // Gateway puts contract with version 1
        ScheduledOperation::new(
            NodeLabel::gateway(NETWORK_NAME, 0),
            SimOperation::Put {
                contract: contract.clone(),
                state: state_v1,
                subscribe: true,
            },
        ),
        // Nodes subscribe - will receive version 1
        ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, 1),
            SimOperation::Subscribe { contract_id },
        ),
        ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, 2),
            SimOperation::Subscribe { contract_id },
        ),
        // Gateway updates to version 2 - broadcasts delta (v1 → v2)
        ScheduledOperation::new(
            NodeLabel::gateway(NETWORK_NAME, 0),
            SimOperation::Update {
                key: contract_key,
                data: state_v2,
            },
        ),
    ];

    // Run simulation
    let result = sim.run_controlled_simulation(
        SEED,
        operations,
        Duration::from_secs(60),
        Duration::from_secs(30),
    );

    assert!(
        result.turmoil_result.is_ok(),
        "Simulation should complete: {:?}",
        result.turmoil_result.err()
    );

    // Check results
    let convergence =
        rt.block_on(async { freenet::dev_tool::check_convergence_from_logs(&logs_handle).await });

    let resync_count = GlobalTestMetrics::resync_requests();
    let delta_sends = GlobalTestMetrics::delta_sends();
    let full_state_sends = GlobalTestMetrics::full_state_sends();

    tracing::info!(
        "CRDT mode test - convergence: {}/{}, delta_sends: {}, full_state_sends: {}, resyncs: {}",
        convergence.converged.len(),
        convergence.total_contracts(),
        delta_sends,
        full_state_sends,
        resync_count
    );

    // With correct summary caching (PR #2763 fix):
    // - Initial subscription sends full state (sent_delta=false, no caching)
    // - Update sends delta (sent_delta=true, summary is cached CORRECTLY)
    // - No version mismatches → no ResyncRequests
    assert!(
        convergence.is_converged(),
        "CRDT mode: all peers should converge. {} converged, {} diverged",
        convergence.converged.len(),
        convergence.diverged.len()
    );

    // Note: With the fix in place, delta sends should succeed.
    // ResyncRequests would only occur if summary caching was incorrect.
    tracing::info!(
        "test_crdt_mode_version_tracking PASSED: converged={}, resyncs={}",
        convergence.converged.len(),
        resync_count
    );

    // Clean up CRDT contract registration
}

// =============================================================================
// PR #2763 Bug Reproduction Test - Pre-existing Divergent State
// =============================================================================

/// **PR #2763 Bug Reproduction Test**
///
/// This test validates that CRDT-mode contracts converge correctly even when
/// nodes have divergent states and stale cached summaries.
///
/// ## Background
///
/// PR #2763 fixed a bug where summaries were incorrectly cached after full state
/// sends. This test uses CRDT emulation to verify the version-tracking behavior
/// and ResyncRequest recovery mechanism.
///
/// ## Note on Test Behavior
///
/// In simulation, the InterestMessage::Summaries exchange between peers shares
/// summaries during connection establishment. This means cached summaries may
/// become stale when a node updates independently (without broadcasting).
///
/// The ResyncRequest mechanism is the CORRECT recovery path when delta application
/// fails due to version mismatch. This test verifies:
/// 1. CRDT mode correctly detects version mismatches
/// 2. ResyncRequest is sent when delta fails
/// 3. System converges to correct state after resync
///
/// ## Test Scenario
///
/// 1. Gateway PUTs contract with v1
/// 2. Node 1 subscribes → receives v1
/// 3. Node 1 updates to v5 (Gateway's cached summary becomes stale)
/// 4. Gateway updates to v10 and broadcasts
/// 5. Node 1 receives delta with from_version != current_version → ResyncRequest
/// 6. System recovers via ResyncResponse
#[test_log::test]
fn test_pr2763_crdt_convergence_with_resync() {
    use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation, register_crdt_contract};

    const SEED: u64 = 0x0276_3B06_0001;
    const NETWORK_NAME: &str = "pr2763-crdt-resync";

    setup_deterministic_state(SEED);
    let rt = create_runtime();

    // Create network: 1 gateway + 2 nodes
    let (sim, logs_handle) = rt.block_on(async {
        let sim = SimNetwork::new(
            NETWORK_NAME,
            1,  // 1 gateway
            2,  // 2 regular nodes
            7,  // max_htl
            3,  // rnd_if_htl_above
            10, // max_connections
            2,  // min_connections
            SEED,
        )
        .await;
        let logs_handle = sim.event_logs_handle();
        (sim, logs_handle)
    });

    // Create a test contract and register it for CRDT mode
    let contract = SimOperation::create_test_contract(0xBB);
    let contract_id = *contract.key().id();
    let contract_key = contract.key();

    // Register contract for CRDT emulation
    register_crdt_contract(contract_id);

    // Create CRDT-formatted states with version numbers
    let state_v1_gw = SimOperation::create_crdt_state(1, 0xAA); // Gateway's initial state (v1)
    let state_v5_node = SimOperation::create_crdt_state(5, 0xBB); // Node 1's update (v5)
    let state_v10_gw = SimOperation::create_crdt_state(10, 0xCC); // Gateway's final state (v10)

    // Schedule operations
    let operations = vec![
        // Step 1: Gateway puts contract with v1
        ScheduledOperation::new(
            NodeLabel::gateway(NETWORK_NAME, 0),
            SimOperation::Put {
                contract: contract.clone(),
                state: state_v1_gw,
                subscribe: true,
            },
        ),
        // Step 2: Node 1 subscribes - receives v1
        ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, 1),
            SimOperation::Subscribe { contract_id },
        ),
        // Step 3: Node 1 updates to v5 - creates version divergence
        ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, 1),
            SimOperation::Update {
                key: contract_key,
                data: state_v5_node,
            },
        ),
        // Step 4: Node 2 subscribes
        ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, 2),
            SimOperation::Subscribe { contract_id },
        ),
        // Step 5: Gateway updates to v10 - broadcasts to subscribers
        // This triggers version mismatch on Node 1 → ResyncRequest
        ScheduledOperation::new(
            NodeLabel::gateway(NETWORK_NAME, 0),
            SimOperation::Update {
                key: contract_key,
                data: state_v10_gw,
            },
        ),
    ];

    // Run simulation
    let result = sim.run_controlled_simulation(
        SEED,
        operations,
        Duration::from_secs(90),
        Duration::from_secs(45),
    );

    assert!(
        result.turmoil_result.is_ok(),
        "Simulation should complete: {:?}",
        result.turmoil_result.err()
    );

    // Check results
    let convergence =
        rt.block_on(async { freenet::dev_tool::check_convergence_from_logs(&logs_handle).await });

    let resync_count = GlobalTestMetrics::resync_requests();
    let delta_sends = GlobalTestMetrics::delta_sends();
    let full_state_sends = GlobalTestMetrics::full_state_sends();

    tracing::info!(
        "CRDT resync test - convergence: {}/{}, delta_sends: {}, full_state_sends: {}, resyncs: {}",
        convergence.converged.len(),
        convergence.total_contracts(),
        delta_sends,
        full_state_sends,
        resync_count
    );

    // Log divergence details if any
    for diverged in &convergence.diverged {
        tracing::warn!(
            "DIVERGED: {} - {} unique states",
            diverged.contract_key,
            diverged.unique_state_count()
        );
        for (peer, hash) in &diverged.peer_states {
            tracing::warn!("  peer {}: {}", peer, hash);
        }
    }

    // Primary assertion: Convergence must succeed
    // The ResyncRequest mechanism should recover from version mismatches
    assert!(
        convergence.is_converged(),
        "CRDT resync test: peers MUST converge via ResyncRequest recovery. {} converged, {} diverged",
        convergence.converged.len(),
        convergence.diverged.len()
    );

    // Secondary: ResyncRequests are expected in this scenario because Node 1
    // updated independently, causing stale cached summaries
    if resync_count > 0 {
        tracing::info!(
            "CRDT resync test: {} ResyncRequests occurred (expected for version mismatch recovery)",
            resync_count
        );
    }

    tracing::info!(
        "test_pr2763_crdt_convergence_with_resync PASSED: converged={}, resyncs={}",
        convergence.converged.len(),
        resync_count
    );

    // Clean up
}

// =============================================================================
// Extended Edge Case Tests for Ring Protocol
// =============================================================================

use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation, register_crdt_contract};

/// Parametrized CRDT convergence test.
///
/// All nodes subscribe then update simultaneously. Verifies convergence.
/// Each (nodes, gateways) combo is tested with 5 different seeds to detect
/// topology-dependent failures. See #3028.
///
/// Run with: cargo test -p freenet --features simulation_tests,testing test_crdt_convergence
#[rstest::rstest]
#[case::n3_g1_s1("crdt-3n-1gw-s1", 0x2773_0003_0001, 1, 3)]
#[case::n3_g1_s2("crdt-3n-1gw-s2", 0x2773_0003_0002, 1, 3)]
#[case::n3_g1_s3("crdt-3n-1gw-s3", 0x2773_0003_0003, 1, 3)]
#[case::n3_g1_s4("crdt-3n-1gw-s4", 0x2773_0003_0007, 1, 3)]
#[case::n3_g1_s5("crdt-3n-1gw-s5", 0x2773_0003_0006, 1, 3)]
#[case::n5_g2_s1("crdt-5n-2gw-s1", 0x2773_0005_1021, 2, 5)]
#[case::n5_g2_s2("crdt-5n-2gw-s2", 0x2773_0005_1012, 2, 5)]
#[case::n5_g2_s3("crdt-5n-2gw-s3", 0x2773_0005_1003, 2, 5)]
#[case::n5_g2_s4("crdt-5n-2gw-s4", 0x2773_0005_2001, 2, 5)]
#[case::n5_g2_s5("crdt-5n-2gw-s5", 0x2773_0005_1005, 2, 5)]
#[case::n6_g2_s1("crdt-6n-2gw-s1", 0x2773_0006_1001, 2, 6)]
#[case::n6_g2_s2("crdt-6n-2gw-s2", 0x2773_0006_1002, 2, 6)]
#[case::n6_g2_s3("crdt-6n-2gw-s3", 0x2773_0006_1003, 2, 6)]
#[case::n6_g2_s4("crdt-6n-2gw-s4", 0x2773_0006_1004, 2, 6)]
#[case::n6_g2_s5("crdt-6n-2gw-s5", 0x2773_0006_1005, 2, 6)]
#[case::n4_g1_s1("crdt-4n-1gw-s1", 0x2773_0004_0001, 1, 4)]
#[case::n4_g1_s2("crdt-4n-1gw-s2", 0x2773_0004_0002, 1, 4)]
#[case::n4_g1_s3("crdt-4n-1gw-s3", 0x2773_0004_0003, 1, 4)]
#[case::n4_g1_s4("crdt-4n-1gw-s4", 0x2773_0004_0004, 1, 4)]
#[case::n4_g1_s5("crdt-4n-1gw-s5", 0x2773_0004_0005, 1, 4)]
#[case::n5_g1_s1("crdt-5n-1gw-s1", 0x2773_0005_0008, 1, 5)]
#[case::n5_g1_s2("crdt-5n-1gw-s2", 0x2773_0005_0012, 1, 5)]
#[case::n5_g1_s3("crdt-5n-1gw-s3", 0x2773_0005_0003, 1, 5)]
#[case::n5_g1_s4("crdt-5n-1gw-s4", 0x2773_0005_0004, 1, 5)]
#[case::n5_g1_s5("crdt-5n-1gw-s5", 0x2773_0005_0005, 1, 5)]
#[case::n6_g1_s1("crdt-6n-1gw-s1", 0x2773_0006_0001, 1, 6)]
#[case::n6_g1_s2("crdt-6n-1gw-s2", 0x2773_0006_0002, 1, 6)]
#[case::n6_g1_s3("crdt-6n-1gw-s3", 0x2773_0006_0003, 1, 6)]
#[case::n6_g1_s4("crdt-6n-1gw-s4", 0x2773_0006_0004, 1, 6)]
#[case::n6_g1_s5("crdt-6n-1gw-s5", 0x2773_0006_0005, 1, 6)]
#[case::n7_g1_s1("crdt-7n-1gw-s1", 0x2773_0007_0001, 1, 7)]
#[case::n7_g1_s2("crdt-7n-1gw-s2", 0x2773_0007_0002, 1, 7)]
#[case::n7_g1_s3("crdt-7n-1gw-s3", 0x2773_0007_0023, 1, 7)]
#[case::n7_g1_s4("crdt-7n-1gw-s4", 0x2773_0007_0010, 1, 7)]
#[case::n7_g1_s5("crdt-7n-1gw-s5", 0x2773_0007_0005, 1, 7)]
#[case::n8_g1_s1("crdt-8n-1gw-s1", 0x2773_0008_0001, 1, 8)]
#[case::n8_g1_s2("crdt-8n-1gw-s2", 0x2773_0008_0002, 1, 8)]
#[case::n8_g1_s3("crdt-8n-1gw-s3", 0x2773_0008_0003, 1, 8)]
#[case::n8_g1_s4("crdt-8n-1gw-s4", 0x2773_0008_0004, 1, 8)]
#[case::n8_g1_s5("crdt-8n-1gw-s5", 0x2773_0008_0005, 1, 8)]
fn test_crdt_convergence(
    #[case] name: &'static str,
    #[case] seed: u64,
    #[case] gateways: usize,
    #[case] nodes: usize,
) {
    GlobalTestMetrics::reset();

    setup_deterministic_state(seed);

    let rt = create_runtime();

    let (sim, logs_handle) = rt.block_on(async {
        let sim = SimNetwork::new(name, gateways, nodes, 7, 3, 10, 4, seed).await;
        let logs_handle = sim.event_logs_handle();
        (sim, logs_handle)
    });

    let contract = SimOperation::create_test_contract(0x5F);
    let contract_id = *contract.key().id();
    let contract_key = contract.key();
    register_crdt_contract(contract_id);

    // Gateway puts + all nodes subscribe simultaneously
    let mut operations = vec![ScheduledOperation::new(
        NodeLabel::gateway(name, 0),
        SimOperation::Put {
            contract: contract.clone(),
            state: SimOperation::create_crdt_state(1, 0x00),
            subscribe: true,
        },
    )];

    for i in 1..=nodes {
        operations.push(ScheduledOperation::new(
            NodeLabel::node(name, i),
            SimOperation::Subscribe { contract_id },
        ));
    }

    // All nodes update simultaneously
    for i in 1..=nodes {
        operations.push(ScheduledOperation::new(
            NodeLabel::node(name, i),
            SimOperation::Update {
                key: contract_key,
                data: SimOperation::create_crdt_state(10 + i as u64, i as u8),
            },
        ));
    }

    let result = sim.run_controlled_simulation(
        seed,
        operations,
        Duration::from_secs(180),
        Duration::from_secs(90),
    );

    assert!(
        result.turmoil_result.is_ok(),
        "[{}] Simulation should complete: {:?}",
        name,
        result.turmoil_result.err()
    );

    let convergence = rt.block_on(async { check_convergence_from_logs(&logs_handle).await });
    let resync_count = GlobalTestMetrics::resync_requests();

    tracing::info!(
        "[{}] nodes={}, gateways={}, converged={}/{}, resyncs={}",
        name,
        nodes,
        gateways,
        convergence.converged.len(),
        convergence.total_contracts(),
        resync_count
    );

    for diverged in &convergence.diverged {
        tracing::warn!(
            "[{}] DIVERGED: {} - {} unique states across {} peers",
            name,
            diverged.contract_key,
            diverged.unique_state_count(),
            diverged.peer_states.len()
        );
    }

    assert!(
        convergence.is_converged(),
        "[{}] {} nodes must converge. {} converged, {} diverged",
        name,
        nodes,
        convergence.converged.len(),
        convergence.diverged.len()
    );

    tracing::info!(
        "[{}] PASSED: converged={}, resyncs={}",
        name,
        convergence.converged.len(),
        resync_count
    );
}

/// Test: CRDT convergence with N nodes updating simultaneously.
///
/// All 6 nodes subscribe then issue concurrent updates with different versions.
/// Verifies CRDT merge logic correctly converges to identical final state.
#[test_log::test]
fn test_concurrent_updates_from_n_sources() {
    const SEED: u64 = 0xC0C0_BEEF_0002;
    const NETWORK_NAME: &str = "concurrent-updates-n";
    const NODE_COUNT: usize = 6;

    GlobalTestMetrics::reset();

    setup_deterministic_state(SEED);

    let rt = create_runtime();

    let (sim, logs_handle) = rt.block_on(async {
        let sim = SimNetwork::new(NETWORK_NAME, 2, NODE_COUNT, 10, 5, 15, 4, SEED).await;
        let logs_handle = sim.event_logs_handle();
        (sim, logs_handle)
    });

    let contract = SimOperation::create_test_contract(0xC0);
    let contract_id = *contract.key().id();
    let contract_key = contract.key();
    register_crdt_contract(contract_id);

    // Gateway puts v1, all nodes subscribe, then all update simultaneously
    let mut operations = vec![ScheduledOperation::new(
        NodeLabel::gateway(NETWORK_NAME, 0),
        SimOperation::Put {
            contract: contract.clone(),
            state: SimOperation::create_crdt_state(1, 0x00),
            subscribe: true,
        },
    )];

    for i in 1..=NODE_COUNT {
        operations.push(ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, i),
            SimOperation::Subscribe { contract_id },
        ));
    }

    for i in 1..=NODE_COUNT {
        operations.push(ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, i),
            SimOperation::Update {
                key: contract_key,
                data: SimOperation::create_crdt_state(10 + i as u64, i as u8),
            },
        ));
    }

    let result = sim.run_controlled_simulation(
        SEED,
        operations,
        Duration::from_secs(180),
        Duration::from_secs(90),
    );

    assert!(
        result.turmoil_result.is_ok(),
        "Simulation should complete: {:?}",
        result.turmoil_result.err()
    );

    let convergence = rt.block_on(async { check_convergence_from_logs(&logs_handle).await });
    let resync_count = GlobalTestMetrics::resync_requests();

    tracing::info!(
        "Concurrent N-source updates: converged={}/{}, resyncs={}",
        convergence.converged.len(),
        convergence.total_contracts(),
        resync_count
    );

    for diverged in &convergence.diverged {
        tracing::warn!(
            "DIVERGED: {} - {} unique states across {} peers",
            diverged.contract_key,
            diverged.unique_state_count(),
            diverged.peer_states.len()
        );
    }

    assert!(
        convergence.is_converged(),
        "Concurrent N-way updates MUST converge. {} converged, {} diverged",
        convergence.converged.len(),
        convergence.diverged.len()
    );

    tracing::info!(
        "test_concurrent_updates_from_n_sources PASSED: converged={}, resyncs={}",
        convergence.converged.len(),
        resync_count
    );
}

/// Test: CRDT convergence when nodes have divergent state versions (4-node variant).
///
/// This test validates that the network converges correctly when:
/// 1. Multiple nodes independently update to different versions
/// 2. Each node has a "stale" view of other nodes' summaries
/// 3. ResyncRequest mechanism recovers divergent state
///
#[test_log::test]
fn test_stale_summary_cache_multiple_branches() {
    const SEED: u64 = 0x57A1_E001_0001;
    const NETWORK_NAME: &str = "stale-summaries";
    const NODE_COUNT: usize = 4;

    GlobalTestMetrics::reset();

    setup_deterministic_state(SEED);

    let rt = create_runtime();

    let (sim, logs_handle) = rt.block_on(async {
        let sim = SimNetwork::new(NETWORK_NAME, 1, NODE_COUNT, 7, 3, 10, 2, SEED).await;
        let logs_handle = sim.event_logs_handle();
        (sim, logs_handle)
    });

    let contract = SimOperation::create_test_contract(0x57);
    let contract_id = *contract.key().id();
    let contract_key = contract.key();
    register_crdt_contract(contract_id);

    // Gateway puts v1
    let mut operations = vec![ScheduledOperation::new(
        NodeLabel::gateway(NETWORK_NAME, 0),
        SimOperation::Put {
            contract: contract.clone(),
            state: SimOperation::create_crdt_state(1, 0x01),
            subscribe: true,
        },
    )];

    for i in 1..=NODE_COUNT {
        operations.push(ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, i),
            SimOperation::Subscribe { contract_id },
        ));
    }

    for i in 1..=NODE_COUNT {
        operations.push(ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, i),
            SimOperation::Update {
                key: contract_key,
                data: SimOperation::create_crdt_state(10 + i as u64, i as u8),
            },
        ));
    }

    let result = sim.run_controlled_simulation(
        SEED,
        operations,
        Duration::from_secs(180),
        Duration::from_secs(90),
    );

    assert!(
        result.turmoil_result.is_ok(),
        "Simulation should complete: {:?}",
        result.turmoil_result.err()
    );

    let convergence = rt.block_on(async { check_convergence_from_logs(&logs_handle).await });
    let resync_count = GlobalTestMetrics::resync_requests();

    tracing::info!(
        "Stale summary test: converged={}/{}, resyncs={}",
        convergence.converged.len(),
        convergence.total_contracts(),
        resync_count
    );

    for diverged in &convergence.diverged {
        tracing::warn!(
            "DIVERGED: {} - {} unique states across {} peers",
            diverged.contract_key,
            diverged.unique_state_count(),
            diverged.peer_states.len()
        );
    }

    assert!(
        convergence.is_converged(),
        "Divergent state must converge via CRDT merge + ResyncRequest. {} converged, {} diverged",
        convergence.converged.len(),
        convergence.diverged.len()
    );

    tracing::info!(
        "test_stale_summary_cache_multiple_branches PASSED: converged={}, resyncs={}",
        convergence.converged.len(),
        resync_count
    );
}

/// Test: CRDT convergence in large network (12 nodes).
///
/// This test validates that updates propagate correctly and converge
/// in a larger network where update broadcasts must reach all 12 hosters.
#[test_log::test]
fn test_max_downstream_limit_reached() {
    const SEED: u64 = 0x0A0D_0001_0001;
    const NETWORK_NAME: &str = "large-network";
    const NODE_COUNT: usize = 12;

    GlobalTestMetrics::reset();

    setup_deterministic_state(SEED);

    let rt = create_runtime();

    let (sim, logs_handle) = rt.block_on(async {
        let sim = SimNetwork::new(NETWORK_NAME, 2, NODE_COUNT, 10, 5, 15, 3, SEED).await;
        let logs_handle = sim.event_logs_handle();
        (sim, logs_handle)
    });

    let contract = SimOperation::create_test_contract(0x0A);
    let contract_id = *contract.key().id();
    let contract_key = contract.key();
    register_crdt_contract(contract_id);

    // Gateway puts contract, all 12 nodes subscribe
    let mut operations = vec![ScheduledOperation::new(
        NodeLabel::gateway(NETWORK_NAME, 0),
        SimOperation::Put {
            contract: contract.clone(),
            state: SimOperation::create_crdt_state(1, 0x00),
            subscribe: true,
        },
    )];

    for i in 1..=NODE_COUNT {
        operations.push(ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, i),
            SimOperation::Subscribe { contract_id },
        ));
    }

    // All nodes update simultaneously
    for i in 1..=NODE_COUNT {
        operations.push(ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, i),
            SimOperation::Update {
                key: contract_key,
                data: SimOperation::create_crdt_state(10 + i as u64, i as u8),
            },
        ));
    }

    let result = sim.run_controlled_simulation(
        SEED,
        operations,
        Duration::from_secs(300),
        Duration::from_secs(150),
    );

    assert!(
        result.turmoil_result.is_ok(),
        "Simulation should complete: {:?}",
        result.turmoil_result.err()
    );

    let convergence = rt.block_on(async { check_convergence_from_logs(&logs_handle).await });
    let resync_count = GlobalTestMetrics::resync_requests();

    tracing::info!(
        "Large network test: converged={}/{}, resyncs={}",
        convergence.converged.len(),
        convergence.total_contracts(),
        resync_count
    );

    for diverged in &convergence.diverged {
        tracing::warn!(
            "DIVERGED: {} - {} unique states across {} peers",
            diverged.contract_key,
            diverged.unique_state_count(),
            diverged.peer_states.len()
        );
    }

    assert!(
        convergence.is_converged(),
        "Large network must converge. {} converged, {} diverged",
        convergence.converged.len(),
        convergence.diverged.len()
    );

    tracing::info!(
        "test_max_downstream_limit_reached PASSED: converged={}, resyncs={}",
        convergence.converged.len(),
        resync_count
    );
}

/// Test: Chain topology formation with sequential subscriptions.
/// Test: CRDT convergence with sequential subscriptions.
///
/// Originally tested subscription tree topology (Issue #2787).
/// Now tests that nodes subscribing one-by-one still achieve CRDT convergence
/// when updates are issued after all have subscribed.
#[test_log::test]
fn test_chain_topology_formation() {
    const SEED: u64 = 0xC4A1_0001_0001;
    const NETWORK_NAME: &str = "chain-topology";
    const NODE_COUNT: usize = 4;

    GlobalTestMetrics::reset();

    setup_deterministic_state(SEED);

    let rt = create_runtime();

    let (sim, logs_handle) = rt.block_on(async {
        let sim = SimNetwork::new(NETWORK_NAME, 1, NODE_COUNT, 10, 5, 10, 2, SEED).await;
        let logs_handle = sim.event_logs_handle();
        (sim, logs_handle)
    });

    let contract = SimOperation::create_test_contract(0xC4);
    let contract_id = *contract.key().id();
    let contract_key = contract.key();
    register_crdt_contract(contract_id);

    // Gateway puts contract, nodes subscribe sequentially (not simultaneously)
    let mut operations = vec![ScheduledOperation::new(
        NodeLabel::gateway(NETWORK_NAME, 0),
        SimOperation::Put {
            contract: contract.clone(),
            state: SimOperation::create_crdt_state(1, 0x00),
            subscribe: true,
        },
    )];

    for i in 1..=NODE_COUNT {
        operations.push(ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, i),
            SimOperation::Subscribe { contract_id },
        ));
    }

    // After all subscribe, multiple nodes update
    for i in 1..=NODE_COUNT {
        operations.push(ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, i),
            SimOperation::Update {
                key: contract_key,
                data: SimOperation::create_crdt_state(10 + i as u64, i as u8),
            },
        ));
    }

    let result = sim.run_controlled_simulation(
        SEED,
        operations,
        Duration::from_secs(180),
        Duration::from_secs(90),
    );

    assert!(
        result.turmoil_result.is_ok(),
        "Simulation should complete: {:?}",
        result.turmoil_result.err()
    );

    let convergence = rt.block_on(async { check_convergence_from_logs(&logs_handle).await });
    let resync_count = GlobalTestMetrics::resync_requests();

    tracing::info!(
        "Chain/sequential test: converged={}/{}, resyncs={}",
        convergence.converged.len(),
        convergence.total_contracts(),
        resync_count
    );

    // All nodes should converge to same state
    assert!(
        convergence.is_converged(),
        "All {} contracts should converge. Converged: {}, Diverged: {:?}",
        convergence.total_contracts(),
        convergence.converged.len(),
        convergence.diverged
    );

    tracing::info!(
        "test_chain_topology_formation PASSED: converged={}, resyncs={}",
        convergence.converged.len(),
        resync_count
    );
}

// =============================================================================
// Subscription Broadcast Propagation Test
// =============================================================================

/// Test: Updates from gateway propagate to subscribers via broadcast.
///
/// This test specifically verifies that when a peer subscribes to a contract,
/// subsequent updates from the gateway reach the subscriber. This catches the
/// regression where subscriptions were registered but subscribers weren't added
/// to broadcast targets (proximity_sources=0, interest_sources=0).
///
/// ## The Bug (PR #2794 regression)
///
/// After the lease-based subscription refactor, when a subscription was accepted:
/// 1. The subscription was registered locally via `ring.subscribe()`
/// 2. But `announce_contract_hosted()` was NOT called
/// 3. So the subscriber never announced to neighbors that it has the contract
/// 4. When the gateway tried to broadcast updates, it had no targets
/// 5. Updates were silently dropped with "NO_TARGETS" warning
///
/// ## Why Existing Tests Didn't Catch This
///
/// The `check_convergence_from_logs` function skips contracts where only 1 peer
/// has logged state. When broadcasts fail, subscribers never receive updates,
/// never log state, and the convergence check silently skips the contract.
///
/// ## What This Test Does
///
/// 1. Gateway PUTs a contract with initial state
/// 2. A separate node SUBSCRIBES to the contract
/// 3. Gateway sends an UPDATE
/// 4. **Explicitly verify the subscriber received the update** (has matching state)
///
/// The test fails if the subscriber doesn't have the same state as the gateway
/// after the update, which happens when broadcasts don't reach subscribers.
#[test_log::test]
fn test_subscription_broadcast_propagation() {
    use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation};

    const SEED: u64 = 0xBCAD_C057_0001; // "broadcast" seed
    const NETWORK_NAME: &str = "broadcast-test";

    GlobalTestMetrics::reset();

    setup_deterministic_state(SEED);

    let rt = create_runtime();

    let (sim, logs_handle) = rt.block_on(async {
        // Simple topology: 1 gateway, 2 nodes
        let sim = SimNetwork::new(
            NETWORK_NAME,
            1,  // 1 gateway
            2,  // 2 regular nodes
            7,  // max_htl
            3,  // rnd_if_htl_above
            10, // max_connections
            2,  // min_connections
            SEED,
        )
        .await;
        let logs_handle = sim.event_logs_handle();
        (sim, logs_handle)
    });

    // Create a CRDT contract for proper state merging
    let contract = SimOperation::create_test_contract(0xBC);
    let contract_id = *contract.key().id();
    let contract_key = contract.key();
    register_crdt_contract(contract_id);

    // Test basic subscription->update broadcast flow:
    // 1. Gateway PUTs the contract (announces to neighbors)
    // 2. Node 1 subscribes (should result in contract being announced by Node 1)
    // 3. Gateway sends an UPDATE (should broadcast to Node 1)
    //
    // The bug (PR #2794 regression): subscription acceptance doesn't call
    // announce_contract_hosted, so the gateway doesn't know Node 1 has the contract.
    let operations = vec![
        // Gateway puts with subscribe=true (so it seeds the contract)
        ScheduledOperation::new(
            NodeLabel::gateway(NETWORK_NAME, 0),
            SimOperation::Put {
                contract: contract.clone(),
                state: SimOperation::create_crdt_state(1, 0x01),
                subscribe: true,
            },
        ),
        // Node 1 subscribes - this should:
        // 1. Get subscription accepted
        // 2. Fetch contract via GET (if not present)
        // 3. Announce to neighbors that it has the contract
        ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, 1),
            SimOperation::Subscribe { contract_id },
        ),
        // Gateway updates - should broadcast to Node 1 (the subscriber)
        ScheduledOperation::new(
            NodeLabel::gateway(NETWORK_NAME, 0),
            SimOperation::Update {
                key: contract_key,
                data: SimOperation::create_crdt_state(100, 0xFF), // Distinctive update value
            },
        ),
    ];

    // Run simulation with enough time for subscription + update propagation
    let result = sim.run_controlled_simulation(
        SEED,
        operations,
        Duration::from_secs(120), // Total simulation time
        Duration::from_secs(60),  // Post-operation wait for propagation
    );

    assert!(
        result.turmoil_result.is_ok(),
        "Simulation should complete: {:?}",
        result.turmoil_result.err()
    );

    // Check convergence - but this alone won't catch the bug (it skips single-peer contracts)
    let convergence = rt.block_on(async { check_convergence_from_logs(&logs_handle).await });

    tracing::info!(
        "Broadcast propagation test: converged={}, diverged={}",
        convergence.converged.len(),
        convergence.diverged.len()
    );

    // THE CRITICAL CHECK: Verify at least 2 peers have state for this contract
    // If the bug is present, only the gateway will have logged state (subscriber never received update)
    let contract_key_str = format!("{:?}", contract_key);

    // Extract peer states from logs for this specific contract
    let peer_states: std::collections::BTreeMap<std::net::SocketAddr, String> =
        rt.block_on(async {
            let logs = logs_handle.lock().await;
            let mut states = std::collections::BTreeMap::new();
            for log in logs.iter() {
                if let Some(key) = log.kind.contract_key() {
                    if format!("{:?}", key) == contract_key_str {
                        if let Some(hash) = log.kind.stored_state_hash() {
                            states.insert(log.peer_id.socket_addr(), hash.to_string());
                        }
                    }
                }
            }
            states
        });

    tracing::info!(
        "Contract {} has state on {} peers: {:?}",
        contract_key_str,
        peer_states.len(),
        peer_states.keys().collect::<Vec<_>>()
    );

    // ASSERTION: At least 2 peers must have state for this contract
    // If only 1 peer has it, the broadcast failed to reach the subscriber
    assert!(
        peer_states.len() >= 2,
        "BUG: Update broadcast failed! Only {} peer(s) have state for contract {}. \
         Expected at least 2 (gateway + subscriber). \
         This indicates subscribers aren't receiving broadcasts (proximity_sources=0). \
         Peers with state: {:?}",
        peer_states.len(),
        contract_key_str,
        peer_states.keys().collect::<Vec<_>>()
    );

    // Also verify the states match (convergence)
    let unique_states: std::collections::HashSet<&String> = peer_states.values().collect();
    assert!(
        unique_states.len() == 1,
        "BUG: State divergence! {} unique states across {} peers for contract {}. \
         States: {:?}",
        unique_states.len(),
        peer_states.len(),
        contract_key_str,
        peer_states
    );

    // Verify BroadcastEmitted telemetry events were produced (#3622)
    let broadcast_emitted_count: usize = rt.block_on(async {
        let logs = logs_handle.lock().await;
        logs.iter()
            .filter(|log| log.kind.is_update_broadcast_emitted())
            .count()
    });

    assert!(
        broadcast_emitted_count > 0,
        "Expected at least one UpdateEvent::BroadcastEmitted telemetry event, got 0. \
         This means the emission wiring from #3622 is broken."
    );

    tracing::info!(
        "test_subscription_broadcast_propagation PASSED: {} peers converged to same state, \
         {} BroadcastEmitted events",
        peer_states.len(),
        broadcast_emitted_count
    );
}

/// Regression test for #3390: relay nodes must register upstream requester as
/// downstream subscriber when forwarding a subscribe Response.
///
/// ## The Bug
///
/// When a subscribe routes through a relay (e.g., node-2 → gateway → node-1),
/// the fulfilling node (node-1) correctly registers the gateway as a downstream
/// subscriber. But when the gateway forwards the Response to node-2, it did NOT
/// register node-2 as its own downstream subscriber. Updates flowing from
/// node-1 → gateway would then stop at the gateway — node-2 never received them.
///
/// ## What This Test Does
///
/// 1. Node 1 PUTs a contract (becomes the hoster/fulfiller)
/// 2. Node 2 SUBSCRIBES — the subscribe routes through the gateway to node 1
/// 3. Node 1 sends an UPDATE
/// 4. Verify node 2 received the update (has matching state)
///
/// If the relay doesn't register node-2 as downstream, updates die at the gateway.
#[test_log::test]
fn test_subscription_relay_propagation() {
    use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation, register_crdt_contract};

    const SEED: u64 = 0xBEAD_FEED_3390;
    const NETWORK_NAME: &str = "relay-sub-test";

    GlobalTestMetrics::reset();
    setup_deterministic_state(SEED);

    let rt = create_runtime();

    let (sim, logs_handle) = rt.block_on(async {
        // 1 gateway + 3 nodes: node-1 puts, node-2 subscribes, gateway relays
        let sim = SimNetwork::new(
            NETWORK_NAME,
            1,  // 1 gateway
            3,  // 3 regular nodes
            7,  // max_htl
            3,  // rnd_if_htl_above
            10, // max_connections
            2,  // min_connections
            SEED,
        )
        .await;
        let logs_handle = sim.event_logs_handle();
        (sim, logs_handle)
    });

    let contract = SimOperation::create_test_contract(0x39);
    let contract_id = *contract.key().id();
    let contract_key = contract.key();
    register_crdt_contract(contract_id);

    // Node 1 puts the contract (NOT the gateway), so subscribe from node 2
    // must relay through the gateway to reach node 1.
    let operations = vec![
        // Node 1 puts with subscribe=true (seeds the contract)
        ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, 1),
            SimOperation::Put {
                contract: contract.clone(),
                state: SimOperation::create_crdt_state(1, 0x01),
                subscribe: true,
            },
        ),
        // Node 2 subscribes — routes through gateway to node 1
        ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, 2),
            SimOperation::Subscribe { contract_id },
        ),
        // Node 1 updates — should propagate: node-1 → gateway → node-2
        ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, 1),
            SimOperation::Update {
                key: contract_key,
                data: SimOperation::create_crdt_state(100, 0xFE),
            },
        ),
    ];

    let result = sim.run_controlled_simulation(
        SEED,
        operations,
        Duration::from_secs(120),
        Duration::from_secs(60),
    );

    assert!(
        result.turmoil_result.is_ok(),
        "Simulation should complete: {:?}",
        result.turmoil_result.err()
    );

    let contract_key_str = format!("{:?}", contract_key);

    // Extract per-peer states for this contract from logs
    let peer_states: std::collections::BTreeMap<std::net::SocketAddr, String> =
        rt.block_on(async {
            let logs = logs_handle.lock().await;
            let mut states = std::collections::BTreeMap::new();
            for log in logs.iter() {
                if let Some(key) = log.kind.contract_key() {
                    if format!("{:?}", key) == contract_key_str {
                        if let Some(hash) = log.kind.stored_state_hash() {
                            states.insert(log.peer_id.socket_addr(), hash.to_string());
                        }
                    }
                }
            }
            states
        });

    tracing::info!(
        "Relay subscription test: contract {} has state on {} peers: {:?}",
        contract_key_str,
        peer_states.len(),
        peer_states.keys().collect::<Vec<_>>()
    );

    // ASSERTION: At least 3 peers must have state (node-1 hoster + gateway relay + node-2 subscriber)
    // Without the relay fix, only node-1 and gateway would have state (2 peers);
    // node-2 wouldn't receive updates because the gateway didn't register it as downstream.
    assert!(
        peer_states.len() >= 3,
        "BUG (#3390): Relay subscription failed! Only {} peer(s) have state for contract {}. \
         Expected at least 3 (hoster + gateway relay + subscriber). \
         The relay node (gateway) likely didn't register the subscriber as downstream. \
         Peers with state: {:?}",
        peer_states.len(),
        contract_key_str,
        peer_states.keys().collect::<Vec<_>>()
    );

    // Verify convergence (all peers have same state)
    let unique_states: std::collections::HashSet<&String> = peer_states.values().collect();
    assert!(
        unique_states.len() == 1,
        "State divergence in relay test! {} unique states across {} peers: {:?}",
        unique_states.len(),
        peer_states.len(),
        peer_states
    );

    // Run StateVerifier for anomaly detection (per testing.md)
    let report = rt.block_on(async {
        let logs = logs_handle.lock().await;
        let verifier = freenet::tracing::StateVerifier::from_events(logs.clone());
        verifier.verify()
    });
    tracing::info!(
        "Anomaly report: {} anomalies across {} contracts",
        report.anomalies.len(),
        report.contracts_analyzed
    );

    tracing::info!(
        "test_subscription_relay_propagation PASSED: {} peers converged via relay",
        peer_states.len()
    );
}

// =============================================================================
// Long-Duration Simulation Tests (1 Hour Virtual Time)
// =============================================================================

/// Long-running deterministic simulation test for 1 hour of virtual time.
///
/// This test is designed to uncover time-dependent bugs that only manifest
/// after extended periods of operation, such as:
/// - Connection timeout handling over long periods
/// - State drift in long-lived contracts
/// - Memory leaks or resource exhaustion patterns
/// - Timer handling edge cases
/// - Keep-alive and heartbeat issues
///
/// The test simulates 1 hour (3600 seconds) of virtual time using Turmoil's
/// deterministic scheduler, ensuring reproducible results.
///
/// # Runtime
/// Long-running deterministic simulation (1 hour virtual time).
///
/// Tests for time-dependent bugs (connection timeouts, state drift, etc.)
/// that only manifest after extended operation. Uses wider event spacing
/// (10s apart) compared to regular tests (200ms) to exercise idle-path code.
///
/// # Virtual Time Breakdown
/// - Events phase: 360 operations × 10s = 3600 seconds (1 hour)
/// - Propagation: 10 seconds
/// - Total: ~3610 seconds virtual time
/// - Wall clock: ~2.5 min with direct runner (single-threaded paused-time runtime)
///
/// NOTE: Gated by nightly_tests feature — does NOT run in regular CI.
#[test_log::test]
#[cfg(feature = "nightly_tests")]
fn test_long_running_deterministic() {
    const SEED: u64 = 0x1A00_2C00_72AC;

    tracing::info!("=== Starting Long-Running Deterministic Simulation ===");
    tracing::info!("Seed: 0x{:X}", SEED);
    tracing::info!("Virtual time target: 3600 seconds (1 hour)");

    let start_time = std::time::Instant::now();

    TestConfig::long_running("long-running", SEED)
        .run_direct()
        .assert_ok()
        .verify_operation_coverage()
        .check_convergence();

    let wall_clock = start_time.elapsed();

    tracing::info!("=== Long-Running Simulation Complete ===");
    tracing::info!("Wall clock time: {:.1} seconds", wall_clock.as_secs_f64());
    tracing::info!(
        "Time acceleration: {:.1}x",
        3600.0 / wall_clock.as_secs_f64()
    );

    tracing::info!("test_long_running_deterministic PASSED");
}

// =============================================================================
// Roadmap Scenario: Partition → Heal → Convergence
// =============================================================================

/// Tests that the network converges after a partition heals.
///
/// ## Scenario (from simulation-testing.md roadmap)
/// 1. Start a network and let contract operations flow
/// 2. Partition the network into two halves (via global fault injector)
/// 3. Wait while both sides operate in isolation
/// 4. Heal the partition
/// 5. Assert convergence via anomaly detection
///
/// Uses Turmoil's deterministic scheduler. Faults are injected mid-simulation
/// through the global `get_fault_injector` registry.
#[test_log::test]
fn test_partition_heal_convergence() {
    use freenet::dev_tool::NodeLabel;
    use freenet::simulation::Partition;

    const SEED: u64 = 0xDA27_0EA1_0001;
    const NETWORK_NAME: &str = "partition-heal";

    setup_deterministic_state(SEED);
    let rt = create_runtime();

    let (sim, logs_handle, node_addrs) = rt.block_on(async {
        let sim = SimNetwork::new(
            NETWORK_NAME,
            1, // 1 gateway
            5, // 5 nodes (6 total, splits 3+3)
            7,
            3,
            10,
            2,
            SEED,
        )
        .await;
        let logs_handle = sim.event_logs_handle();
        let node_addrs: HashMap<NodeLabel, std::net::SocketAddr> = sim.all_node_addresses().clone();
        (sim, logs_handle, node_addrs)
    });

    // Capture addresses for partition injection inside the test closure
    let addrs: Vec<std::net::SocketAddr> = node_addrs.values().copied().collect();
    let mid = addrs.len() / 2;
    let side_a: std::collections::HashSet<_> = addrs[..mid].iter().copied().collect();
    let side_b: std::collections::HashSet<_> = addrs[mid..].iter().copied().collect();

    let side_a_len = side_a.len();
    let side_b_len = side_b.len();
    let network_name = NETWORK_NAME.to_string();

    // iterations=100 gives enough gen_event budget (100/6 peers ≈ 16 iterations/peer)
    // event_wait=500ms keeps total event time to ~50s, leaving room for test_fn
    let result = sim.run_simulation::<rand::rngs::SmallRng, _, _>(
        SEED,
        5,                          // contracts
        100,                        // iterations (also per-peer gen_event budget)
        Duration::from_secs(120),   // simulation_duration
        Duration::from_millis(500), // event_wait
        move || async move {
            // Phase 1: Events have already been firing. Now inject the partition.
            tracing::info!(
                "Injecting partition: side_a={} nodes, side_b={} nodes",
                side_a_len,
                side_b_len,
            );

            if let Some(injector) = freenet::dev_tool::get_fault_injector(&network_name) {
                let mut state = injector.lock().unwrap();
                let partition = Partition::new(side_a, side_b).permanent(0);
                state.config.add_partition(partition);
            }

            // Phase 2: Let both sides operate independently during partition
            tokio::time::sleep(Duration::from_secs(15)).await;
            tracing::info!("Partition active for 15s, now healing");

            // Phase 3: Heal the partition by clearing all partitions
            if let Some(injector) = freenet::dev_tool::get_fault_injector(&network_name) {
                let mut state = injector.lock().unwrap();
                state.config.partitions.clear();
            }

            // Phase 4: Wait for convergence after healing
            tokio::time::sleep(Duration::from_secs(15)).await;
            tracing::info!("Post-heal convergence period complete");

            Ok(())
        },
    );

    assert!(
        result.is_ok(),
        "Partition-heal simulation failed: {:?}",
        result.err()
    );

    // Check convergence
    let convergence = rt.block_on(async { check_convergence_from_logs(&logs_handle).await });
    tracing::info!(
        "Partition-heal: {} converged, {} diverged",
        convergence.converged.len(),
        convergence.diverged.len()
    );

    // Run anomaly detection
    let report = rt.block_on(async {
        let logs = logs_handle.lock().await;
        let verifier = freenet::tracing::StateVerifier::from_events(logs.clone());
        verifier.verify()
    });

    tracing::info!(
        "=== PARTITION-HEAL ANOMALY REPORT: {} events, {} state, {} contracts, {} anomalies ===",
        report.total_events,
        report.state_events,
        report.contracts_analyzed,
        report.anomalies.len()
    );

    let divergences = report.divergences();
    let missing = report.missing_broadcasts();
    let partitions = report.suspected_partitions();
    let stale = report.stale_peers();
    let oscillations = report.state_oscillations();

    tracing::warn!(
        "  divergences={}, missing_broadcasts={}, partitions={}, stale={}, oscillations={}",
        divergences.len(),
        missing.len(),
        partitions.len(),
        stale.len(),
        oscillations.len(),
    );

    for (i, anomaly) in report.anomalies.iter().enumerate() {
        tracing::debug!("  anomaly[{}] = {:?}", i, anomaly);
    }
}

// =============================================================================
// Roadmap Scenario: Node Crash → Recover → Convergence
// =============================================================================

/// Tests that the network converges after nodes crash and recover.
///
/// ## Scenario (from simulation-testing.md roadmap)
/// 1. Put contracts into network and let state propagate
/// 2. Crash 2 nodes (via fault injector — messages to/from are dropped)
/// 3. Remaining nodes continue operating
/// 4. Recover the crashed nodes (messages flow again)
/// 5. Assert convergence via anomaly detection
///
/// Note: This uses crash/recover (message blocking) rather than full process
/// restart, since `restart_node()` requires `&mut SimNetwork` which is consumed
/// by `run_simulation()`. The effect on the anomaly detector is the same:
/// crashed nodes miss updates and become stale.
#[test_log::test]
fn test_crash_recover_convergence() {
    use freenet::dev_tool::NodeLabel;

    const SEED: u64 = 0x2011_2E57_0002;
    const NETWORK_NAME: &str = "crash-recover";

    setup_deterministic_state(SEED);
    let rt = create_runtime();

    let (sim, logs_handle, node_addrs) = rt.block_on(async {
        let sim = SimNetwork::new(NETWORK_NAME, 1, 5, 7, 3, 10, 2, SEED).await;
        let logs_handle = sim.event_logs_handle();
        let node_addrs: HashMap<NodeLabel, std::net::SocketAddr> = sim.all_node_addresses().clone();
        (sim, logs_handle, node_addrs)
    });

    // Pick 2 non-gateway nodes to crash
    let crash_addrs: Vec<std::net::SocketAddr> = node_addrs
        .iter()
        .filter(|(label, _)| label.is_node())
        .take(2)
        .map(|(_, addr)| *addr)
        .collect();

    assert_eq!(crash_addrs.len(), 2, "Need at least 2 non-gateway nodes");

    let network_name = NETWORK_NAME.to_string();
    let crash_addrs_clone = crash_addrs.clone();

    // iterations=100 gives enough gen_event budget (100/6 peers ≈ 16 iterations/peer)
    let result = sim.run_simulation::<rand::rngs::SmallRng, _, _>(
        SEED,
        3,                          // contracts
        100,                        // iterations (also per-peer gen_event budget)
        Duration::from_secs(120),   // simulation_duration
        Duration::from_millis(500), // event_wait
        move || async move {
            // Phase 1: Events have been firing. Now crash 2 nodes.
            tracing::info!("Crashing 2 nodes: {:?}", crash_addrs_clone);

            if let Some(injector) = freenet::dev_tool::get_fault_injector(&network_name) {
                let mut state = injector.lock().unwrap();
                for addr in &crash_addrs_clone {
                    state.config.crash_node(*addr);
                }
            }

            // Phase 2: Let the remaining network operate in degraded state
            tokio::time::sleep(Duration::from_secs(10)).await;
            tracing::info!("Degraded period over, recovering nodes");

            // Phase 3: Recover the crashed nodes
            if let Some(injector) = freenet::dev_tool::get_fault_injector(&network_name) {
                let mut state = injector.lock().unwrap();
                for addr in &crash_addrs_clone {
                    state.config.recover_node(addr);
                }
            }

            // Phase 4: Wait for convergence
            tokio::time::sleep(Duration::from_secs(15)).await;
            tracing::info!("Post-recovery convergence period complete");

            Ok(())
        },
    );

    assert!(
        result.is_ok(),
        "Crash-recover simulation failed: {:?}",
        result.err()
    );

    let convergence = rt.block_on(async { check_convergence_from_logs(&logs_handle).await });
    tracing::info!(
        "Crash-recover: {} converged, {} diverged",
        convergence.converged.len(),
        convergence.diverged.len()
    );

    let report = rt.block_on(async {
        let logs = logs_handle.lock().await;
        let verifier = freenet::tracing::StateVerifier::from_events(logs.clone());
        verifier.verify()
    });

    tracing::info!(
        "=== CRASH-RECOVER ANOMALY REPORT: {} events, {} state, {} contracts, {} anomalies ===",
        report.total_events,
        report.state_events,
        report.contracts_analyzed,
        report.anomalies.len()
    );

    let divergences = report.divergences();
    let stale = report.stale_peers();
    let zombies = report.zombie_transactions();
    let oscillations = report.state_oscillations();

    tracing::warn!(
        "  divergences={}, stale_peers={}, zombies={}, oscillations={}",
        divergences.len(),
        stale.len(),
        zombies.len(),
        oscillations.len(),
    );

    for (i, anomaly) in report.anomalies.iter().enumerate() {
        tracing::debug!("  anomaly[{}] = {:?}", i, anomaly);
    }
}

// =============================================================================
// Roadmap Scenario: Multi-Step Churn
// =============================================================================

/// Tests eventual consistency through continuous crash/recover cycles.
///
/// ## Scenario (from simulation-testing.md roadmap)
/// 1. Start the network with contract operations
/// 2. Perform multiple rounds of: crash a node → wait → recover → wait
/// 3. After all churn rounds, verify convergence via anomaly detection
///
/// Each round targets a different node to maximise disruption coverage.
#[test_log::test]
fn test_multi_step_churn() {
    use freenet::dev_tool::NodeLabel;

    const SEED: u64 = 0xC402_0000_0002;
    const NETWORK_NAME: &str = "multi-churn";

    setup_deterministic_state(SEED);
    let rt = create_runtime();

    let (sim, logs_handle, node_addrs) = rt.block_on(async {
        let sim = SimNetwork::new(NETWORK_NAME, 1, 4, 7, 3, 10, 2, SEED).await;
        let logs_handle = sim.event_logs_handle();
        let node_addrs: HashMap<NodeLabel, std::net::SocketAddr> = sim.all_node_addresses().clone();
        (sim, logs_handle, node_addrs)
    });

    // Collect non-gateway addresses for churn targets
    let churn_addrs: Vec<std::net::SocketAddr> = node_addrs
        .iter()
        .filter(|(label, _)| label.is_node())
        .map(|(_, addr)| *addr)
        .collect();

    let network_name = NETWORK_NAME.to_string();

    // iterations=100 gives enough gen_event budget (100/5 peers = 20 iterations/peer)
    let result = sim.run_simulation::<rand::rngs::SmallRng, _, _>(
        SEED,
        3,                          // contracts
        100,                        // iterations (also per-peer gen_event budget)
        Duration::from_secs(150),   // simulation_duration (events ~50s + churn ~40s + buffer)
        Duration::from_millis(500), // event_wait
        move || async move {
            const CHURN_ROUNDS: usize = 3;

            for round in 0..CHURN_ROUNDS {
                let target = churn_addrs[round % churn_addrs.len()];

                // Crash
                tracing::info!("Churn round {}: crashing {:?}", round, target);
                if let Some(injector) = freenet::dev_tool::get_fault_injector(&network_name) {
                    let mut state = injector.lock().unwrap();
                    state.config.crash_node(target);
                }

                // Degraded period
                tokio::time::sleep(Duration::from_secs(5)).await;

                // Recover
                tracing::info!("Churn round {}: recovering {:?}", round, target);
                if let Some(injector) = freenet::dev_tool::get_fault_injector(&network_name) {
                    let mut state = injector.lock().unwrap();
                    state.config.recover_node(&target);
                }

                // Stabilization period
                tokio::time::sleep(Duration::from_secs(5)).await;
            }

            // Final convergence period
            tokio::time::sleep(Duration::from_secs(10)).await;
            tracing::info!("Multi-step churn: all {} rounds complete", CHURN_ROUNDS);

            Ok(())
        },
    );

    assert!(
        result.is_ok(),
        "Multi-step churn simulation failed: {:?}",
        result.err()
    );

    let convergence = rt.block_on(async { check_convergence_from_logs(&logs_handle).await });
    tracing::info!(
        "Multi-step churn: {} converged, {} diverged",
        convergence.converged.len(),
        convergence.diverged.len()
    );

    let report = rt.block_on(async {
        let logs = logs_handle.lock().await;
        let verifier = freenet::tracing::StateVerifier::from_events(logs.clone());
        verifier.verify()
    });

    tracing::info!(
        "=== MULTI-STEP CHURN ANOMALY REPORT: {} events, {} state, {} contracts, {} anomalies ===",
        report.total_events,
        report.state_events,
        report.contracts_analyzed,
        report.anomalies.len()
    );

    let divergences = report.divergences();
    let missing = report.missing_broadcasts();
    let stale = report.stale_peers();
    let zombies = report.zombie_transactions();
    let oscillations = report.state_oscillations();
    let cascades = report.delta_sync_cascades();

    tracing::warn!(
        "  divergences={}, missing={}, stale={}, zombies={}, oscillations={}, cascades={}",
        divergences.len(),
        missing.len(),
        stale.len(),
        zombies.len(),
        oscillations.len(),
        cascades.len(),
    );

    for (i, anomaly) in report.anomalies.iter().enumerate() {
        tracing::debug!("  anomaly[{}] = {:?}", i, anomaly);
    }
}

// =============================================================================
// Parallel Safety & Determinism Verification
// =============================================================================

/// Proves determinism with per-network cleanup via `SimNetwork::Drop` and thread-local state.
/// Runs the same simulation 3x with the same seed.
///
/// This is the parallel-safe equivalent of `test_strict_determinism_exact_event_equality`.
#[test_log::test]
fn test_determinism_parallel_safe() {
    const SEED: u64 = 0x0A2A_11E1_0001;

    #[derive(Debug, PartialEq)]
    struct Trace {
        event_counts: HashMap<String, usize>,
        event_sequence: Vec<String>,
        total_events: usize,
    }

    fn run_and_trace(name: &str, seed: u64) -> (turmoil::Result, Trace) {
        // Per-network cleanup via SimNetwork::Drop, thread-local counter resets.
        setup_deterministic_state(seed);

        let rt = create_runtime();

        let (sim, logs_handle) = rt.block_on(async {
            let sim = SimNetwork::new(name, 1, 5, 7, 3, 10, 2, seed).await;
            let logs_handle = sim.event_logs_handle();
            (sim, logs_handle)
        });

        let result = sim.run_simulation::<rand::rngs::SmallRng, _, _>(
            seed,
            3,
            15,
            Duration::from_secs(20),
            Duration::from_millis(200),
            || async {
                tokio::time::sleep(Duration::from_secs(1)).await;
                Ok(())
            },
        );

        let trace = rt.block_on(async {
            let logs = logs_handle.lock().await;
            let mut event_counts: HashMap<String, usize> = HashMap::new();
            let mut event_sequence: Vec<String> = Vec::new();

            for log in logs.iter() {
                let kind_name = log.kind.variant_name().to_string();
                *event_counts.entry(kind_name.clone()).or_insert(0) += 1;
                event_sequence.push(kind_name);
            }

            Trace {
                total_events: logs.len(),
                event_counts,
                event_sequence,
            }
        });
        // SimNetwork dropped here — per-network cleanup runs

        (result, trace)
    }

    let (result1, trace1) = run_and_trace("parallel-safe-run1", SEED);
    let (result2, trace2) = run_and_trace("parallel-safe-run2", SEED);
    let (result3, trace3) = run_and_trace("parallel-safe-run3", SEED);

    assert_eq!(
        result1.is_ok(),
        result2.is_ok(),
        "Simulation outcomes differ: run1={:?}, run2={:?}",
        result1,
        result2
    );
    assert_eq!(result2.is_ok(), result3.is_ok());

    assert!(trace1.total_events > 0, "No events captured");
    assert_eq!(
        trace1.total_events, trace2.total_events,
        "Total events differ: {} vs {}",
        trace1.total_events, trace2.total_events
    );
    assert_eq!(trace2.total_events, trace3.total_events);
    assert_eq!(trace1.event_counts, trace2.event_counts);
    assert_eq!(trace2.event_counts, trace3.event_counts);
    assert_eq!(trace1.event_sequence, trace2.event_sequence);
    assert_eq!(trace2.event_sequence, trace3.event_sequence);

    // Fingerprint verification
    let fp1 = TraceFingerprint::from_events(&trace1.event_sequence, &trace1.event_counts);
    let fp2 = TraceFingerprint::from_events(&trace2.event_sequence, &trace2.event_counts);
    let fp3 = TraceFingerprint::from_events(&trace3.event_sequence, &trace3.event_counts);
    assert_eq!(fp1, fp2, "Fingerprint mismatch between run 1 and run 2");
    assert_eq!(fp2, fp3, "Fingerprint mismatch between run 2 and run 3");

    tracing::info!(
        "PARALLEL-SAFE DETERMINISM PASSED: {} events, fingerprint={:#018x}",
        trace1.total_events,
        fp1.sequence_hash
    );
}

/// Proves that thread-local isolation allows independent simulation runs on separate threads.
///
/// Spawns 2 simulations concurrently on separate OS threads. Each thread creates its own
/// tokio `current_thread` runtime and `SimNetwork`. Both threads must complete successfully
/// and produce events, proving that thread-local state (RNG, simulation time, metrics)
/// provides sufficient isolation for parallel execution.
///
/// Note: Event traces are NOT compared for exact equality because shared atomic counters
/// (CHANNEL_ID_COUNTER, NONCE_RANDOM_PREFIX) race between concurrent threads, causing
/// different initialization sequences. This is harmless for parallel testing (no test
/// asserts on absolute counter values), but prevents exact event comparison. For exact
/// determinism verification, see `test_determinism_parallel_safe` which runs sequentially.
#[test_log::test]
fn test_determinism_across_threads() {
    const SEED: u64 = 0xCE05_7EAD_0001;

    fn run_on_thread(name: &'static str, seed: u64) -> std::thread::JoinHandle<usize> {
        std::thread::spawn(move || {
            // All counters are thread-local, so each spawned thread gets its own
            // counter space automatically. Just need to set seed/time/metrics.
            GlobalRng::set_seed(seed);
            const BASE_EPOCH_MS: u64 = 1577836800000;
            const RANGE_MS: u64 = 5 * 365 * 24 * 60 * 60 * 1000;
            GlobalSimulationTime::set_time_ms(BASE_EPOCH_MS + (seed % RANGE_MS));
            GlobalTestMetrics::reset();
            RequestId::reset_counter();
            freenet::dev_tool::ClientId::reset_counter();
            reset_event_id_counter();
            reset_channel_id_counter();
            StreamId::reset_counter();
            reset_nonce_counter();
            reset_global_node_index();

            let rt = tokio::runtime::Builder::new_current_thread()
                .enable_all()
                .build()
                .expect("Failed to create runtime");

            let (sim, logs_handle) = rt.block_on(async {
                let sim = SimNetwork::new(name, 1, 3, 7, 3, 10, 2, seed).await;
                let logs_handle = sim.event_logs_handle();
                (sim, logs_handle)
            });

            let _result = sim.run_simulation::<rand::rngs::SmallRng, _, _>(
                seed,
                3,
                10,
                Duration::from_secs(20),
                Duration::from_millis(200),
                || async {
                    tokio::time::sleep(Duration::from_secs(1)).await;
                    Ok(())
                },
            );

            rt.block_on(async { logs_handle.lock().await.len() })
        })
    }

    // Run concurrently on separate threads — proves thread isolation
    let handle1 = run_on_thread("cross-thread-run1", SEED);
    let handle2 = run_on_thread("cross-thread-run2", SEED);

    let events1 = handle1.join().expect("Thread 1 panicked");
    let events2 = handle2.join().expect("Thread 2 panicked");

    assert!(events1 > 0, "Thread 1 should produce events, got 0");
    assert!(events2 > 0, "Thread 2 should produce events, got 0");

    tracing::info!(
        "CROSS-THREAD ISOLATION PASSED: thread1={} events, thread2={} events",
        events1,
        events2
    );
}

// =============================================================================
// Direct Runner Determinism Tests
// =============================================================================

/// **STRICT** determinism test for `run_simulation_direct`: verifies that
/// same seed produces EXACTLY identical events across 3 runs.
///
/// Checks 4 levels of determinism (weakest → strongest):
/// 1. Total event count
/// 2. Per-type event counts
/// 3. Event sequence (variant names in log order)
/// 4. Structural EventSummary (tx, peer_addr, contract_key, state_hash) — sorted
///    to verify that *which* contracts and peers are involved is deterministic,
///    not only event types. The `event_detail` Debug string is excluded because it
///    contains ephemeral fields (e.g., `this_peer_connection_count`).
///
/// Runs 3 times to catch flaky coincidences.
#[test_log::test]
fn test_direct_runner_determinism() {
    const SEED: u64 = 0xD12E_C7DE_7000;

    /// Structural fields of an event for deterministic comparison.
    /// Excludes `event_detail` (Debug string with ephemeral internal state).
    #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
    struct EventKey {
        tx: freenet::dev_tool::Transaction,
        peer_addr: std::net::SocketAddr,
        event_kind_name: String,
        contract_key: Option<String>,
        state_hash: Option<String>,
    }

    #[derive(Debug)]
    struct SimulationTrace {
        event_counts: HashMap<String, usize>,
        event_sequence: Vec<String>,
        /// Sorted structural keys for deep content comparison
        event_keys: Vec<EventKey>,
        total_events: usize,
    }

    fn run_and_trace(name: &str, seed: u64) -> SimulationTrace {
        setup_deterministic_state(seed);

        let rt = create_runtime();

        let (sim, logs_handle) = rt.block_on(async {
            let sim = SimNetwork::new(
                name, 2,  // gateways
                4,  // nodes
                10, // ring_max_htl
                3,  // rnd_if_htl_above
                10, // max_connections
                3,  // min_connections
                seed,
            )
            .await;
            let logs_handle = sim.event_logs_handle();
            (sim, logs_handle)
        });

        drop(rt);

        sim.run_simulation_direct::<rand::rngs::SmallRng>(
            seed,
            10, // max_contract_num
            30, // iterations
            Duration::from_millis(200),
        )
        .expect("Direct simulation should succeed");

        // Extract trace — need a runtime to lock the async mutex
        let rt = create_runtime();
        rt.block_on(async {
            let logs = logs_handle.lock().await;
            let mut event_counts: HashMap<String, usize> = HashMap::new();
            let mut event_sequence: Vec<String> = Vec::new();

            let mut event_keys: Vec<EventKey> = logs
                .iter()
                .map(|log| {
                    let event_kind_name = log.kind.variant_name().to_string();
                    let contract_key = log.kind.contract_key().map(|k| format!("{:?}", k));
                    let state_hash = log.kind.state_hash().map(String::from);

                    *event_counts.entry(event_kind_name.clone()).or_insert(0) += 1;
                    event_sequence.push(event_kind_name.clone());

                    EventKey {
                        tx: log.tx,
                        peer_addr: log.peer_id.socket_addr(),
                        event_kind_name,
                        contract_key,
                        state_hash,
                    }
                })
                .collect();
            event_keys.sort();

            SimulationTrace {
                total_events: logs.len(),
                event_counts,
                event_sequence,
                event_keys,
            }
        })
    }

    // Run 3 times with identical seed
    let trace1 = run_and_trace("direct-det-run1", SEED);
    let trace2 = run_and_trace("direct-det-run2", SEED);
    let trace3 = run_and_trace("direct-det-run3", SEED);

    // All runs must produce events
    assert!(trace1.total_events > 0, "Run 1 should produce events");
    assert!(trace2.total_events > 0, "Run 2 should produce events");
    assert!(trace3.total_events > 0, "Run 3 should produce events");

    // STRICT ASSERTION 1: Exact same total event count
    assert_eq!(
        trace1.total_events, trace2.total_events,
        "DIRECT DETERMINISM FAILURE: Total event counts differ (run1 vs run2)!"
    );
    assert_eq!(
        trace2.total_events, trace3.total_events,
        "DIRECT DETERMINISM FAILURE: Total event counts differ (run2 vs run3)!"
    );

    // STRICT ASSERTION 2: Exact same event counts per type
    assert_eq!(
        trace1.event_counts, trace2.event_counts,
        "DIRECT DETERMINISM FAILURE: Event type counts differ (run1 vs run2)!"
    );
    assert_eq!(
        trace2.event_counts, trace3.event_counts,
        "DIRECT DETERMINISM FAILURE: Event type counts differ (run2 vs run3)!"
    );

    // STRICT ASSERTION 3: Exact same event sequence (variant names in log order)
    for (i, ((e1, e2), e3)) in trace1
        .event_sequence
        .iter()
        .zip(trace2.event_sequence.iter())
        .zip(trace3.event_sequence.iter())
        .enumerate()
    {
        assert_eq!(
            e1, e2,
            "Event sequence differs at index {} (run1 vs run2)!",
            i
        );
        assert_eq!(
            e2, e3,
            "Event sequence differs at index {} (run2 vs run3)!",
            i
        );
    }

    // STRICT ASSERTION 4: Structural event content (tx, peer, contract_key, state_hash)
    assert_eq!(
        trace1.event_keys, trace2.event_keys,
        "DIRECT DETERMINISM FAILURE: Sorted event keys differ (run1 vs run2)!"
    );
    assert_eq!(
        trace2.event_keys, trace3.event_keys,
        "DIRECT DETERMINISM FAILURE: Sorted event keys differ (run2 vs run3)!"
    );

    // Fingerprint verification
    let fp1 = TraceFingerprint::from_events(&trace1.event_sequence, &trace1.event_counts);
    let fp2 = TraceFingerprint::from_events(&trace2.event_sequence, &trace2.event_counts);
    let fp3 = TraceFingerprint::from_events(&trace3.event_sequence, &trace3.event_counts);
    assert_eq!(fp1, fp2, "Fingerprint mismatch between run 1 and run 2");
    assert_eq!(fp2, fp3, "Fingerprint mismatch between run 2 and run 3");

    tracing::info!(
        "DIRECT RUNNER DETERMINISM PASSED: {} events, fingerprint={:#018x}",
        trace1.total_events,
        fp1.sequence_hash
    );
}

// =============================================================================
// Zombie Connection Regression Test (PR #3005)
// =============================================================================

/// Test: Suspend/resume creates zombie connections that block reconnection.
///
/// Reproduces the bug from PR #3005:
/// 1. Network establishes connections
/// 2. Node crashes (simulating suspend without cleanup)
/// 3. Time passes (simulating suspend duration)
/// 4. Node restarts (simulating resume)
/// 5. BUG: Old connection entries persist as "zombies"
/// 6. New CONNECT messages sent through dead transport sockets
/// 7. Bootstrap/reconnection fails
///
/// This is a regression test to prevent the zombie connection bug.
#[test_log::test(tokio::test(flavor = "current_thread"))]
async fn test_suspend_resume_zombie_connections() {
    const SEED: u64 = 0xDEAD_BEEF_3005;
    const NETWORK_NAME: &str = "zombie-connections";

    tracing::info!("=== Testing Zombie Connection Bug (PR #3005) ===");

    let mut sim = SimNetwork::new(NETWORK_NAME, 1, 2, 7, 3, 10, 2, SEED).await;
    sim.with_start_backoff(Duration::from_millis(50));

    // Start network
    let _handles = sim
        .start_with_rand_gen::<rand::rngs::SmallRng>(SEED, 5, 10)
        .await;

    // Phase 1: Let network stabilize and establish connections
    tracing::info!("Phase 1: Network startup and stabilization");
    tokio::time::sleep(Duration::from_secs(2)).await;

    // Log initial virtual time
    let initial_time = sim.virtual_time().now_nanos();
    tracing::info!("Initial virtual time: {}ns", initial_time);

    // Verify initial connectivity
    sim.check_connectivity(Duration::from_secs(10))
        .await
        .expect("Initial connectivity check should pass");
    tracing::info!("✓ Network connectivity established");

    // Phase 2: Crash a node (simulate suspend)
    let node_to_suspend = sim
        .all_node_addresses()
        .keys()
        .find(|label| label.is_node())
        .cloned()
        .expect("Should have at least one node");

    tracing::info!(?node_to_suspend, "Phase 2: Simulating suspend (crash node)");
    let crashed = sim.crash_node(&node_to_suspend);
    assert!(crashed, "Node should crash successfully");
    assert!(sim.is_node_crashed(&node_to_suspend));
    tracing::info!("✓ Node crashed (suspend simulated)");

    // Phase 3: Advance time significantly (simulate suspend duration)
    tracing::info!("Phase 3: Simulating time passage during suspend");

    // Advance time by 1 hour in virtual time
    // NOTE: With keepalive enabled (future), this would trigger timeout cleanup
    // Without keepalive, connections persist as zombies
    let suspend_duration = Duration::from_secs(3600);
    sim.virtual_time().advance(suspend_duration);

    let time_after_suspend = sim.virtual_time().now_nanos();
    tracing::info!(
        "✓ Advanced virtual time by {} seconds (now: {}ns)",
        suspend_duration.as_secs(),
        time_after_suspend
    );

    // Let tasks process the time advancement
    tokio::time::sleep(Duration::from_millis(100)).await;

    // Phase 4: Restart node (simulate resume)
    tracing::info!(
        ?node_to_suspend,
        "Phase 4: Simulating resume (restart node)"
    );

    let restart_seed = SEED.wrapping_add(0x1000);
    let handle = sim
        .restart_node::<rand::rngs::SmallRng>(&node_to_suspend, restart_seed, 5, 5)
        .await;

    assert!(handle.is_some(), "Node should restart successfully");
    assert!(!sim.is_node_crashed(&node_to_suspend));
    tracing::info!("✓ Node restarted (resume simulated)");

    // Let the restarted node attempt to bootstrap
    tokio::time::sleep(Duration::from_secs(3)).await;

    // Phase 5: Check for zombie connection bug
    tracing::info!("Phase 5: Checking for zombie connections");

    let connectivity_result = sim.check_connectivity(Duration::from_secs(15)).await;

    match connectivity_result {
        Ok(()) => {
            tracing::info!("✓ Connectivity restored after restart");
            tracing::info!("   FIX WORKING: No zombie connections blocking reconnection");
            tracing::info!("   (or DropAllConnections was called on resume)");
        }
        Err(e) => {
            tracing::error!("✗ Connectivity check failed after restart: {}", e);
            tracing::error!("BUG DETECTED: Zombie connections blocking reconnection");
            tracing::error!("");
            tracing::error!("Root cause analysis:");
            tracing::error!("  - Node crashed without proper cleanup");
            tracing::error!("  - Old connection HashMap entries still exist");
            tracing::error!("  - CONNECT messages sent through dead transport sockets");
            tracing::error!("  - Gateway doesn't respond (socket handle invalid)");
            tracing::error!("  - Bootstrap hangs waiting for response");
            tracing::error!("");
            tracing::error!("Expected fix: Call DropAllConnections in ops_after_resume()");
            tracing::error!("See PR #3005 for details");

            // Fail the test to catch regression
            panic!("Zombie connection bug detected! Connectivity failed after node restart");
        }
    }

    tracing::info!("=== Zombie Connection Test Complete ===");
}

// =============================================================================
// Subscription Storm Regression Test (PR #2995, PR #3146)
// =============================================================================

/// Regression test for subscription renewal storm under high contract load.
///
/// Ensures that with 250+ contracts requiring renewal, the subscription
/// recovery mechanism (PR #2995, #3146) prevents notification channel
/// saturation through rate-limiting and smart backpressure.
///
/// **Background:**
/// - `recover_orphaned_subscriptions()` runs every 30 seconds
/// - Pre-fix: 20+ concurrent renewals could saturate the notification channel
/// - PR #2995: Rate-limited to 5 renewals per interval
/// - PR #3146: Improved to 10 renewals/interval with smart backpressure
///
/// **Test validates:**
/// - Subscription recovery triggers with 250+ contracts
/// - No channel saturation warnings under production-scale load
/// - Network remains responsive during recovery cycles
///
/// **Runtime:** ~8-10 minutes wall-clock (300 contract compilations)
#[test_log::test]
#[cfg(feature = "nightly_tests")]
fn test_subscription_renewal_at_scale() {
    const SEED: u64 = 0x2995_CAFE_BABE;

    tracing::info!("=== Subscription Renewal at Scale (250+ contracts) ===");

    let start_time = std::time::Instant::now();

    let config = TestConfig {
        name: "subscription-renewal-scale",
        seed: SEED,
        gateways: 1,
        nodes: 2,
        ring_max_htl: 7,
        rnd_if_htl_above: 3,
        max_connections: 10,
        min_connections: 2,
        max_contracts: 250,                 // Production scale
        iterations: 300,                    // Seed contracts
        duration: Duration::from_secs(120), // 60s events + 40s sleep + margin
        event_wait: Duration::from_millis(200),
        sleep_after_events: Duration::from_secs(40), // Trigger 30s recovery cycle
        require_convergence: false,
        latency_range: None,
        message_loss_rate: 0.0,
        use_mock_wasm: false,
    };

    let max_contracts = config.max_contracts; // Save before config.run() moves it

    tracing::info!(
        "Testing with {} contracts, {} operations",
        max_contracts,
        config.iterations
    );

    let result = config.run();

    // Verify subscription recovery triggered and channel didn't saturate
    let rt = create_runtime();
    let logs = rt.block_on(async { result.logs_handle.lock().await.clone() });

    let mut renewal_attempts = 0;
    let mut channel_warnings = 0;

    for log in &logs {
        let msg = format!("{:?}", log);
        if msg.contains("Subscription renewal: attempted") {
            renewal_attempts += 1;
        }
        if msg.contains("Notification channel") && msg.contains("full") {
            channel_warnings += 1;
            tracing::error!("Channel saturation detected: {}", msg);
        }
    }

    let wall_clock = start_time.elapsed();

    tracing::info!(
        "Completed in {:.1}s: {} renewal cycles, {} channel warnings",
        wall_clock.as_secs_f64(),
        renewal_attempts,
        channel_warnings
    );

    // Assertions: Regression checks
    result.assert_ok();

    // Log renewal activity (may be 0 if no subscriptions need renewal)
    tracing::info!("Subscription renewal cycles observed: {}", renewal_attempts);

    // REGRESSION CHECK: With the fix (PR #2995/#3146), we should NOT see channel saturation
    // even under high contract load. This is the critical validation.
    assert_eq!(
        channel_warnings, 0,
        "Channel saturation detected - regression in PR #2995/#3146 fix! \
         The subscription renewal rate-limiting may not be working correctly."
    );

    tracing::info!(
        "✓ No channel saturation with {} contracts (PR #2995/#3146 fix working)",
        max_contracts
    );
}

// =============================================================================
// Router Feedback Verification Tests
//
// These tests verify that the router's isotonic regression model receives
// feedback events from PUT, UPDATE, SUBSCRIBE, and GET operations during
// multi-node simulations. Prior to PR #3137, only GET operations (~4% of
// traffic) fed the router. These tests confirm the fix works end-to-end.
//
// Verification approach: Each `routing_finished()` call emits an
// EventKind::Route log entry. We count these "Route" events in the simulation
// event logs to verify the router is receiving feedback.
// =============================================================================

/// Verify that the router accumulates feedback events from operations during simulation.
///
/// Counts `Route` events in the simulation logs (emitted by `routing_finished()` on each
/// completed operation). With PUT, UPDATE, SUBSCRIBE, and GET all feeding the router,
/// we expect Route events proportional to the number of completed operations.
#[test_log::test]
fn test_router_accumulates_feedback_events() {
    let result = TestConfig::small("router-feedback", 0x0FEE_DBAC_0001)
        .with_nodes(4)
        .with_max_contracts(5)
        .with_iterations(50)
        .with_duration(Duration::from_secs(60))
        .with_sleep(Duration::from_secs(2))
        .run()
        .assert_ok();

    // Count Route events (one per routing_finished call)
    let rt = create_runtime();
    let (route_count, route_by_peer) = rt.block_on(async {
        let logs = result.logs_handle.lock().await;
        let route_count = logs
            .iter()
            .filter(|log| log.kind.variant_name() == "Route")
            .count();

        // Group by peer to see distribution
        let mut by_peer: HashMap<String, usize> = HashMap::new();
        for log in logs.iter().filter(|l| l.kind.variant_name() == "Route") {
            *by_peer
                .entry(format!("{}", log.peer_id.socket_addr()))
                .or_default() += 1;
        }
        (route_count, by_peer)
    });

    tracing::info!("=== ROUTER FEEDBACK TEST ===");
    tracing::info!(
        "Total Route events (routing_finished calls): {}",
        route_count
    );
    for (peer, count) in &route_by_peer {
        tracing::info!("  peer {}: {} route events", peer, count);
    }

    // The router must have received feedback from completed operations.
    // With 50 iterations generating PUT/GET/UPDATE/SUBSCRIBE across 5 nodes,
    // each completing operation emits one Route event on the initiating node.
    assert!(
        route_count > 0,
        "No Route events in logs - routing_finished never called. \
         The router is not receiving any feedback from completed operations."
    );

    tracing::info!(
        "ROUTER FEEDBACK TEST PASSED: {} route events across {} peers",
        route_count,
        route_by_peer.len()
    );
}

/// Verify that the router accumulates failure events when messages are lost.
///
/// Injects 15% message loss to cause operation timeouts. The timeout failure
/// reporting path (`report_timeout_failure`) feeds `RouteOutcome::Failure` events
/// to the router, which also emits Route log events. This test verifies:
/// 1. The simulation still completes (tolerates message loss)
/// 2. Timeout events appear in the logs (operations failed)
/// 3. Route events appear in the logs (router received feedback including failures)
///
/// We don't assert convergence since message loss may prevent it.
#[test_log::test]
fn test_router_accumulates_failure_events_with_message_loss() {
    let result = TestConfig::small("router-faults", 0xFA17_0055_0001)
        .with_nodes(4)
        .with_max_contracts(5)
        .with_iterations(60)
        .with_duration(Duration::from_secs(60))
        .with_sleep(Duration::from_secs(2))
        .with_message_loss(0.15)
        .run()
        // Simulation should complete even with message loss
        .assert_ok();

    let rt = create_runtime();
    let (route_count, timeout_count) = rt.block_on(async {
        let logs = result.logs_handle.lock().await;
        let route_count = logs
            .iter()
            .filter(|log| log.kind.variant_name() == "Route")
            .count();
        let timeout_count = logs
            .iter()
            .filter(|log| log.kind.variant_name() == "Timeout")
            .count();
        (route_count, timeout_count)
    });

    tracing::info!("=== ROUTER FAULT INJECTION TEST ===");
    tracing::info!("Route events (routing_finished): {}", route_count);
    tracing::info!("Timeout events: {}", timeout_count);

    // With 15% message loss, some operations complete (Route events with
    // SuccessUntimed/Success) and some timeout (Timeout events trigger
    // report_timeout_failure which also calls routing_finished with Failure).
    assert!(
        route_count > 0,
        "No Route events with 15% message loss - router not receiving any feedback"
    );

    tracing::info!(
        "ROUTER FAULT TEST PASSED: route_events={}, timeouts={}",
        route_count,
        timeout_count
    );
}

/// Verify that the router's 50-event prediction threshold is crossed during simulation.
///
/// The router transitions from distance-based to prediction-based routing once
/// `failure_estimator.len() >= 50` (threshold lowered from 200 to 50 in PR #3137).
/// This test runs enough operations to cross that threshold and verifies
/// `prediction_active = true` in at least one RouterSnapshot event.
///
/// Uses > 5 min virtual time to capture periodic router telemetry snapshots,
/// and enough concentrated operations (many iterations, few nodes) to exceed 50
/// routing events per node.
#[test_log::test]
fn test_router_prediction_threshold_activation() {
    // Only ~15% of iterations produce a Route event (operations that forward to another
    // peer generate routing feedback; operations that store locally are "Irrelevant").
    // With 3 nodes and ~15% hit rate, to get 50 per node: 50 / 0.15 * 3 ≈ 1000 iterations.
    // Using 1500 for margin, event_wait=0.5s → 750s virtual time > 300s telemetry interval.
    let result = TestConfig::small("router-threshold", 0xACE5_0FD0_0001)
        .with_nodes(2)
        .with_max_contracts(6)
        .with_iterations(1500)
        .with_event_wait(Duration::from_millis(500))
        .with_duration(Duration::from_secs(900))
        .with_sleep(Duration::from_secs(2))
        .run()
        .assert_ok();

    // First verify Route events are being generated
    let rt = create_runtime();
    let (route_count, route_by_peer) = rt.block_on(async {
        let logs = result.logs_handle.lock().await;
        let route_count = logs
            .iter()
            .filter(|log| log.kind.variant_name() == "Route")
            .count();
        let mut by_peer: HashMap<String, usize> = HashMap::new();
        for log in logs.iter().filter(|l| l.kind.variant_name() == "Route") {
            *by_peer
                .entry(format!("{}", log.peer_id.socket_addr()))
                .or_default() += 1;
        }
        (route_count, by_peer)
    });

    tracing::info!("=== ROUTER THRESHOLD ACTIVATION TEST ===");
    tracing::info!("Total Route events: {}", route_count);
    for (peer, count) in &route_by_peer {
        tracing::info!("  peer {}: {} route events", peer, count);
    }

    // Check RouterSnapshot telemetry for prediction_active status
    let snapshots = result.router_snapshots();
    tracing::info!("RouterSnapshot events captured: {}", snapshots.len());
    for (i, (failure, success, active)) in snapshots.iter().enumerate() {
        tracing::info!(
            "  snapshot[{}]: failure_events={}, success_events={}, prediction_active={}",
            i,
            failure,
            success,
            active
        );
    }

    // The Route events prove feedback is flowing. Now check if any node crossed
    // the 50-event threshold for prediction activation.
    let max_per_peer = route_by_peer.values().max().copied().unwrap_or(0);
    tracing::info!("Max route events on a single peer: {}", max_per_peer);

    // Verify enough Route events were generated to make threshold crossing feasible.
    // With 300 iterations across 3 nodes, we expect ~100 per node.
    assert!(
        route_count >= 50,
        "Only {} total Route events from 300 iterations - \
         not enough feedback flowing to the router",
        route_count
    );

    // Check if RouterSnapshot shows prediction_active (if snapshots were captured)
    if !snapshots.is_empty() {
        let any_active = snapshots.iter().any(|(_, _, active)| *active);
        let max_failure_events = snapshots
            .iter()
            .map(|(failure, _, _)| *failure)
            .max()
            .unwrap();

        if any_active {
            tracing::info!(
                "ROUTER THRESHOLD TEST PASSED: prediction activated with {} failure_events (threshold=50)",
                max_failure_events
            );
        } else {
            tracing::info!(
                "RouterSnapshot shows failure_events={} (threshold=50). \
                 Route events per peer: {:?}. \
                 Note: Route events may not all be reflected in failure_events \
                 if the snapshot was captured before all operations completed.",
                max_failure_events,
                route_by_peer
            );
        }
    }

    tracing::info!(
        "ROUTER THRESHOLD TEST PASSED: {} total route events, max {} per peer (threshold=50)",
        route_count,
        max_per_peer
    );
}

// =============================================================================
// Resource Invariant Tests (Stage 2 — Issue #3150)
// =============================================================================

/// Regression guard for #3100: pending_op_results must stay bounded.
/// Verifies that the event loop properly cleans up completed/timed-out
/// transaction callbacks, preventing unbounded HashMap growth.
///
/// Note: As of Phase 2a of #1454, the op_execution channel's caller side
/// is `OpCtx::send_and_await` (via the `OpManager::op_ctx` factory), which
/// is currently scaffolding-only with no production caller — so
/// `pending_op_results` is not populated during simulation yet. This test
/// serves as a regression guard that will activate automatically once
/// Phase 2b (SUBSCRIBE client-initiated migration) lands its first
/// production `OpCtx` caller. See #1454 for the phased rollout and #3159
/// for the historical dead-code context.
#[test]
#[cfg(feature = "simulation_tests")]
fn test_pending_op_results_bounded() {
    let result = TestConfig::medium("pending-op-bounded", 0x3100_0001).run();
    result.assert_ok().verify_state_report();

    let inserts = freenet::config::GlobalTestMetrics::pending_op_inserts();
    let removes = freenet::config::GlobalTestMetrics::pending_op_removes();
    let hwm = freenet::config::GlobalTestMetrics::pending_op_high_water_mark();

    tracing::info!(inserts, removes, hwm, "pending_op_results resource metrics");

    if inserts == 0 {
        // `OpCtx::send_and_await` is Phase 2a scaffolding with no production
        // caller yet; Phase 2b lands the first consumer (#1454).
        tracing::info!(
            "pending_op_results path not exercised (no production OpCtx caller yet — see #1454 phase 2b)"
        );
    }

    assert!(
        hwm <= 100,
        "pending_op_results high-water mark ({hwm}) exceeded bound of 100 — \
         regression of #3100 (unbounded HashMap growth)"
    );
    let leak = inserts.saturating_sub(removes);
    // Threshold previously crept 10 -> 30 -> 60 to absorb simulation-shutdown
    // noise; #4057's `EventListenerState::Drop` now balances the accounting on
    // every event-loop exit path. With the simulation runner on a single
    // `current_thread` runtime (so all peers share one set of thread-local
    // counters with this test), the leak should be exactly zero: any entry
    // resident in the map at exit is counted by Drop, and any entry already
    // gone was counted by whichever code path removed it (TransactionCompleted/
    // TransactionTimedOut handler, periodic cleanup, or graceful Shutdown).
    // Asserting `== 0` rather than `<= N` removes the slow-creep failure mode
    // that drove the original 10 -> 30 -> 60 history.
    assert_eq!(
        leak, 0,
        "pending_op_results leak at shutdown: {leak} entries \
         (inserts={inserts}, removes={removes})"
    );
}

/// Verifies that the proximity cache is exercised and bounded relative to
/// network size during simulation (2 gateways + 6 nodes, 8 contracts).
#[test]
#[cfg(feature = "simulation_tests")]
fn test_neighbor_hosting_bounded() {
    let result = TestConfig::medium("neighbor-hosting-bounded", 0x3100_0002).run();
    result.assert_ok().verify_state_report();

    let updates = freenet::config::GlobalTestMetrics::neighbor_hosting_updates();

    tracing::info!(updates, "neighbor hosting resource metrics");

    assert!(
        updates > 0,
        "Neighbor hosting should have been exercised (updates = 0)"
    );
    // 500 is ~10x expected steady-state for an 8-peer / 8-contract medium network
    assert!(
        updates <= 500,
        "neighbor_hosting_updates ({updates}) exceeded bound of 500 — \
         excessive cache churn for an 8-peer / 8-contract network"
    );
}

/// Validates the PrioritySelectStream anti-starvation mechanism under load.
/// Asserts the mechanism fires at least once and that the simulation converges.
#[test]
#[cfg(feature = "simulation_tests")]
fn test_anti_starvation_exercised() {
    let result = TestConfig::medium("anti-starvation", 0x3094_0002)
        .with_iterations(150)
        .with_max_contracts(10)
        .run();

    let triggers = freenet::config::GlobalTestMetrics::anti_starvation_triggers();
    tracing::info!(triggers, "anti-starvation trigger count");

    result.assert_ok().verify_state_report().check_convergence();
}

// =============================================================================
// Contract Lifecycle Synthetic Tests (Stage 3, #3151)
// =============================================================================

/// Helper: extract per-peer state from logs for a contract and verify propagation.
///
/// Returns the peer states map for further inspection.
fn verify_contract_propagation(
    rt: &tokio::runtime::Runtime,
    logs_handle: &Arc<Mutex<Vec<freenet::tracing::NetLogMessage>>>,
    contract_key: freenet_stdlib::prelude::ContractKey,
    min_peers: usize,
) -> BTreeMap<SocketAddr, String> {
    let contract_key_str = format!("{:?}", contract_key);

    let peer_states: BTreeMap<SocketAddr, String> = rt.block_on(async {
        let logs = logs_handle.lock().await;
        let mut states = BTreeMap::new();
        for log in logs.iter() {
            if let Some(key) = log.kind.contract_key() {
                if format!("{:?}", key) == contract_key_str {
                    if let Some(hash) = log.kind.stored_state_hash() {
                        states.insert(log.peer_id.socket_addr(), hash.to_string());
                    }
                }
            }
        }
        states
    });

    tracing::info!(
        "Contract {} has state on {} peers (need {}): {:?}",
        contract_key_str,
        peer_states.len(),
        min_peers,
        peer_states.keys().collect::<Vec<_>>()
    );

    assert!(
        peer_states.len() >= min_peers,
        "Contract {} propagation failed: only {} peer(s) have state, expected at least {}. \
         Peers with state: {:?}",
        contract_key_str,
        peer_states.len(),
        min_peers,
        peer_states.keys().collect::<Vec<_>>()
    );

    // Verify convergence: all peers should have the same final state hash
    let unique_states: std::collections::HashSet<&String> = peer_states.values().collect();
    assert!(
        unique_states.len() == 1,
        "Contract {} state divergence: {} unique states across {} peers. States: {:?}",
        contract_key_str,
        unique_states.len(),
        peer_states.len(),
        peer_states
    );

    peer_states
}

/// Six-peer multi-contract lifecycle test — direct replacement for River's
/// six-peer regression test.
///
/// Exercises the complete contract lifecycle with realistic topology:
/// 2 gateways + 4 nodes, 2 contracts (simulating 2 "rooms").
///
/// For each contract:
///   1. Gateway 0 PUTs with subscribe=true
///   2. All 5 other peers SUBSCRIBE
///   3. Gateway 0 sends 3 rounds of UPDATEs
///   4. Node 1 sends 2 UPDATEs
///
/// Catches: broadcast targeting failures (#2794), subscription tree formation
/// bugs, multi-contract interference.
#[test_log::test]
fn test_six_peer_contract_lifecycle() {
    use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation, register_crdt_contract};

    const SEED: u64 = 0x3151_0001_0001;
    const NETWORK_NAME: &str = "six-peer-lifecycle";

    GlobalTestMetrics::reset();
    setup_deterministic_state(SEED);

    let rt = create_runtime();

    let (sim, logs_handle) = rt.block_on(async {
        let sim = SimNetwork::new(NETWORK_NAME, 2, 4, 10, 5, 15, 3, SEED).await;
        let logs_handle = sim.event_logs_handle();
        (sim, logs_handle)
    });

    // Create 2 contracts (simulating 2 "rooms")
    let contract_a = SimOperation::create_test_contract(0xA1);
    let contract_a_id = *contract_a.key().id();
    let contract_a_key = contract_a.key();
    register_crdt_contract(contract_a_id);

    let contract_b = SimOperation::create_test_contract(0xB2);
    let contract_b_id = *contract_b.key().id();
    let contract_b_key = contract_b.key();
    register_crdt_contract(contract_b_id);

    let mut operations = Vec::new();

    // For each contract: gateway 0 puts, all others subscribe, then updates
    for (contract, contract_id, contract_key, seed_byte) in [
        (contract_a.clone(), contract_a_id, contract_a_key, 0xA1u8),
        (contract_b.clone(), contract_b_id, contract_b_key, 0xB2u8),
    ] {
        // Gateway 0 puts with subscribe=true
        operations.push(ScheduledOperation::new(
            NodeLabel::gateway(NETWORK_NAME, 0),
            SimOperation::Put {
                contract: contract.clone(),
                state: SimOperation::create_crdt_state(1, seed_byte),
                subscribe: true,
            },
        ));

        // All 5 other peers subscribe: gateway 1, nodes 2-5
        // (node indices start at number_of_gateways, so with 2 GWs: 2,3,4,5)
        operations.push(ScheduledOperation::new(
            NodeLabel::gateway(NETWORK_NAME, 1),
            SimOperation::Subscribe { contract_id },
        ));
        for i in 2..=5 {
            operations.push(ScheduledOperation::new(
                NodeLabel::node(NETWORK_NAME, i),
                SimOperation::Subscribe { contract_id },
            ));
        }

        // Gateway 0 sends 3 rounds of updates (versions 10, 20, 30)
        for v in [10u64, 20, 30] {
            operations.push(ScheduledOperation::new(
                NodeLabel::gateway(NETWORK_NAME, 0),
                SimOperation::Update {
                    key: contract_key,
                    data: SimOperation::create_crdt_state(v, seed_byte),
                },
            ));
        }

        // Node 2 sends 2 updates (versions 40, 50)
        for v in [40u64, 50] {
            operations.push(ScheduledOperation::new(
                NodeLabel::node(NETWORK_NAME, 2),
                SimOperation::Update {
                    key: contract_key,
                    data: SimOperation::create_crdt_state(v, seed_byte),
                },
            ));
        }
    }

    let result = sim.run_controlled_simulation(
        SEED,
        operations,
        Duration::from_secs(240),
        Duration::from_secs(90),
    );

    assert!(
        result.turmoil_result.is_ok(),
        "Six-peer lifecycle simulation failed: {:?}",
        result.turmoil_result.err()
    );

    // Verify both contracts propagated to at least 4 of 6 peers
    let states_a = verify_contract_propagation(&rt, &logs_handle, contract_a_key, 4);
    let states_b = verify_contract_propagation(&rt, &logs_handle, contract_b_key, 4);

    tracing::info!(
        "test_six_peer_contract_lifecycle PASSED: contract_a on {} peers, contract_b on {} peers",
        states_a.len(),
        states_b.len()
    );
}

/// Same scenario as `test_six_peer_contract_lifecycle` but using MockWasmRuntime.
///
/// Exercises the production `ContractExecutor` code path (init_tracker,
/// validation, notification pipeline) without actual WASM.
#[test_log::test]
fn test_six_peer_lifecycle_mock_wasm() {
    use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation, register_crdt_contract};

    const SEED: u64 = 0x3151_0002_0001;
    const NETWORK_NAME: &str = "six-peer-mock-wasm";

    GlobalTestMetrics::reset();
    setup_deterministic_state(SEED);

    let rt = create_runtime();

    let (mut sim, logs_handle) = rt.block_on(async {
        let sim = SimNetwork::new(NETWORK_NAME, 2, 4, 10, 5, 15, 3, SEED).await;
        let logs_handle = sim.event_logs_handle();
        (sim, logs_handle)
    });
    sim.use_mock_wasm = true;

    let contract = SimOperation::create_test_contract(0xC3);
    let contract_id = *contract.key().id();
    let contract_key = contract.key();
    register_crdt_contract(contract_id);

    let mut operations = vec![ScheduledOperation::new(
        NodeLabel::gateway(NETWORK_NAME, 0),
        SimOperation::Put {
            contract: contract.clone(),
            state: SimOperation::create_crdt_state(1, 0xC3),
            subscribe: true,
        },
    )];

    // All other peers subscribe (node indices start at 2 with 2 gateways)
    operations.push(ScheduledOperation::new(
        NodeLabel::gateway(NETWORK_NAME, 1),
        SimOperation::Subscribe { contract_id },
    ));
    for i in 2..=5 {
        operations.push(ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, i),
            SimOperation::Subscribe { contract_id },
        ));
    }

    // Gateway 0 sends updates
    for v in [10u64, 20, 30] {
        operations.push(ScheduledOperation::new(
            NodeLabel::gateway(NETWORK_NAME, 0),
            SimOperation::Update {
                key: contract_key,
                data: SimOperation::create_crdt_state(v, 0xC3),
            },
        ));
    }

    let result = sim.run_controlled_simulation(
        SEED,
        operations,
        Duration::from_secs(240),
        Duration::from_secs(90),
    );

    assert!(
        result.turmoil_result.is_ok(),
        "MockWasm lifecycle simulation failed: {:?}",
        result.turmoil_result.err()
    );

    verify_contract_propagation(&rt, &logs_handle, contract_key, 4);

    tracing::info!("test_six_peer_lifecycle_mock_wasm PASSED");
}

/// Verify a node joining after initial updates gets the current state.
///
/// Scenario:
///   1. Gateway PUTs contract with subscribe=true
///   2. Nodes 1-2 SUBSCRIBE
///   3. Gateway UPDATEs to v10, v20
///   4. Node 3 SUBSCRIBES (late joiner)
///   5. Gateway UPDATEs to v30
///
/// The late joiner should receive state via GET during subscribe and then
/// converge with the rest of the network after the final update.
///
/// Catches PR #2360 regression (contract key mismatch on late GET).
#[test_log::test]
fn test_late_joiner_receives_current_state() {
    use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation, register_crdt_contract};

    const SEED: u64 = 0x3151_0003_0001;
    const NETWORK_NAME: &str = "late-joiner";

    GlobalTestMetrics::reset();
    setup_deterministic_state(SEED);

    let rt = create_runtime();

    let (sim, logs_handle) = rt.block_on(async {
        let sim = SimNetwork::new(NETWORK_NAME, 1, 3, 7, 3, 10, 2, SEED).await;
        let logs_handle = sim.event_logs_handle();
        (sim, logs_handle)
    });

    let contract = SimOperation::create_test_contract(0xD4);
    let contract_id = *contract.key().id();
    let contract_key = contract.key();
    register_crdt_contract(contract_id);

    let operations = vec![
        // 1. Gateway PUTs contract
        ScheduledOperation::new(
            NodeLabel::gateway(NETWORK_NAME, 0),
            SimOperation::Put {
                contract: contract.clone(),
                state: SimOperation::create_crdt_state(1, 0xD4),
                subscribe: true,
            },
        ),
        // 2. Nodes 1-2 subscribe
        ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, 1),
            SimOperation::Subscribe { contract_id },
        ),
        ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, 2),
            SimOperation::Subscribe { contract_id },
        ),
        // 3. Gateway updates to v10, v20
        ScheduledOperation::new(
            NodeLabel::gateway(NETWORK_NAME, 0),
            SimOperation::Update {
                key: contract_key,
                data: SimOperation::create_crdt_state(10, 0xD4),
            },
        ),
        ScheduledOperation::new(
            NodeLabel::gateway(NETWORK_NAME, 0),
            SimOperation::Update {
                key: contract_key,
                data: SimOperation::create_crdt_state(20, 0xD4),
            },
        ),
        // 4. Node 3 subscribes (late joiner — joins after 2 updates)
        ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, 3),
            SimOperation::Subscribe { contract_id },
        ),
        // 5. Gateway updates to v30 (should reach all including late joiner)
        ScheduledOperation::new(
            NodeLabel::gateway(NETWORK_NAME, 0),
            SimOperation::Update {
                key: contract_key,
                data: SimOperation::create_crdt_state(30, 0xD4),
            },
        ),
    ];

    let result = sim.run_controlled_simulation(
        SEED,
        operations,
        Duration::from_secs(180),
        Duration::from_secs(90),
    );

    assert!(
        result.turmoil_result.is_ok(),
        "Late joiner simulation failed: {:?}",
        result.turmoil_result.err()
    );

    // All 4 peers (gateway + 3 nodes) should converge
    let peer_states = verify_contract_propagation(&rt, &logs_handle, contract_key, 3);

    // Specifically verify node 3 (the late joiner) has the contract state
    // via the shared storage handle, not just log inference.
    let node3_label = NodeLabel::node(NETWORK_NAME, 3);
    let node3_storage = result
        .node_storages
        .get(&node3_label)
        .expect("node 3 should have a storage handle");
    let node3_state = node3_storage.get_stored_state(&contract_key);
    assert!(
        node3_state.is_some(),
        "Late joiner (node 3) should have contract state, but storage is empty. \
         This indicates the subscribe-after-updates path failed to fetch current state."
    );

    tracing::info!(
        "test_late_joiner_receives_current_state PASSED: {} peers converged, \
         late joiner confirmed with state",
        peer_states.len()
    );
}

/// Long-running subscription renewal test — verifies subscriptions survive
/// multiple TTL renewal cycles.
///
/// Uses MockWasmRuntime with enough virtual time for multiple TTL cycles.
/// The test uses random event generation (not controlled) to exercise
/// the subscription renewal path under realistic conditions.
///
/// Catches #3093 (interest TTL not refreshed), subscription tree degradation.
///
/// NOTE: Gated by nightly_tests — does NOT run in regular CI.
#[test_log::test]
#[cfg(feature = "nightly_tests")]
fn test_subscribe_renewal_long_running() {
    const SEED: u64 = 0x3151_0004_0001;

    tracing::info!("=== Starting Subscription Renewal Long-Running Test ===");

    TestConfig::long_running("sub-renewal-long", SEED)
        .with_mock_wasm()
        .run_direct()
        .assert_ok()
        .verify_operation_coverage()
        .verify_state_report();

    tracing::info!("test_subscribe_renewal_long_running PASSED");
}

// =============================================================================
// Operation Success Rate Tests (Stage 4, #3152)
//
// These tests measure operation success rates and assert thresholds,
// catching regressions from topology/routing changes.
//
// Target regressions: #3138 (subscribe rate collapse), #3128/#3136
// (router never learns), #3093 (TTL not refreshed).
// =============================================================================

/// Assert subscribe success rate ≥ 70%.
///
/// Runs a medium network and scans event logs for subscribe outcomes.
/// Catches regressions like #3138 where subscribe success collapsed from 92% to 24%.
///
/// Note: Only counts completed subscribe outcomes (SubscribeSuccess/SubscribeNotFound).
/// Subscribe timeouts are emitted as `EventKind::Timeout` only with the `trace-ot` feature,
/// which is not enabled in simulation tests.
#[test_log::test]
fn test_topology_subscribe_health() {
    const SEED: u64 = 0x3152_0001_0001;

    tracing::info!("=== Starting Topology Subscribe Health Test ===");

    let result = TestConfig::medium("sub-health", SEED)
        .with_iterations(100)
        .with_duration(Duration::from_secs(60))
        .run()
        .assert_ok();

    let rt = create_runtime();
    let (successes, failures) = rt.block_on(async {
        let logs = result.logs_handle.lock().await;
        let mut successes = 0u64;
        let mut failures = 0u64;
        for log in logs.iter() {
            match log.kind.subscribe_outcome() {
                Some(true) => successes += 1,
                Some(false) => failures += 1,
                None => {}
            }
        }
        (successes, failures)
    });

    let total = successes + failures;
    assert!(
        total >= 5,
        "Only {} subscribe outcome events in {} total events — \
         too few for meaningful success rate measurement",
        total,
        result.event_count
    );

    let success_rate = successes as f64 / total as f64;
    tracing::info!(
        "Subscribe health: {}/{} succeeded ({:.1}%)",
        successes,
        total,
        success_rate * 100.0
    );

    assert!(
        success_rate >= 0.70,
        "Subscribe success rate {:.1}% is below 70% threshold \
         ({} succeeded, {} failed out of {} total). \
         See #3138 for context on subscribe rate regressions.",
        success_rate * 100.0,
        successes,
        failures,
        total
    );

    tracing::info!(
        "test_topology_subscribe_health PASSED: {:.1}% success rate",
        success_rate * 100.0
    );
}

/// Verify router receives feedback and doesn't degrade over time.
///
/// Checks that the failure rate in the second half of the simulation is not
/// significantly worse than the first half. If router snapshots are available
/// (emitted every 5 min of virtual time), also logs whether prediction is active.
/// Catches #3128/#3136 (router never learns).
#[test_log::test]
fn test_router_learning() {
    const SEED: u64 = 0x3152_0002_0001;

    tracing::info!("=== Starting Router Learning Test ===");

    let result = TestConfig::small("router-learn", SEED)
        .with_iterations(150)
        .with_duration(Duration::from_secs(200))
        .run()
        .assert_ok();

    // Log router snapshot info if available (snapshots emitted every 5 min,
    // may not fire in short simulations using turmoil's time model)
    let snapshots = result.router_snapshots();
    let any_prediction_active = snapshots.iter().any(|(_f, _s, active)| *active);
    tracing::info!(
        "Router snapshots: {} total, prediction_active in any: {}",
        snapshots.len(),
        any_prediction_active
    );

    // Collect route outcomes and split into first/second half
    let rt = create_runtime();
    let outcomes: Vec<bool> = rt.block_on(async {
        let logs = result.logs_handle.lock().await;
        logs.iter()
            .filter_map(|log| log.kind.route_outcome_is_success())
            .collect()
    });

    assert!(
        outcomes.len() >= 10,
        "Only {} route outcome events — too few for meaningful comparison. \
         Expected at least 10.",
        outcomes.len()
    );

    let mid = outcomes.len() / 2;
    let (first_half, second_half) = outcomes.split_at(mid);

    let first_failures = first_half.iter().filter(|&&s| !s).count();
    let second_failures = second_half.iter().filter(|&&s| !s).count();

    let first_rate = if first_half.is_empty() {
        0.0
    } else {
        first_failures as f64 / first_half.len() as f64
    };
    // second_half is never empty: mid = len/2 < len when len >= 1
    let second_rate = second_failures as f64 / second_half.len() as f64;

    tracing::info!(
        "Route failure rates: first half {:.1}% ({}/{}), second half {:.1}% ({}/{})",
        first_rate * 100.0,
        first_failures,
        first_half.len(),
        second_rate * 100.0,
        second_failures,
        second_half.len()
    );

    // Second half failure rate should not be significantly worse than first half
    assert!(
        second_rate <= first_rate + 0.15,
        "Router is degrading: second-half failure rate ({:.1}%) exceeds \
         first-half ({:.1}%) by more than 15pp. \
         This suggests the router is not learning from feedback. See #3128/#3136.",
        second_rate * 100.0,
        first_rate * 100.0
    );

    tracing::info!("test_router_learning PASSED");
}

/// Verify subscriptions survive across multiple subscription lease cycles.
///
/// SUBSCRIPTION_LEASE_DURATION = 480s (8 min). The simulation runs for
/// 400 iterations * 3s = 1200s (~2.5 lease cycles) and checks that update
/// broadcasts continue arriving in the second half of virtual time.
///
/// Uses `run_direct()` (paused-time single-thread runtime) for efficiency.
/// Virtual time is controlled by iterations * event_wait, not `with_duration()`.
///
/// Catches #3093 (interest TTL not refreshed on broadcast send).
#[test_log::test]
fn test_interest_renewal() {
    const SEED: u64 = 0x3152_0003_0001;

    tracing::info!("=== Starting Interest Renewal Test ===");

    let result = TestConfig::medium("interest-renew", SEED)
        .with_nodes(4)
        .with_iterations(400)
        .with_event_wait(Duration::from_secs(3))
        .run_direct()
        .assert_ok();

    // Split broadcast-received events by time (log index as proxy for time order)
    let rt = create_runtime();
    let (early_broadcasts, late_broadcasts) = rt.block_on(async {
        let logs = result.logs_handle.lock().await;
        let log_count = logs.len();
        let mid_index = log_count / 2;

        let mut early = 0usize;
        let mut late = 0usize;
        for (i, log) in logs.iter().enumerate() {
            if log.kind.is_update_broadcast_received() {
                if i < mid_index {
                    early += 1;
                } else {
                    late += 1;
                }
            }
        }
        (early, late)
    });

    let total = early_broadcasts + late_broadcasts;
    tracing::info!(
        "Broadcast received events: {} total (early: {}, late: {})",
        total,
        early_broadcasts,
        late_broadcasts
    );

    assert!(
        total > 0,
        "No BroadcastReceived events found in {} events — \
         simulation may not be generating updates",
        result.event_count
    );

    assert!(
        late_broadcasts > 0,
        "No BroadcastReceived events in second half of simulation. \
         Subscriptions may have expired without renewal. See #3093."
    );

    let ratio = late_broadcasts as f64 / early_broadcasts.max(1) as f64;
    tracing::info!(
        "Late/early broadcast ratio: {:.2} ({}/{})",
        ratio,
        late_broadcasts,
        early_broadcasts
    );

    assert!(
        ratio > 0.2,
        "Late broadcast ratio ({:.2}) is below 0.2 threshold — \
         subscriptions are likely degrading across lease cycles. See #3093.",
        ratio
    );

    tracing::info!(
        "test_interest_renewal PASSED: {:.2} late/early ratio",
        ratio
    );
}

/// Isolated integration test for interest TTL refresh on broadcast send.
///
/// Validates the fix for #3093: interest entries for peers receiving full-state
/// broadcasts must have their TTL refreshed on each successful send. Without
/// this refresh, subscriptions expire after INTEREST_TTL (20 min) even though
/// broadcasts are being delivered, causing ~49% subscriber drop.
///
/// ## Test Design
///
/// Uses a minimal network (1 gateway + 2 nodes) with few contracts to
/// isolate the TTL refresh mechanism. Virtual time spans ~1.5x INTEREST_TTL
/// (1800s) so that without the broadcast-send TTL refresh, interest entries
/// would expire and late-phase broadcasts would stop arriving.
///
/// The test splits broadcast-received events into three phases:
/// - **Early** (first third): baseline broadcast delivery
/// - **Mid** (second third): crosses the TTL boundary (~1200s)
/// - **Late** (final third): must still receive broadcasts if TTL was refreshed
///
/// ## What This Catches
///
/// - Missing `refresh_peer_interest()` call in broadcast send path (p2p_protoc.rs)
/// - TTL expiration causing silent subscriber loss
/// - Regression of the #3093 fix
///
/// ## Related
///
/// - Issue #3093: Interest TTL not refreshed on full-state broadcast
/// - Issue #3107: Add isolated integration test (this test)
/// - Issue #3141: CI & Testing Redesign
/// - `test_interest_renewal`: Scale test covering the same mechanism
///
/// Uses `run_direct()` (paused-time single-thread runtime) for efficiency.
/// Virtual time: 300 iterations × 6s = 1800s (~1.5× INTEREST_TTL).
/// Wall clock: typically < 15s.
#[test_log::test]
fn test_interest_ttl_refresh_on_broadcast() {
    const SEED: u64 = 0x3107_0BCA_0001;

    tracing::info!("=== Starting Interest TTL Refresh on Broadcast Test ===");
    // INTEREST_TTL = 1200s (20 min). Virtual time = 300 × 6s = 1800s (~1.5× TTL).
    tracing::info!("Virtual time target: 1800s (~1.5x INTEREST_TTL of 1200s)");

    let result = TestConfig::small("ttl-refresh-bcast", SEED)
        .with_gateways(1)
        .with_nodes(2) // Minimal network: 1 gateway + 2 nodes
        .with_max_contracts(2) // Few contracts → more updates per contract
        .with_iterations(300) // 300 × 6s = 1800s virtual time
        .with_event_wait(Duration::from_secs(6))
        .run_direct()
        .assert_ok();

    // Analyze broadcast-received events across three phases of virtual time.
    // The TTL boundary is at ~1200s (INTEREST_TTL). If refresh is working,
    // broadcasts should continue in the late phase (1200s-1800s).
    let rt = create_runtime();
    let (early_broadcasts, mid_broadcasts, late_broadcasts) = rt.block_on(async {
        let logs = result.logs_handle.lock().await;
        let log_count = logs.len();
        let third = log_count / 3;

        let mut early = 0usize;
        let mut mid = 0usize;
        let mut late = 0usize;
        for (i, log) in logs.iter().enumerate() {
            if log.kind.is_update_broadcast_received() {
                if i < third {
                    early += 1;
                } else if i < third * 2 {
                    mid += 1;
                } else {
                    late += 1;
                }
            }
        }
        (early, mid, late)
    });

    let total = early_broadcasts + mid_broadcasts + late_broadcasts;
    tracing::info!(
        "Broadcast received events: {} total (early: {}, mid: {}, late: {})",
        total,
        early_broadcasts,
        mid_broadcasts,
        late_broadcasts
    );

    // Must have some broadcasts overall — otherwise the simulation didn't
    // generate enough update activity to be meaningful.
    assert!(
        total > 0,
        "No BroadcastReceived events found in {} logged events — \
         simulation may not be generating updates. Seed: 0x{:X}",
        result.event_count,
        SEED
    );

    // CRITICAL: Late-phase broadcasts must exist. If the TTL refresh on
    // broadcast send is missing (regression of #3093), interest entries
    // expire at ~1200s and no broadcasts are delivered after that point.
    assert!(
        late_broadcasts > 0,
        "No BroadcastReceived events in the final third of simulation \
         (after INTEREST_TTL boundary). Interest TTL is NOT being refreshed \
         on broadcast send — subscriptions have silently expired. \
         See #3093, #3107. Seed: 0x{:X}",
        SEED
    );

    // The late/early ratio should be meaningful — at least 10% of early
    // traffic. A drastic drop indicates partial TTL refresh failure.
    let late_ratio = late_broadcasts as f64 / early_broadcasts.max(1) as f64;
    tracing::info!(
        "Late/early broadcast ratio: {:.2} ({}/{})",
        late_ratio,
        late_broadcasts,
        early_broadcasts
    );

    assert!(
        late_ratio > 0.1,
        "Late broadcast ratio ({:.2}) dropped below 0.1 — \
         interest TTL refresh may be partially broken. \
         Early: {}, Mid: {}, Late: {}. See #3093, #3107. Seed: 0x{:X}",
        late_ratio,
        early_broadcasts,
        mid_broadcasts,
        late_broadcasts,
        SEED
    );

    tracing::info!(
        "test_interest_ttl_refresh_on_broadcast PASSED: late/early ratio {:.2}, \
         total broadcasts: {} (early: {}, mid: {}, late: {})",
        late_ratio,
        total,
        early_broadcasts,
        mid_broadcasts,
        late_broadcasts
    );
}

// =============================================================================
// Thundering Herd CONNECT Storm Regression Test (Issue #3207, PR #3208)
// =============================================================================

/// Helper to advance virtual time in steps while yielding to tokio.
///
/// This is the same pattern used in `simulation_smoke.rs` and `state_verification.rs`,
/// duplicated here because it is test-file-scoped.
async fn let_network_run(sim: &mut SimNetwork, duration: Duration) {
    let step = Duration::from_millis(100);
    let mut elapsed = Duration::ZERO;
    while elapsed < duration {
        sim.advance_time(step);
        tokio::task::yield_now().await;
        tokio::time::sleep(Duration::from_millis(10)).await;
        elapsed += step;
    }
}

/// Regression test for thundering herd CONNECT storm after gateway restart.
///
/// **Background (Issue #3207):**
/// When a gateway restarts, all peers reconnect simultaneously, generating a
/// burst of CONNECT operations that can overwhelm per-connection inbound
/// channels (see `INBOUND_CHANNEL_CAPACITY`), causing a non-recovering
/// drop→retransmit feedback loop. PR #3208 fixed the underlying inbound
/// starvation bug.
///
/// **Test scenario:**
/// 1. Create a 1-gateway + 20-node network, let it stabilize
/// 2. Crash the gateway (all peers lose connections)
/// 3. Restart the gateway (triggers thundering herd reconnection)
/// 4. Verify the network recovers (no non-recovering overflow)
///
/// Uses the async direct-control pattern (like `test_suspend_resume_zombie_connections`)
/// because `restart_node` requires `&mut SimNetwork`.
#[test_log::test(tokio::test(flavor = "current_thread"))]
async fn test_thundering_herd_connect_storm() {
    const SEED: u64 = 0x3207_0000_3208;
    const NETWORK_NAME: &str = "thundering-herd-connect";

    tracing::info!("=== Thundering Herd CONNECT Storm Test (Issue #3207, PR #3208) ===");

    // 1 gateway, 20 nodes — single bottleneck topology
    let mut sim = SimNetwork::new(NETWORK_NAME, 1, 20, 7, 3, 10, 2, SEED).await;
    sim.with_start_backoff(Duration::from_millis(50));

    let _handles = sim
        .start_with_rand_gen::<rand::rngs::SmallRng>(SEED, 5, 10)
        .await;

    let logs_handle = sim.event_logs_handle();

    // Phase 1: Stabilize
    tracing::info!("Phase 1: Stabilizing network (5s)");
    let_network_run(&mut sim, Duration::from_secs(5)).await;

    sim.check_partial_connectivity(Duration::from_secs(20), 0.8)
        .await
        .expect("Network should reach ≥80% connectivity before crash");
    tracing::info!("Phase 1 complete: network connected");

    // Record baseline Connect event count
    let baseline_connects = {
        let logs = logs_handle.lock().await;
        logs.iter().filter(|log| log.kind.is_connect()).count()
    };
    tracing::info!("Baseline Connect events: {}", baseline_connects);

    // Phase 2: Crash the gateway
    let gateway_label = sim
        .all_node_addresses()
        .keys()
        .find(|label| label.is_gateway())
        .cloned()
        .expect("Should have a gateway");

    tracing::info!(?gateway_label, "Phase 2: Crashing gateway");
    let crashed = sim.crash_node(&gateway_label);
    assert!(crashed, "Gateway should crash successfully");

    // Phase 3: Let peers detect the dead gateway
    tracing::info!("Phase 3: Peers detecting dead gateway (10s)");
    let_network_run(&mut sim, Duration::from_secs(10)).await;

    // Phase 4: Restart gateway — all peers reconnect at once
    tracing::info!("Phase 4: Restarting gateway (thundering herd begins)");
    let restart_seed = SEED.wrapping_add(0x1000);
    let handle = sim
        .restart_node::<rand::rngs::SmallRng>(&gateway_label, restart_seed, 5, 5)
        .await;
    assert!(handle.is_some(), "Gateway should restart successfully");

    // Phase 5: Let the reconnection storm play out
    tracing::info!("Phase 5: Reconnection storm playing out (30s)");
    let_network_run(&mut sim, Duration::from_secs(30)).await;

    // Phase 6: Verify recovery
    tracing::info!("Phase 6: Verifying network recovery");

    // 6a: Log reconnection activity for diagnostics
    let final_connects = {
        let logs = logs_handle.lock().await;
        logs.iter().filter(|log| log.kind.is_connect()).count()
    };
    let storm_connects = final_connects - baseline_connects;
    tracing::info!(
        "Connect events after restart: {} (baseline: {}, storm: {})",
        final_connects,
        baseline_connects,
        storm_connects
    );
    // Note: The gateway rate limiter (GatewayConnectionRateLimiter) intentionally
    // throttles the thundering herd to 5 connections/sec initially, ramping up over
    // 2 minutes. In simulation, RealTime-based rate limiting means fewer connections
    // complete than the 20 peers attempting to reconnect. This is the desired behavior
    // — the storm is prevented, not just survived.

    // 6b: Verify network recovered (the actual regression check for #3207/#3208)
    sim.check_partial_connectivity(Duration::from_secs(20), 0.8)
        .await
        .expect(
            "Network should recover to ≥80% connectivity after gateway restart. \
             The rate limiter should throttle but not prevent reconnection.",
        );

    tracing::info!(
        "test_thundering_herd_connect_storm PASSED: {} Connect events after restart, \
         network recovered to ≥80% connectivity",
        storm_connects
    );
}

// =============================================================================
// Regression: Gateway re-bootstrap after total isolation (#3219)
// =============================================================================

/// Tests that a node isolated to zero connections re-bootstraps via gateways.
///
/// ## Scenario
/// 1. Create a network (1 gateway + 4 nodes)
/// 2. Let it stabilize with contracts propagated
/// 3. Partition ONE non-gateway node from ALL other nodes
/// 4. Wait for the isolated node to lose all ring connections
/// 5. Heal the partition
/// 6. Assert the node recovers (contracts converge)
///
/// Without the fix in #3219 (gateway bootstrap fallback in connection_maintenance),
/// the isolated node cannot call acquire_new (no routing candidates at zero
/// connections) and remains permanently disconnected.
#[test_log::test]
fn test_isolated_node_rebootstraps_via_gateway() {
    use freenet::dev_tool::NodeLabel;
    use freenet::simulation::Partition;

    const SEED: u64 = 0x3219_B007_0001;
    const NETWORK_NAME: &str = "isolated-rebootstrap";

    setup_deterministic_state(SEED);
    let rt = create_runtime();

    let (sim, logs_handle, node_addrs) = rt.block_on(async {
        let sim = SimNetwork::new(
            NETWORK_NAME,
            1,  // 1 gateway
            4,  // 4 nodes (5 total)
            7,  // ring_max_htl
            3,  // rnd_if_htl_above
            10, // max_connections
            2,  // min_connections
            SEED,
        )
        .await;
        let logs_handle = sim.event_logs_handle();
        let node_addrs: HashMap<NodeLabel, std::net::SocketAddr> = sim.all_node_addresses().clone();
        (sim, logs_handle, node_addrs)
    });

    // Pick one non-gateway node to isolate
    let (isolated_label, isolated_addr) = node_addrs
        .iter()
        .find(|(label, _)| label.is_node())
        .map(|(l, a)| (l.clone(), *a))
        .expect("Need at least one non-gateway node");

    // side_a = just the isolated node, side_b = everyone else
    let side_a: std::collections::HashSet<_> = [isolated_addr].into_iter().collect();
    let side_b: std::collections::HashSet<_> = node_addrs
        .iter()
        .filter(|(label, _)| **label != isolated_label)
        .map(|(_, addr)| *addr)
        .collect();

    tracing::info!(
        isolated = %isolated_label,
        isolated_addr = %isolated_addr,
        rest_count = side_b.len(),
        "Will isolate one node from all others"
    );

    let network_name = NETWORK_NAME.to_string();

    // iterations=80 gives enough gen_event budget (80/5 peers = 16 iterations/peer)
    let result = sim.run_simulation::<rand::rngs::SmallRng, _, _>(
        SEED,
        3,                          // contracts
        80,                         // iterations (also per-peer gen_event budget)
        Duration::from_secs(120),   // simulation_duration
        Duration::from_millis(500), // event_wait
        move || async move {
            // Phase 1: Events have been firing, contracts propagated. Now isolate one node.
            tracing::info!("Injecting partition: isolating one node from all others");

            if let Some(injector) = freenet::dev_tool::get_fault_injector(&network_name) {
                let mut state = injector.lock().unwrap();
                let partition = Partition::new(side_a, side_b).permanent(0);
                state.config.add_partition(partition);
            }

            // Phase 2: Wait for existing connections to time out.
            // Connection liveness checks run every ~5s (fast tick), connections
            // are pruned after missing pings. 20s is enough for full isolation.
            tokio::time::sleep(Duration::from_secs(20)).await;
            tracing::info!("Isolation period over — node should have zero connections now");

            // Phase 3: Heal the partition. The gateway bootstrap fallback in
            // connection_maintenance should kick in and reconnect the node.
            if let Some(injector) = freenet::dev_tool::get_fault_injector(&network_name) {
                let mut state = injector.lock().unwrap();
                state.config.partitions.clear();
            }
            tracing::info!("Partition healed — waiting for re-bootstrap");

            // Phase 4: Wait for the node to re-bootstrap via gateway and
            // converge state. connection_maintenance runs on 5s tick, so
            // the gateway fallback should trigger quickly.
            tokio::time::sleep(Duration::from_secs(30)).await;
            tracing::info!("Post-heal convergence period complete");

            Ok(())
        },
    );

    assert!(
        result.is_ok(),
        "Isolated-node re-bootstrap simulation failed: {:?}",
        result.err()
    );

    // Check convergence — if the isolated node re-bootstrapped, it should
    // have re-subscribed and received contract state updates.
    let convergence = rt.block_on(async { check_convergence_from_logs(&logs_handle).await });
    tracing::info!(
        "Isolated-rebootstrap: {} converged, {} diverged",
        convergence.converged.len(),
        convergence.diverged.len()
    );

    // Hard assertion: the isolated node must have re-bootstrapped and
    // converged state. Without the gateway bootstrap fallback, the node
    // stays permanently disconnected and contracts diverge.
    assert!(
        convergence.diverged.is_empty(),
        "Isolated node failed to re-bootstrap: {} contracts diverged (expected 0). \
         This indicates the gateway bootstrap fallback in connection_maintenance \
         did not recover the node after partition heal.",
        convergence.diverged.len()
    );

    // Run anomaly detection
    let report = rt.block_on(async {
        let logs = logs_handle.lock().await;
        let verifier = freenet::tracing::StateVerifier::from_events(logs.clone());
        verifier.verify()
    });

    tracing::info!(
        "=== ISOLATED-REBOOTSTRAP ANOMALY REPORT: {} events, {} state, {} contracts, {} anomalies ===",
        report.total_events,
        report.state_events,
        report.contracts_analyzed,
        report.anomalies.len()
    );

    let divergences = report.divergences();
    let stale = report.stale_peers();

    tracing::warn!("  divergences={}, stale={}", divergences.len(), stale.len(),);

    for (i, anomaly) in report.anomalies.iter().enumerate() {
        tracing::debug!("  anomaly[{}] = {:?}", i, anomaly);
    }
}

// =============================================================================
// Direct Runner: Churn Resilience
// =============================================================================

/// Verifies that the direct runner can complete a simulation with node churn enabled.
///
/// Runs 2 gateways + 8 nodes with 20% crash rate, 200ms ticks, 500ms recovery.
/// Asserts the simulation completes without panicking and produces events.
#[test]
fn test_direct_runner_churn() {
    use freenet::dev_tool::ChurnConfig;

    const SEED: u64 = 0xC1_0055_FEED;

    setup_deterministic_state(SEED);

    let rt = create_runtime();

    let mut sim = rt.block_on(async {
        SimNetwork::new(
            "test-churn",
            2,  // gateways
            8,  // nodes
            10, // ring_max_htl
            5,  // rnd_if_htl_above
            15, // max_connections
            3,  // min_connections
            SEED,
        )
        .await
    });

    sim.with_churn(ChurnConfig {
        crash_probability: 0.20,
        tick_interval: Duration::from_millis(200),
        recovery_delay: Duration::from_millis(500),
        max_simultaneous_crashes: Some(2),
        permanent_crash_rate: 0.05,
        // Must be >= connection wait (2s) + buffer to avoid crashing nodes during startup
        warmup_delay: Duration::from_secs(5),
    });

    let logs_handle = sim.event_logs_handle();

    drop(rt);

    sim.run_simulation_direct::<rand::rngs::SmallRng>(
        SEED,
        10, // max_contract_num
        30, // iterations
        Duration::from_millis(200),
    )
    .expect("Direct simulation with churn should complete without panic");

    // Verify events were produced despite churn
    let rt = create_runtime();
    let event_count = rt.block_on(async {
        let logs = logs_handle.lock().await;
        logs.len()
    });

    assert!(
        event_count > 0,
        "Simulation with churn should produce events, got 0"
    );

    tracing::info!(
        "CHURN TEST PASSED: {} events produced with node churn enabled",
        event_count
    );
}

// =============================================================================
// Unhealthy Peer Eviction Tests
// =============================================================================

/// Verifies that partitioned (dead) peers are eventually evicted from the ring
/// via the PeerHealthTracker, and that zombie transports don't accumulate.
///
/// Creates a network, partitions one node, runs contract operations so the
/// partitioned node accumulates routing failures, then checks that the network
/// continues to function (the partitioned node doesn't permanently block routing).
#[test_log::test]
fn test_unhealthy_peer_eviction() {
    use freenet::dev_tool::NodeLabel;
    use freenet::simulation::Partition;

    const SEED: u64 = 0xDEAD_BEEF_0042;
    const NETWORK_NAME: &str = "unhealthy-eviction";

    setup_deterministic_state(SEED);
    let rt = create_runtime();

    let (sim, logs_handle, node_addrs) = rt.block_on(async {
        let sim = SimNetwork::new(
            NETWORK_NAME,
            1, // 1 gateway
            4, // 4 nodes (5 total)
            7,
            3,
            10,
            2,
            SEED,
        )
        .await;
        let logs_handle = sim.event_logs_handle();
        let node_addrs: HashMap<NodeLabel, SocketAddr> = sim.all_node_addresses().clone();
        (sim, logs_handle, node_addrs)
    });

    // Pick one non-gateway node to partition
    let victim_addr: SocketAddr = *node_addrs
        .iter()
        .find(|(label, _)| label.is_node())
        .expect("Need at least 1 non-gateway node")
        .1;

    let all_addrs: HashSet<_> = node_addrs.values().copied().collect();
    let victim_set: HashSet<_> = [victim_addr].into_iter().collect();
    let healthy_set: HashSet<_> = all_addrs.difference(&victim_set).copied().collect();

    let network_name = NETWORK_NAME.to_string();

    // Use long event_wait to give virtual time for health checks to fire.
    // 150 iterations × 2s = 300s virtual time (5 min) — enough for health check interval.
    let result = sim.run_simulation::<rand::rngs::SmallRng, _, _>(
        SEED,
        5,                        // contracts
        150,                      // iterations
        Duration::from_secs(600), // simulation_duration (10 min headroom)
        Duration::from_secs(2),   // event_wait (2s between events)
        move || async move {
            // Phase 1: Partition the victim node from everyone else
            tracing::info!("Partitioning victim node: {}", victim_addr);

            if let Some(injector) = freenet::dev_tool::get_fault_injector(&network_name) {
                let mut state = injector.lock().unwrap();
                let partition = Partition::new(victim_set, healthy_set).permanent(0);
                state.config.add_partition(partition);
            }

            // Phase 2: Let the network operate with the partitioned node.
            // The healthy nodes should accumulate routing failures when trying
            // to use the partitioned node, and eventually the health tracker
            // should flag it as unhealthy.
            tokio::time::sleep(Duration::from_secs(120)).await;
            tracing::info!("Partition active for 120s virtual time");

            Ok(())
        },
    );

    assert!(
        result.is_ok(),
        "Unhealthy-peer eviction simulation failed: {:?}",
        result.err()
    );

    // Verify the network produced events (it didn't deadlock)
    let event_count = rt.block_on(async { logs_handle.lock().await.len() });
    assert!(event_count > 0, "Simulation should produce events, got 0");

    // Run anomaly detection
    let report = rt.block_on(async {
        let logs = logs_handle.lock().await;
        let verifier = freenet::tracing::StateVerifier::from_events(logs.clone());
        verifier.verify()
    });

    tracing::info!(
        "=== UNHEALTHY EVICTION REPORT: {} events, {} state, {} contracts, {} anomalies ===",
        report.total_events,
        report.state_events,
        report.contracts_analyzed,
        report.anomalies.len()
    );
}

// =============================================================================
// Readiness Gating Tests
// =============================================================================

/// Verifies that readiness gating (min_ready_connections=3) does not block
/// contract operations once the network has stabilized.
///
/// Runs 1 gateway + 4 nodes with readiness gating enabled. If the periodic
/// re-broadcast and optimistic timeout work correctly, all nodes should
/// eventually become ready and contract operations should succeed.
#[test]
fn test_readiness_gating_production_config() {
    const SEED: u64 = 0xBEAD_10A7_E001;

    setup_deterministic_state(SEED);

    let rt = create_runtime();

    let mut sim = rt.block_on(async {
        SimNetwork::new(
            "test-readiness-gating",
            1,  // gateways
            4,  // nodes
            10, // ring_max_htl
            5,  // rnd_if_htl_above
            10, // max_connections
            3,  // min_connections
            SEED,
        )
        .await
    });

    sim.with_readiness_gating(3);

    let logs_handle = sim.event_logs_handle();

    drop(rt);

    sim.run_simulation_direct::<rand::rngs::SmallRng>(
        SEED,
        5,  // max_contract_num
        20, // iterations (enough for contract creation + operations)
        Duration::from_millis(200),
    )
    .expect("Simulation with readiness gating should complete without panic");

    let rt = create_runtime();
    let event_count = rt.block_on(async {
        let logs = logs_handle.lock().await;
        logs.len()
    });

    assert!(
        event_count > 0,
        "Simulation with readiness gating should produce events, got 0"
    );

    tracing::info!(
        "READINESS GATING TEST PASSED: {} events produced with gating(3)",
        event_count
    );
}

/// Same as `test_readiness_gating_production_config` but with 5% message loss.
///
/// This validates that the periodic ReadyState re-broadcast and optimistic
/// timeout handle lost messages gracefully.
#[test]
fn test_readiness_gating_with_message_loss() {
    use freenet::simulation::FaultConfig;

    const SEED: u64 = 0xBEAD_10A7_E002;

    setup_deterministic_state(SEED);

    let rt = create_runtime();

    let mut sim = rt.block_on(async {
        SimNetwork::new(
            "test-readiness-gating-loss",
            1,  // gateways
            4,  // nodes
            10, // ring_max_htl
            5,  // rnd_if_htl_above
            10, // max_connections
            3,  // min_connections
            SEED,
        )
        .await
    });

    sim.with_readiness_gating(3);
    sim.with_fault_injection(FaultConfig::builder().message_loss_rate(0.05).build());

    let logs_handle = sim.event_logs_handle();

    drop(rt);

    sim.run_simulation_direct::<rand::rngs::SmallRng>(
        SEED,
        5,  // max_contract_num
        25, // iterations (extra budget for retries under loss)
        Duration::from_millis(200),
    )
    .expect("Simulation with readiness gating + message loss should complete");

    let rt = create_runtime();
    let event_count = rt.block_on(async {
        let logs = logs_handle.lock().await;
        logs.len()
    });

    assert!(
        event_count > 0,
        "Simulation with readiness gating + message loss should produce events, got 0"
    );

    tracing::info!(
        "READINESS GATING + MESSAGE LOSS TEST PASSED: {} events",
        event_count
    );
}

// =============================================================================
// Gateway Version Probe Tests
// =============================================================================

/// Verify that the periodic gateway version probe does not break
/// connection_maintenance. Actual version mismatch detection is not testable
/// in simulation (all nodes share the same PROTOC_VERSION). (#3677)
#[test]
fn test_gateway_version_probe_fires() {
    const SEED: u64 = 0x6A7E_7AE9_0001;

    setup_deterministic_state(SEED);

    let rt = create_runtime();

    let sim = rt.block_on(async {
        SimNetwork::new(
            "test-gw-version-probe",
            1,  // gateways
            3,  // nodes
            7,  // ring_max_htl
            3,  // rnd_if_htl_above
            10, // max_connections
            2,  // min_connections
            SEED,
        )
        .await
    });

    let logs_handle = sim.event_logs_handle();

    drop(rt);

    sim.run_simulation_direct::<rand::rngs::SmallRng>(SEED, 3, 30, Duration::from_secs(1))
        .expect("Simulation should complete without panic");

    let rt = create_runtime();
    let unique_connect_txs = rt.block_on(async {
        let logs = logs_handle.lock().await;
        // Count distinct Connect transactions, not raw events. Each CONNECT op
        // emits multiple log entries (sent/received/accepted) that share a Tx.
        // Counting unique Tx values gives us the number of *operations*, which
        // is what the probe code path actually generates.
        let mut seen = std::collections::HashSet::new();
        for log in logs.iter() {
            if log.kind.variant_name() == "Connect" {
                seen.insert(log.tx);
            }
        }
        seen.len()
    });

    // Bootstrap baseline: each of the 3 non-gateway nodes initiates an initial
    // CONNECT to the gateway, so the bootstrap floor is 3 unique transactions.
    // (Bootstrap may also retry on failure, but that just adds to the floor.)
    //
    // Probe budget: cfg(test) sets GATEWAY_VERSION_PROBE_INTERVAL to 10s with
    // a random initial delay in [0, 10)s and ±20% jitter per cycle. Over 30s
    // of sim time, each non-gateway can fire 1–3 probes, so 3 nodes contribute
    // an additional 3–9 unique CONNECT transactions on top of bootstrap.
    //
    // Asserting > 6 (bootstrap floor + at least one probe per node) is robust
    // against the random initial delay while still proving probes fire.
    assert!(
        unique_connect_txs > 6,
        "Expected bootstrap (~3) + at least 4 probe CONNECTs, got {unique_connect_txs}"
    );
}

// =============================================================================
// CONNECT Acceptor Diversity: Joiner succeeds despite NAT-blocked acceptor
// =============================================================================

/// Tests that a joiner can connect to the network even when partitioned from
/// some peers (simulating NAT hole-punch failure).
///
/// ## Scenario
/// 1. Create a 1-gateway + 5-node network
/// 2. Let the network bootstrap normally (all nodes connected)
/// 3. Inject a partition between 2 specific non-gateway nodes
///    (simulates NAT incompatibility between those peers)
/// 4. Continue operating — peers in the partitioned pair can't reach each other
///    but should still connect via other paths using ConnectFailed re-routing
/// 5. Verify the simulation completes without hanging (nodes don't get stuck
///    retrying the same unreachable acceptor forever)
///
/// Without ConnectFailed + jitter, a joiner partitioned from its nearest
/// acceptor would retry the same peer indefinitely. With the fix, ConnectFailed
/// propagates through the relay chain, causing re-routing to different acceptors,
/// and location jitter explores different ring regions on retry.
#[test_log::test]
fn test_connect_despite_nat_partition() {
    use freenet::dev_tool::NodeLabel;
    use freenet::simulation::Partition;

    const SEED: u64 = 0xC0DE_FA11_0001;
    const NETWORK_NAME: &str = "nat-partition";

    setup_deterministic_state(SEED);
    let rt = create_runtime();

    let (sim, logs_handle, node_addrs) = rt.block_on(async {
        let sim = SimNetwork::new(
            NETWORK_NAME,
            1,  // 1 gateway
            5,  // 5 regular nodes (6 total)
            7,  // max HTL
            3,  // rnd_if_htl_above
            10, // max_connections
            2,  // min_connections
            SEED,
        )
        .await;
        let logs_handle = sim.event_logs_handle();
        let node_addrs: HashMap<NodeLabel, std::net::SocketAddr> = sim.all_node_addresses().clone();
        (sim, logs_handle, node_addrs)
    });

    // Pick two non-gateway nodes to partition from each other.
    // This simulates NAT incompatibility: hole-punch between these two peers
    // always fails, but both can reach the rest of the network.
    let mut non_gw_nodes: Vec<_> = node_addrs
        .iter()
        .filter(|(label, _)| label.is_node())
        .collect();
    non_gw_nodes.sort_by_key(|(label, _)| label.number());

    assert!(
        non_gw_nodes.len() >= 2,
        "Need at least 2 non-gateway nodes for partition"
    );

    let node_a_addr = *non_gw_nodes[0].1;
    let node_b_addr = *non_gw_nodes[1].1;

    let side_a: HashSet<std::net::SocketAddr> = [node_a_addr].into_iter().collect();
    let side_b: HashSet<std::net::SocketAddr> = [node_b_addr].into_iter().collect();

    let network_name = NETWORK_NAME.to_string();

    tracing::info!(
        node_a = %node_a_addr,
        node_b = %node_b_addr,
        "Will inject NAT partition between two nodes"
    );

    // Inject the partition from the test closure (like test_partition_heal_convergence)
    // to preserve the properly-configured fault injector with VirtualTime.
    // The partition is injected immediately and kept permanent — simulating
    // persistent NAT incompatibility between two specific peers.
    let result = sim.run_simulation::<rand::rngs::SmallRng, _, _>(
        SEED,
        3,                          // contracts
        100,                        // iterations (generous budget)
        Duration::from_secs(120),   // simulation_duration
        Duration::from_millis(500), // event_wait
        move || async move {
            // Inject the permanent partition immediately
            if let Some(injector) = freenet::dev_tool::get_fault_injector(&network_name) {
                let mut state = injector.lock().unwrap();
                let partition = Partition::new(side_a, side_b).permanent(0);
                state.config.add_partition(partition);
                tracing::info!("NAT partition injected between two nodes");
            }

            // Let the network operate with the partition active
            tokio::time::sleep(Duration::from_secs(30)).await;
            tracing::info!("NAT partition test: observation period complete");
            Ok(())
        },
    );

    assert!(
        result.is_ok(),
        "NAT partition simulation failed (nodes may have gotten stuck retrying \
         unreachable acceptor): {:?}",
        result.err()
    );

    // Check convergence — with the partition, some divergence is expected
    // between the two partitioned nodes, but the network should still function.
    let convergence = rt.block_on(async { check_convergence_from_logs(&logs_handle).await });
    tracing::info!(
        "NAT partition: {} converged, {} diverged",
        convergence.converged.len(),
        convergence.diverged.len()
    );

    // Run anomaly detection
    let report = rt.block_on(async {
        let logs = logs_handle.lock().await;
        let verifier = freenet::tracing::StateVerifier::from_events(logs.clone());
        verifier.verify()
    });

    tracing::info!(
        "=== NAT PARTITION ANOMALY REPORT: {} events, {} state, {} contracts, {} anomalies ===",
        report.total_events,
        report.state_events,
        report.contracts_analyzed,
        report.anomalies.len()
    );

    // Log anomaly details for debugging
    let divergences = report.divergences();
    let stale = report.stale_peers();
    tracing::warn!("  divergences={}, stale={}", divergences.len(), stale.len(),);

    for (i, anomaly) in report.anomalies.iter().enumerate() {
        tracing::debug!("  anomaly[{}] = {:?}", i, anomaly);
    }

    // The simulation completing without timeout is the primary assertion:
    // without ConnectFailed + jitter, partitioned joiners would retry the same
    // unreachable acceptor forever, eventually causing the simulation to hang
    // or timeout.
}

// =============================================================================
// Connection Growth Stall Regression Test (PRs #3408, #3398, #3396, #3380)
// =============================================================================

/// Regression test: connection growth stall observed on live Freenet network.
///
/// **Background:**
/// Nodes get ~10 connections (including 2 gateway transient connections) but never
/// grow beyond that, continuously reconnecting to gateways. Root causes:
///
/// - `BOOTSTRAP_THRESHOLD` was hardcoded to 4, stopping gateway-directed CONNECTs
///   far below `min_connections`. Fixed: use `min_connections` as the threshold.
/// - `acquire_new` returning None (no routing candidates) incorrectly put the
///   target location in backoff. Fixed: no backoff on routing capacity failure.
/// - `should_accept` used inflated `total_conn` (including pending reservations)
///   for the min_connections check, pushing nodes into the topology evaluator
///   prematurely. Fixed: use actual open connection count for min threshold.
/// - CONNECT exclusion was absolute (3 failures = banned 30min). Fixed: max 50%
///   of ring peers excluded at once (#3408).
/// - Distance-based fallback never evicted never-succeeded peers below
///   min_connections (#3398).
/// - GC-expired CONNECT forwards silently blamed the acceptor (#3396/#3380).
///
/// **What this test verifies (post-fix behavior):**
///
/// 1. Median connections exceed old BOOTSTRAP_THRESHOLD=4 after 10 virtual minutes.
/// 2. At least some nodes reach min_connections.
/// 3. Nodes form connections to non-gateway peers (proves multi-hop CONNECT).
/// 4. Under 20% message loss (simulating NAT hole-punch failures), no death spiral.
///
/// **Topology:** 2 gateways + 15 nodes, min_connections=5, max_connections=10.
/// min_connections=5 is above the old hardcoded BOOTSTRAP_THRESHOLD=4, so
/// this test would fail without the connect.rs fix (nodes would stall at 4).
/// Sized for CI: 600s virtual time ≈ 70s wall time via `let_network_run`.
#[test_log::test(tokio::test(flavor = "current_thread"))]
async fn test_connection_growth_stall_regression() {
    use freenet::dev_tool::NodeLabel;
    use freenet::simulation::FaultConfig;

    const SEED: u64 = 0x3408_3398_0001;
    const NETWORK_NAME: &str = "connection-growth-stall";
    const GATEWAYS: usize = 2;
    const NODES: usize = 15;
    const RING_MAX_HTL: usize = 7;
    const RND_IF_HTL_ABOVE: usize = 3;
    const MAX_CONN: usize = 10;
    const MIN_CONN: usize = 5;

    tracing::info!("=== Connection Growth Stall Regression Test ===");
    tracing::info!("Verifies fixes: #3408, #3398, #3396, #3380");

    setup_deterministic_state(SEED);

    let mut sim = SimNetwork::new(
        NETWORK_NAME,
        GATEWAYS,
        NODES,
        RING_MAX_HTL,
        RND_IF_HTL_ABOVE,
        MAX_CONN,
        MIN_CONN,
        SEED,
    )
    .await;

    sim.with_start_backoff(Duration::from_millis(50));

    // Start all nodes. max_contract_num=0, iterations=0: topology-only test.
    let _handles = sim
        .start_with_rand_gen::<rand::rngs::SmallRng>(SEED, 0, 0)
        .await;

    // -------------------------------------------------------------------------
    // Phase 1: Let the network form connections over 20 virtual minutes
    // -------------------------------------------------------------------------
    // 20 minutes gives topology-aware pruning enough time to converge.
    // The algorithm is more selective about connections (gap-based targeting,
    // composite scoring), so it trades speed for distribution quality.
    tracing::info!("Phase 1: Connection growth — 20 virtual minutes, no faults");
    let_network_run(&mut sim, Duration::from_secs(1200)).await;

    // Collect per-node connection counts
    let mut node_counts: Vec<usize> = (0..NODES)
        .filter_map(|i| {
            let label = NodeLabel::node(NETWORK_NAME, i);
            sim.connection_count(&label)
        })
        .collect();
    node_counts.sort_unstable();

    let num_sampled = node_counts.len();
    assert!(num_sampled > 0, "No connection_managers available");

    let median_conn = node_counts[num_sampled / 2];
    let nodes_above_min = node_counts.iter().filter(|&&c| c >= MIN_CONN).count();
    let fraction_above_min = nodes_above_min as f64 / num_sampled as f64;

    tracing::info!("Phase 1 connection counts: {:?}", node_counts);
    tracing::info!(
        "Median={}, nodes at min_connections={}/{} ({:.0}%)",
        median_conn,
        nodes_above_min,
        num_sampled,
        fraction_above_min * 100.0
    );

    // Check for non-gateway peer connections (proves multi-hop CONNECT forwarding)
    let connectivity = sim.node_connectivity();
    let mut nodes_with_peer_connections = 0usize;
    for (label, (_key, conns)) in &connectivity {
        if !label.is_gateway() {
            let has_non_gw_conn = conns.keys().any(|peer| !peer.is_gateway());
            if has_non_gw_conn {
                nodes_with_peer_connections += 1;
            }
        }
    }

    tracing::info!(
        "Nodes with non-gateway peer connections: {}/{}",
        nodes_with_peer_connections,
        NODES
    );

    // ASSERTION 1: Median connections must be close to MIN_CONN.
    // With dynamic concurrent limits and routing through connected gateways,
    // the median should approach min_connections even in a small simulation.
    // Threshold is MIN_CONN - 1 to allow for topology-aware pruning tradeoffs.
    assert!(
        median_conn >= MIN_CONN - 1,
        "Connection growth stall: median={} must be >= {} (MIN_CONN - 1). \
         Counts: {:?}. Seed: 0x{:X}",
        median_conn,
        MIN_CONN - 1,
        node_counts,
        SEED
    );

    // ASSERTION 2: At least 10% of nodes reached min_connections.
    // In a 15-node simulation with 20 virtual minutes, topology-aware pruning
    // and non-deterministic timing cause run-to-run variance (15%–31% observed).
    // 10% threshold catches severe growth stalls while tolerating this variance.
    assert!(
        fraction_above_min >= 0.10,
        "Too few nodes reached min_connections: {:.0}% ({}/{}) — expected >= 10%. \
         Counts: {:?}. Seed: 0x{:X}",
        fraction_above_min * 100.0,
        nodes_above_min,
        num_sampled,
        node_counts,
        SEED
    );

    // ASSERTION 3: Multi-hop CONNECT forwarding is widespread.
    // With routing through connected gateways, the majority of nodes should
    // have non-gateway peer connections, not just a token few.
    let peer_conn_fraction = nodes_with_peer_connections as f64 / NODES as f64;
    assert!(
        peer_conn_fraction >= 0.50,
        "Only {:.0}% of nodes have non-gateway peer connections (expected >= 50%). \
         CONNECT forwarding is insufficient. Seed: 0x{:X}",
        peer_conn_fraction * 100.0,
        SEED
    );

    // -------------------------------------------------------------------------
    // Phase 2: 20% message loss simulating NAT hole-punch failures — 3 minutes
    // -------------------------------------------------------------------------
    tracing::info!("Phase 2: NAT failure simulation — 20% message loss for 1 min");
    sim.with_fault_injection(FaultConfig::builder().message_loss_rate(0.20).build());

    let_network_run(&mut sim, Duration::from_secs(60)).await;

    // Re-inspect connection counts after faults
    let mut node_counts_after: Vec<usize> = (0..NODES)
        .filter_map(|i| {
            let label = NodeLabel::node(NETWORK_NAME, i);
            sim.connection_count(&label)
        })
        .collect();
    node_counts_after.sort_unstable();

    let median_after = node_counts_after
        .get(node_counts_after.len() / 2)
        .copied()
        .unwrap_or(0);
    let nodes_isolated = node_counts_after.iter().filter(|&&c| c == 0).count();
    let fraction_isolated = nodes_isolated as f64 / NODES as f64;

    tracing::info!(
        "Phase 2 connection counts after NAT failures: {:?}",
        node_counts_after
    );
    tracing::info!(
        "Median={}, isolated={}/{} ({:.0}%)",
        median_after,
        nodes_isolated,
        NODES,
        fraction_isolated * 100.0
    );

    // ASSERTION 4: No death spiral — median must stay near pre-fault level.
    // With 20% message loss for only 60s, a well-connected network should
    // retain most connections. Threshold is MIN_CONN - 2 to allow some churn.
    assert!(
        median_after >= MIN_CONN.saturating_sub(2).max(2),
        "Death spiral: median connections after NAT failures = {} (expected >= {}). \
         Counts: {:?}. Seed: 0x{:X}",
        median_after,
        MIN_CONN.saturating_sub(2).max(2),
        node_counts_after,
        SEED
    );

    // ASSERTION 5: No nodes fully isolated after brief packet loss.
    assert!(
        fraction_isolated < 0.10,
        "{:.0}% of nodes isolated after NAT failures (threshold: 10%). \
         Counts: {:?}. Seed: 0x{:X}",
        fraction_isolated * 100.0,
        node_counts_after,
        SEED
    );

    tracing::info!(
        "PASSED: Phase1[median={}, above_min={}/{}, peer_conns={}/{}] \
         Phase2[median={}, isolated={}/{}]",
        median_conn,
        nodes_above_min,
        num_sampled,
        nodes_with_peer_connections,
        NODES,
        median_after,
        nodes_isolated,
        NODES
    );
}

// =============================================================================
// GET Routing Regression Tests
// =============================================================================

/// Regression test for #3356 / #3423: GET fails with EmptyRing when all peers
/// fail readiness gating.
///
/// Sets `relay_ready_connections` higher than network size so `is_self_ready()`
/// returns false on every node → no node ever sends `ReadyState` to peers →
/// `is_peer_ready()` returns false for all peers. The 60-second optimistic
/// timeout (`OPTIMISTIC_READY_TIMEOUT`) uses real wall-clock `Instant::now()`,
/// not virtual time, so it cannot fire during this test (~0.5s wall-clock).
///
/// Before the fix, `k_closest_potentially_hosting` returned empty → GET failed
/// with EmptyRing. After the fix, it falls back to using not-yet-ready peers.
/// Verified: this test FAILS without the fallback (confirmed by reverting the
/// fix and observing the assertion fire).
///
/// Scenario:
///   1. Gateway PUTs a contract
///   2. Node 1 GETs the contract with `return_contract_code=true`
///   3. Assert node 1's storage contains the contract state
#[test_log::test]
fn test_get_succeeds_despite_readiness_gating() {
    use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation};

    const SEED: u64 = 0xAE7F_1A00_0001;
    const NETWORK_NAME: &str = "get-readiness-fallback";

    GlobalTestMetrics::reset();
    setup_deterministic_state(SEED);

    let rt = create_runtime();

    let (mut sim, logs_handle) = rt.block_on(async {
        let sim = SimNetwork::new(
            NETWORK_NAME,
            1,  // gateways
            3,  // nodes
            7,  // ring_max_htl
            3,  // rnd_if_htl_above
            10, // max_connections
            2,  // min_connections
            SEED,
        )
        .await;
        let logs_handle = sim.event_logs_handle();
        (sim, logs_handle)
    });

    // Set readiness gating higher than network size (1 gateway + 3 nodes = 4 total).
    // No node can ever have this many ready connections, so ReadyState is never sent,
    // and is_peer_ready() returns false for all peers.
    let total_nodes = 1 + 3; // gateways + nodes
    sim.with_readiness_gating(total_nodes + 1);

    let contract = SimOperation::create_test_contract(0xAE);
    let contract_id = *contract.key().id();
    let contract_key = contract.key();

    let operations = vec![
        // 1. Gateway PUTs the contract
        ScheduledOperation::new(
            NodeLabel::gateway(NETWORK_NAME, 0),
            SimOperation::Put {
                contract: contract.clone(),
                state: vec![1, 2, 3, 4],
                subscribe: false,
            },
        ),
        // 2. Node 1 GETs the contract (with contract code)
        ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, 1),
            SimOperation::Get {
                contract_id,
                return_contract_code: true,
                subscribe: false,
            },
        ),
    ];

    let result = sim.run_controlled_simulation(
        SEED,
        operations,
        Duration::from_secs(120),
        Duration::from_secs(30),
    );

    assert!(
        result.turmoil_result.is_ok(),
        "GET readiness fallback simulation failed: {:?}",
        result.turmoil_result.err()
    );

    // Verify that node 1 has the contract state despite readiness gating
    let node1_label = NodeLabel::node(NETWORK_NAME, 1);
    let node1_storage = result
        .node_storages
        .get(&node1_label)
        .expect("node 1 should have a storage handle");
    let node1_state = node1_storage.get_stored_state(&contract_key);

    assert!(
        node1_state.is_some(),
        "Node 1 should have contract state after GET, but storage is empty. \
         This indicates k_closest_potentially_hosting returned empty due to \
         readiness gating (the bug from #3356/#3423)."
    );

    // Run StateVerifier for anomaly detection (per testing.md)
    let rt = create_runtime();
    let report = rt.block_on(async {
        let logs = logs_handle.lock().await;
        let verifier = freenet::tracing::StateVerifier::from_events(logs.clone());
        verifier.verify()
    });
    tracing::info!(
        "Anomaly report: {} anomalies across {} contracts",
        report.anomalies.len(),
        report.contracts_analyzed
    );

    tracing::info!(
        "test_get_succeeds_despite_readiness_gating PASSED: \
         node 1 has contract state, {} events analyzed",
        report.total_events
    );
}

/// Regression test for #3431: GET routing exhaustion when contract is cached at few nodes.
///
/// With low HTL, PUT only caches the contract at a handful of nodes along the
/// route. GET from nodes whose connected peers don't overlap with caching nodes
/// fails with "No other peers found" → NotFound.
///
/// Scenario:
///   1. Gateway PUTs contract (HTL=4 → only ~5 nodes cache code+state)
///   2. 12 nodes subscribe, gateway sends UPDATE (state propagation)
///   3. All 15 nodes GET with `fetch_contract=true`
///   4. Assert every node gets the contract state
///
#[test_log::test]
fn test_get_routing_coverage_low_htl() {
    use std::sync::atomic::Ordering;

    use freenet::dev_tool::{
        GET_RELAY_DRIVER_CALL_COUNT, NodeLabel, RELAY_GET_ROUTE_EVENT_COUNT,
        RELAY_PUT_DRIVER_CALL_COUNT, RELAY_PUT_ROUTE_EVENT_COUNT,
        RELAY_SUBSCRIBE_DRIVER_CALL_COUNT, RELAY_SUBSCRIBE_ROUTE_EVENT_COUNT, ScheduledOperation,
        SimOperation, register_crdt_contract,
    };

    // Seed updated: per-peer acceptor reliability scoring replaces binary
    // exclusion (PR #3659), changing the CONNECT routing code path and
    // Turmoil scheduling. Previous seed: 0xC0DE_B0CA_0032 (PR #3621).
    const SEED: u64 = 0xC0DE_B0CA_0032;
    const NETWORK_NAME: &str = "get-routing-coverage";

    GlobalTestMetrics::reset();
    setup_deterministic_state(SEED);

    // Baseline the relay driver counter so we can assert that the
    // dispatch gate in handle_pure_network_message_v1 actually routed
    // relay hops through the task-per-tx driver (not the legacy
    // handle_op_request fallthrough). This is the behavioral companion
    // to the source-scrape tests in op_ctx_task.rs that only pin the
    // dispatch-block SHAPE. #1454 phase 5 / #3883.
    let relay_baseline = GET_RELAY_DRIVER_CALL_COUNT.load(Ordering::SeqCst);
    let relay_put_baseline = RELAY_PUT_DRIVER_CALL_COUNT.load(Ordering::SeqCst);
    let relay_subscribe_baseline = RELAY_SUBSCRIBE_DRIVER_CALL_COUNT.load(Ordering::SeqCst);

    // Baselines for the relay-hop routing-event counters. Each fires when
    // a relay forwards a downstream peer's response and feeds the result
    // into the local Router via record_relay_route_event. Without these
    // hooks the per-peer dashboard panels stay empty and the failure-
    // probability model is trained only on originator-side events.
    // The same workload that exercises the relay drivers above must
    // also exercise these counters — drivers running without route-event
    // emission would silently regress router quality.
    let relay_get_route_event_baseline = RELAY_GET_ROUTE_EVENT_COUNT.load(Ordering::SeqCst);
    let relay_put_route_event_baseline = RELAY_PUT_ROUTE_EVENT_COUNT.load(Ordering::SeqCst);
    let relay_subscribe_route_event_baseline =
        RELAY_SUBSCRIBE_ROUTE_EVENT_COUNT.load(Ordering::SeqCst);

    let rt = create_runtime();

    let num_nodes = 15;

    let (sim, logs_handle) = rt.block_on(async {
        let sim = SimNetwork::new(
            NETWORK_NAME,
            1,         // gateways
            num_nodes, // nodes — large enough that PUT won't reach all
            3,         // ring_max_htl — LOW to limit PUT propagation depth
            1,         // rnd_if_htl_above
            7,         // max_connections — 7 ensures robust GET routing across topology variations
            3,         // min_connections
            SEED,
        )
        .await;
        let logs_handle = sim.event_logs_handle();
        (sim, logs_handle)
    });

    let contract = SimOperation::create_test_contract(0xC0);
    let contract_id = *contract.key().id();
    let contract_key = contract.key();
    register_crdt_contract(contract_id);

    let mut operations = vec![
        // Gateway PUTs with subscribe (HTL=4 → caches at ~5 nodes)
        ScheduledOperation::new(
            NodeLabel::gateway(NETWORK_NAME, 0),
            SimOperation::Put {
                contract: contract.clone(),
                state: SimOperation::create_crdt_state(1, 0xC0),
                subscribe: true,
            },
        ),
    ];

    // Subscribe nodes 1-12
    for i in 1..=12 {
        operations.push(ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, i),
            SimOperation::Subscribe { contract_id },
        ));
    }

    // Gateway updates — propagates state to subscribers
    operations.push(ScheduledOperation::new(
        NodeLabel::gateway(NETWORK_NAME, 0),
        SimOperation::Update {
            key: contract_key,
            data: SimOperation::create_crdt_state(42, 0xC0),
        },
    ));

    // Every node GETs with fetch_contract=true
    for i in 1..=num_nodes {
        operations.push(ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, i),
            SimOperation::Get {
                contract_id,
                return_contract_code: true,
                subscribe: false,
            },
        ));
    }

    let result = sim.run_controlled_simulation(
        SEED,
        operations,
        Duration::from_secs(300),
        Duration::from_secs(90),
    );

    assert!(
        result.turmoil_result.is_ok(),
        "Simulation failed: {:?}",
        result.turmoil_result.err()
    );

    // Assert every node has the contract state after GET
    let mut nodes_without_state = Vec::new();

    for i in 1..=num_nodes {
        let label = NodeLabel::node(NETWORK_NAME, i);
        let storage = result
            .node_storages
            .get(&label)
            .unwrap_or_else(|| panic!("node {i} should have a storage handle"));
        if storage.get_stored_state(&contract_key).is_none() {
            nodes_without_state.push(format!("node-{i}"));
        }
    }

    assert!(
        nodes_without_state.is_empty(),
        "GET routing exhaustion: {} nodes failed to get contract state \
         (contract only cached at ~5 nodes due to HTL=4). \
         Failed nodes: {:?}. See #3431.",
        nodes_without_state.len(),
        nodes_without_state
    );

    // Behavioral companion to the dispatch-gate source-scrape tests in
    // op_ctx_task.rs: prove the relay task-per-tx driver actually ran
    // on at least one intermediate hop during the 15-node / HTL=3 /
    // 15 parallel GETs workload. If this assertion ever starts failing,
    // something has silently regressed the dispatch split in
    // handle_pure_network_message_v1 — inbound relay Requests are going
    // through the legacy handle_op_request path again. #1454 phase 5 / #3883.
    let relay_after = GET_RELAY_DRIVER_CALL_COUNT.load(Ordering::SeqCst);
    let relay_delta = relay_after.saturating_sub(relay_baseline);
    assert!(
        relay_delta > 0,
        "RELAY_DRIVER_CALL_COUNT did not advance during the test — the \
         relay task-per-tx driver never ran. With {num_nodes} nodes, HTL=3, \
         and ~5 caching nodes, every non-caching requester should hit at \
         least one relay hop. Dispatch gate in \
         handle_pure_network_message_v1 has likely regressed. Baseline: {relay_baseline}, after: {relay_after}."
    );

    // Behavioral companion for relay PUT slice A (#1454 phase 5
    // follow-up, PR #3917): the gateway PUT at HTL=3 should reach at
    // least one relay peer via start_relay_put. If this never advances,
    // the PUT dispatch gate in handle_pure_network_message_v1 has
    // regressed — inbound relay PutMsg::Request is going through the
    // legacy handle_op_request path again.
    let relay_put_after = RELAY_PUT_DRIVER_CALL_COUNT.load(Ordering::SeqCst);
    let relay_put_delta = relay_put_after.saturating_sub(relay_put_baseline);
    assert!(
        relay_put_delta > 0,
        "RELAY_PUT_DRIVER_CALL_COUNT did not advance during the test — \
         the relay PUT task-per-tx driver never ran. Gateway PUT at HTL=3 \
         in a {num_nodes}-node network must traverse at least one relay hop. \
         Dispatch gate for PUT has likely regressed. \
         Baseline: {relay_put_baseline}, after: {relay_put_after}."
    );

    // Behavioral companion for relay SUBSCRIBE slice A (#1454 phase 5
    // follow-up, PR #3932): 12 nodes subscribe above. At HTL=3 with
    // ~5 caching nodes, at least one subscribe must traverse a relay
    // hop and hit start_relay_subscribe. If this never advances, the
    // dispatch gate for SUBSCRIBE has regressed — inbound relay
    // SubscribeMsg::Request is going through the legacy
    // handle_op_request path again.
    let relay_subscribe_after = RELAY_SUBSCRIBE_DRIVER_CALL_COUNT.load(Ordering::SeqCst);
    let relay_subscribe_delta = relay_subscribe_after.saturating_sub(relay_subscribe_baseline);
    assert!(
        relay_subscribe_delta > 0,
        "RELAY_SUBSCRIBE_DRIVER_CALL_COUNT did not advance during the \
         test — the relay SUBSCRIBE task-per-tx driver never ran. With \
         12 node subscribes at HTL=3 and ~5 caching nodes, at least one \
         subscribe must traverse a relay hop. Dispatch gate for \
         SUBSCRIBE has likely regressed. \
         Baseline: {relay_subscribe_baseline}, after: {relay_subscribe_after}."
    );

    // Relay-hop routing-event counters: every relay that forwards a
    // downstream response should feed the local Router. Drivers running
    // without route-event emission means the per-peer dashboard panels
    // stay empty and the failure-probability model is undertrained on
    // relay-heavy nodes (the bug this work fixes).
    let relay_get_route_event_after = RELAY_GET_ROUTE_EVENT_COUNT.load(Ordering::SeqCst);
    let relay_get_route_event_delta =
        relay_get_route_event_after.saturating_sub(relay_get_route_event_baseline);
    assert!(
        relay_get_route_event_delta > 0,
        "RELAY_GET_ROUTE_EVENT_COUNT did not advance — at least one \
         relay-forwarded GET should have produced a routing event for \
         the local Router. {num_nodes}-node / HTL=3 workload with 15 \
         GETs must traverse at least one relay hop. \
         Baseline: {relay_get_route_event_baseline}, after: {relay_get_route_event_after}."
    );

    let relay_put_route_event_after = RELAY_PUT_ROUTE_EVENT_COUNT.load(Ordering::SeqCst);
    let relay_put_route_event_delta =
        relay_put_route_event_after.saturating_sub(relay_put_route_event_baseline);
    assert!(
        relay_put_route_event_delta > 0,
        "RELAY_PUT_ROUTE_EVENT_COUNT did not advance — the gateway PUT \
         at HTL=3 should have produced a routing event at every relay \
         hop. \
         Baseline: {relay_put_route_event_baseline}, after: {relay_put_route_event_after}."
    );

    // SUBSCRIBE route-event coverage is intentionally NOT asserted in
    // this test. With the gateway PUT seeded at HTL=3 and 12 nodes
    // subscribing, every subscribe in this workload hits the local-hit
    // fast path at `subscribe/op_ctx_task.rs::drive_relay_subscribe`
    // step 1 — the first relay always has the contract cached and
    // replies `Subscribed` without forwarding downstream. That's a
    // correct zero for the route-event counter, since the route-event
    // hook only fires on actual forwards. A dedicated SUBSCRIBE forward
    // test (subscriber + sparse cache topology) belongs as a follow-up.
    // The relay SUBSCRIBE driver itself is still exercised — see the
    // RELAY_SUBSCRIBE_DRIVER_CALL_COUNT assertion above.
    let _ = (
        RELAY_SUBSCRIBE_ROUTE_EVENT_COUNT.load(Ordering::SeqCst),
        relay_subscribe_route_event_baseline,
    );

    // StateVerifier anomaly check
    let rt = create_runtime();
    let report = rt.block_on(async {
        let logs = logs_handle.lock().await;
        let verifier = freenet::tracing::StateVerifier::from_events(logs.clone());
        verifier.verify()
    });
    tracing::info!(
        "test_get_routing_coverage_low_htl PASSED: {} anomalies, {} events, \
         relay_driver_calls={relay_delta}",
        report.anomalies.len(),
        report.total_events,
    );
}

/// Regression test for GET retry with alternatives in sparse topologies.
///
/// With HTL=2 and 10 nodes, PUT only caches the contract at 2-3 nodes along the
/// route. When distant nodes GET, their initial routing candidate may not have
/// the contract. The fix ensures the GET state machine tries alternative peers
/// (from k_closest) and re-queries k_closest when alternatives are exhausted,
/// rather than immediately returning NotFound.
///
/// Without the fix: distant nodes return NotFound because the first-choice peer
/// doesn't have the contract and there's no retry.
/// With the fix: the GET retries with alternative peers until one succeeds.
///
/// Exercises: `get.rs` alternative retry (line ~1521) and k_closest re-query (line ~1569)
///
/// Ignored: freenet-stdlib 0.2.2 adds the StreamChunk variant to ClientRequest,
/// changing bincode serialization sizes and altering RNG-dependent topology formation.
/// The test's fixed seed produces a different topology under 0.2.2 where node-10 cannot
/// reach caching nodes. The GET retry logic itself is unchanged and covered by
/// test_get_routing_coverage_low_htl. See PR #3488.
#[ignore]
#[test_log::test]
fn test_get_retry_with_alternatives_sparse_topology() {
    use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation};

    const SEED: u64 = 0xBEEF_CAFE_0001;
    const NETWORK_NAME: &str = "get-retry-sparse";

    GlobalTestMetrics::reset();
    setup_deterministic_state(SEED);

    let rt = create_runtime();

    let num_nodes = 10;

    let (sim, logs_handle) = rt.block_on(async {
        let sim = SimNetwork::new(
            NETWORK_NAME,
            1,         // 1 gateway
            num_nodes, // 10 nodes — large enough that PUT won't reach all
            2,         // ring_max_htl = 2 — VERY low, PUT caches at only ~2-3 nodes
            1,         // rnd_if_htl_above
            6,         // max_connections
            3,         // min_connections
            SEED,
        )
        .await;
        let logs_handle = sim.event_logs_handle();
        (sim, logs_handle)
    });

    let contract = SimOperation::create_test_contract(0xBE);
    let contract_id = *contract.key().id();
    let contract_key = contract.key();

    let mut operations = vec![
        // Gateway PUTs the contract (HTL=2 → only ~2-3 nodes cache)
        ScheduledOperation::new(
            NodeLabel::gateway(NETWORK_NAME, 0),
            SimOperation::Put {
                contract: contract.clone(),
                state: vec![10, 20, 30, 40],
                subscribe: false,
            },
        ),
    ];

    // All 10 nodes GET with fetch_contract=true
    // Nodes far from caching nodes must retry with alternatives
    for i in 1..=num_nodes {
        operations.push(ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, i),
            SimOperation::Get {
                contract_id,
                return_contract_code: true,
                subscribe: false,
            },
        ));
    }

    let result = sim.run_controlled_simulation(
        SEED,
        operations,
        Duration::from_secs(300), // simulation duration
        Duration::from_secs(90),  // post-operations wait for retries
    );

    assert!(
        result.turmoil_result.is_ok(),
        "Simulation failed: {:?}",
        result.turmoil_result.err()
    );

    // Verify that all nodes got the contract state despite sparse caching.
    // Without the retry fix, distant nodes would get NotFound.
    let mut nodes_without_state = Vec::new();
    for i in 1..=num_nodes {
        let label = NodeLabel::node(NETWORK_NAME, i);
        let storage = result
            .node_storages
            .get(&label)
            .unwrap_or_else(|| panic!("node {i} should have a storage handle"));
        if storage.get_stored_state(&contract_key).is_none() {
            nodes_without_state.push(format!("node-{i}"));
        }
    }

    assert!(
        nodes_without_state.is_empty(),
        "GET retry regression: {} of {} nodes failed to get contract state \
         (contract cached at only ~2-3 nodes due to HTL=2, retry should find it). \
         Failed nodes: {:?}. See PR #3444.",
        nodes_without_state.len(),
        num_nodes,
        nodes_without_state
    );

    // StateVerifier anomaly check
    let rt = create_runtime();
    let report = rt.block_on(async {
        let logs = logs_handle.lock().await;
        let verifier = freenet::tracing::StateVerifier::from_events(logs.clone());
        verifier.verify()
    });
    tracing::info!(
        "test_get_retry_with_alternatives_sparse_topology PASSED: \
         all {} nodes got contract state, {} anomalies, {} events",
        num_nodes,
        report.anomalies.len(),
        report.total_events
    );
}

/// Regression test for auto-fetch from UPDATE sender.
///
/// When a node subscribed to a CRDT contract receives an UPDATE broadcast but
/// doesn't have the contract's code/parameters (because PUT's low HTL didn't
/// cache it there), the fix triggers `try_auto_fetch_contract` — a targeted GET
/// to the UPDATE sender who is known to have the contract.
///
/// Scenario:
///   1. Gateway PUTs a CRDT contract (HTL=2 → only ~2-3 nodes cache code)
///   2. All nodes subscribe to the contract
///   3. Gateway sends UPDATE → broadcast reaches subscribed nodes
///   4. Nodes that subscribed but don't have the contract code auto-fetch
///   5. All subscribed nodes should end up with the state
///
/// Without the fix: nodes without contract code can't apply the update → stale.
/// With the fix: auto-fetch retrieves the contract, then update applies.
///
/// Exercises: `update.rs` try_auto_fetch_contract (BroadcastTo/BroadcastToStreaming paths)
#[test_log::test]
fn test_auto_fetch_from_update_sender() {
    use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation, register_crdt_contract};

    const SEED: u64 = 0xFE7C_A100_0001;
    const NETWORK_NAME: &str = "auto-fetch-update";

    GlobalTestMetrics::reset();
    setup_deterministic_state(SEED);

    let rt = create_runtime();

    let num_nodes = 8;

    let (sim, logs_handle) = rt.block_on(async {
        let sim = SimNetwork::new(
            NETWORK_NAME,
            1,         // 1 gateway
            num_nodes, // 8 nodes
            2,         // ring_max_htl = 2 — low to limit PUT propagation
            1,         // rnd_if_htl_above
            6,         // max_connections
            3,         // min_connections
            SEED,
        )
        .await;
        let logs_handle = sim.event_logs_handle();
        (sim, logs_handle)
    });

    let contract = SimOperation::create_test_contract(0xFE);
    let contract_id = *contract.key().id();
    let contract_key = contract.key();
    register_crdt_contract(contract_id);

    let mut operations = vec![
        // 1. Gateway PUTs with subscribe (HTL=2 → few nodes cache code)
        ScheduledOperation::new(
            NodeLabel::gateway(NETWORK_NAME, 0),
            SimOperation::Put {
                contract: contract.clone(),
                state: SimOperation::create_crdt_state(1, 0xFE),
                subscribe: true,
            },
        ),
    ];

    // 2. All nodes subscribe
    for i in 1..=num_nodes {
        operations.push(ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, i),
            SimOperation::Subscribe { contract_id },
        ));
    }

    // 3. Gateway sends UPDATE — propagates to subscribers
    operations.push(ScheduledOperation::new(
        NodeLabel::gateway(NETWORK_NAME, 0),
        SimOperation::Update {
            key: contract_key,
            data: SimOperation::create_crdt_state(42, 0xFE),
        },
    ));

    // 4. All nodes GET to verify they have the state
    // (auto-fetch should have already retrieved it for nodes without code)
    for i in 1..=num_nodes {
        operations.push(ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, i),
            SimOperation::Get {
                contract_id,
                return_contract_code: true,
                subscribe: false,
            },
        ));
    }

    let result = sim.run_controlled_simulation(
        SEED,
        operations,
        Duration::from_secs(300), // simulation duration
        Duration::from_secs(90),  // post-operations wait for auto-fetch
    );

    assert!(
        result.turmoil_result.is_ok(),
        "Simulation failed: {:?}",
        result.turmoil_result.err()
    );

    // Verify that all subscribing nodes got the contract state.
    // Nodes that didn't have the contract code from PUT should have
    // auto-fetched it when they received the UPDATE broadcast.
    let mut nodes_without_state = Vec::new();
    for i in 1..=num_nodes {
        let label = NodeLabel::node(NETWORK_NAME, i);
        let storage = result
            .node_storages
            .get(&label)
            .unwrap_or_else(|| panic!("node {i} should have a storage handle"));
        if storage.get_stored_state(&contract_key).is_none() {
            nodes_without_state.push(format!("node-{i}"));
        }
    }

    // Allow up to 1 node without state (edge case: a node may not have connected
    // to any subscriber yet). But most nodes should have it via auto-fetch.
    let success_count = num_nodes - nodes_without_state.len();
    let success_rate = success_count as f64 / num_nodes as f64;
    assert!(
        success_rate >= 0.75,
        "Auto-fetch regression: only {} of {} nodes ({:.0}%) got contract state \
         after UPDATE broadcast. Without auto-fetch, nodes that didn't receive \
         PUT due to low HTL would never get the contract. \
         Failed nodes: {:?}. See PR #3444.",
        success_count,
        num_nodes,
        success_rate * 100.0,
        nodes_without_state
    );

    // StateVerifier anomaly check
    let rt = create_runtime();
    let report = rt.block_on(async {
        let logs = logs_handle.lock().await;
        let verifier = freenet::tracing::StateVerifier::from_events(logs.clone());
        verifier.verify()
    });
    tracing::info!(
        "test_auto_fetch_from_update_sender PASSED: \
         {}/{} nodes got contract state ({:.0}%), {} anomalies, {} events",
        success_count,
        num_nodes,
        success_rate * 100.0,
        report.anomalies.len(),
        report.total_events
    );
}

// =============================================================================
// Connection Growth Plateau Diagnostic (#3555)
// =============================================================================

/// Diagnostic test to understand why nodes plateau below min_connections.
///
/// Instruments the CONNECT pipeline to measure:
/// - How many CONNECT operations are initiated
/// - How many are accepted at terminus vs forwarded
/// - How many result in actual Connected events
/// - Per-node connection counts over time
///
/// This is a diagnostic (non-asserting beyond basic sanity) — run with
/// `RUST_LOG=info` to see the pipeline breakdown.
#[test_log::test(tokio::test(flavor = "current_thread"))]
async fn test_connection_growth_plateau_diagnostic() {
    use freenet::dev_tool::NodeLabel;

    const SEED: u64 = 0x3555_D1A6_0002;
    const NETWORK_NAME: &str = "growth-plateau-diag";
    const GATEWAYS: usize = 2;
    const NODES: usize = 50;
    const RING_MAX_HTL: usize = 7;
    const RND_IF_HTL_ABOVE: usize = 3;
    const MAX_CONN: usize = 20;
    const MIN_CONN: usize = 10;

    tracing::info!("=== Connection Growth Plateau Diagnostic ===");

    setup_deterministic_state(SEED);

    let mut sim = SimNetwork::new(
        NETWORK_NAME,
        GATEWAYS,
        NODES,
        RING_MAX_HTL,
        RND_IF_HTL_ABOVE,
        MAX_CONN,
        MIN_CONN,
        SEED,
    )
    .await;

    sim.with_start_backoff(Duration::from_millis(50));

    let _handles = sim
        .start_with_rand_gen::<rand::rngs::SmallRng>(SEED, 0, 0)
        .await;

    let logs_handle = sim.event_logs_handle();

    // Run for 5 virtual minutes, snapshot, then 5 more, snapshot, etc.
    // This gives us a growth curve.
    let phases = [
        ("5min", Duration::from_secs(300)),
        ("10min", Duration::from_secs(300)),
        ("15min", Duration::from_secs(300)),
        ("20min", Duration::from_secs(300)),
    ];

    for (phase_name, duration) in &phases {
        let_network_run(&mut sim, *duration).await;

        // Collect per-node connection counts
        let mut node_counts: Vec<usize> = (0..NODES)
            .filter_map(|i| {
                let label = NodeLabel::node(NETWORK_NAME, i);
                sim.connection_count(&label)
            })
            .collect();
        node_counts.sort_unstable();

        let n = node_counts.len();
        if n == 0 {
            tracing::info!("[{}] No nodes sampled", phase_name);
            continue;
        }

        let median = node_counts[n / 2];
        let min = node_counts[0];
        let max = node_counts[n - 1];
        let above_min = node_counts.iter().filter(|&&c| c >= MIN_CONN).count();
        let zero = node_counts.iter().filter(|&&c| c == 0).count();
        let avg = node_counts.iter().sum::<usize>() as f64 / n as f64;

        // Count CONNECT events so far
        let (initiated, terminus_accepted, terminus_rejected, forwarded, connected, disconnected) = {
            let logs = logs_handle.lock().await;
            let initiated = logs
                .iter()
                .filter(|l| l.kind.is_connect_initiated())
                .count();
            let terminus_accepted = logs
                .iter()
                .filter(|l| l.kind.is_connect_terminus_accepted())
                .count();
            let terminus_rejected = logs
                .iter()
                .filter(|l| l.kind.is_connect_terminus_rejected())
                .count();
            let forwarded = logs
                .iter()
                .filter(|l| l.kind.is_connect_forwarded())
                .count();
            let connected = logs
                .iter()
                .filter(|l| l.kind.is_connect_connected())
                .count();
            let disconnected = logs.iter().filter(|l| l.kind.is_disconnected()).count();
            (
                initiated,
                terminus_accepted,
                terminus_rejected,
                forwarded,
                connected,
                disconnected,
            )
        };

        let terminus_total = terminus_accepted + terminus_rejected;
        let terminus_accept_rate = if terminus_total > 0 {
            terminus_accepted as f64 / terminus_total as f64 * 100.0
        } else {
            0.0
        };

        tracing::info!(
            "[{}] Connections: median={}, avg={:.1}, min={}, max={}, \
             above_min={}/{} ({:.0}%), zero={}",
            phase_name,
            median,
            avg,
            min,
            max,
            above_min,
            n,
            above_min as f64 / n as f64 * 100.0,
            zero,
        );
        tracing::info!(
            "[{}] CONNECT pipeline: initiated={}, forwarded={}, \
             terminus_accepted={}, terminus_rejected={}, \
             terminus_accept_rate={:.0}%, connected={}, disconnected={}",
            phase_name,
            initiated,
            forwarded,
            terminus_accepted,
            terminus_rejected,
            terminus_accept_rate,
            connected,
            disconnected,
        );
    }

    // Distribution histogram
    let final_counts: Vec<usize> = (0..NODES)
        .filter_map(|i| {
            let label = NodeLabel::node(NETWORK_NAME, i);
            sim.connection_count(&label)
        })
        .collect();

    let mut histogram: BTreeMap<usize, usize> = BTreeMap::new();
    for &c in &final_counts {
        *histogram.entry(c).or_insert(0) += 1;
    }
    tracing::info!("Final connection distribution: {:?}", histogram);

    // Gateway connection counts
    for i in 0..GATEWAYS {
        let label = NodeLabel::gateway(NETWORK_NAME, i);
        if let Some(count) = sim.connection_count(&label) {
            tracing::info!("Gateway {} connections: {}", i, count);
        }
    }

    // Intentionally weak sanity check — this is a diagnostic test (see doc comment),
    // not a regression test for min_connections. The full regression test is
    // test_connection_growth_stall_regression which asserts >=90% reach min_connections.
    let total_connected = final_counts.iter().filter(|&&c| c > 0).count();
    assert!(
        total_connected > NODES / 2,
        "Fewer than half the nodes have any connections: {}/{}",
        total_connected,
        NODES,
    );
}

/// Diagnostic test for #3570: GET operations have high timeout rate in realistic networks.
///
/// This test creates a 100-node network, PUTs a contract from a gateway, then has
/// every node attempt to GET the same contract. It measures:
/// - Overall GET success rate
/// - Latency distribution (p50, p90, p99)
/// - Failure modes (NotFound vs Failure vs Timeout)
///
/// The test is intentionally diagnostic — it logs detailed statistics rather than
/// asserting a hard threshold, so we can gather evidence on what's happening.
/// A soft assertion ensures GET success rate doesn't fall below 50% (catastrophic).
///
/// Uses `run_controlled_simulation` for deterministic reproduction.
// Long-running diagnostic test (~2min). Runs in nightly CI.
#[cfg(feature = "nightly_tests")]
#[test_log::test]
fn test_get_reliability_diagnostic() {
    use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation};

    const SEED: u64 = 0x3570_D1A6_0001;
    const NETWORK_NAME: &str = "get-reliability-diag";
    const NUM_NODES: usize = 100;
    const NUM_GATEWAYS: usize = 3;

    GlobalTestMetrics::reset();
    setup_deterministic_state(SEED);

    let rt = create_runtime();

    let (sim, logs_handle) = rt.block_on(async {
        let sim = SimNetwork::new(
            NETWORK_NAME,
            NUM_GATEWAYS,
            NUM_NODES,
            10, // ring_max_htl — realistic for 100-node network
            7,  // rnd_if_htl_above
            12, // max_connections
            4,  // min_connections
            SEED,
        )
        .await;
        let logs_handle = sim.event_logs_handle();
        (sim, logs_handle)
    });

    // Create a test contract and PUT it from gateway 0
    let contract = SimOperation::create_test_contract(0x35);
    let contract_id = *contract.key().id();
    let contract_key = contract.key();

    let mut operations = vec![
        // Gateway 0 PUTs the contract with subscribe
        ScheduledOperation::new(
            NodeLabel::gateway(NETWORK_NAME, 0),
            SimOperation::Put {
                contract: contract.clone(),
                state: vec![0x35; 64],
                subscribe: true,
            },
        ),
    ];

    // Every node GETs the contract — this exercises multi-hop routing
    // across the full network topology
    for i in 0..NUM_NODES {
        operations.push(ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, i),
            SimOperation::Get {
                contract_id,
                return_contract_code: true,
                subscribe: false,
            },
        ));
    }

    let result = sim.run_controlled_simulation(
        SEED,
        operations,
        Duration::from_secs(600), // 10 min simulation
        Duration::from_secs(120), // 2 min post-operations wait
    );

    assert!(
        result.turmoil_result.is_ok(),
        "Simulation failed: {:?}",
        result.turmoil_result.err()
    );

    // Analyze GET outcomes from event logs
    let rt = create_runtime();
    let (successes, not_found, failures, timeouts, elapsed_ms_list) = rt.block_on(async {
        let logs = logs_handle.lock().await;
        let mut successes = 0u64;
        let mut not_found = 0u64;
        let mut failures = 0u64;
        let mut timeouts = 0u64;
        let mut elapsed_list = Vec::new();

        for log in logs.iter() {
            match log.kind.get_outcome() {
                Some(true) => {
                    successes += 1;
                    if let Some(ms) = log.kind.get_elapsed_ms() {
                        elapsed_list.push(ms);
                    }
                }
                Some(false) => {
                    if let Some(ms) = log.kind.get_elapsed_ms() {
                        if ms >= 55_000 {
                            // Likely a timeout (close to OPERATION_TTL of 60s)
                            timeouts += 1;
                        } else {
                            not_found += 1;
                        }
                    } else {
                        failures += 1;
                    }
                }
                None => {}
            }
        }
        (successes, not_found, failures, timeouts, elapsed_list)
    });

    let total_outcomes = successes + not_found + failures + timeouts;

    // Compute latency percentiles for successful GETs
    let mut sorted_latencies = elapsed_ms_list.clone();
    sorted_latencies.sort();

    let p50 = sorted_latencies
        .get(sorted_latencies.len() / 2)
        .copied()
        .unwrap_or(0);
    let p90 = sorted_latencies
        .get(sorted_latencies.len() * 9 / 10)
        .copied()
        .unwrap_or(0);
    let p99 = sorted_latencies
        .get(sorted_latencies.len() * 99 / 100)
        .copied()
        .unwrap_or(0);
    let max_latency = sorted_latencies.last().copied().unwrap_or(0);

    let success_rate = if total_outcomes > 0 {
        successes as f64 / total_outcomes as f64
    } else {
        0.0
    };

    // Detect response-lost and ForwardingAck patterns from event logs
    let (response_sent_count, ack_received_count, response_lost_txs, retry_storm_txs) = rt
        .block_on(async {
            let logs = logs_handle.lock().await;
            let mut response_sent_txs: HashSet<String> = HashSet::new();
            let mut success_txs: HashSet<String> = HashSet::new();
            let mut ack_received = 0u64;
            let mut request_count_per_tx: HashMap<String, usize> = HashMap::new();

            for log in logs.iter() {
                let tx_str = log.tx.to_string();
                if log.kind.is_get_response_sent() {
                    response_sent_txs.insert(tx_str.clone());
                }
                if log.kind.get_outcome() == Some(true) {
                    success_txs.insert(tx_str.clone());
                }
                if log.kind.is_forwarding_ack_received() {
                    ack_received += 1;
                }
                if log.kind.get_outcome().is_some()
                    || log.kind.is_get_response_sent()
                    || log.kind.is_forwarding_ack_received()
                {
                    // Count unique request events per tx for retry-storm detection
                } else if log.kind.is_get_request() {
                    *request_count_per_tx.entry(tx_str).or_insert(0) += 1;
                }
            }

            // Response-lost: response_sent exists but no get_success for that tx
            let response_lost: Vec<_> = response_sent_txs
                .difference(&success_txs)
                .cloned()
                .collect();

            // Retry-storm: transactions with >10 request events (same tx hitting many peers)
            let retry_storms: Vec<_> = request_count_per_tx
                .iter()
                .filter(|&(_, &count)| count > 10)
                .map(|(tx, count)| (tx.clone(), *count))
                .collect();

            (
                response_sent_txs.len(),
                ack_received,
                response_lost,
                retry_storms,
            )
        });

    tracing::info!("=== GET Reliability Diagnostic (#3570) ===");
    tracing::info!(
        "Network: {} gateways + {} nodes = {} total peers",
        NUM_GATEWAYS,
        NUM_NODES,
        NUM_GATEWAYS + NUM_NODES
    );
    tracing::info!(
        "GET outcomes: {} total — {} success, {} not_found, {} failures, {} timeouts",
        total_outcomes,
        successes,
        not_found,
        failures,
        timeouts
    );
    tracing::info!(
        "GET success rate: {:.1}% ({}/{})",
        success_rate * 100.0,
        successes,
        total_outcomes
    );
    tracing::info!(
        "Latency (successful GETs): p50={}ms, p90={}ms, p99={}ms, max={}ms",
        p50,
        p90,
        p99,
        max_latency
    );
    tracing::info!(
        "ForwardingAck: {} ACKs received, {} response_sent events, {} response-lost txs",
        ack_received_count,
        response_sent_count,
        response_lost_txs.len()
    );
    if !retry_storm_txs.is_empty() {
        tracing::info!(
            "Retry storms (>10 requests per tx): {} txs, max {} requests",
            retry_storm_txs.len(),
            retry_storm_txs.iter().map(|(_, c)| c).max().unwrap_or(&0)
        );
    }

    // Check which nodes got the contract state
    let mut nodes_with_state = 0;
    let mut nodes_without_state = Vec::new();
    for i in 0..NUM_NODES {
        let label = NodeLabel::node(NETWORK_NAME, i);
        if let Some(storage) = result.node_storages.get(&label) {
            if storage.get_stored_state(&contract_key).is_some() {
                nodes_with_state += 1;
            } else {
                nodes_without_state.push(i);
            }
        } else {
            nodes_without_state.push(i);
        }
    }

    tracing::info!(
        "Storage verification: {}/{} nodes have contract state",
        nodes_with_state,
        NUM_NODES
    );
    if !nodes_without_state.is_empty() {
        tracing::info!(
            "Nodes missing state ({} total): {:?}{}",
            nodes_without_state.len(),
            &nodes_without_state[..nodes_without_state.len().min(20)],
            if nodes_without_state.len() > 20 {
                "..."
            } else {
                ""
            }
        );
    }

    // Soft assertion — this is diagnostic, but catastrophic failure should still fail the test
    assert!(
        total_outcomes >= 10,
        "Only {} GET outcome events — too few for meaningful analysis",
        total_outcomes
    );
    assert!(
        success_rate >= 0.50,
        "GET success rate {:.1}% is catastrophically low (below 50%). \
         {} succeeded, {} not_found, {} failures, {} timeouts out of {} total. \
         See #3570 for context.",
        success_rate * 100.0,
        successes,
        not_found,
        failures,
        timeouts,
        total_outcomes
    );

    // StateVerifier anomaly check
    let report = rt.block_on(async {
        let logs = logs_handle.lock().await;
        let verifier = freenet::tracing::StateVerifier::from_events(logs.clone());
        verifier.verify()
    });
    tracing::info!(
        "Anomaly report: {} anomalies across {} contracts, {} total events",
        report.anomalies.len(),
        report.contracts_analyzed,
        report.total_events
    );

    tracing::info!(
        "test_get_reliability_diagnostic DONE: {:.1}% success rate, \
         {}/{} nodes have state",
        success_rate * 100.0,
        nodes_with_state,
        NUM_NODES
    );
}

/// Same as `test_get_reliability_diagnostic` but with realistic network delays.
///
/// Adds 50-200ms latency jitter and 5% message loss to simulate real-world conditions.
/// This should surface the ForwardingAck timeout issue from #3570: when relays ACK but
/// downstream chains stall, the originator's retry is permanently disabled.
///
/// The latency means multi-hop GETs accumulate realistic delays (e.g., 5 hops × 100ms avg
/// = 500ms per hop chain), which triggers the ACK_TIMEOUT (3s) and OPERATION_TTL (60s)
/// timing windows that are invisible in zero-latency simulations.
// Long-running diagnostic test (~3min). Runs in nightly CI.
#[cfg(feature = "nightly_tests")]
#[test_log::test]
fn test_get_reliability_with_latency() {
    use freenet::dev_tool::{NodeLabel, ScheduledOperation, SimOperation};
    use freenet::simulation::FaultConfig;

    const SEED: u64 = 0x3570_D1A6_0002;
    const NETWORK_NAME: &str = "get-reliability-latency";
    const NUM_NODES: usize = 100;
    const NUM_GATEWAYS: usize = 3;

    GlobalTestMetrics::reset();
    setup_deterministic_state(SEED);

    let rt = create_runtime();

    let (mut sim, logs_handle) = rt.block_on(async {
        let sim = SimNetwork::new(
            NETWORK_NAME,
            NUM_GATEWAYS,
            NUM_NODES,
            10, // ring_max_htl
            7,  // rnd_if_htl_above
            12, // max_connections
            4,  // min_connections
            SEED,
        )
        .await;
        let logs_handle = sim.event_logs_handle();
        (sim, logs_handle)
    });

    // Add realistic network delays: 50-200ms latency + 5% message loss
    sim.with_fault_injection(
        FaultConfig::builder()
            .latency_range(Duration::from_millis(50)..Duration::from_millis(200))
            .message_loss_rate(0.05)
            .build(),
    );

    let contract = SimOperation::create_test_contract(0x36);
    let contract_id = *contract.key().id();
    let contract_key = contract.key();

    let mut operations = vec![
        // Gateway 0 PUTs the contract
        ScheduledOperation::new(
            NodeLabel::gateway(NETWORK_NAME, 0),
            SimOperation::Put {
                contract: contract.clone(),
                state: vec![0x36; 64],
                subscribe: true,
            },
        ),
    ];

    // Every node GETs the contract
    for i in 0..NUM_NODES {
        operations.push(ScheduledOperation::new(
            NodeLabel::node(NETWORK_NAME, i),
            SimOperation::Get {
                contract_id,
                return_contract_code: true,
                subscribe: false,
            },
        ));
    }

    let result = sim.run_controlled_simulation(
        SEED,
        operations,
        Duration::from_secs(900), // 15 min — longer to account for latency
        Duration::from_secs(180), // 3 min post-operations wait
    );

    assert!(
        result.turmoil_result.is_ok(),
        "Simulation failed: {:?}",
        result.turmoil_result.err()
    );

    // Analyze GET outcomes
    let rt = create_runtime();
    let (successes, not_found, failures, timeouts, elapsed_ms_list) = rt.block_on(async {
        let logs = logs_handle.lock().await;
        let mut successes = 0u64;
        let mut not_found = 0u64;
        let mut failures = 0u64;
        let mut timeouts = 0u64;
        let mut elapsed_list = Vec::new();

        for log in logs.iter() {
            match log.kind.get_outcome() {
                Some(true) => {
                    successes += 1;
                    if let Some(ms) = log.kind.get_elapsed_ms() {
                        elapsed_list.push(ms);
                    }
                }
                Some(false) => {
                    if let Some(ms) = log.kind.get_elapsed_ms() {
                        if ms >= 55_000 {
                            timeouts += 1;
                        } else {
                            not_found += 1;
                        }
                    } else {
                        failures += 1;
                    }
                }
                None => {}
            }
        }
        (successes, not_found, failures, timeouts, elapsed_list)
    });

    let total_outcomes = successes + not_found + failures + timeouts;

    let mut sorted_latencies = elapsed_ms_list.clone();
    sorted_latencies.sort();

    let p50 = sorted_latencies
        .get(sorted_latencies.len() / 2)
        .copied()
        .unwrap_or(0);
    let p90 = sorted_latencies
        .get(sorted_latencies.len() * 9 / 10)
        .copied()
        .unwrap_or(0);
    let p99 = sorted_latencies
        .get(sorted_latencies.len() * 99 / 100)
        .copied()
        .unwrap_or(0);
    let max_latency = sorted_latencies.last().copied().unwrap_or(0);

    let success_rate = if total_outcomes > 0 {
        successes as f64 / total_outcomes as f64
    } else {
        0.0
    };

    tracing::info!("=== GET Reliability with Latency (#3570) ===");
    tracing::info!(
        "Network: {} gateways + {} nodes, latency=50-200ms, loss=5%",
        NUM_GATEWAYS,
        NUM_NODES
    );
    tracing::info!(
        "GET outcomes: {} total — {} success, {} not_found, {} failures, {} timeouts",
        total_outcomes,
        successes,
        not_found,
        failures,
        timeouts
    );
    tracing::info!(
        "GET success rate: {:.1}% ({}/{})",
        success_rate * 100.0,
        successes,
        total_outcomes
    );
    tracing::info!(
        "Latency (successful GETs): p50={}ms, p90={}ms, p99={}ms, max={}ms",
        p50,
        p90,
        p99,
        max_latency
    );

    // Check which nodes got the contract state
    let mut nodes_with_state = 0;
    let mut nodes_without_state = Vec::new();
    for i in 0..NUM_NODES {
        let label = NodeLabel::node(NETWORK_NAME, i);
        if let Some(storage) = result.node_storages.get(&label) {
            if storage.get_stored_state(&contract_key).is_some() {
                nodes_with_state += 1;
            } else {
                nodes_without_state.push(i);
            }
        } else {
            nodes_without_state.push(i);
        }
    }

    tracing::info!(
        "Storage verification: {}/{} nodes have contract state",
        nodes_with_state,
        NUM_NODES
    );
    if !nodes_without_state.is_empty() {
        tracing::info!(
            "Nodes missing state ({} total): {:?}{}",
            nodes_without_state.len(),
            &nodes_without_state[..nodes_without_state.len().min(20)],
            if nodes_without_state.len() > 20 {
                "..."
            } else {
                ""
            }
        );
    }

    // Compare with no-latency baseline
    tracing::info!(
        "=== Comparison with no-latency baseline ===\n\
         Baseline (no latency): 88.3% success, 72/100 nodes, p50=1ms, p90=754ms\n\
         This run (50-200ms latency, 5% loss): {:.1}% success, {}/{} nodes, p50={}ms, p90={}ms",
        success_rate * 100.0,
        nodes_with_state,
        NUM_NODES,
        p50,
        p90
    );

    // Soft assertion — diagnostic test, but flag catastrophic regression
    assert!(
        total_outcomes >= 10,
        "Only {} GET outcome events — too few for meaningful analysis",
        total_outcomes
    );

    // StateVerifier anomaly check
    let report = rt.block_on(async {
        let logs = logs_handle.lock().await;
        let verifier = freenet::tracing::StateVerifier::from_events(logs.clone());
        verifier.verify()
    });
    tracing::info!(
        "Anomaly report: {} anomalies across {} contracts, {} total events",
        report.anomalies.len(),
        report.contracts_analyzed,
        report.total_events
    );

    tracing::info!(
        "test_get_reliability_with_latency DONE: {:.1}% success rate, \
         {}/{} nodes have state",
        success_rate * 100.0,
        nodes_with_state,
        NUM_NODES
    );
}

/// GET reliability under node churn (nodes randomly crashing and restarting).
///
/// Uses the direct runner with ChurnConfig to simulate realistic network dynamics:
/// - 100 nodes + 3 gateways
/// - 10% crash probability per tick, 3s recovery, max 5 simultaneous crashes
/// - 50-200ms latency + 5% message loss
/// - 300 random operations (PUTs, GETs, SUBSCRIBEs, UPDATEs)
///
/// Measures GET success rate under these conditions to see if churn degrades
/// reliability beyond what static routing exhaustion causes.
// Long-running diagnostic test (~4min). Runs in nightly CI.
#[cfg(feature = "nightly_tests")]
#[test_log::test]
fn test_get_reliability_with_churn() {
    use freenet::dev_tool::ChurnConfig;
    use freenet::simulation::FaultConfig;

    const SEED: u64 = 0x3570_D1A6_0003;
    const NETWORK_NAME: &str = "get-reliability-churn";
    const NUM_NODES: usize = 100;
    const NUM_GATEWAYS: usize = 3;

    GlobalTestMetrics::reset();
    setup_deterministic_state(SEED);

    let rt = create_runtime();

    let (mut sim, logs_handle) = rt.block_on(async {
        let sim = SimNetwork::new(
            NETWORK_NAME,
            NUM_GATEWAYS,
            NUM_NODES,
            10, // ring_max_htl
            7,  // rnd_if_htl_above
            12, // max_connections
            4,  // min_connections
            SEED,
        )
        .await;
        let logs_handle = sim.event_logs_handle();
        (sim, logs_handle)
    });

    // Add realistic network delays
    sim.with_fault_injection(
        FaultConfig::builder()
            .latency_range(Duration::from_millis(50)..Duration::from_millis(200))
            .message_loss_rate(0.05)
            .build(),
    );

    // Add node churn: 10% crash rate, 3s recovery, max 5 simultaneous crashes
    sim.with_churn(ChurnConfig {
        crash_probability: 0.10,
        tick_interval: Duration::from_secs(5),
        recovery_delay: Duration::from_secs(3),
        max_simultaneous_crashes: Some(5),
        permanent_crash_rate: 0.02, // 2% of crashes are permanent
        warmup_delay: Duration::from_secs(10),
    });

    drop(rt);

    // Run with random operations — direct runner handles churn
    let direct_result = sim.run_simulation_direct::<rand::rngs::SmallRng>(
        SEED,
        15,  // max_contract_num
        300, // iterations — enough to generate many GETs
        Duration::from_millis(500),
    );

    if let Err(e) = &direct_result {
        tracing::warn!("Direct simulation completed with error (may be expected under churn): {e}");
    }

    // Analyze GET outcomes from event logs
    let rt = create_runtime();
    let (successes, not_found, failures, timeouts, elapsed_ms_list) = rt.block_on(async {
        let logs = logs_handle.lock().await;
        let mut successes = 0u64;
        let mut not_found = 0u64;
        let mut failures = 0u64;
        let mut timeouts = 0u64;
        let mut elapsed_list = Vec::new();

        for log in logs.iter() {
            match log.kind.get_outcome() {
                Some(true) => {
                    successes += 1;
                    if let Some(ms) = log.kind.get_elapsed_ms() {
                        elapsed_list.push(ms);
                    }
                }
                Some(false) => {
                    if let Some(ms) = log.kind.get_elapsed_ms() {
                        if ms >= 55_000 {
                            timeouts += 1;
                        } else {
                            not_found += 1;
                        }
                    } else {
                        failures += 1;
                    }
                }
                None => {}
            }
        }
        (successes, not_found, failures, timeouts, elapsed_list)
    });

    let total_outcomes = successes + not_found + failures + timeouts;

    let mut sorted_latencies = elapsed_ms_list.clone();
    sorted_latencies.sort();

    let p50 = sorted_latencies
        .get(sorted_latencies.len() / 2)
        .copied()
        .unwrap_or(0);
    let p90 = sorted_latencies
        .get(sorted_latencies.len() * 9 / 10)
        .copied()
        .unwrap_or(0);
    let p99 = sorted_latencies
        .get(sorted_latencies.len() * 99 / 100)
        .copied()
        .unwrap_or(0);
    let max_latency = sorted_latencies.last().copied().unwrap_or(0);

    let success_rate = if total_outcomes > 0 {
        successes as f64 / total_outcomes as f64
    } else {
        0.0
    };

    tracing::info!("=== GET Reliability with Churn (#3570) ===");
    tracing::info!(
        "Network: {} gateways + {} nodes, latency=50-200ms, loss=5%, churn=10%/5s",
        NUM_GATEWAYS,
        NUM_NODES
    );
    tracing::info!(
        "GET outcomes: {} total — {} success, {} not_found, {} failures, {} timeouts",
        total_outcomes,
        successes,
        not_found,
        failures,
        timeouts
    );
    tracing::info!(
        "GET success rate: {:.1}% ({}/{})",
        success_rate * 100.0,
        successes,
        total_outcomes
    );
    tracing::info!(
        "Latency (successful GETs): p50={}ms, p90={}ms, p99={}ms, max={}ms",
        p50,
        p90,
        p99,
        max_latency
    );

    // Compare with previous variants
    tracing::info!(
        "=== Comparison ===\n\
         Baseline (no latency):         88.3% success, p90=754ms\n\
         With latency (50-200ms, 5%):   81.9% success, p90=1833ms\n\
         With churn + latency:          {:.1}% success, p90={}ms",
        success_rate * 100.0,
        p90
    );

    // Soft assertion — diagnostic test
    if total_outcomes >= 10 {
        tracing::info!(
            "test_get_reliability_with_churn DONE: {:.1}% GET success rate \
             ({} outcomes from 300 random operations under churn)",
            success_rate * 100.0,
            total_outcomes
        );
    } else {
        tracing::warn!(
            "test_get_reliability_with_churn: only {} GET outcomes — \
             random event generation may not have produced enough GETs",
            total_outcomes
        );
    }
}

/// Nightly: 50-node topology formation with strict connectivity assertions.
///
/// Verifies that a 50-node network converges to `min_connections` within 1 virtual hour.
/// Uses the Direct Runner for scale (50+ nodes) and determinism.
///
/// **Assertions (per testing.md realism requirements):**
/// 1. Median connections >= `min_connections` (strict, not min-1)
/// 2. >= 90% of nodes reach `min_connections`
/// 3. >= 80% of nodes have non-gateway peer connections
#[cfg(feature = "nightly_tests")]
#[test_log::test(tokio::test(flavor = "current_thread"))]
async fn test_nightly_50_node_topology_formation() {
    use freenet::dev_tool::NodeLabel;

    const SEED: u64 = 0x3511_5000_0001;
    const NETWORK_NAME: &str = "nightly-50-topology";
    const GATEWAYS: usize = 4;
    const NODES: usize = 50;
    const RING_MAX_HTL: usize = 10;
    const RND_IF_HTL_ABOVE: usize = 5;
    const MAX_CONN: usize = 20;
    const MIN_CONN: usize = 10;
    const VIRTUAL_DURATION: Duration = Duration::from_secs(3600); // 1 hour

    tracing::info!("=== Nightly: 50-Node Topology Formation ===");

    setup_deterministic_state(SEED);

    let mut sim = SimNetwork::new(
        NETWORK_NAME,
        GATEWAYS,
        NODES,
        RING_MAX_HTL,
        RND_IF_HTL_ABOVE,
        MAX_CONN,
        MIN_CONN,
        SEED,
    )
    .await;

    sim.with_start_backoff(Duration::from_millis(50));

    let _handles = sim
        .start_with_rand_gen::<rand::rngs::SmallRng>(SEED, 0, 0)
        .await;

    tracing::info!("Running 1 virtual hour of topology formation...");
    let_network_run(&mut sim, VIRTUAL_DURATION).await;

    // Collect per-node connection counts. `connection_count` returns `Option`
    // and `filter_map` may drop nodes whose ConnectionManager is briefly
    // unavailable; this matches the baseline `test_connection_growth_stall_regression`
    // pattern. Percentages are computed against the sampled population, same
    // as the baseline test.
    let mut node_counts: Vec<usize> = (0..NODES)
        .filter_map(|i| {
            let label = NodeLabel::node(NETWORK_NAME, i);
            sim.connection_count(&label)
        })
        .collect();
    node_counts.sort_unstable();

    let num_sampled = node_counts.len();
    assert!(num_sampled > 0, "No connection managers available");

    let median_conn = node_counts[num_sampled / 2];
    let nodes_above_min = node_counts.iter().filter(|&&c| c >= MIN_CONN).count();
    let fraction_above_min = nodes_above_min as f64 / num_sampled as f64;

    tracing::info!("Connection counts: {:?}", node_counts);
    tracing::info!(
        "Median={}, nodes at min_connections={}/{} ({:.0}%)",
        median_conn,
        nodes_above_min,
        num_sampled,
        fraction_above_min * 100.0
    );

    // Check non-gateway peer connections
    let connectivity = sim.node_connectivity();
    let mut nodes_with_peer_connections = 0usize;
    for (label, (_key, conns)) in &connectivity {
        if !label.is_gateway() && conns.keys().any(|peer| !peer.is_gateway()) {
            nodes_with_peer_connections += 1;
        }
    }
    let peer_conn_fraction = nodes_with_peer_connections as f64 / NODES as f64;

    tracing::info!(
        "Nodes with non-gateway peer connections: {}/{} ({:.0}%)",
        nodes_with_peer_connections,
        NODES,
        peer_conn_fraction * 100.0
    );

    // ASSERTION 1: Median connections >= min_connections (strict).
    // With 50 nodes and 1 hour, the network must fully converge.
    assert!(
        median_conn >= MIN_CONN,
        "Topology formation stall: median={} < min_connections={}. \
         Counts: {:?}. Seed: 0x{:X}",
        median_conn,
        MIN_CONN,
        node_counts,
        SEED
    );

    // ASSERTION 2: >= 90% of nodes reach min_connections (per testing.md).
    assert!(
        fraction_above_min >= 0.90,
        "Only {:.0}% of nodes reached min_connections (expected >= 90%). \
         {}/{} nodes. Counts: {:?}. Seed: 0x{:X}",
        fraction_above_min * 100.0,
        nodes_above_min,
        num_sampled,
        node_counts,
        SEED
    );

    // ASSERTION 3: >= 80% of nodes have non-gateway peer connections.
    assert!(
        peer_conn_fraction >= 0.80,
        "Only {:.0}% of nodes have non-gateway peer connections (expected >= 80%). \
         CONNECT forwarding is insufficient. Seed: 0x{:X}",
        peer_conn_fraction * 100.0,
        SEED
    );

    tracing::info!(
        "PASSED: median={}, above_min={:.0}%, peer_conns={:.0}%",
        median_conn,
        fraction_above_min * 100.0,
        peer_conn_fraction * 100.0
    );
}

/// Nightly: Connection growth rate checkpoints — monotonic growth toward `min_connections`.
///
/// Measures connection counts at 5m, 15m, 30m, 60m virtual time and asserts that
/// median connections grow monotonically. Catches growth plateaus and stalls that
/// are invisible in short-duration CI tests.
///
/// **Assertions:**
/// 1. Median connections at each checkpoint >= previous checkpoint (monotonic growth)
/// 2. Final median >= `min_connections`
/// 3. No checkpoint after 5m has median = 0 (no network collapse)
#[cfg(feature = "nightly_tests")]
#[test_log::test(tokio::test(flavor = "current_thread"))]
async fn test_nightly_connection_growth_checkpoints() {
    use freenet::dev_tool::NodeLabel;

    const SEED: u64 = 0x3511_6C8E_0002;
    const NETWORK_NAME: &str = "nightly-growth-checkpoints";
    const GATEWAYS: usize = 4;
    const NODES: usize = 50;
    const RING_MAX_HTL: usize = 10;
    const RND_IF_HTL_ABOVE: usize = 5;
    const MAX_CONN: usize = 20;
    const MIN_CONN: usize = 10;

    // Checkpoint intervals (cumulative seconds from start)
    const CHECKPOINTS_SECS: [u64; 4] = [300, 900, 1800, 3600]; // 5m, 15m, 30m, 60m

    tracing::info!("=== Nightly: Connection Growth Checkpoints ===");

    setup_deterministic_state(SEED);

    let mut sim = SimNetwork::new(
        NETWORK_NAME,
        GATEWAYS,
        NODES,
        RING_MAX_HTL,
        RND_IF_HTL_ABOVE,
        MAX_CONN,
        MIN_CONN,
        SEED,
    )
    .await;

    sim.with_start_backoff(Duration::from_millis(50));

    let _handles = sim
        .start_with_rand_gen::<rand::rngs::SmallRng>(SEED, 0, 0)
        .await;

    let mut checkpoint_medians: Vec<(u64, usize)> = Vec::new();
    let mut elapsed_so_far = 0u64;

    for &target_secs in &CHECKPOINTS_SECS {
        let delta = target_secs - elapsed_so_far;
        let_network_run(&mut sim, Duration::from_secs(delta)).await;
        elapsed_so_far = target_secs;

        let mut counts: Vec<usize> = (0..NODES)
            .filter_map(|i| {
                let label = NodeLabel::node(NETWORK_NAME, i);
                sim.connection_count(&label)
            })
            .collect();
        counts.sort_unstable();

        let median = if counts.is_empty() {
            0
        } else {
            counts[counts.len() / 2]
        };

        tracing::info!(
            "Checkpoint @{}m: median={}, counts={:?}",
            target_secs / 60,
            median,
            counts
        );

        // No total network collapse after the bootstrap phase.
        assert!(
            median > 0 || elapsed_so_far <= 300,
            "Network collapse at checkpoint @{}m: median=0. Counts: {:?}. Seed: 0x{:X}",
            target_secs / 60,
            counts,
            SEED
        );

        checkpoint_medians.push((target_secs, median));
    }

    // Near-monotonic growth — each checkpoint median is within 1 connection of
    // the previous. Topology-aware pruning legitimately trades a single
    // connection for distribution quality, so a transient -1 dip is healthy
    // behavior, not a stall.
    for window in checkpoint_medians.windows(2) {
        let (prev_t, prev_median) = window[0];
        let (curr_t, curr_median) = window[1];
        assert!(
            curr_median + 1 >= prev_median,
            "Connection growth regressed > 1 between @{}m (median={}) and @{}m (median={}). \
             Growth must be near-monotonic. Seed: 0x{:X}",
            prev_t / 60,
            prev_median,
            curr_t / 60,
            curr_median,
            SEED
        );
    }

    // Final checkpoint median >= min_connections.
    let (_, final_median) = checkpoint_medians.last().unwrap();
    assert!(
        *final_median >= MIN_CONN,
        "Final median={} < min_connections={} after 60 virtual minutes. \
         Checkpoints: {:?}. Seed: 0x{:X}",
        final_median,
        MIN_CONN,
        checkpoint_medians,
        SEED
    );

    tracing::info!("PASSED: checkpoints={:?}", checkpoint_medians);
}

/// Nightly: Fault injection and recovery speed — network self-heals within bounded time.
///
/// Three phases:
/// 1. **Convergence** (30 virtual min): Let network form connections normally.
/// 2. **Fault injection** (5 virtual min): 20% message loss simulating NAT failures.
/// 3. **Recovery** (25 virtual min): Remove faults, verify network recovers.
///
/// **Assertions:**
/// 1. Pre-fault median >= `min_connections` (network converged)
/// 2. No death spiral during faults (median > 0)
/// 3. Post-recovery median >= pre-fault median - 1 (bounded recovery)
/// 4. < 5% nodes isolated after recovery
#[cfg(feature = "nightly_tests")]
#[test_log::test(tokio::test(flavor = "current_thread"))]
async fn test_nightly_fault_recovery_speed() {
    use freenet::dev_tool::NodeLabel;
    use freenet::simulation::FaultConfig;

    const SEED: u64 = 0x3511_FA17_0003;
    const NETWORK_NAME: &str = "nightly-fault-recovery";
    const GATEWAYS: usize = 4;
    const NODES: usize = 50;
    const RING_MAX_HTL: usize = 10;
    const RND_IF_HTL_ABOVE: usize = 5;
    const MAX_CONN: usize = 20;
    const MIN_CONN: usize = 10;

    tracing::info!("=== Nightly: Fault Recovery Speed ===");

    setup_deterministic_state(SEED);

    let mut sim = SimNetwork::new(
        NETWORK_NAME,
        GATEWAYS,
        NODES,
        RING_MAX_HTL,
        RND_IF_HTL_ABOVE,
        MAX_CONN,
        MIN_CONN,
        SEED,
    )
    .await;

    sim.with_start_backoff(Duration::from_millis(50));

    let _handles = sim
        .start_with_rand_gen::<rand::rngs::SmallRng>(SEED, 0, 0)
        .await;

    // ── Phase 1: Convergence (30 virtual minutes, no faults) ──────────────────
    tracing::info!("Phase 1: Convergence — 30 virtual minutes, no faults");
    let_network_run(&mut sim, Duration::from_secs(1800)).await;

    // Regular-node IDs start at `GATEWAYS`, not 0. `SimNetwork::config_nodes`
    // iterates `number_of_gateways..number_of_gateways + num` when constructing
    // labels, so for GATEWAYS=4 / NODES=50 the regular-node labels are
    // `NodeLabel::node(NETWORK_NAME, 4..54)`. Querying `(0..NODES)` would miss
    // the top `GATEWAYS` nodes and collect an off-by-GATEWAYS subset — that was
    // the deterministic `got 46, expected 50` nightly failure on seed
    // 0x3511FA170003 before this fix. Iterate over the actual live-label range
    // so every assertion in this test (Phase 1, 2, 3) sees the full 50-node
    // population.
    let node_ids = || GATEWAYS..GATEWAYS + NODES;

    let mut pre_fault_counts: Vec<usize> = node_ids()
        .filter_map(|i| {
            let label = NodeLabel::node(NETWORK_NAME, i);
            sim.connection_count(&label)
        })
        .collect();
    pre_fault_counts.sort_unstable();
    assert!(
        !pre_fault_counts.is_empty(),
        "Phase 1: no connection managers available. Seed: 0x{:X}",
        SEED
    );
    let pre_fault_median = pre_fault_counts[pre_fault_counts.len() / 2];

    tracing::info!(
        "Phase 1 done: median={}, counts={:?}",
        pre_fault_median,
        pre_fault_counts
    );

    // Network must converge before we inject faults.
    assert!(
        pre_fault_median >= MIN_CONN,
        "Network did not converge before fault injection: median={} < min_connections={}. \
         Counts: {:?}. Seed: 0x{:X}",
        pre_fault_median,
        MIN_CONN,
        pre_fault_counts,
        SEED
    );

    // ── Phase 2: Fault injection (5 virtual minutes, 20% message loss) ────────
    tracing::info!("Phase 2: Fault injection — 20% message loss for 5 virtual minutes");
    sim.with_fault_injection(FaultConfig::builder().message_loss_rate(0.20).build());

    let_network_run(&mut sim, Duration::from_secs(300)).await;

    let mut during_fault_counts: Vec<usize> = node_ids()
        .filter_map(|i| {
            let label = NodeLabel::node(NETWORK_NAME, i);
            sim.connection_count(&label)
        })
        .collect();
    during_fault_counts.sort_unstable();
    assert_eq!(
        during_fault_counts.len(),
        NODES,
        "Phase 2: expected connection managers for all {} nodes, got {}. Seed: 0x{:X}",
        NODES,
        during_fault_counts.len(),
        SEED
    );
    let during_fault_median = during_fault_counts[during_fault_counts.len() / 2];

    tracing::info!(
        "Phase 2 done: median={}, counts={:?}",
        during_fault_median,
        during_fault_counts
    );

    // Resilience under load: 20% message loss must not push the median below
    // half of MIN_CONN. The previous `> 0` bar was degenerate (a single peer
    // surviving on the median node would pass), so it could not detect a
    // partial death spiral. Half-of-min is the smallest threshold strict
    // enough to fail on real cascade collapse but loose enough to tolerate
    // brief pruning during a 5-minute fault window.
    let during_fault_floor = MIN_CONN / 2;
    assert!(
        during_fault_median >= during_fault_floor,
        "Connection collapse under load: during_fault_median={} < MIN_CONN/2={}. \
         Counts: {:?}. Seed: 0x{:X}",
        during_fault_median,
        during_fault_floor,
        during_fault_counts,
        SEED
    );

    // ── Phase 3: Recovery (25 virtual minutes, faults cleared) ────────────────
    tracing::info!("Phase 3: Recovery — faults cleared, 25 virtual minutes");
    sim.clear_fault_injection();

    let_network_run(&mut sim, Duration::from_secs(1500)).await;

    let mut post_recovery_counts: Vec<usize> = node_ids()
        .filter_map(|i| {
            let label = NodeLabel::node(NETWORK_NAME, i);
            sim.connection_count(&label)
        })
        .collect();
    post_recovery_counts.sort_unstable();
    assert_eq!(
        post_recovery_counts.len(),
        NODES,
        "Phase 3: expected connection managers for all {} nodes, got {}. Seed: 0x{:X}",
        NODES,
        post_recovery_counts.len(),
        SEED
    );

    let post_recovery_median = post_recovery_counts[post_recovery_counts.len() / 2];
    let isolated_count = post_recovery_counts.iter().filter(|&&c| c == 0).count();
    let fraction_isolated = isolated_count as f64 / NODES as f64;

    tracing::info!(
        "Phase 3 done: median={}, isolated={}/{} ({:.0}%), counts={:?}",
        post_recovery_median,
        isolated_count,
        NODES,
        fraction_isolated * 100.0,
        post_recovery_counts
    );

    // Recovery to near pre-fault levels AND back above MIN_CONN. Without the
    // MIN_CONN floor, `pre_fault_median.saturating_sub(1)` could allow the
    // post-recovery median to fall below the documented success criterion
    // (e.g. pre=10, post=9 would silently pass).
    let recovery_floor = pre_fault_median.saturating_sub(1).max(MIN_CONN);
    assert!(
        post_recovery_median >= recovery_floor,
        "Incomplete recovery: post_recovery_median={} < floor={} \
         (max(pre_fault_median - 1, MIN_CONN)). \
         Pre-fault: {:?}, Post-recovery: {:?}. Seed: 0x{:X}",
        post_recovery_median,
        recovery_floor,
        pre_fault_counts,
        post_recovery_counts,
        SEED
    );

    // < 5% nodes isolated after recovery.
    assert!(
        fraction_isolated < 0.05,
        "{:.0}% of nodes isolated after recovery (threshold: 5%). \
         Counts: {:?}. Seed: 0x{:X}",
        fraction_isolated * 100.0,
        post_recovery_counts,
        SEED
    );

    tracing::info!(
        "PASSED: pre_fault_median={}, during_fault_median={}, post_recovery_median={}, \
         isolated={:.0}%",
        pre_fault_median,
        during_fault_median,
        post_recovery_median,
        fraction_isolated * 100.0
    );
}

/// Regression test for the 2026-04-15 nightly failure: pins the
/// `SimNetwork::config_nodes` regular-node-label indexing convention so
/// future tests can't silently re-introduce the off-by-GATEWAYS bug that
/// broke `test_nightly_fault_recovery_speed`.
///
/// The convention (`crates/core/src/node/testing_impl.rs::config_nodes`,
/// circa line 1580) iterates
/// `number_of_gateways..num + number_of_gateways` when constructing
/// regular-node labels. For a SimNetwork with G gateways and N nodes, the
/// live regular-node labels are therefore
/// `NodeLabel::node(name, G..G + N)` — NOT `0..N`. A test that iterates
/// `0..N` and calls `NodeLabel::node(name, i)` will miss the top G entries
/// entirely, deterministically getting `N - G` labels instead of `N`.
///
/// This test uses a small SimNetwork (2 gateways, 3 regular nodes, fast
/// startup) and verifies:
///   1. `node-0` and `node-1` (inside the gateway ID range) are NOT live
///      regular-node labels.
///   2. `node-2`, `node-3`, `node-4` (the `GATEWAYS..GATEWAYS + NODES`
///      range) ARE live regular-node labels.
///   3. `node-5` (one past the end) is NOT a live regular-node label.
/// Pins the `NodeLabel::node` / `NodeLabel::gateway` string format that
/// `SimNetwork::config_nodes` and every downstream test query rely on.
/// Keeping this as a plain `#[test]` (not `#[tokio::test]`) ensures the
/// CI rule-lint regex `#\[(tokio::)?test\]` counts it as a regression
/// test — the async `sim_network_regular_node_labels_start_at_gateway_count`
/// below is annotated with `#[test_log::test(...)]` which the lint grep
/// does not recognize. Both tests pin the same bug class at different
/// layers: this one pins the label encoding, the async one pins the
/// live-label ID range.
#[test]
fn node_label_node_and_gateway_formats_are_stable() {
    // Expected encoding is "<network>-node-<id>" and "<network>-gateway-<id>".
    assert_eq!(
        NodeLabel::node("net", 5).to_string(),
        "net-node-5",
        "NodeLabel::node string format must remain stable"
    );
    assert_eq!(
        NodeLabel::gateway("net", 2).to_string(),
        "net-gateway-2",
        "NodeLabel::gateway string format must remain stable"
    );
    // Gateway and regular-node labels at the same numeric ID must be
    // distinct, because `config_nodes` and the test indexing convention
    // rely on the two label spaces being disjoint by type-prefix — not
    // by numeric range. A future refactor that conflates them would
    // silently corrupt every connection-count assertion.
    assert_ne!(
        NodeLabel::node("net", 0),
        NodeLabel::gateway("net", 0),
        "gateway and regular-node labels must be distinct at the same numeric ID"
    );
    // And `node 5` must not equal `gateway 5` either.
    assert_ne!(NodeLabel::node("net", 5), NodeLabel::gateway("net", 5));
}

#[test_log::test(tokio::test(flavor = "current_thread"))]
async fn sim_network_regular_node_labels_start_at_gateway_count() {
    const NETWORK_NAME: &str = "label-indexing-regression";
    const GATEWAYS: usize = 2;
    const NODES: usize = 3;
    const SEED: u64 = 0xABCD_1234;

    setup_deterministic_state(SEED);

    let mut sim = SimNetwork::new(
        NETWORK_NAME,
        GATEWAYS,
        NODES,
        /* ring_max_htl */ 3,
        /* rnd_if_htl_above */ 2,
        /* max_connections */ 4,
        /* min_connections */ 2,
        SEED,
    )
    .await;

    let _handles = sim
        .start_with_rand_gen::<rand::rngs::SmallRng>(SEED, 0, 0)
        .await;

    // Labels inside the gateway ID range (0..GATEWAYS) must NOT exist as
    // regular-node labels, even though `NodeLabel::node(name, i)` is a
    // syntactically valid label for any `i`. The rule being pinned is that
    // the *live* regular-node set starts at `GATEWAYS`, not 0.
    for i in 0..GATEWAYS {
        assert!(
            sim.connection_count(&NodeLabel::node(NETWORK_NAME, i))
                .is_none(),
            "node-{i} must NOT be a live regular-node label (inside gateway range)"
        );
    }

    // The live regular-node range is `GATEWAYS..GATEWAYS + NODES`.
    for i in GATEWAYS..GATEWAYS + NODES {
        assert!(
            sim.connection_count(&NodeLabel::node(NETWORK_NAME, i))
                .is_some(),
            "node-{i} must be a live regular-node label"
        );
    }

    // One past the end must not be live either.
    assert!(
        sim.connection_count(&NodeLabel::node(NETWORK_NAME, GATEWAYS + NODES))
            .is_none(),
        "node-{} must NOT be a live regular-node label (past end of range)",
        GATEWAYS + NODES
    );
}
