//! Deterministic simulation helpers for distributed media invariants.
//!
//! The simulator is intentionally small and synchronous. It gives tests a fast
//! way to model duplicate publishers, delayed delivery, backfill, and archive
//! convergence without booting nodes or depending on wall-clock time.

use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct SimulationSeed(pub u64);

impl SimulationSeed {
    pub const fn new(value: u64) -> Self {
        Self(value)
    }

    pub fn replay_hint(self) -> String {
        format!("EC_SIM_SEED={:016x}", self.0)
    }
}

#[derive(Debug, Clone)]
pub struct SimulationRng {
    state: u64,
}

impl SimulationRng {
    pub fn new(seed: SimulationSeed) -> Self {
        let state = if seed.0 == 0 {
            0x9e37_79b9_7f4a_7c15
        } else {
            seed.0
        };
        Self { state }
    }

    pub fn next_u64(&mut self) -> u64 {
        let mut x = self.state;
        x ^= x >> 12;
        x ^= x << 25;
        x ^= x >> 27;
        self.state = x;
        x.wrapping_mul(0x2545_f491_4f6c_dd1d)
    }

    pub fn range_inclusive(&mut self, min: u64, max: u64) -> u64 {
        if min >= max {
            return min;
        }
        min + (self.next_u64() % (max - min + 1))
    }

    pub fn chance_per_million(&mut self, threshold: u32) -> bool {
        threshold > 0 && self.range_inclusive(0, 999_999) < u64::from(threshold)
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SeededSimulationCampaignReport<Failure> {
    pub name: String,
    pub seed_start: SimulationSeed,
    pub iterations: u64,
    pub passed: u64,
    pub failed: u64,
    pub first_failure: Option<Failure>,
}

impl<Failure> SeededSimulationCampaignReport<Failure> {
    pub fn all_passed(&self) -> bool {
        self.failed == 0 && self.passed == self.iterations && self.first_failure.is_none()
    }
}

pub fn run_seeded_simulation_campaign<Failure, RunOne>(
    name: &str,
    seed_start: SimulationSeed,
    iterations: u64,
    mut run_one: RunOne,
) -> SeededSimulationCampaignReport<Failure>
where
    RunOne: FnMut(SimulationSeed) -> Option<Failure>,
{
    let mut campaign = SeededSimulationCampaignReport {
        name: name.to_string(),
        seed_start,
        iterations,
        passed: 0,
        failed: 0,
        first_failure: None,
    };

    for offset in 0..iterations {
        let seed = SimulationSeed(seed_start.0.wrapping_add(offset));
        if let Some(failure) = run_one(seed) {
            campaign.failed += 1;
            if campaign.first_failure.is_none() {
                campaign.first_failure = Some(failure);
            }
        } else {
            campaign.passed += 1;
        }
    }

    campaign
}

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct MediaKey {
    pub stream_id: String,
    pub rendition_id: String,
    pub track_name: String,
    pub sequence: u64,
}

impl MediaKey {
    pub fn new(stream_id: &str, rendition_id: &str, track_name: &str, sequence: u64) -> Self {
        Self {
            stream_id: stream_id.to_string(),
            rendition_id: rendition_id.to_string(),
            track_name: track_name.to_string(),
            sequence,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MediaObservation {
    pub key: MediaKey,
    pub source_node: String,
    pub blake3_hash: String,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub media_time_ms: Option<u64>,
    pub observed_ms: u64,
}

impl MediaObservation {
    pub fn new(key: MediaKey, source_node: &str, blake3_hash: &str, observed_ms: u64) -> Self {
        Self::new_with_media_time_ms(key, source_node, blake3_hash, None, observed_ms)
    }

    pub fn new_with_media_time_ms(
        key: MediaKey,
        source_node: &str,
        blake3_hash: &str,
        media_time_ms: Option<u64>,
        observed_ms: u64,
    ) -> Self {
        Self {
            key,
            source_node: source_node.to_string(),
            blake3_hash: blake3_hash.to_string(),
            media_time_ms,
            observed_ms,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MediaConvergenceSummary {
    pub stream_id: String,
    pub rendition_id: String,
    pub track_name: String,
    pub start_sequence: u64,
    pub end_sequence: u64,
    pub expected_sequences: u64,
    pub observed_sequences: u64,
    pub missing_sequences: Vec<u64>,
    pub matching_duplicate_sequences: Vec<u64>,
    pub divergent_sequences: Vec<u64>,
    pub source_local_divergent_sequences: Vec<u64>,
    #[serde(default)]
    pub media_timing_missing_records: u64,
    #[serde(default)]
    pub media_timing_conflict_sequences: Vec<u64>,
    pub duplicate_source_records: u64,
}

impl MediaConvergenceSummary {
    pub fn ok(&self) -> bool {
        self.missing_sequences.is_empty()
            && self.divergent_sequences.is_empty()
            && self.media_timing_missing_records == 0
            && self.media_timing_conflict_sequences.is_empty()
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct MediaObservationEntry {
    first_observed_ms: u64,
    media_time_ms: Option<u64>,
}

#[derive(Debug, Clone, Default)]
pub struct MediaConvergenceIndex {
    by_key: BTreeMap<MediaKey, BTreeMap<String, BTreeMap<String, MediaObservationEntry>>>,
}

impl MediaConvergenceIndex {
    pub fn observe(&mut self, observation: MediaObservation) {
        self.by_key
            .entry(observation.key)
            .or_default()
            .entry(observation.blake3_hash)
            .or_default()
            .entry(observation.source_node)
            .and_modify(|entry| {
                entry.first_observed_ms = entry.first_observed_ms.min(observation.observed_ms);
                if entry.media_time_ms.is_none() {
                    entry.media_time_ms = observation.media_time_ms;
                }
            })
            .or_insert(MediaObservationEntry {
                first_observed_ms: observation.observed_ms,
                media_time_ms: observation.media_time_ms,
            });
    }

    pub fn summarize(
        &self,
        stream_id: &str,
        rendition_id: &str,
        track_name: &str,
        start_sequence: u64,
        end_sequence: u64,
    ) -> MediaConvergenceSummary {
        self.summarize_sequence_set(
            stream_id,
            rendition_id,
            track_name,
            start_sequence,
            end_sequence,
            start_sequence..end_sequence,
        )
    }

    pub fn summarize_observed_sequences(
        &self,
        stream_id: &str,
        rendition_id: &str,
        track_name: &str,
        start_sequence: u64,
        end_sequence: u64,
    ) -> MediaConvergenceSummary {
        let observed_sequences = self
            .by_key
            .keys()
            .filter(|key| {
                key.stream_id == stream_id
                    && key.rendition_id == rendition_id
                    && key.track_name == track_name
                    && key.sequence >= start_sequence
                    && key.sequence < end_sequence
            })
            .map(|key| key.sequence)
            .collect::<Vec<_>>();
        self.summarize_sequence_set(
            stream_id,
            rendition_id,
            track_name,
            start_sequence,
            end_sequence,
            observed_sequences,
        )
    }

    fn summarize_sequence_set<I>(
        &self,
        stream_id: &str,
        rendition_id: &str,
        track_name: &str,
        start_sequence: u64,
        end_sequence: u64,
        sequences: I,
    ) -> MediaConvergenceSummary
    where
        I: IntoIterator<Item = u64>,
    {
        let mut observed_sequences = 0;
        let mut missing_sequences = Vec::new();
        let mut matching_duplicate_sequences = Vec::new();
        let mut divergent_sequences = Vec::new();
        let mut source_local_divergent_sequences = Vec::new();
        let mut media_timing_missing_records = 0;
        let mut media_timing_conflict_sequences = Vec::new();
        let mut duplicate_source_records = 0;
        let mut expected_sequences = 0;

        for sequence in sequences {
            expected_sequences += 1;
            let key = MediaKey::new(stream_id, rendition_id, track_name, sequence);
            let Some(hashes) = self.by_key.get(&key) else {
                missing_sequences.push(sequence);
                continue;
            };
            observed_sequences += 1;

            let duplicate_sources_for_sequence = hashes
                .values()
                .filter(|sources| sources.len() > 1)
                .map(|sources| sources.len() as u64)
                .sum::<u64>();
            duplicate_source_records += duplicate_sources_for_sequence;

            let mut hash_count_by_source = BTreeMap::<&str, u64>::new();
            let mut media_times = BTreeSet::<u64>::new();
            for sources in hashes.values() {
                for (source, entry) in sources {
                    *hash_count_by_source.entry(source.as_str()).or_default() += 1;
                    if let Some(media_time_ms) = entry.media_time_ms {
                        media_times.insert(media_time_ms);
                    } else {
                        media_timing_missing_records += 1;
                    }
                }
            }
            if hash_count_by_source
                .values()
                .any(|hash_count| *hash_count > 1)
            {
                source_local_divergent_sequences.push(sequence);
            }
            if media_times.len() > 1 {
                media_timing_conflict_sequences.push(sequence);
            }

            if hashes.len() > 1 {
                divergent_sequences.push(sequence);
            } else if duplicate_sources_for_sequence > 0 {
                matching_duplicate_sequences.push(sequence);
            }
        }

        MediaConvergenceSummary {
            stream_id: stream_id.to_string(),
            rendition_id: rendition_id.to_string(),
            track_name: track_name.to_string(),
            start_sequence,
            end_sequence,
            expected_sequences,
            observed_sequences,
            missing_sequences,
            matching_duplicate_sequences,
            divergent_sequences,
            source_local_divergent_sequences,
            media_timing_missing_records,
            media_timing_conflict_sequences,
            duplicate_source_records,
        }
    }

    pub fn duplicate_complete_at_ms(
        &self,
        stream_id: &str,
        rendition_id: &str,
        track_name: &str,
        start_sequence: u64,
        end_sequence: u64,
    ) -> Option<u64> {
        let mut complete_at_ms = 0;

        for sequence in start_sequence..end_sequence {
            let key = MediaKey::new(stream_id, rendition_id, track_name, sequence);
            let hashes = self.by_key.get(&key)?;
            if hashes.len() != 1 {
                return None;
            }
            let sources = hashes.values().next()?;
            if sources.len() < 2 {
                return None;
            }
            if sources.values().any(|entry| entry.media_time_ms.is_none()) {
                return None;
            }
            let media_times = sources
                .values()
                .filter_map(|entry| entry.media_time_ms)
                .collect::<BTreeSet<_>>();
            if media_times.len() != 1 {
                return None;
            }
            let sequence_complete_at_ms = sources
                .values()
                .map(|entry| entry.first_observed_ms)
                .max()?;
            complete_at_ms = complete_at_ms.max(sequence_complete_at_ms);
        }

        Some(complete_at_ms)
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SimEvent {
    Observe(MediaObservation),
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SimTraceEntry {
    pub at_ms: u64,
    pub order: u64,
    pub event: SimEvent,
}

#[derive(Debug, Clone, Default)]
pub struct DeterministicSimulation {
    now_ms: u64,
    next_order: u64,
    queue: BTreeMap<(u64, u64), SimEvent>,
    convergence: MediaConvergenceIndex,
    trace: Vec<SimTraceEntry>,
}

impl DeterministicSimulation {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn now_ms(&self) -> u64 {
        self.now_ms
    }

    pub fn convergence(&self) -> &MediaConvergenceIndex {
        &self.convergence
    }

    pub fn trace(&self) -> &[SimTraceEntry] {
        &self.trace
    }

    pub fn schedule(&mut self, at_ms: u64, event: SimEvent) {
        let order = self.next_order;
        self.next_order += 1;
        self.queue.insert((at_ms, order), event);
    }

    pub fn schedule_observation(
        &mut self,
        at_ms: u64,
        source_node: &str,
        stream_id: &str,
        rendition_id: &str,
        track_name: &str,
        sequence: u64,
        blake3_hash: &str,
    ) {
        self.schedule_observation_with_media_time_ms(
            at_ms,
            source_node,
            stream_id,
            rendition_id,
            track_name,
            sequence,
            Some(sequence),
            blake3_hash,
        );
    }

    #[allow(clippy::too_many_arguments)]
    pub fn schedule_observation_with_media_time_ms(
        &mut self,
        at_ms: u64,
        source_node: &str,
        stream_id: &str,
        rendition_id: &str,
        track_name: &str,
        sequence: u64,
        media_time_ms: Option<u64>,
        blake3_hash: &str,
    ) {
        self.schedule(
            at_ms,
            SimEvent::Observe(MediaObservation::new_with_media_time_ms(
                MediaKey::new(stream_id, rendition_id, track_name, sequence),
                source_node,
                blake3_hash,
                media_time_ms,
                at_ms,
            )),
        );
    }

    pub fn run_until(&mut self, deadline_ms: u64) {
        while let Some((&(at_ms, order), _)) = self.queue.iter().next() {
            if at_ms > deadline_ms {
                break;
            }
            let event = self
                .queue
                .remove(&(at_ms, order))
                .expect("event existed while iterating");
            self.now_ms = at_ms;
            self.trace.push(SimTraceEntry {
                at_ms,
                order,
                event: event.clone(),
            });
            self.apply(event);
        }
        self.now_ms = self.now_ms.max(deadline_ms);
    }

    pub fn run_to_idle(&mut self) {
        while let Some((&(at_ms, order), _)) = self.queue.iter().next() {
            let event = self
                .queue
                .remove(&(at_ms, order))
                .expect("event existed while iterating");
            self.now_ms = at_ms;
            self.trace.push(SimTraceEntry {
                at_ms,
                order,
                event: event.clone(),
            });
            self.apply(event);
        }
    }

    fn apply(&mut self, event: SimEvent) {
        match event {
            SimEvent::Observe(observation) => self.convergence.observe(observation),
        }
    }
}

pub fn simulated_media_hash(
    stream_id: &str,
    rendition_id: &str,
    track_name: &str,
    sequence: u64,
    profile_id: &str,
) -> String {
    simulated_media_hash_with_source_material(
        stream_id,
        rendition_id,
        track_name,
        sequence,
        profile_id,
        "shared-source-window",
    )
}

pub fn simulated_media_hash_with_source_material(
    stream_id: &str,
    rendition_id: &str,
    track_name: &str,
    sequence: u64,
    profile_id: &str,
    source_material: &str,
) -> String {
    let mut hasher = blake3::Hasher::new();
    hasher.update(stream_id.as_bytes());
    hasher.update(b"\0");
    hasher.update(rendition_id.as_bytes());
    hasher.update(b"\0");
    hasher.update(track_name.as_bytes());
    hasher.update(b"\0");
    hasher.update(&sequence.to_le_bytes());
    hasher.update(b"\0");
    hasher.update(profile_id.as_bytes());
    hasher.update(b"\0");
    hasher.update(source_material.as_bytes());
    hasher.finalize().to_hex().to_string()
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SimulationPartition {
    pub source_node: String,
    pub start_ms: u64,
    pub end_ms: u64,
    pub release_delay_ms: u64,
}

impl SimulationPartition {
    pub fn new(source_node: &str, start_ms: u64, end_ms: u64, release_delay_ms: u64) -> Self {
        Self {
            source_node: source_node.to_string(),
            start_ms,
            end_ms,
            release_delay_ms,
        }
    }

    fn contains(&self, source_node: &str, at_ms: u64) -> bool {
        self.source_node == source_node && at_ms >= self.start_ms && at_ms < self.end_ms
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SimulationOutage {
    pub source_node: String,
    pub start_ms: u64,
    pub end_ms: u64,
    pub backfill_delay_ms: u64,
}

impl SimulationOutage {
    pub fn new(source_node: &str, start_ms: u64, end_ms: u64, backfill_delay_ms: u64) -> Self {
        Self {
            source_node: source_node.to_string(),
            start_ms,
            end_ms,
            backfill_delay_ms,
        }
    }

    fn contains_production(&self, source_node: &str, produced_ms: u64) -> bool {
        self.source_node == source_node && produced_ms >= self.start_ms && produced_ms < self.end_ms
    }

    fn contains_time(&self, source_node: &str, at_ms: u64) -> bool {
        self.source_node == source_node && at_ms >= self.start_ms && at_ms < self.end_ms
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EncoderDriftFault {
    pub source_node: String,
    pub sequence: u64,
    pub profile_id: String,
}

impl EncoderDriftFault {
    pub fn new(source_node: &str, sequence: u64, profile_id: &str) -> Self {
        Self {
            source_node: source_node.to_string(),
            sequence,
            profile_id: profile_id.to_string(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DuplicatePublisherScenario {
    pub seed: SimulationSeed,
    pub stream_id: String,
    pub rendition_id: String,
    pub track_name: String,
    pub profile_id: String,
    pub publisher_nodes: Vec<String>,
    pub start_sequence: u64,
    pub end_sequence: u64,
    pub segment_step_ms: u64,
    pub base_network_delay_ms: u64,
    pub max_jitter_ms: u64,
    pub transient_drop_per_million: u32,
    pub backfill_after_ms: u64,
    #[serde(default)]
    pub publisher_sequence_offsets: BTreeMap<String, u64>,
    #[serde(default)]
    pub publisher_media_time_offsets_ms: BTreeMap<String, u64>,
    #[serde(default)]
    pub missing_media_timing_publishers: BTreeSet<String>,
    #[serde(default)]
    pub publisher_source_material: BTreeMap<String, String>,
    pub partitions: Vec<SimulationPartition>,
    pub publisher_outages: Vec<SimulationOutage>,
    pub encoder_drifts: Vec<EncoderDriftFault>,
}

impl DuplicatePublisherScenario {
    pub fn new(
        seed: SimulationSeed,
        publisher_nodes: Vec<String>,
        stream_id: &str,
        rendition_id: &str,
        track_name: &str,
        profile_id: &str,
        start_sequence: u64,
        end_sequence: u64,
    ) -> Self {
        Self {
            seed,
            stream_id: stream_id.to_string(),
            rendition_id: rendition_id.to_string(),
            track_name: track_name.to_string(),
            profile_id: profile_id.to_string(),
            publisher_nodes,
            start_sequence,
            end_sequence,
            segment_step_ms: 1_000,
            base_network_delay_ms: 25,
            max_jitter_ms: 250,
            transient_drop_per_million: 0,
            backfill_after_ms: 5_000,
            publisher_sequence_offsets: BTreeMap::new(),
            publisher_media_time_offsets_ms: BTreeMap::new(),
            missing_media_timing_publishers: BTreeSet::new(),
            publisher_source_material: BTreeMap::new(),
            partitions: Vec::new(),
            publisher_outages: Vec::new(),
            encoder_drifts: Vec::new(),
        }
    }

    pub fn expected_sequences(&self) -> u64 {
        self.end_sequence.saturating_sub(self.start_sequence)
    }

    fn profile_for(&self, source_node: &str, sequence: u64) -> &str {
        self.encoder_drifts
            .iter()
            .find(|fault| fault.source_node == source_node && fault.sequence == sequence)
            .map(|fault| fault.profile_id.as_str())
            .unwrap_or(self.profile_id.as_str())
    }

    fn content_sequence_for(&self, source_node: &str, sequence: u64) -> u64 {
        sequence.saturating_add(
            self.publisher_sequence_offsets
                .get(source_node)
                .copied()
                .unwrap_or(0),
        )
    }

    fn media_time_for(&self, source_node: &str, content_sequence: u64) -> Option<u64> {
        if self.missing_media_timing_publishers.contains(source_node) {
            return None;
        }
        let base_ms = content_sequence
            .saturating_sub(self.start_sequence)
            .saturating_mul(self.segment_step_ms);
        Some(
            base_ms.saturating_add(
                self.publisher_media_time_offsets_ms
                    .get(source_node)
                    .copied()
                    .unwrap_or(0),
            ),
        )
    }

    fn source_material_for(&self, source_node: &str) -> &str {
        self.publisher_source_material
            .get(source_node)
            .map(String::as_str)
            .unwrap_or("shared-source-window")
    }

    fn delivery_after_partitions(&self, source_node: &str, at_ms: u64) -> (u64, u64) {
        let mut adjusted_ms = at_ms;
        let mut applied = 0;

        loop {
            let Some(partition) = self
                .partitions
                .iter()
                .find(|partition| partition.contains(source_node, adjusted_ms))
            else {
                return (adjusted_ms, applied);
            };
            adjusted_ms = partition.end_ms.saturating_add(partition.release_delay_ms);
            applied += 1;
        }
    }

    fn publisher_outage_at(
        &self,
        source_node: &str,
        produced_ms: u64,
    ) -> Option<&SimulationOutage> {
        self.publisher_outages
            .iter()
            .find(|outage| outage.contains_production(source_node, produced_ms))
    }
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct SimulationFaultStats {
    pub scheduled_observations: u64,
    pub transient_dropped_observations: u64,
    pub backfill_observations: u64,
    pub partition_delayed_observations: u64,
    pub publisher_outage_observations: u64,
    pub encoder_drift_observations: u64,
    pub publisher_phase_offset_observations: u64,
    #[serde(default)]
    pub source_material_mismatch_observations: u64,
    pub max_delivery_ms: u64,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DuplicatePublisherSimulationReport {
    pub seed: SimulationSeed,
    pub replay_hint: String,
    pub source_count: u64,
    pub summary: MediaConvergenceSummary,
    pub duplicate_complete_at_ms: Option<u64>,
    pub fault_stats: SimulationFaultStats,
    pub trace: Vec<SimTraceEntry>,
}

impl DuplicatePublisherSimulationReport {
    pub fn duplicate_complete(&self) -> bool {
        self.summary.ok()
            && self.summary.matching_duplicate_sequences.len() as u64
                == self.summary.expected_sequences
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DuplicatePublisherInvariantConfig {
    pub require_source_count: u64,
    pub require_duplicate_complete: bool,
    #[serde(default = "default_true")]
    pub require_media_timing: bool,
    pub max_duplicate_complete_ms: Option<u64>,
}

impl DuplicatePublisherInvariantConfig {
    pub fn duplicate_complete_with_deadline(max_duplicate_complete_ms: u64) -> Self {
        Self {
            require_source_count: 2,
            require_duplicate_complete: true,
            require_media_timing: true,
            max_duplicate_complete_ms: Some(max_duplicate_complete_ms),
        }
    }
}

impl Default for DuplicatePublisherInvariantConfig {
    fn default() -> Self {
        Self {
            require_source_count: 2,
            require_duplicate_complete: true,
            require_media_timing: true,
            max_duplicate_complete_ms: None,
        }
    }
}

const fn default_true() -> bool {
    true
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DuplicatePublisherInvariantReport {
    pub ok: bool,
    pub replay_hint: String,
    pub failures: Vec<String>,
    pub duplicate_complete_at_ms: Option<u64>,
    pub max_duplicate_complete_ms: Option<u64>,
}

pub fn check_duplicate_publisher_invariants(
    report: &DuplicatePublisherSimulationReport,
    config: &DuplicatePublisherInvariantConfig,
) -> DuplicatePublisherInvariantReport {
    let mut failures = Vec::new();

    if report.source_count < config.require_source_count {
        failures.push("insufficient_source_count".to_string());
    }
    if !report.summary.missing_sequences.is_empty() {
        failures.push("missing_sequences".to_string());
    }
    if !report.summary.divergent_sequences.is_empty() {
        failures.push("divergent_sequences".to_string());
    }
    if report.fault_stats.source_material_mismatch_observations > 0 {
        failures.push("source_material_mismatch_observations".to_string());
    }
    if config.require_media_timing && report.summary.media_timing_missing_records > 0 {
        failures.push("media_timing_missing_records".to_string());
    }
    if config.require_media_timing && !report.summary.media_timing_conflict_sequences.is_empty() {
        failures.push("media_timing_conflict_sequences".to_string());
    }
    if config.require_duplicate_complete && !report.duplicate_complete() {
        failures.push("duplicate_incomplete".to_string());
    }
    if let Some(max_duplicate_complete_ms) = config.max_duplicate_complete_ms {
        match report.duplicate_complete_at_ms {
            Some(duplicate_complete_at_ms)
                if duplicate_complete_at_ms <= max_duplicate_complete_ms => {}
            Some(_) => failures.push("duplicate_complete_deadline_exceeded".to_string()),
            None => failures.push("duplicate_complete_deadline_unreached".to_string()),
        }
    }

    DuplicatePublisherInvariantReport {
        ok: failures.is_empty(),
        replay_hint: report.replay_hint.clone(),
        failures,
        duplicate_complete_at_ms: report.duplicate_complete_at_ms,
        max_duplicate_complete_ms: config.max_duplicate_complete_ms,
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DuplicatePublisherShrinkStep {
    pub dimension: String,
    pub before: String,
    pub after: String,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DuplicatePublisherShrunkFailure {
    pub seed: SimulationSeed,
    pub replay_hint: String,
    pub attempts: u64,
    pub steps: Vec<DuplicatePublisherShrinkStep>,
    pub scenario: DuplicatePublisherScenario,
    pub invariant: DuplicatePublisherInvariantReport,
    pub report: DuplicatePublisherSimulationReport,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DuplicatePublisherCampaignFailure {
    pub seed: SimulationSeed,
    pub replay_hint: String,
    pub scenario: DuplicatePublisherScenario,
    pub invariant: DuplicatePublisherInvariantReport,
    pub report: DuplicatePublisherSimulationReport,
    pub shrunk_failure: Option<DuplicatePublisherShrunkFailure>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DuplicatePublisherCampaignReport {
    pub name: String,
    pub seed_start: SimulationSeed,
    pub iterations: u64,
    pub passed: u64,
    pub failed: u64,
    pub first_failure: Option<DuplicatePublisherCampaignFailure>,
    pub max_duplicate_complete_ms_observed: u64,
    pub max_delivery_ms_observed: u64,
    pub total_transient_dropped_observations: u64,
    pub total_partition_delayed_observations: u64,
    pub total_publisher_outage_observations: u64,
    pub total_backfill_observations: u64,
    #[serde(default)]
    pub total_publisher_phase_offset_observations: u64,
    #[serde(default)]
    pub total_source_material_mismatch_observations: u64,
    #[serde(default)]
    pub total_media_timing_missing_records: u64,
    #[serde(default)]
    pub total_media_timing_conflict_sequences: u64,
}

impl DuplicatePublisherCampaignReport {
    pub fn all_passed(&self) -> bool {
        self.failed == 0 && self.passed == self.iterations && self.first_failure.is_none()
    }
}

pub fn run_duplicate_publisher_simulation(
    scenario: &DuplicatePublisherScenario,
) -> DuplicatePublisherSimulationReport {
    let mut rng = SimulationRng::new(scenario.seed);
    let mut sim = DeterministicSimulation::new();
    let mut stats = SimulationFaultStats::default();
    let source_materials = scenario
        .publisher_nodes
        .iter()
        .map(|node| scenario.source_material_for(node).to_string())
        .collect::<BTreeSet<_>>();
    let source_material_mismatch_active = source_materials.len() > 1;

    for sequence in scenario.start_sequence..scenario.end_sequence {
        let media_offset_ms = (sequence - scenario.start_sequence) * scenario.segment_step_ms;

        for source_node in &scenario.publisher_nodes {
            let initial_delivery_ms = media_offset_ms
                .saturating_add(scenario.base_network_delay_ms)
                .saturating_add(rng.range_inclusive(0, scenario.max_jitter_ms));
            let profile_id = scenario.profile_for(source_node, sequence);
            let content_sequence = scenario.content_sequence_for(source_node, sequence);
            let media_time_ms = scenario.media_time_for(source_node, content_sequence);
            let source_material = scenario.source_material_for(source_node);
            let hash = simulated_media_hash_with_source_material(
                &scenario.stream_id,
                &scenario.rendition_id,
                &scenario.track_name,
                content_sequence,
                profile_id,
                source_material,
            );

            if profile_id != scenario.profile_id {
                stats.encoder_drift_observations += 1;
            }
            if content_sequence != sequence {
                stats.publisher_phase_offset_observations += 1;
            }
            if source_material_mismatch_active {
                stats.source_material_mismatch_observations += 1;
            }

            if let Some(outage) = scenario.publisher_outage_at(source_node, media_offset_ms) {
                stats.publisher_outage_observations += 1;
                stats.backfill_observations += 1;
                let retry_ms = outage
                    .end_ms
                    .saturating_add(outage.backfill_delay_ms)
                    .saturating_add(rng.range_inclusive(0, scenario.max_jitter_ms));
                let (retry_ms, retry_partition_delays) =
                    scenario.delivery_after_partitions(source_node, retry_ms);
                stats.partition_delayed_observations += retry_partition_delays;
                stats.max_delivery_ms = stats.max_delivery_ms.max(retry_ms);
                stats.scheduled_observations += 1;
                sim.schedule_observation_with_media_time_ms(
                    retry_ms,
                    source_node,
                    &scenario.stream_id,
                    &scenario.rendition_id,
                    &scenario.track_name,
                    sequence,
                    media_time_ms,
                    &hash,
                );
                continue;
            }

            let (scheduled_ms, partition_delays) =
                scenario.delivery_after_partitions(source_node, initial_delivery_ms);
            stats.partition_delayed_observations += partition_delays;

            if rng.chance_per_million(scenario.transient_drop_per_million) {
                stats.transient_dropped_observations += 1;
                stats.backfill_observations += 1;
                let retry_ms = scheduled_ms
                    .saturating_add(scenario.backfill_after_ms)
                    .saturating_add(rng.range_inclusive(0, scenario.max_jitter_ms));
                let (retry_ms, retry_partition_delays) =
                    scenario.delivery_after_partitions(source_node, retry_ms);
                stats.partition_delayed_observations += retry_partition_delays;
                stats.max_delivery_ms = stats.max_delivery_ms.max(retry_ms);
                stats.scheduled_observations += 1;
                sim.schedule_observation_with_media_time_ms(
                    retry_ms,
                    source_node,
                    &scenario.stream_id,
                    &scenario.rendition_id,
                    &scenario.track_name,
                    sequence,
                    media_time_ms,
                    &hash,
                );
                continue;
            }

            stats.scheduled_observations += 1;
            stats.max_delivery_ms = stats.max_delivery_ms.max(scheduled_ms);
            sim.schedule_observation_with_media_time_ms(
                scheduled_ms,
                source_node,
                &scenario.stream_id,
                &scenario.rendition_id,
                &scenario.track_name,
                sequence,
                media_time_ms,
                &hash,
            );
        }
    }

    sim.run_to_idle();
    let summary = sim.convergence().summarize(
        &scenario.stream_id,
        &scenario.rendition_id,
        &scenario.track_name,
        scenario.start_sequence,
        scenario.end_sequence,
    );
    let duplicate_complete_at_ms = sim.convergence().duplicate_complete_at_ms(
        &scenario.stream_id,
        &scenario.rendition_id,
        &scenario.track_name,
        scenario.start_sequence,
        scenario.end_sequence,
    );

    DuplicatePublisherSimulationReport {
        seed: scenario.seed,
        replay_hint: scenario.seed.replay_hint(),
        source_count: scenario.publisher_nodes.len() as u64,
        summary,
        duplicate_complete_at_ms,
        fault_stats: stats,
        trace: sim.trace().to_vec(),
    }
}

fn evaluate_duplicate_publisher_failure(
    scenario: &DuplicatePublisherScenario,
    invariant_config: &DuplicatePublisherInvariantConfig,
    attempts: &mut u64,
) -> Option<(
    DuplicatePublisherSimulationReport,
    DuplicatePublisherInvariantReport,
)> {
    *attempts += 1;
    let report = run_duplicate_publisher_simulation(scenario);
    let invariant = check_duplicate_publisher_invariants(&report, invariant_config);
    (!invariant.ok).then_some((report, invariant))
}

#[allow(clippy::too_many_arguments)]
fn accept_duplicate_publisher_shrink(
    current: &mut DuplicatePublisherScenario,
    current_report: &mut DuplicatePublisherSimulationReport,
    current_invariant: &mut DuplicatePublisherInvariantReport,
    steps: &mut Vec<DuplicatePublisherShrinkStep>,
    attempts: &mut u64,
    invariant_config: &DuplicatePublisherInvariantConfig,
    dimension: &str,
    before: String,
    after: String,
    candidate: DuplicatePublisherScenario,
) -> bool {
    let Some((report, invariant)) =
        evaluate_duplicate_publisher_failure(&candidate, invariant_config, attempts)
    else {
        return false;
    };

    *current = candidate;
    *current_report = report;
    *current_invariant = invariant;
    steps.push(DuplicatePublisherShrinkStep {
        dimension: dimension.to_string(),
        before,
        after,
    });
    true
}

pub fn shrink_duplicate_publisher_failure(
    scenario: &DuplicatePublisherScenario,
    invariant_config: &DuplicatePublisherInvariantConfig,
) -> Option<DuplicatePublisherShrunkFailure> {
    let mut attempts = 0;
    let (mut current_report, mut current_invariant) =
        evaluate_duplicate_publisher_failure(scenario, invariant_config, &mut attempts)?;
    let mut current = scenario.clone();
    let mut steps = Vec::new();

    let mut partition_index = 0;
    while partition_index < current.partitions.len() {
        let mut candidate = current.clone();
        candidate.partitions.remove(partition_index);
        let before = current.partitions.len().to_string();
        let after = candidate.partitions.len().to_string();
        if !accept_duplicate_publisher_shrink(
            &mut current,
            &mut current_report,
            &mut current_invariant,
            &mut steps,
            &mut attempts,
            invariant_config,
            "partitions.len",
            before,
            after,
            candidate,
        ) {
            partition_index += 1;
        }
    }

    let mut drift_index = 0;
    while drift_index < current.encoder_drifts.len() {
        let mut candidate = current.clone();
        candidate.encoder_drifts.remove(drift_index);
        let before = current.encoder_drifts.len().to_string();
        let after = candidate.encoder_drifts.len().to_string();
        if !accept_duplicate_publisher_shrink(
            &mut current,
            &mut current_report,
            &mut current_invariant,
            &mut steps,
            &mut attempts,
            invariant_config,
            "encoder_drifts.len",
            before,
            after,
            candidate,
        ) {
            drift_index += 1;
        }
    }

    if !current.publisher_sequence_offsets.is_empty() {
        let mut candidate = current.clone();
        candidate.publisher_sequence_offsets.clear();
        accept_duplicate_publisher_shrink(
            &mut current,
            &mut current_report,
            &mut current_invariant,
            &mut steps,
            &mut attempts,
            invariant_config,
            "publisher_sequence_offsets.len",
            scenario.publisher_sequence_offsets.len().to_string(),
            "0".to_string(),
            candidate,
        );
    }

    if !current.publisher_source_material.is_empty() {
        let mut candidate = current.clone();
        candidate.publisher_source_material.clear();
        accept_duplicate_publisher_shrink(
            &mut current,
            &mut current_report,
            &mut current_invariant,
            &mut steps,
            &mut attempts,
            invariant_config,
            "publisher_source_material.len",
            scenario.publisher_source_material.len().to_string(),
            "0".to_string(),
            candidate,
        );
    }

    let mut outage_index = 0;
    while outage_index < current.publisher_outages.len() {
        let mut candidate = current.clone();
        candidate.publisher_outages.remove(outage_index);
        let before = current.publisher_outages.len().to_string();
        let after = candidate.publisher_outages.len().to_string();
        if !accept_duplicate_publisher_shrink(
            &mut current,
            &mut current_report,
            &mut current_invariant,
            &mut steps,
            &mut attempts,
            invariant_config,
            "publisher_outages.len",
            before,
            after,
            candidate,
        ) {
            outage_index += 1;
        }
    }

    if current.transient_drop_per_million > 0 {
        let mut candidate = current.clone();
        candidate.transient_drop_per_million = 0;
        accept_duplicate_publisher_shrink(
            &mut current,
            &mut current_report,
            &mut current_invariant,
            &mut steps,
            &mut attempts,
            invariant_config,
            "transient_drop_per_million",
            scenario.transient_drop_per_million.to_string(),
            "0".to_string(),
            candidate,
        );
    }

    if current.max_jitter_ms > 0 {
        let mut candidate = current.clone();
        candidate.max_jitter_ms = 0;
        let before = current.max_jitter_ms.to_string();
        accept_duplicate_publisher_shrink(
            &mut current,
            &mut current_report,
            &mut current_invariant,
            &mut steps,
            &mut attempts,
            invariant_config,
            "max_jitter_ms",
            before,
            "0".to_string(),
            candidate,
        );
    }

    if current.base_network_delay_ms > 0 {
        let mut candidate = current.clone();
        candidate.base_network_delay_ms = 0;
        let before = current.base_network_delay_ms.to_string();
        accept_duplicate_publisher_shrink(
            &mut current,
            &mut current_report,
            &mut current_invariant,
            &mut steps,
            &mut attempts,
            invariant_config,
            "base_network_delay_ms",
            before,
            "0".to_string(),
            candidate,
        );
    }

    let mut low_count = 1;
    let mut high_count = current.expected_sequences();
    while low_count < high_count {
        let mid_count = low_count + ((high_count - low_count) / 2);
        let mut candidate = current.clone();
        candidate.end_sequence = candidate.start_sequence.saturating_add(mid_count);
        let before = current.expected_sequences().to_string();
        let after = candidate.expected_sequences().to_string();
        if accept_duplicate_publisher_shrink(
            &mut current,
            &mut current_report,
            &mut current_invariant,
            &mut steps,
            &mut attempts,
            invariant_config,
            "sequence_count",
            before,
            after,
            candidate,
        ) {
            high_count = mid_count;
        } else {
            low_count = mid_count + 1;
        }
    }

    Some(DuplicatePublisherShrunkFailure {
        seed: current.seed,
        replay_hint: current.seed.replay_hint(),
        attempts,
        steps,
        scenario: current,
        invariant: current_invariant,
        report: current_report,
    })
}

pub fn run_duplicate_publisher_campaign<F>(
    name: &str,
    seed_start: SimulationSeed,
    iterations: u64,
    invariant_config: &DuplicatePublisherInvariantConfig,
    mut build_scenario: F,
) -> DuplicatePublisherCampaignReport
where
    F: FnMut(SimulationSeed) -> DuplicatePublisherScenario,
{
    let mut campaign = DuplicatePublisherCampaignReport {
        name: name.to_string(),
        seed_start,
        iterations,
        passed: 0,
        failed: 0,
        first_failure: None,
        max_duplicate_complete_ms_observed: 0,
        max_delivery_ms_observed: 0,
        total_transient_dropped_observations: 0,
        total_partition_delayed_observations: 0,
        total_publisher_outage_observations: 0,
        total_backfill_observations: 0,
        total_publisher_phase_offset_observations: 0,
        total_source_material_mismatch_observations: 0,
        total_media_timing_missing_records: 0,
        total_media_timing_conflict_sequences: 0,
    };

    let generic = run_seeded_simulation_campaign(name, seed_start, iterations, |seed| {
        let scenario = build_scenario(seed);
        let report = run_duplicate_publisher_simulation(&scenario);
        let invariant = check_duplicate_publisher_invariants(&report, invariant_config);

        campaign.max_delivery_ms_observed = campaign
            .max_delivery_ms_observed
            .max(report.fault_stats.max_delivery_ms);
        if let Some(duplicate_complete_at_ms) = report.duplicate_complete_at_ms {
            campaign.max_duplicate_complete_ms_observed = campaign
                .max_duplicate_complete_ms_observed
                .max(duplicate_complete_at_ms);
        }
        campaign.total_transient_dropped_observations +=
            report.fault_stats.transient_dropped_observations;
        campaign.total_partition_delayed_observations +=
            report.fault_stats.partition_delayed_observations;
        campaign.total_publisher_outage_observations +=
            report.fault_stats.publisher_outage_observations;
        campaign.total_backfill_observations += report.fault_stats.backfill_observations;
        campaign.total_publisher_phase_offset_observations +=
            report.fault_stats.publisher_phase_offset_observations;
        campaign.total_source_material_mismatch_observations +=
            report.fault_stats.source_material_mismatch_observations;
        campaign.total_media_timing_missing_records += report.summary.media_timing_missing_records;
        campaign.total_media_timing_conflict_sequences +=
            report.summary.media_timing_conflict_sequences.len() as u64;

        if invariant.ok {
            None
        } else {
            let shrunk_failure = shrink_duplicate_publisher_failure(&scenario, invariant_config);
            Some(DuplicatePublisherCampaignFailure {
                seed,
                replay_hint: report.replay_hint.clone(),
                scenario,
                invariant,
                report,
                shrunk_failure,
            })
        }
    });

    campaign.passed = generic.passed;
    campaign.failed = generic.failed;
    campaign.first_failure = generic.first_failure;

    campaign
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ControlPlanePropagationScenario {
    pub seed: SimulationSeed,
    pub nodes: Vec<String>,
    pub origin_node: String,
    pub topic: String,
    pub announcement_id: String,
    pub fanout: u64,
    pub gossip_interval_ms: u64,
    pub max_gossip_rounds: u64,
    pub base_network_delay_ms: u64,
    pub max_jitter_ms: u64,
    pub transient_drop_per_million: u32,
    pub partitions: Vec<SimulationPartition>,
    pub node_outages: Vec<SimulationOutage>,
}

impl ControlPlanePropagationScenario {
    pub fn new(
        seed: SimulationSeed,
        nodes: Vec<String>,
        origin_node: &str,
        topic: &str,
        announcement_id: &str,
    ) -> Self {
        Self {
            seed,
            nodes,
            origin_node: origin_node.to_string(),
            topic: topic.to_string(),
            announcement_id: announcement_id.to_string(),
            fanout: 3,
            gossip_interval_ms: 35,
            max_gossip_rounds: 10,
            base_network_delay_ms: 8,
            max_jitter_ms: 40,
            transient_drop_per_million: 100_000,
            partitions: Vec::new(),
            node_outages: Vec::new(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ControlPlaneMessage {
    pub from_node: String,
    pub to_node: String,
    pub topic: String,
    pub announcement_id: String,
    pub sent_ms: u64,
    pub round: u64,
}

impl ControlPlaneMessage {
    fn new(
        scenario: &ControlPlanePropagationScenario,
        from_node: &str,
        to_node: &str,
        sent_ms: u64,
        round: u64,
    ) -> Self {
        Self {
            from_node: from_node.to_string(),
            to_node: to_node.to_string(),
            topic: scenario.topic.clone(),
            announcement_id: scenario.announcement_id.clone(),
            sent_ms,
            round,
        }
    }
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ControlPlaneFaultStats {
    pub scheduled_messages: u64,
    pub delivered_messages: u64,
    pub transient_dropped_messages: u64,
    pub duplicate_messages: u64,
    pub partition_delayed_messages: u64,
    pub node_outage_delayed_messages: u64,
    pub node_outage_delayed_rounds: u64,
    pub max_delivery_ms: u64,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ControlPlaneTraceEvent {
    NodeLearned {
        node: String,
        from_node: Option<String>,
        announcement_id: String,
    },
    GossipRound {
        node: String,
        round: u64,
    },
    GossipRoundDelayed {
        node: String,
        round: u64,
        delayed_until_ms: u64,
    },
    MessageScheduled {
        message: ControlPlaneMessage,
        deliver_at_ms: u64,
    },
    MessageDropped {
        message: ControlPlaneMessage,
    },
    MessageDelayed {
        message: ControlPlaneMessage,
        delayed_until_ms: u64,
        partition_delays: u64,
        node_outage_delays: u64,
    },
    MessageDelivered {
        message: ControlPlaneMessage,
    },
    DuplicateIgnored {
        message: ControlPlaneMessage,
    },
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ControlPlaneTraceEntry {
    pub at_ms: u64,
    pub order: u64,
    pub event: ControlPlaneTraceEvent,
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum ControlPlaneQueuedEvent {
    GossipRound { node: String, round: u64 },
    DeliverMessage(ControlPlaneMessage),
}

#[derive(Debug, Default)]
struct ControlPlaneSimulationState {
    now_ms: u64,
    next_queue_order: u64,
    next_trace_order: u64,
    queue: BTreeMap<(u64, u64), ControlPlaneQueuedEvent>,
    known_at: BTreeMap<String, u64>,
    trace: Vec<ControlPlaneTraceEntry>,
    stats: ControlPlaneFaultStats,
}

impl ControlPlaneSimulationState {
    fn schedule(&mut self, at_ms: u64, event: ControlPlaneQueuedEvent) {
        let order = self.next_queue_order;
        self.next_queue_order += 1;
        self.queue.insert((at_ms, order), event);
    }

    fn record(&mut self, at_ms: u64, event: ControlPlaneTraceEvent) {
        let order = self.next_trace_order;
        self.next_trace_order += 1;
        self.trace.push(ControlPlaneTraceEntry {
            at_ms,
            order,
            event,
        });
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ControlPlanePropagationReport {
    pub seed: SimulationSeed,
    pub replay_hint: String,
    pub node_count: u64,
    pub known_count: u64,
    pub known_nodes: Vec<String>,
    pub known_at_by_node: BTreeMap<String, u64>,
    pub missing_nodes: Vec<String>,
    pub propagation_complete_at_ms: Option<u64>,
    pub fault_stats: ControlPlaneFaultStats,
    pub trace: Vec<ControlPlaneTraceEntry>,
}

impl ControlPlanePropagationReport {
    pub fn propagation_complete(&self) -> bool {
        self.missing_nodes.is_empty()
            && self.known_count == self.node_count
            && self.propagation_complete_at_ms.is_some()
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ControlPlanePropagationInvariantConfig {
    pub require_node_count: u64,
    pub require_complete: bool,
    pub max_propagation_complete_ms: Option<u64>,
}

impl ControlPlanePropagationInvariantConfig {
    pub fn complete_with_deadline(
        require_node_count: u64,
        max_propagation_complete_ms: u64,
    ) -> Self {
        Self {
            require_node_count,
            require_complete: true,
            max_propagation_complete_ms: Some(max_propagation_complete_ms),
        }
    }
}

impl Default for ControlPlanePropagationInvariantConfig {
    fn default() -> Self {
        Self {
            require_node_count: 2,
            require_complete: true,
            max_propagation_complete_ms: None,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ControlPlanePropagationInvariantReport {
    pub ok: bool,
    pub replay_hint: String,
    pub failures: Vec<String>,
    pub propagation_complete_at_ms: Option<u64>,
    pub max_propagation_complete_ms: Option<u64>,
}

pub fn check_control_plane_propagation_invariants(
    report: &ControlPlanePropagationReport,
    config: &ControlPlanePropagationInvariantConfig,
) -> ControlPlanePropagationInvariantReport {
    let mut failures = Vec::new();

    if report.node_count < config.require_node_count {
        failures.push("insufficient_node_count".to_string());
    }
    if config.require_complete && !report.propagation_complete() {
        failures.push("propagation_incomplete".to_string());
    }
    if let Some(max_propagation_complete_ms) = config.max_propagation_complete_ms {
        match report.propagation_complete_at_ms {
            Some(propagation_complete_at_ms)
                if propagation_complete_at_ms <= max_propagation_complete_ms => {}
            Some(_) => failures.push("propagation_deadline_exceeded".to_string()),
            None => failures.push("propagation_deadline_unreached".to_string()),
        }
    }

    ControlPlanePropagationInvariantReport {
        ok: failures.is_empty(),
        replay_hint: report.replay_hint.clone(),
        failures,
        propagation_complete_at_ms: report.propagation_complete_at_ms,
        max_propagation_complete_ms: config.max_propagation_complete_ms,
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ControlPlanePropagationCampaignFailure {
    pub seed: SimulationSeed,
    pub replay_hint: String,
    pub scenario: ControlPlanePropagationScenario,
    pub invariant: ControlPlanePropagationInvariantReport,
    pub report: ControlPlanePropagationReport,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ControlPlanePropagationCampaignReport {
    pub name: String,
    pub seed_start: SimulationSeed,
    pub iterations: u64,
    pub passed: u64,
    pub failed: u64,
    pub first_failure: Option<ControlPlanePropagationCampaignFailure>,
    pub max_propagation_complete_ms_observed: u64,
    pub max_delivery_ms_observed: u64,
    pub total_transient_dropped_messages: u64,
    pub total_partition_delayed_messages: u64,
    pub total_node_outage_delayed_messages: u64,
    pub total_duplicate_messages: u64,
}

impl ControlPlanePropagationCampaignReport {
    pub fn all_passed(&self) -> bool {
        self.failed == 0 && self.passed == self.iterations && self.first_failure.is_none()
    }
}

pub fn run_control_plane_propagation_simulation(
    scenario: &ControlPlanePropagationScenario,
) -> ControlPlanePropagationReport {
    let mut rng = SimulationRng::new(scenario.seed);
    let mut state = ControlPlaneSimulationState::default();
    let node_set = scenario.nodes.iter().cloned().collect::<BTreeSet<_>>();

    if node_set.contains(&scenario.origin_node) {
        state.known_at.insert(scenario.origin_node.clone(), 0);
        state.record(
            0,
            ControlPlaneTraceEvent::NodeLearned {
                node: scenario.origin_node.clone(),
                from_node: None,
                announcement_id: scenario.announcement_id.clone(),
            },
        );
        state.schedule(
            0,
            ControlPlaneQueuedEvent::GossipRound {
                node: scenario.origin_node.clone(),
                round: 0,
            },
        );
    }

    while let Some((&(at_ms, order), _)) = state.queue.iter().next() {
        let event = state
            .queue
            .remove(&(at_ms, order))
            .expect("event existed while iterating");
        state.now_ms = at_ms;
        match event {
            ControlPlaneQueuedEvent::GossipRound { node, round } => {
                run_control_plane_gossip_round(&mut state, scenario, &mut rng, &node, round, at_ms);
            }
            ControlPlaneQueuedEvent::DeliverMessage(message) => {
                run_control_plane_message_delivery(&mut state, scenario, &message, at_ms);
            }
        }
    }

    let known_nodes = scenario
        .nodes
        .iter()
        .filter(|node| state.known_at.contains_key(*node))
        .cloned()
        .collect::<Vec<_>>();
    let missing_nodes = scenario
        .nodes
        .iter()
        .filter(|node| !state.known_at.contains_key(*node))
        .cloned()
        .collect::<Vec<_>>();
    let propagation_complete_at_ms = missing_nodes
        .is_empty()
        .then(|| state.known_at.values().copied().max())
        .flatten();

    ControlPlanePropagationReport {
        seed: scenario.seed,
        replay_hint: scenario.seed.replay_hint(),
        node_count: scenario.nodes.len() as u64,
        known_count: known_nodes.len() as u64,
        known_nodes,
        known_at_by_node: state.known_at,
        missing_nodes,
        propagation_complete_at_ms,
        fault_stats: state.stats,
        trace: state.trace,
    }
}

fn run_control_plane_gossip_round(
    state: &mut ControlPlaneSimulationState,
    scenario: &ControlPlanePropagationScenario,
    rng: &mut SimulationRng,
    node: &str,
    round: u64,
    at_ms: u64,
) {
    if !state.known_at.contains_key(node) {
        return;
    }
    if round >= scenario.max_gossip_rounds {
        return;
    }

    let (available_at_ms, outage_delays) =
        control_node_available_after_faults(scenario, node, at_ms);
    if available_at_ms > at_ms {
        state.stats.node_outage_delayed_rounds += outage_delays;
        state.record(
            at_ms,
            ControlPlaneTraceEvent::GossipRoundDelayed {
                node: node.to_string(),
                round,
                delayed_until_ms: available_at_ms,
            },
        );
        state.schedule(
            available_at_ms,
            ControlPlaneQueuedEvent::GossipRound {
                node: node.to_string(),
                round,
            },
        );
        return;
    }

    state.record(
        at_ms,
        ControlPlaneTraceEvent::GossipRound {
            node: node.to_string(),
            round,
        },
    );

    for peer in control_plane_fanout_peers(scenario, rng, node) {
        schedule_control_plane_message(state, scenario, rng, node, &peer, at_ms, round);
    }

    let next_round = round.saturating_add(1);
    if next_round < scenario.max_gossip_rounds {
        state.schedule(
            at_ms.saturating_add(scenario.gossip_interval_ms),
            ControlPlaneQueuedEvent::GossipRound {
                node: node.to_string(),
                round: next_round,
            },
        );
    }
}

fn run_control_plane_message_delivery(
    state: &mut ControlPlaneSimulationState,
    scenario: &ControlPlanePropagationScenario,
    message: &ControlPlaneMessage,
    at_ms: u64,
) {
    state.stats.delivered_messages += 1;
    state.record(
        at_ms,
        ControlPlaneTraceEvent::MessageDelivered {
            message: message.clone(),
        },
    );

    if state.known_at.contains_key(&message.to_node) {
        state.stats.duplicate_messages += 1;
        state.record(
            at_ms,
            ControlPlaneTraceEvent::DuplicateIgnored {
                message: message.clone(),
            },
        );
        return;
    }

    state.known_at.insert(message.to_node.clone(), at_ms);
    state.record(
        at_ms,
        ControlPlaneTraceEvent::NodeLearned {
            node: message.to_node.clone(),
            from_node: Some(message.from_node.clone()),
            announcement_id: scenario.announcement_id.clone(),
        },
    );
    state.schedule(
        at_ms,
        ControlPlaneQueuedEvent::GossipRound {
            node: message.to_node.clone(),
            round: 0,
        },
    );
}

fn schedule_control_plane_message(
    state: &mut ControlPlaneSimulationState,
    scenario: &ControlPlanePropagationScenario,
    rng: &mut SimulationRng,
    from_node: &str,
    to_node: &str,
    at_ms: u64,
    round: u64,
) {
    let message = ControlPlaneMessage::new(scenario, from_node, to_node, at_ms, round);
    state.stats.scheduled_messages += 1;

    if rng.chance_per_million(scenario.transient_drop_per_million) {
        state.stats.transient_dropped_messages += 1;
        state.record(
            at_ms,
            ControlPlaneTraceEvent::MessageDropped {
                message: message.clone(),
            },
        );
        return;
    }

    let initial_delivery_ms = at_ms
        .saturating_add(scenario.base_network_delay_ms)
        .saturating_add(rng.range_inclusive(0, scenario.max_jitter_ms));
    let (deliver_at_ms, partition_delays, node_outage_delays) =
        control_message_delivery_after_faults(scenario, from_node, to_node, initial_delivery_ms);

    state.stats.partition_delayed_messages += partition_delays;
    state.stats.node_outage_delayed_messages += node_outage_delays;
    state.stats.max_delivery_ms = state.stats.max_delivery_ms.max(deliver_at_ms);

    if deliver_at_ms > initial_delivery_ms {
        state.record(
            at_ms,
            ControlPlaneTraceEvent::MessageDelayed {
                message: message.clone(),
                delayed_until_ms: deliver_at_ms,
                partition_delays,
                node_outage_delays,
            },
        );
    }
    state.record(
        at_ms,
        ControlPlaneTraceEvent::MessageScheduled {
            message: message.clone(),
            deliver_at_ms,
        },
    );
    state.schedule(
        deliver_at_ms,
        ControlPlaneQueuedEvent::DeliverMessage(message),
    );
}

fn control_plane_fanout_peers(
    scenario: &ControlPlanePropagationScenario,
    rng: &mut SimulationRng,
    node: &str,
) -> Vec<String> {
    let candidates = scenario
        .nodes
        .iter()
        .filter(|candidate| candidate.as_str() != node)
        .cloned()
        .collect::<Vec<_>>();
    if candidates.is_empty() || scenario.fanout == 0 {
        return Vec::new();
    }
    let fanout = (scenario.fanout as usize).min(candidates.len());
    if fanout == candidates.len() {
        return candidates;
    }

    let start = rng.range_inclusive(0, candidates.len() as u64 - 1) as usize;
    (0..fanout)
        .map(|offset| candidates[(start + offset) % candidates.len()].clone())
        .collect()
}

fn control_message_delivery_after_faults(
    scenario: &ControlPlanePropagationScenario,
    from_node: &str,
    to_node: &str,
    at_ms: u64,
) -> (u64, u64, u64) {
    let mut adjusted_ms = at_ms;
    let mut partition_delays = 0;
    let mut node_outage_delays = 0;

    loop {
        if let Some(partition) = scenario.partitions.iter().find(|partition| {
            (partition.source_node == from_node || partition.source_node == to_node)
                && partition.contains(&partition.source_node, adjusted_ms)
        }) {
            adjusted_ms = partition.end_ms.saturating_add(partition.release_delay_ms);
            partition_delays += 1;
            continue;
        }
        if let Some(outage) = scenario.node_outages.iter().find(|outage| {
            (outage.source_node == from_node || outage.source_node == to_node)
                && outage.contains_time(&outage.source_node, adjusted_ms)
        }) {
            adjusted_ms = outage.end_ms.saturating_add(outage.backfill_delay_ms);
            node_outage_delays += 1;
            continue;
        }
        return (adjusted_ms, partition_delays, node_outage_delays);
    }
}

fn control_node_available_after_faults(
    scenario: &ControlPlanePropagationScenario,
    node: &str,
    at_ms: u64,
) -> (u64, u64) {
    let mut adjusted_ms = at_ms;
    let mut node_outage_delays = 0;

    loop {
        if let Some(outage) = scenario
            .node_outages
            .iter()
            .find(|outage| outage.contains_time(node, adjusted_ms))
        {
            adjusted_ms = outage.end_ms.saturating_add(outage.backfill_delay_ms);
            node_outage_delays += 1;
            continue;
        }
        return (adjusted_ms, node_outage_delays);
    }
}

pub fn run_control_plane_propagation_campaign<F>(
    name: &str,
    seed_start: SimulationSeed,
    iterations: u64,
    invariant_config: &ControlPlanePropagationInvariantConfig,
    mut build_scenario: F,
) -> ControlPlanePropagationCampaignReport
where
    F: FnMut(SimulationSeed) -> ControlPlanePropagationScenario,
{
    let mut campaign = ControlPlanePropagationCampaignReport {
        name: name.to_string(),
        seed_start,
        iterations,
        passed: 0,
        failed: 0,
        first_failure: None,
        max_propagation_complete_ms_observed: 0,
        max_delivery_ms_observed: 0,
        total_transient_dropped_messages: 0,
        total_partition_delayed_messages: 0,
        total_node_outage_delayed_messages: 0,
        total_duplicate_messages: 0,
    };

    let generic = run_seeded_simulation_campaign(name, seed_start, iterations, |seed| {
        let scenario = build_scenario(seed);
        let report = run_control_plane_propagation_simulation(&scenario);
        let invariant = check_control_plane_propagation_invariants(&report, invariant_config);

        campaign.max_delivery_ms_observed = campaign
            .max_delivery_ms_observed
            .max(report.fault_stats.max_delivery_ms);
        if let Some(propagation_complete_at_ms) = report.propagation_complete_at_ms {
            campaign.max_propagation_complete_ms_observed = campaign
                .max_propagation_complete_ms_observed
                .max(propagation_complete_at_ms);
        }
        campaign.total_transient_dropped_messages += report.fault_stats.transient_dropped_messages;
        campaign.total_partition_delayed_messages += report.fault_stats.partition_delayed_messages;
        campaign.total_node_outage_delayed_messages +=
            report.fault_stats.node_outage_delayed_messages;
        campaign.total_duplicate_messages += report.fault_stats.duplicate_messages;

        if invariant.ok {
            None
        } else {
            Some(ControlPlanePropagationCampaignFailure {
                seed,
                replay_hint: report.replay_hint.clone(),
                scenario,
                invariant,
                report,
            })
        }
    });

    campaign.passed = generic.passed;
    campaign.failed = generic.failed;
    campaign.first_failure = generic.first_failure;

    campaign
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PublisherSequenceClock {
    #[default]
    Global,
    LocalActivation,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SystemDuplicatePublisherScenario {
    pub seed: SimulationSeed,
    pub control: ControlPlanePropagationScenario,
    pub media: DuplicatePublisherScenario,
    pub publisher_activation_delay_ms: u64,
    pub publisher_backfill_delay_ms: u64,
    pub sequence_clock: PublisherSequenceClock,
}

impl SystemDuplicatePublisherScenario {
    pub fn new(
        seed: SimulationSeed,
        control: ControlPlanePropagationScenario,
        media: DuplicatePublisherScenario,
    ) -> Self {
        Self {
            seed,
            control,
            media,
            publisher_activation_delay_ms: 0,
            publisher_backfill_delay_ms: 120,
            sequence_clock: PublisherSequenceClock::Global,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SystemDuplicatePublisherSimulationReport {
    pub seed: SimulationSeed,
    pub replay_hint: String,
    pub publisher_activation_ms: BTreeMap<String, u64>,
    pub control: ControlPlanePropagationReport,
    pub media: DuplicatePublisherSimulationReport,
    pub system_complete_at_ms: Option<u64>,
}

impl SystemDuplicatePublisherSimulationReport {
    pub fn system_complete(&self) -> bool {
        self.control.propagation_complete()
            && self.media.duplicate_complete()
            && self.system_complete_at_ms.is_some()
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SystemDuplicatePublisherInvariantConfig {
    pub require_control_complete: bool,
    pub require_media_duplicate_complete: bool,
    #[serde(default = "default_true")]
    pub require_media_timing: bool,
    pub max_system_complete_ms: Option<u64>,
}

impl SystemDuplicatePublisherInvariantConfig {
    pub fn complete_with_deadline(max_system_complete_ms: u64) -> Self {
        Self {
            require_control_complete: true,
            require_media_duplicate_complete: true,
            require_media_timing: true,
            max_system_complete_ms: Some(max_system_complete_ms),
        }
    }
}

impl Default for SystemDuplicatePublisherInvariantConfig {
    fn default() -> Self {
        Self {
            require_control_complete: true,
            require_media_duplicate_complete: true,
            require_media_timing: true,
            max_system_complete_ms: None,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SystemDuplicatePublisherInvariantReport {
    pub ok: bool,
    pub replay_hint: String,
    pub failures: Vec<String>,
    pub system_complete_at_ms: Option<u64>,
    pub max_system_complete_ms: Option<u64>,
}

pub fn check_system_duplicate_publisher_invariants(
    report: &SystemDuplicatePublisherSimulationReport,
    config: &SystemDuplicatePublisherInvariantConfig,
) -> SystemDuplicatePublisherInvariantReport {
    let mut failures = Vec::new();

    if config.require_control_complete && !report.control.propagation_complete() {
        failures.push("control_propagation_incomplete".to_string());
    }
    if !report.media.summary.missing_sequences.is_empty() {
        failures.push("media_missing_sequences".to_string());
    }
    if !report.media.summary.divergent_sequences.is_empty() {
        failures.push("media_divergent_sequences".to_string());
    }
    if report
        .media
        .fault_stats
        .source_material_mismatch_observations
        > 0
    {
        failures.push("media_source_material_mismatch_observations".to_string());
    }
    if !report
        .media
        .summary
        .source_local_divergent_sequences
        .is_empty()
    {
        failures.push("media_source_local_divergent_sequences".to_string());
    }
    if config.require_media_timing && report.media.summary.media_timing_missing_records > 0 {
        failures.push("media_timing_missing_records".to_string());
    }
    if config.require_media_timing
        && !report
            .media
            .summary
            .media_timing_conflict_sequences
            .is_empty()
    {
        failures.push("media_timing_conflict_sequences".to_string());
    }
    if config.require_media_duplicate_complete && !report.media.duplicate_complete() {
        failures.push("media_duplicate_incomplete".to_string());
    }
    if let Some(max_system_complete_ms) = config.max_system_complete_ms {
        match report.system_complete_at_ms {
            Some(system_complete_at_ms) if system_complete_at_ms <= max_system_complete_ms => {}
            Some(_) => failures.push("system_complete_deadline_exceeded".to_string()),
            None => failures.push("system_complete_deadline_unreached".to_string()),
        }
    }

    SystemDuplicatePublisherInvariantReport {
        ok: failures.is_empty(),
        replay_hint: report.replay_hint.clone(),
        failures,
        system_complete_at_ms: report.system_complete_at_ms,
        max_system_complete_ms: config.max_system_complete_ms,
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SystemDuplicatePublisherCampaignFailure {
    pub seed: SimulationSeed,
    pub replay_hint: String,
    pub scenario: SystemDuplicatePublisherScenario,
    pub invariant: SystemDuplicatePublisherInvariantReport,
    pub report: SystemDuplicatePublisherSimulationReport,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SystemDuplicatePublisherCampaignRunSummary {
    pub seed: SimulationSeed,
    pub replay_hint: String,
    pub system_complete_at_ms: Option<u64>,
    pub control_propagation_complete_at_ms: Option<u64>,
    pub media_duplicate_complete_at_ms: Option<u64>,
    pub control_trace_events: u64,
    pub media_trace_events: u64,
    pub total_trace_events: u64,
    pub control_transient_dropped_messages: u64,
    pub control_partition_delayed_messages: u64,
    pub control_node_outage_delays: u64,
    pub control_duplicate_messages: u64,
    pub media_transient_dropped_observations: u64,
    pub media_partition_delayed_observations: u64,
    pub media_publisher_outage_observations: u64,
    pub media_backfill_observations: u64,
    pub media_publisher_phase_offset_observations: u64,
    pub media_source_material_mismatch_observations: u64,
    pub media_timing_missing_records: u64,
    pub media_timing_conflict_sequences: u64,
}

impl SystemDuplicatePublisherCampaignRunSummary {
    fn from_report(report: &SystemDuplicatePublisherSimulationReport) -> Self {
        Self {
            seed: report.seed,
            replay_hint: report.replay_hint.clone(),
            system_complete_at_ms: report.system_complete_at_ms,
            control_propagation_complete_at_ms: report.control.propagation_complete_at_ms,
            media_duplicate_complete_at_ms: report.media.duplicate_complete_at_ms,
            control_trace_events: report.control.trace.len() as u64,
            media_trace_events: report.media.trace.len() as u64,
            total_trace_events: report.control.trace.len() as u64 + report.media.trace.len() as u64,
            control_transient_dropped_messages: report
                .control
                .fault_stats
                .transient_dropped_messages,
            control_partition_delayed_messages: report
                .control
                .fault_stats
                .partition_delayed_messages,
            control_node_outage_delays: report.control.fault_stats.node_outage_delayed_messages
                + report.control.fault_stats.node_outage_delayed_rounds,
            control_duplicate_messages: report.control.fault_stats.duplicate_messages,
            media_transient_dropped_observations: report
                .media
                .fault_stats
                .transient_dropped_observations,
            media_partition_delayed_observations: report
                .media
                .fault_stats
                .partition_delayed_observations,
            media_publisher_outage_observations: report
                .media
                .fault_stats
                .publisher_outage_observations,
            media_backfill_observations: report.media.fault_stats.backfill_observations,
            media_publisher_phase_offset_observations: report
                .media
                .fault_stats
                .publisher_phase_offset_observations,
            media_source_material_mismatch_observations: report
                .media
                .fault_stats
                .source_material_mismatch_observations,
            media_timing_missing_records: report.media.summary.media_timing_missing_records,
            media_timing_conflict_sequences: report
                .media
                .summary
                .media_timing_conflict_sequences
                .len() as u64,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SystemDuplicatePublisherCampaignReport {
    pub name: String,
    pub seed_start: SimulationSeed,
    pub iterations: u64,
    pub passed: u64,
    pub failed: u64,
    pub first_failure: Option<SystemDuplicatePublisherCampaignFailure>,
    pub max_system_complete_ms_observed: u64,
    pub max_control_propagation_ms_observed: u64,
    pub max_media_duplicate_complete_ms_observed: u64,
    #[serde(default)]
    pub total_system_complete_ms_observed: u64,
    #[serde(default)]
    pub total_control_propagation_ms_observed: u64,
    #[serde(default)]
    pub total_media_duplicate_complete_ms_observed: u64,
    #[serde(default)]
    pub total_control_trace_events: u64,
    #[serde(default)]
    pub total_media_trace_events: u64,
    #[serde(default)]
    pub total_trace_events: u64,
    pub total_control_transient_drops: u64,
    #[serde(default)]
    pub total_control_partition_delays: u64,
    #[serde(default)]
    pub total_control_node_outage_delays: u64,
    #[serde(default)]
    pub total_control_duplicate_messages: u64,
    pub total_media_transient_drops: u64,
    #[serde(default)]
    pub total_media_partition_delays: u64,
    #[serde(default)]
    pub total_media_publisher_outages: u64,
    pub total_media_backfill_observations: u64,
    #[serde(default)]
    pub total_media_publisher_phase_offsets: u64,
    #[serde(default)]
    pub total_media_source_material_mismatches: u64,
    #[serde(default)]
    pub total_media_timing_missing_records: u64,
    #[serde(default)]
    pub total_media_timing_conflict_sequences: u64,
    #[serde(default)]
    pub seeds_with_system_convergence_time: u64,
    #[serde(default)]
    pub seeds_with_control_propagation_time: u64,
    #[serde(default)]
    pub seeds_with_media_duplicate_convergence_time: u64,
    #[serde(default)]
    pub seeds_with_control_transient_drops: u64,
    #[serde(default)]
    pub seeds_with_control_partition_delays: u64,
    #[serde(default)]
    pub seeds_with_control_node_outage_delays: u64,
    #[serde(default)]
    pub seeds_with_control_duplicate_messages: u64,
    #[serde(default)]
    pub seeds_with_media_transient_drops: u64,
    #[serde(default)]
    pub seeds_with_media_partition_delays: u64,
    #[serde(default)]
    pub seeds_with_media_publisher_outages: u64,
    #[serde(default)]
    pub seeds_with_media_backfill_observations: u64,
    #[serde(default)]
    pub seeds_with_media_publisher_phase_offsets: u64,
    #[serde(default)]
    pub seeds_with_media_source_material_mismatches: u64,
    #[serde(default)]
    pub seeds_with_media_timing_missing_records: u64,
    #[serde(default)]
    pub seeds_with_media_timing_conflict_sequences: u64,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub slowest_system_runs: Vec<SystemDuplicatePublisherCampaignRunSummary>,
}

impl SystemDuplicatePublisherCampaignReport {
    pub fn all_passed(&self) -> bool {
        self.failed == 0 && self.passed == self.iterations && self.first_failure.is_none()
    }

    pub fn fault_coverage_failures(&self) -> Vec<String> {
        let mut failures = Vec::new();
        if self.max_system_complete_ms_observed == 0 {
            failures.push("system_convergence_time_unobserved".to_string());
        }
        if self.max_control_propagation_ms_observed == 0 {
            failures.push("control_propagation_time_unobserved".to_string());
        }
        if self.max_media_duplicate_complete_ms_observed == 0 {
            failures.push("media_duplicate_convergence_time_unobserved".to_string());
        }
        if self.seeds_with_control_transient_drops == 0 {
            failures.push("control_transient_drops_uncovered".to_string());
        }
        if self.seeds_with_control_partition_delays == 0 {
            failures.push("control_partition_delays_uncovered".to_string());
        }
        if self.seeds_with_control_node_outage_delays == 0 {
            failures.push("control_node_outage_delays_uncovered".to_string());
        }
        if self.seeds_with_control_duplicate_messages == 0 {
            failures.push("control_duplicate_messages_uncovered".to_string());
        }
        if self.seeds_with_media_transient_drops == 0 {
            failures.push("media_transient_drops_uncovered".to_string());
        }
        if self.seeds_with_media_partition_delays == 0 {
            failures.push("media_partition_delays_uncovered".to_string());
        }
        if self.seeds_with_media_publisher_outages == 0 {
            failures.push("media_publisher_outages_uncovered".to_string());
        }
        if self.seeds_with_media_backfill_observations == 0 {
            failures.push("media_backfill_uncovered".to_string());
        }
        failures
    }

    pub fn fault_coverage_ok(&self) -> bool {
        self.fault_coverage_failures().is_empty()
    }
}

pub fn run_system_duplicate_publisher_simulation(
    scenario: &SystemDuplicatePublisherScenario,
) -> SystemDuplicatePublisherSimulationReport {
    let control = run_control_plane_propagation_simulation(&scenario.control);
    let mut media_scenario = scenario.media.clone();
    let media_window_ms = media_scenario
        .expected_sequences()
        .saturating_mul(media_scenario.segment_step_ms);
    let mut publisher_activation_ms = BTreeMap::new();
    let mut active_publishers = Vec::new();

    for publisher in &scenario.media.publisher_nodes {
        let Some(learned_at_ms) = control.known_at_by_node.get(publisher).copied() else {
            continue;
        };
        let activation_ms = learned_at_ms.saturating_add(scenario.publisher_activation_delay_ms);
        publisher_activation_ms.insert(publisher.clone(), activation_ms);
        active_publishers.push(publisher.clone());

        if activation_ms > 0 {
            media_scenario.publisher_outages.push(SimulationOutage::new(
                publisher,
                0,
                activation_ms.min(media_window_ms.saturating_add(activation_ms)),
                scenario.publisher_backfill_delay_ms,
            ));
        }

        if scenario.sequence_clock == PublisherSequenceClock::LocalActivation {
            let sequence_offset = ceil_div_u64(activation_ms, media_scenario.segment_step_ms);
            if sequence_offset > 0 {
                media_scenario
                    .publisher_sequence_offsets
                    .insert(publisher.clone(), sequence_offset);
            }
        }
    }

    media_scenario.publisher_nodes = active_publishers;
    let media = run_duplicate_publisher_simulation(&media_scenario);
    let system_complete_at_ms = match (
        control.propagation_complete_at_ms,
        media.duplicate_complete_at_ms,
    ) {
        (Some(control_ms), Some(media_ms))
            if control.propagation_complete() && media.duplicate_complete() =>
        {
            Some(control_ms.max(media_ms))
        }
        _ => None,
    };

    SystemDuplicatePublisherSimulationReport {
        seed: scenario.seed,
        replay_hint: scenario.seed.replay_hint(),
        publisher_activation_ms,
        control,
        media,
        system_complete_at_ms,
    }
}

pub fn run_system_duplicate_publisher_campaign<F>(
    name: &str,
    seed_start: SimulationSeed,
    iterations: u64,
    invariant_config: &SystemDuplicatePublisherInvariantConfig,
    mut build_scenario: F,
) -> SystemDuplicatePublisherCampaignReport
where
    F: FnMut(SimulationSeed) -> SystemDuplicatePublisherScenario,
{
    let mut campaign = SystemDuplicatePublisherCampaignReport {
        name: name.to_string(),
        seed_start,
        iterations,
        passed: 0,
        failed: 0,
        first_failure: None,
        max_system_complete_ms_observed: 0,
        max_control_propagation_ms_observed: 0,
        max_media_duplicate_complete_ms_observed: 0,
        total_system_complete_ms_observed: 0,
        total_control_propagation_ms_observed: 0,
        total_media_duplicate_complete_ms_observed: 0,
        total_control_trace_events: 0,
        total_media_trace_events: 0,
        total_trace_events: 0,
        total_control_transient_drops: 0,
        total_control_partition_delays: 0,
        total_control_node_outage_delays: 0,
        total_control_duplicate_messages: 0,
        total_media_transient_drops: 0,
        total_media_partition_delays: 0,
        total_media_publisher_outages: 0,
        total_media_backfill_observations: 0,
        total_media_publisher_phase_offsets: 0,
        total_media_source_material_mismatches: 0,
        total_media_timing_missing_records: 0,
        total_media_timing_conflict_sequences: 0,
        seeds_with_system_convergence_time: 0,
        seeds_with_control_propagation_time: 0,
        seeds_with_media_duplicate_convergence_time: 0,
        seeds_with_control_transient_drops: 0,
        seeds_with_control_partition_delays: 0,
        seeds_with_control_node_outage_delays: 0,
        seeds_with_control_duplicate_messages: 0,
        seeds_with_media_transient_drops: 0,
        seeds_with_media_partition_delays: 0,
        seeds_with_media_publisher_outages: 0,
        seeds_with_media_backfill_observations: 0,
        seeds_with_media_publisher_phase_offsets: 0,
        seeds_with_media_source_material_mismatches: 0,
        seeds_with_media_timing_missing_records: 0,
        seeds_with_media_timing_conflict_sequences: 0,
        slowest_system_runs: Vec::new(),
    };

    let generic = run_seeded_simulation_campaign(name, seed_start, iterations, |seed| {
        let scenario = build_scenario(seed);
        let report = run_system_duplicate_publisher_simulation(&scenario);
        let invariant = check_system_duplicate_publisher_invariants(&report, invariant_config);
        let run_summary = SystemDuplicatePublisherCampaignRunSummary::from_report(&report);

        if let Some(system_complete_at_ms) = report.system_complete_at_ms {
            campaign.seeds_with_system_convergence_time += 1;
            campaign.total_system_complete_ms_observed = campaign
                .total_system_complete_ms_observed
                .saturating_add(system_complete_at_ms);
            campaign.max_system_complete_ms_observed = campaign
                .max_system_complete_ms_observed
                .max(system_complete_at_ms);
        }
        if let Some(control_ms) = report.control.propagation_complete_at_ms {
            campaign.seeds_with_control_propagation_time += 1;
            campaign.total_control_propagation_ms_observed = campaign
                .total_control_propagation_ms_observed
                .saturating_add(control_ms);
            campaign.max_control_propagation_ms_observed =
                campaign.max_control_propagation_ms_observed.max(control_ms);
        }
        if let Some(media_ms) = report.media.duplicate_complete_at_ms {
            campaign.seeds_with_media_duplicate_convergence_time += 1;
            campaign.total_media_duplicate_complete_ms_observed = campaign
                .total_media_duplicate_complete_ms_observed
                .saturating_add(media_ms);
            campaign.max_media_duplicate_complete_ms_observed = campaign
                .max_media_duplicate_complete_ms_observed
                .max(media_ms);
        }
        campaign.total_control_trace_events = campaign
            .total_control_trace_events
            .saturating_add(report.control.trace.len() as u64);
        campaign.total_media_trace_events = campaign
            .total_media_trace_events
            .saturating_add(report.media.trace.len() as u64);
        campaign.total_trace_events = campaign
            .total_trace_events
            .saturating_add(report.control.trace.len() as u64 + report.media.trace.len() as u64);
        campaign.total_control_transient_drops +=
            report.control.fault_stats.transient_dropped_messages;
        campaign.total_control_partition_delays +=
            report.control.fault_stats.partition_delayed_messages;
        campaign.total_control_node_outage_delays +=
            report.control.fault_stats.node_outage_delayed_messages
                + report.control.fault_stats.node_outage_delayed_rounds;
        campaign.total_control_duplicate_messages += report.control.fault_stats.duplicate_messages;
        campaign.total_media_transient_drops +=
            report.media.fault_stats.transient_dropped_observations;
        campaign.total_media_partition_delays +=
            report.media.fault_stats.partition_delayed_observations;
        campaign.total_media_publisher_outages +=
            report.media.fault_stats.publisher_outage_observations;
        campaign.total_media_backfill_observations +=
            report.media.fault_stats.backfill_observations;
        campaign.total_media_publisher_phase_offsets +=
            report.media.fault_stats.publisher_phase_offset_observations;
        campaign.total_media_source_material_mismatches += report
            .media
            .fault_stats
            .source_material_mismatch_observations;
        campaign.total_media_timing_missing_records +=
            report.media.summary.media_timing_missing_records;
        campaign.total_media_timing_conflict_sequences +=
            report.media.summary.media_timing_conflict_sequences.len() as u64;

        if report.control.fault_stats.transient_dropped_messages > 0 {
            campaign.seeds_with_control_transient_drops += 1;
        }
        if report.control.fault_stats.partition_delayed_messages > 0 {
            campaign.seeds_with_control_partition_delays += 1;
        }
        if report.control.fault_stats.node_outage_delayed_messages
            + report.control.fault_stats.node_outage_delayed_rounds
            > 0
        {
            campaign.seeds_with_control_node_outage_delays += 1;
        }
        if report.control.fault_stats.duplicate_messages > 0 {
            campaign.seeds_with_control_duplicate_messages += 1;
        }
        if report.media.fault_stats.transient_dropped_observations > 0 {
            campaign.seeds_with_media_transient_drops += 1;
        }
        if report.media.fault_stats.partition_delayed_observations > 0 {
            campaign.seeds_with_media_partition_delays += 1;
        }
        if report.media.fault_stats.publisher_outage_observations > 0 {
            campaign.seeds_with_media_publisher_outages += 1;
        }
        if report.media.fault_stats.backfill_observations > 0 {
            campaign.seeds_with_media_backfill_observations += 1;
        }
        if report.media.fault_stats.publisher_phase_offset_observations > 0 {
            campaign.seeds_with_media_publisher_phase_offsets += 1;
        }
        if report
            .media
            .fault_stats
            .source_material_mismatch_observations
            > 0
        {
            campaign.seeds_with_media_source_material_mismatches += 1;
        }
        if report.media.summary.media_timing_missing_records > 0 {
            campaign.seeds_with_media_timing_missing_records += 1;
        }
        if !report
            .media
            .summary
            .media_timing_conflict_sequences
            .is_empty()
        {
            campaign.seeds_with_media_timing_conflict_sequences += 1;
        }
        insert_slowest_system_campaign_run(&mut campaign.slowest_system_runs, run_summary);

        if invariant.ok {
            None
        } else {
            Some(SystemDuplicatePublisherCampaignFailure {
                seed,
                replay_hint: report.replay_hint.clone(),
                scenario,
                invariant,
                report,
            })
        }
    });

    campaign.passed = generic.passed;
    campaign.failed = generic.failed;
    campaign.first_failure = generic.first_failure;

    campaign
}

fn insert_slowest_system_campaign_run(
    runs: &mut Vec<SystemDuplicatePublisherCampaignRunSummary>,
    run: SystemDuplicatePublisherCampaignRunSummary,
) {
    const LIMIT: usize = 16;

    runs.push(run);
    runs.sort_by(|left, right| {
        let left_complete_ms = left.system_complete_at_ms.unwrap_or(u64::MAX);
        let right_complete_ms = right.system_complete_at_ms.unwrap_or(u64::MAX);
        right_complete_ms
            .cmp(&left_complete_ms)
            .then_with(|| left.seed.cmp(&right.seed))
    });
    runs.truncate(LIMIT);
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct FoundationStyleSystemScenarioConfig {
    pub nodes: Vec<String>,
    pub publisher_nodes: Vec<String>,
    pub origin_node: String,
    pub topic: String,
    pub announcement_id: String,
    pub stream_id: String,
    pub rendition_id: String,
    pub track_name: String,
    pub profile_id: String,
    pub sequence_clock: PublisherSequenceClock,
}

impl Default for FoundationStyleSystemScenarioConfig {
    fn default() -> Self {
        Self {
            nodes: vec![
                "forge".to_string(),
                "nuc-a".to_string(),
                "nuc-b".to_string(),
                "tower".to_string(),
                "relay-lax".to_string(),
                "relay-nyc".to_string(),
                "relay-hel".to_string(),
            ],
            publisher_nodes: vec!["nuc-a".to_string(), "nuc-b".to_string()],
            origin_node: "forge".to_string(),
            topic: "ec.control.broadcast.la-kcop".to_string(),
            announcement_id: "la-kcop@42".to_string(),
            stream_id: "la-kcop".to_string(),
            rendition_id: "720p".to_string(),
            track_name: "0.m4s".to_string(),
            profile_id: "x264-hd3-v1".to_string(),
            sequence_clock: PublisherSequenceClock::Global,
        }
    }
}

pub fn foundation_style_system_duplicate_publisher_scenario(
    seed: SimulationSeed,
    config: &FoundationStyleSystemScenarioConfig,
) -> SystemDuplicatePublisherScenario {
    let mut rng = SimulationRng::new(SimulationSeed::new(seed.0 ^ 0x6664_622d_7374_796c));
    let nodes = if config.nodes.is_empty() {
        FoundationStyleSystemScenarioConfig::default().nodes
    } else {
        config.nodes.clone()
    };
    let publisher_nodes = if config.publisher_nodes.is_empty() {
        FoundationStyleSystemScenarioConfig::default().publisher_nodes
    } else {
        config.publisher_nodes.clone()
    };
    let origin_node = if nodes.contains(&config.origin_node) {
        config.origin_node.clone()
    } else {
        nodes
            .first()
            .cloned()
            .unwrap_or_else(|| "forge".to_string())
    };
    let mut control = ControlPlanePropagationScenario::new(
        seed,
        nodes.clone(),
        &origin_node,
        &config.topic,
        &config.announcement_id,
    );
    control.fanout = rng
        .range_inclusive(3, 5)
        .min(nodes.len().saturating_sub(1) as u64);
    control.gossip_interval_ms = rng.range_inclusive(24, 42);
    control.max_gossip_rounds = rng.range_inclusive(12, 16);
    control.base_network_delay_ms = rng.range_inclusive(4, 10);
    control.max_jitter_ms = rng.range_inclusive(25, 65);
    control.transient_drop_per_million = rng.range_inclusive(45_000, 95_000) as u32;
    control.partitions = foundation_style_control_partitions(&mut rng, &nodes, &publisher_nodes);
    control.node_outages = foundation_style_node_outages(&mut rng, &nodes, &origin_node);

    let sequence_count = rng.range_inclusive(40, 72);
    let media_seed = SimulationSeed::new(seed.0 ^ 0x6d65_6469_6121);
    let mut media = DuplicatePublisherScenario::new(
        media_seed,
        publisher_nodes.clone(),
        &config.stream_id,
        &config.rendition_id,
        &config.track_name,
        &config.profile_id,
        0,
        sequence_count,
    );
    media.segment_step_ms = rng.range_inclusive(35, 55);
    media.base_network_delay_ms = rng.range_inclusive(3, 9);
    media.max_jitter_ms = rng.range_inclusive(45, 95);
    media.transient_drop_per_million = rng.range_inclusive(180_000, 320_000) as u32;
    media.backfill_after_ms = rng.range_inclusive(420, 760);
    let media_window_ms = sequence_count.saturating_mul(media.segment_step_ms);
    media.partitions =
        foundation_style_media_partitions(&mut rng, &publisher_nodes, media_window_ms);
    media.publisher_outages =
        foundation_style_publisher_outages(&mut rng, &publisher_nodes, media_window_ms);

    let mut scenario = SystemDuplicatePublisherScenario::new(seed, control, media);
    scenario.publisher_activation_delay_ms = rng.range_inclusive(10, 55);
    scenario.publisher_backfill_delay_ms = rng.range_inclusive(120, 260);
    scenario.sequence_clock = config.sequence_clock;
    scenario
}

fn foundation_style_control_partitions(
    rng: &mut SimulationRng,
    nodes: &[String],
    publisher_nodes: &[String],
) -> Vec<SimulationPartition> {
    let mut partitions = Vec::new();
    if let Some(publisher) = pick_sim_item(rng, publisher_nodes) {
        let start_ms = rng.range_inclusive(0, 160);
        let end_ms = start_ms.saturating_add(rng.range_inclusive(80, 210));
        partitions.push(SimulationPartition::new(
            publisher,
            start_ms,
            end_ms,
            rng.range_inclusive(25, 90),
        ));
    }
    let relay_nodes = nodes
        .iter()
        .filter(|node| !publisher_nodes.contains(*node) && node.as_str() != "forge")
        .cloned()
        .collect::<Vec<_>>();
    if let Some(relay) = pick_sim_item(rng, &relay_nodes) {
        let start_ms = rng.range_inclusive(60, 260);
        let end_ms = start_ms.saturating_add(rng.range_inclusive(70, 180));
        partitions.push(SimulationPartition::new(
            relay,
            start_ms,
            end_ms,
            rng.range_inclusive(25, 80),
        ));
    }
    partitions
}

fn foundation_style_node_outages(
    rng: &mut SimulationRng,
    nodes: &[String],
    origin_node: &str,
) -> Vec<SimulationOutage> {
    let outage_candidates = nodes
        .iter()
        .filter(|node| node.as_str() != origin_node)
        .cloned()
        .collect::<Vec<_>>();
    let Some(node) = pick_sim_item(rng, &outage_candidates) else {
        return Vec::new();
    };
    let start_ms = rng.range_inclusive(80, 260);
    let end_ms = start_ms.saturating_add(rng.range_inclusive(60, 180));
    vec![SimulationOutage::new(
        node,
        start_ms,
        end_ms,
        rng.range_inclusive(25, 95),
    )]
}

fn foundation_style_media_partitions(
    rng: &mut SimulationRng,
    publisher_nodes: &[String],
    media_window_ms: u64,
) -> Vec<SimulationPartition> {
    let Some(publisher) = pick_sim_item(rng, publisher_nodes) else {
        return Vec::new();
    };
    let start_ceiling = media_window_ms.saturating_div(2).max(1);
    let start_ms = rng.range_inclusive(0, start_ceiling);
    let duration_ms = rng.range_inclusive(180, 520).min(media_window_ms.max(180));
    let end_ms = start_ms.saturating_add(duration_ms);
    vec![SimulationPartition::new(
        publisher,
        start_ms,
        end_ms,
        rng.range_inclusive(80, 240),
    )]
}

fn foundation_style_publisher_outages(
    rng: &mut SimulationRng,
    publisher_nodes: &[String],
    media_window_ms: u64,
) -> Vec<SimulationOutage> {
    let Some(publisher) = pick_sim_item(rng, publisher_nodes) else {
        return Vec::new();
    };
    let start_floor = media_window_ms.saturating_div(5);
    let start_ceiling = media_window_ms
        .saturating_mul(4)
        .saturating_div(5)
        .max(start_floor);
    let start_ms = rng.range_inclusive(start_floor, start_ceiling);
    let duration_ms = rng.range_inclusive(140, 420).min(media_window_ms.max(140));
    let end_ms = start_ms.saturating_add(duration_ms);
    vec![SimulationOutage::new(
        publisher,
        start_ms,
        end_ms,
        rng.range_inclusive(140, 360),
    )]
}

fn pick_sim_item<'a>(rng: &mut SimulationRng, items: &'a [String]) -> Option<&'a str> {
    if items.is_empty() {
        return None;
    }
    let index = rng.range_inclusive(0, items.len() as u64 - 1) as usize;
    Some(items[index].as_str())
}

fn ceil_div_u64(value: u64, divisor: u64) -> u64 {
    if divisor == 0 {
        return value;
    }
    value.saturating_add(divisor - 1) / divisor
}
