//! Deterministic property-test generators for the public types of A
//! ([`epiphany_determinism`]), B ([`epiphany_core`]), C ([`epiphany_ops`]), and
//! D ([`epiphany_bundle`]). Agent E's layout-IR types
//! ([`epiphany_layout_ir`]) are generated in [`crate::layout_stub`], which
//! drives the real crate.
//!
//! Every generator draws from the seeded [`Rng`], so a failing case reproduces
//! from its seed (Appendix D §"Randomness": no platform entropy in the harness).
//! Whole **graphs** are generated by Agent B's own positive/negative generators,
//! re-exported as [`graph`].

use epiphany_bundle::{
    chunk_id, content_hash_for, BlobId, BlobRef, ChunkKind, ChunkRef, CommitState,
    CompressionAlgorithm, DocumentId, ExtensionDeclaration, ExtensionId, FrontierBytes, LineageId,
    Manifest, ProfileConstraints, ProfileDeclaration, ProfileId, ProfileRegistryId,
    ReductionAlgorithmVersion, RetentionPolicy, SchemaVersion, SemVer, SnapshotId, SnapshotRef,
    Superblock, WallClockDuration as BundleWallClockDuration, WallClockTime as BundleWallClockTime,
};
use epiphany_core::{
    AnalysisLayerId, AnalyticalAnnotationId, BarlineAlignmentGroupId, BeamId, ChordSymbolId,
    CommentId, EventId, GraphicGestureId, GraphicObjectId, InstrumentId, IntegrityAnomalyId,
    LyricLineId, MarkerId, MeasureId, MusicalDuration, MusicalPosition, ObjectKindRegistryId,
    OperationId, PartDefinitionId, PitchId, RationalTime, RegionId, RepeatStructureId, ReplicaId,
    SlurId, SpannerId, StaffGroupId, StaffId, StaffInstanceId, TieId, TimeSignatureId,
    TransactionId, TupletId, TypedObjectId, ViewId, VoiceId, WallClockDuration, WallClockTime,
};
use epiphany_determinism::{
    CanonicalEncode, CanonicalF64, ChunkId, ContentHash, DomainTag, QuantizedCoord, Tolerance,
    ToleranceClass, ToleranceGovernance,
};
use epiphany_ops::valuegen;
use epiphany_ops::{
    AnomalousReplicaSegment, AuthorId, CausalContext, ChangeRegionTimeModelOp, ConflictId,
    ConflictKind, ConflictKindRegistryId, ConflictRecord, ConflictRegistry,
    ConflictResolutionState, CreateCrossCuttingOp, CreateRegionOp, CreateStaffInstanceOp,
    CreateVoiceOp, CrossCuttingValue, DeleteCrossCuttingOp, DeleteEventOp, DeleteIdentifiedPitchOp,
    DeleteRegionOp, DeleteStaffInstanceOp, DeleteVoiceOp, ExtensionPreconditionId, FieldPath,
    HybridLogicalClock, InsertEventOp, InsertIdentifiedPitchOp, IntegrityAnomaly,
    IntegrityAnomalyKind, IntegrityAnomalyRegistryId, MaterializedState, ModifyCrossCuttingOp,
    ModifyEventOp, ModifyIdentifiedPitchOp, NoOpReason, ObjectKind, ObjectState, OperationEffect,
    OperationEnvelope, OperationKind, OperationKindRegistryId, OperationPayload, OperationSet,
    OperationStamp, PendingReason, PositionRemapping, PreconditionFailureReason,
    PreconditionFailureRegistryId, ReanchorReason, ReanchorReasonRegistryId, ReanchorResult,
    RepairKind, RepairKindRegistryId, RepairRecord, ReplicaAnomalyReason, ReplicaAnomalyRegistryId,
    ResolutionAction, ResolutionRegistryId, ResolveConflictPayload, RespellPitchOp,
    SerializedCanonicalInputs, SetMetadataOp, SetMetricGridOp, SetUserPageBreakOp,
    SetUserSystemBreakOp, TransactionCategory, TransactionDescriptor, TransposeOp,
    TupletCompensation, TupletCompensationKind, UndoPolicy, UndoTransactionPayload,
};

use crate::rng::Rng;

/// Agent B's score-graph generators and shrinkers, re-exported so the testkit's
/// consumers have one import for *all* generators: `testkit::generators::graph`.
pub use epiphany_core::generators as graph;

// ===========================================================================
// Agent A — epiphany-determinism
// ===========================================================================

/// A canonical staff-space coordinate. Every `i64` unit count is valid.
pub fn quantized_coord(rng: &mut Rng) -> QuantizedCoord {
    QuantizedCoord::from_units(rng.next_u64() as i64)
}

/// A finite canonical `f64`. Non-finite bit patterns (NaN/inf), a tiny fraction
/// of the space, are re-drawn; `-0.0` is canonicalized to `+0.0` by the type.
pub fn canonical_f64(rng: &mut Rng) -> CanonicalF64 {
    for _ in 0..8 {
        if let Some(c) = CanonicalF64::new(f64::from_bits(rng.next_u64())) {
            return c;
        }
    }
    CanonicalF64::new(0.0).unwrap()
}

/// A content hash (any 32 bytes are valid).
pub fn content_hash(rng: &mut Rng) -> ContentHash {
    ContentHash(rng.array32())
}

/// A chunk id (newtype over [`ContentHash`]).
pub fn chunk_id_gen(rng: &mut Rng) -> ChunkId {
    ChunkId(content_hash(rng))
}

/// A valid domain tag, drawn from the built-in `MUSC*` vocabulary.
pub fn domain_tag(rng: &mut Rng) -> DomainTag {
    *rng.choose(&DomainTag::BUILTINS)
}

/// A typed tolerance (not a canonical-serialized type — generated for
/// completeness of the "every public type" charter, exercised by validation
/// tests rather than the byte round-trip).
pub fn tolerance(rng: &mut Rng) -> Tolerance {
    let class = *rng.choose(&[
        ToleranceClass::AcousticCents,
        ToleranceClass::LayoutCoordinate,
        ToleranceClass::QualityMetric,
        ToleranceClass::TempoIntegration,
        ToleranceClass::SolverResidual,
    ]);
    let governance = *rng.choose(&[
        ToleranceGovernance::Equality,
        ToleranceGovernance::Validation,
        ToleranceGovernance::Diagnostic,
    ]);
    let absolute = (rng.range(1, 1_000_000) as f64) / 1_000_000.0;
    Tolerance::absolute(class, absolute, governance).expect("finite positive tolerance")
}

// ===========================================================================
// Agent B — epiphany-core: identifiers, time, typed-object id
// ===========================================================================

/// A non-reserved replica identifier (never [`ReplicaId::SYSTEM_DERIVED`]).
pub fn replica_id(rng: &mut Rng) -> ReplicaId {
    ReplicaId::from_entropy(rng.next_u64().to_le_bytes()).unwrap_or(ReplicaId(0x5151_5151))
}

/// An operation identifier with a non-reserved replica.
pub fn operation_id(rng: &mut Rng) -> OperationId {
    OperationId::new(replica_id(rng), rng.next_u64())
}

/// An opaque author identifier.
pub fn author_id(rng: &mut Rng) -> AuthorId {
    AuthorId(((rng.next_u64() as u128) << 64) | rng.next_u64() as u128)
}

/// A content-addressed conflict identifier value.
pub fn conflict_id(rng: &mut Rng) -> ConflictId {
    ConflictId(((rng.next_u64() as u128) << 64) | rng.next_u64() as u128)
}

macro_rules! id_generator {
    ($(#[$m:meta])* $fn_name:ident -> $ty:ident) => {
        $(#[$m])*
        pub fn $fn_name(rng: &mut Rng) -> $ty {
            $ty::new(replica_id(rng), rng.next_u64())
        }
    };
}

id_generator!(/// An event identifier.
    event_id -> EventId);
id_generator!(/// A pitch identifier.
    pitch_id -> PitchId);
id_generator!(/// A voice identifier.
    voice_id -> VoiceId);
id_generator!(/// A staff identifier.
    staff_id -> StaffId);
id_generator!(/// A staff-instance identifier.
    staff_instance_id -> StaffInstanceId);
id_generator!(/// A staff-group identifier.
    staff_group_id -> StaffGroupId);
id_generator!(/// A region identifier.
    region_id -> RegionId);
id_generator!(/// An instrument identifier.
    instrument_id -> InstrumentId);
id_generator!(/// A part-definition identifier.
    part_definition_id -> PartDefinitionId);
id_generator!(/// A measure identifier.
    measure_id -> MeasureId);
id_generator!(/// A barline-alignment-group identifier.
    barline_alignment_group_id -> BarlineAlignmentGroupId);
id_generator!(/// A tuplet identifier.
    tuplet_id -> TupletId);
id_generator!(/// A slur identifier.
    slur_id -> SlurId);
id_generator!(/// A tie identifier.
    tie_id -> TieId);
id_generator!(/// A beam identifier.
    beam_id -> BeamId);
id_generator!(/// A spanner identifier.
    spanner_id -> SpannerId);
id_generator!(/// A marker identifier.
    marker_id -> MarkerId);
id_generator!(/// An analytical-annotation identifier.
    analytical_annotation_id -> AnalyticalAnnotationId);
id_generator!(/// A comment identifier.
    comment_id -> CommentId);
id_generator!(/// A graphic-object identifier.
    graphic_object_id -> GraphicObjectId);
id_generator!(/// A graphic-gesture identifier.
    graphic_gesture_id -> GraphicGestureId);
id_generator!(/// A time-signature identifier.
    time_signature_id -> TimeSignatureId);
id_generator!(/// An analysis-layer identifier.
    analysis_layer_id -> AnalysisLayerId);
id_generator!(/// A repeat-structure identifier.
    repeat_structure_id -> RepeatStructureId);
id_generator!(/// A lyric-line identifier.
    lyric_line_id -> LyricLineId);
id_generator!(/// A chord-symbol identifier.
    chord_symbol_id -> ChordSymbolId);
id_generator!(/// A view identifier.
    view_id -> ViewId);
id_generator!(/// An object-kind registry identifier.
    object_kind_registry_id -> ObjectKindRegistryId);
id_generator!(/// A transaction identifier.
    transaction_id -> TransactionId);
id_generator!(/// An integrity-anomaly identifier.
    integrity_anomaly_id -> IntegrityAnomalyId);

/// A small (inline) rational time value with a non-zero denominator.
pub fn rational_time_small(rng: &mut Rng) -> RationalTime {
    loop {
        let num = (rng.next_u64() % (1 << 20)) as i64 - (1 << 19);
        let den = rng.range(1, 4096) as i64;
        if let Some(r) = RationalTime::new(num, den) {
            return r;
        }
    }
}

/// A promoted (arbitrary-precision `Large`) rational time value. The numerator
/// is forced far enough beyond the inline `i32` range that even after dividing
/// by the (small) denominator's gcd the reduced numerator still exceeds
/// `i32::MAX`, so the value cannot demote to the inline arm.
pub fn rational_time_large(rng: &mut Rng) -> RationalTime {
    // `i32::MAX * 16` guarantees `num / den > i32::MAX` for every den <= 13.
    let big = (i32::MAX as i64) * 16 + (rng.next_u64() % 1_000_000_000) as i64;
    let den = *rng.choose(&[1i64, 3, 7, 11, 13]);
    RationalTime::new(big, den).expect("non-zero denominator")
}

/// A rational time value, half inline and half promoted — so the round-trip
/// corpus exercises both [`RationalTime`] arms.
pub fn rational_time(rng: &mut Rng) -> RationalTime {
    if rng.boolean() {
        rational_time_small(rng)
    } else {
        rational_time_large(rng)
    }
}

/// A musical position (rational, possibly promoted).
pub fn musical_position(rng: &mut Rng) -> MusicalPosition {
    MusicalPosition(rational_time(rng))
}

/// A musical duration (rational, possibly promoted).
pub fn musical_duration(rng: &mut Rng) -> MusicalDuration {
    MusicalDuration(rational_time(rng))
}

/// A wall-clock time (signed nanoseconds).
pub fn wallclock_time(rng: &mut Rng) -> WallClockTime {
    WallClockTime(rng.next_u64() as i64)
}

/// A wall-clock duration (signed nanoseconds).
pub fn wallclock_duration(rng: &mut Rng) -> WallClockDuration {
    WallClockDuration(rng.next_u64() as i64)
}

/// A tagged object identifier across the **whole** graph identity family — every
/// [`TypedObjectId`] variant including the variable-width `Registered` form, so
/// the round-trip harness exercises every discriminant and both encodings.
pub fn typed_object_id(rng: &mut Rng) -> TypedObjectId {
    match rng.below(28) {
        0 => TypedObjectId::Event(event_id(rng)),
        1 => TypedObjectId::Pitch(pitch_id(rng)),
        2 => TypedObjectId::Voice(voice_id(rng)),
        3 => TypedObjectId::Staff(staff_id(rng)),
        4 => TypedObjectId::StaffInstance(staff_instance_id(rng)),
        5 => TypedObjectId::StaffGroup(staff_group_id(rng)),
        6 => TypedObjectId::Region(region_id(rng)),
        7 => TypedObjectId::Instrument(instrument_id(rng)),
        8 => TypedObjectId::PartDefinition(part_definition_id(rng)),
        9 => TypedObjectId::Measure(measure_id(rng)),
        10 => TypedObjectId::BarlineAlignmentGroup(barline_alignment_group_id(rng)),
        11 => TypedObjectId::Slur(slur_id(rng)),
        12 => TypedObjectId::Tie(tie_id(rng)),
        13 => TypedObjectId::Beam(beam_id(rng)),
        14 => TypedObjectId::Spanner(spanner_id(rng)),
        15 => TypedObjectId::Marker(marker_id(rng)),
        16 => TypedObjectId::AnalyticalAnnotation(analytical_annotation_id(rng)),
        17 => TypedObjectId::Comment(comment_id(rng)),
        18 => TypedObjectId::GraphicObject(graphic_object_id(rng)),
        19 => TypedObjectId::GraphicGesture(graphic_gesture_id(rng)),
        20 => TypedObjectId::TimeSignature(time_signature_id(rng)),
        21 => TypedObjectId::AnalysisLayer(analysis_layer_id(rng)),
        22 => TypedObjectId::Tuplet(tuplet_id(rng)),
        23 => TypedObjectId::RepeatStructure(repeat_structure_id(rng)),
        24 => TypedObjectId::LyricLine(lyric_line_id(rng)),
        25 => TypedObjectId::ChordSymbol(chord_symbol_id(rng)),
        26 => TypedObjectId::View(view_id(rng)),
        _ => TypedObjectId::Registered(
            object_kind_registry_id(rng),
            ((rng.next_u64() as u128) << 64) | rng.next_u64() as u128,
        ),
    }
}

// ===========================================================================
// Agent C — epiphany-ops: real operation envelopes
// ===========================================================================

/// The shared object-identifier namespace authoring replicas mint *references*
/// into, so operations from different replicas address the *same* events,
/// pitches, and voices — which is what makes their reductions interact
/// (tombstones, already-applied, field conflicts) and what gives the convergence
/// harness something non-trivial to converge.
const OBJ_REPLICA: ReplicaId = ReplicaId(0x0B7E_C700);

fn obj_event(n: u64) -> EventId {
    EventId::new(OBJ_REPLICA, n)
}
fn obj_pitch(n: u64) -> PitchId {
    PitchId::new(OBJ_REPLICA, n)
}

/// A conflict-resolution action (every core and registered variant,
/// including `Dismiss`).
pub fn resolution_action(rng: &mut Rng) -> ResolutionAction {
    match rng.below(6) {
        0 => ResolutionAction::AcceptLoser,
        1 => ResolutionAction::KeepWinner,
        2 => ResolutionAction::Override {
            override_operation: operation_id(rng),
        },
        3 => ResolutionAction::Reanchor {
            new_target: typed_object_id(rng),
        },
        4 => ResolutionAction::Dismiss,
        _ => ResolutionAction::Registered(ResolutionRegistryId(
            ((rng.next_u64() as u128) << 64) | rng.next_u64() as u128,
        )),
    }
}

/// An undo policy (`StrictInverse`/`BestEffort`/`Cascade`).
pub fn undo_policy(rng: &mut Rng) -> UndoPolicy {
    *rng.choose(&[
        UndoPolicy::StrictInverse,
        UndoPolicy::BestEffort,
        UndoPolicy::Cascade,
    ])
}

/// A conflict kind covering every core and extension variant.
pub fn conflict_kind(rng: &mut Rng) -> ConflictKind {
    match rng.below(6) {
        0 => ConflictKind::StructuralFieldCollision {
            winner: operation_id(rng),
            loser: operation_id(rng),
            field: FieldPath(String::from("event.pitch")),
        },
        1 => ConflictKind::TransactionConflict {
            transaction: transaction_id(rng),
            failed_members: vec![operation_id(rng), operation_id(rng)],
        },
        2 => ConflictKind::TombstonedTarget {
            target: typed_object_id(rng),
            operation: operation_id(rng),
        },
        3 => ConflictKind::ReanchorFailure {
            original_referent: typed_object_id(rng),
            referencing_object: typed_object_id(rng),
        },
        4 => ConflictKind::TimeModelMigrationFailure {
            region: region_id(rng),
            incompatible_events: vec![typed_object_id(rng), typed_object_id(rng)],
        },
        _ => ConflictKind::ExtensionConflict {
            kind_id: ConflictKindRegistryId(
                ((rng.next_u64() as u128) << 64) | rng.next_u64() as u128,
            ),
            details: rng.byte_vec(0, 24),
        },
    }
}

/// A conflict resolution state covering every variant.
pub fn conflict_resolution_state(rng: &mut Rng) -> ConflictResolutionState {
    match rng.below(3) {
        0 => ConflictResolutionState::Unresolved,
        1 => ConflictResolutionState::Resolved {
            by: operation_id(rng),
            action: resolution_action(rng),
        },
        _ => ConflictResolutionState::Dismissed {
            by: operation_id(rng),
        },
    }
}

/// A self-consistent content-addressed conflict record.
pub fn conflict_record(rng: &mut Rng) -> ConflictRecord {
    let mut record = ConflictRecord::new(
        conflict_kind(rng),
        vec![operation_id(rng), operation_id(rng)],
        vec![typed_object_id(rng), typed_object_id(rng)],
    );
    record.resolution_state = conflict_resolution_state(rng);
    record
}

/// A canonical conflict registry containing a few generated records.
pub fn conflict_registry(rng: &mut Rng) -> ConflictRegistry {
    let mut registry = ConflictRegistry::new();
    for _ in 0..rng.range_usize(0, 4) {
        registry.insert(conflict_record(rng));
    }
    registry
}

/// A typed precondition failure (every core and registered variant).
pub fn precondition_failure_reason(rng: &mut Rng) -> PreconditionFailureReason {
    match rng.below(10) {
        0 => PreconditionFailureReason::TargetMissing,
        1 => PreconditionFailureReason::TargetTombstoned,
        2 => PreconditionFailureReason::WrongRegionTimeModel,
        3 => PreconditionFailureReason::TupletCompensationInvalid,
        4 => PreconditionFailureReason::EventDurationInvalid,
        5 => PreconditionFailureReason::PositionOutsideRegion,
        6 => PreconditionFailureReason::PitchSpaceMismatch,
        7 => PreconditionFailureReason::VoiceMissing,
        8 => PreconditionFailureReason::ExtensionPrecondition(ExtensionPreconditionId(
            rng.next_u64() as u128,
        )),
        _ => PreconditionFailureReason::Registered(PreconditionFailureRegistryId(
            rng.next_u64() as u128
        )),
    }
}

/// A no-op reason covering every variant.
pub fn no_op_reason(rng: &mut Rng) -> NoOpReason {
    match rng.below(5) {
        0 => NoOpReason::TargetTombstoned,
        1 => NoOpReason::AlreadyApplied,
        2 => NoOpReason::SupersededByLaterOperation {
            superseder: operation_id(rng),
        },
        3 => NoOpReason::PreconditionFailedUnderReduction {
            reason: precondition_failure_reason(rng),
        },
        _ => NoOpReason::TransactionConflict,
    }
}

/// A re-anchor reason covering every variant.
pub fn reanchor_reason(rng: &mut Rng) -> ReanchorReason {
    match rng.below(6) {
        0 => ReanchorReason::SameVoiceNearer,
        1 => ReanchorReason::SameStaffInstanceNearer,
        2 => ReanchorReason::SameStaffNearer,
        3 => ReanchorReason::SameRegionNearer,
        4 => ReanchorReason::ExplicitFallback,
        _ => ReanchorReason::DeclaredByExtension(ReanchorReasonRegistryId(rng.next_u64() as u128)),
    }
}

/// A tuplet compensation result covering every variant.
pub fn tuplet_compensation_kind(rng: &mut Rng) -> TupletCompensationKind {
    *rng.choose(&[
        TupletCompensationKind::ReplaceWithRest,
        TupletCompensationKind::RewriteTuplets,
        TupletCompensationKind::CascadeDeleteTuplets,
    ])
}

/// A repair kind covering every core and registered variant.
pub fn repair_kind(rng: &mut Rng) -> RepairKind {
    match rng.below(8) {
        0 => RepairKind::Reanchored {
            from: typed_object_id(rng),
            to: typed_object_id(rng),
            reason: reanchor_reason(rng),
        },
        1 => RepairKind::SpannerTruncated {
            removed_members: vec![typed_object_id(rng), typed_object_id(rng)],
        },
        2 => RepairKind::Orphaned,
        3 => RepairKind::CascadeDeleted,
        4 => RepairKind::AttachmentTombstoned,
        5 => RepairKind::VoicePromoted {
            from: voice_id(rng),
            to: voice_id(rng),
        },
        6 => RepairKind::TupletCompensated {
            compensation_kind: tuplet_compensation_kind(rng),
        },
        _ => RepairKind::Registered(RepairKindRegistryId(rng.next_u64() as u128)),
    }
}

/// A complete repair record.
pub fn repair_record(rng: &mut Rng) -> RepairRecord {
    RepairRecord {
        kind: repair_kind(rng),
        target: typed_object_id(rng),
    }
}

/// An operation effect covering every variant.
pub fn operation_effect(rng: &mut Rng) -> OperationEffect {
    match rng.below(5) {
        0 => OperationEffect::Applied,
        1 => OperationEffect::AppliedWithRepair {
            repairs: vec![repair_record(rng)],
        },
        2 => OperationEffect::Conflicted {
            conflict: ConflictId(rng.next_u64() as u128),
        },
        3 => OperationEffect::TombstonedTarget {
            target: typed_object_id(rng),
        },
        _ => OperationEffect::NoOp {
            reason: no_op_reason(rng),
        },
    }
}

/// The result of a re-anchor attempt (every variant).
pub fn reanchor_result(rng: &mut Rng) -> ReanchorResult {
    match rng.below(5) {
        0 => ReanchorResult::Reanchored {
            new_target: typed_object_id(rng),
            reason: reanchor_reason(rng),
        },
        1 => ReanchorResult::TombstonedTarget,
        2 => ReanchorResult::Orphaned,
        3 => ReanchorResult::Conflicted {
            conflict: ConflictId(rng.next_u64() as u128),
        },
        _ => ReanchorResult::CascadeDeleted,
    }
}

/// An integrity anomaly kind covering every core and registered variant.
pub fn integrity_anomaly_kind(rng: &mut Rng) -> IntegrityAnomalyKind {
    match rng.below(4) {
        0 => IntegrityAnomalyKind::SystemIdentifierCollision {
            kind: match rng.below(3) {
                0 => ObjectKind::Voice,
                1 => ObjectKind::Pitch,
                _ => ObjectKind::Registered(OperationKindRegistryId(rng.next_u64() as u128)),
            },
            colliding_counter: rng.next_u64(),
            input_set_a: SerializedCanonicalInputs(rng.byte_vec(0, 16)),
            input_set_b: SerializedCanonicalInputs(rng.byte_vec(0, 16)),
        },
        1 => IntegrityAnomalyKind::OperationSlotEquivocated {
            operation_id: operation_id(rng),
        },
        2 => IntegrityAnomalyKind::ReplicaStreamQuarantined {
            replica: replica_id(rng),
            first_bad_counter: rng.next_u64(),
        },
        _ => IntegrityAnomalyKind::Registered(IntegrityAnomalyRegistryId(rng.next_u64() as u128)),
    }
}

/// A content-derived integrity anomaly.
pub fn integrity_anomaly(rng: &mut Rng) -> IntegrityAnomaly {
    IntegrityAnomaly::new(integrity_anomaly_kind(rng))
}

/// A per-replica anomaly reason covering core and registered forms.
pub fn replica_anomaly_reason(rng: &mut Rng) -> ReplicaAnomalyReason {
    if rng.boolean() {
        ReplicaAnomalyReason::HlcMonotonicityViolation {
            violating_pair: (operation_id(rng), operation_id(rng)),
        }
    } else {
        ReplicaAnomalyReason::Registered(ReplicaAnomalyRegistryId(rng.next_u64() as u128))
    }
}

/// An anomalous replica segment with a sorted excluded-id list.
pub fn anomalous_replica_segment(rng: &mut Rng) -> AnomalousReplicaSegment {
    let replica = replica_id(rng);
    let first_bad_counter = rng.range(0, 32);
    AnomalousReplicaSegment {
        replica,
        first_bad_counter,
        reason: replica_anomaly_reason(rng),
        excluded: (first_bad_counter..first_bad_counter + rng.range(1, 4))
            .map(|counter| OperationId::new(replica, counter))
            .collect(),
    }
}

/// An object-existence state covering live and tombstoned forms.
pub fn object_state(rng: &mut Rng) -> ObjectState {
    if rng.boolean() {
        ObjectState::Live
    } else {
        ObjectState::Tombstoned {
            deleted_by: operation_id(rng),
            minted_by: operation_id(rng),
        }
    }
}

/// A pending reason covering every blocker category.
pub fn pending_reason(rng: &mut Rng) -> PendingReason {
    let blocker = operation_id(rng);
    match rng.below(4) {
        0 => PendingReason::MissingCausalPredecessor { missing: blocker },
        1 => PendingReason::DependsOnEquivocated { on: blocker },
        2 => PendingReason::DependsOnExcluded { on: blocker },
        _ => PendingReason::DependsOnPending { on: blocker },
    }
}

/// A random representative operation payload over a shared id space of `events`
/// events and `pitches` pitches. Covers the primitive reduction disciplines
/// (Chapter 6 §6.10: insert, delete, respell, cross-cutting create,
/// layout-semantic break, time-model change, transaction declaration,
/// registered extension op) and the two meta-operations (resolve-conflict and
/// undo-transaction).
///
/// Zero-sized id spaces are treated as size 1 (the shared object namespace
/// always has at least one addressable id), so this never calls
/// [`Rng::below`](crate::rng::Rng::below) with a zero bound.
pub fn operation_payload(rng: &mut Rng, events: u64, pitches: u64) -> OperationPayload {
    let events = events.max(1);
    let pitches = pitches.max(1);
    // ~1 in 6 operations is a meta-operation (resolve-conflict / undo).
    match rng.below(12) {
        10 => {
            return OperationPayload::ResolveConflict(ResolveConflictPayload {
                target: ConflictId(((rng.next_u64() as u128) << 64) | rng.next_u64() as u128),
                action: resolution_action(rng),
            })
        }
        11 => {
            return OperationPayload::UndoTransaction(UndoTransactionPayload {
                target: TransactionId::new(OBJ_REPLICA, rng.below(events)),
                policy: undo_policy(rng),
            })
        }
        _ => {}
    }
    let kind = match rng.below(24) {
        0 => {
            let pitches = if rng.boolean() {
                vec![obj_pitch(rng.below(pitches))]
            } else {
                vec![]
            };
            OperationKind::InsertEvent(InsertEventOp {
                staff_instance: StaffInstanceId::new(OBJ_REPLICA, rng.below(2)),
                event: valuegen::insert_event_value(
                    obj_event(rng.below(events)),
                    VoiceId::new(OBJ_REPLICA, rng.below(4)),
                    MusicalPosition(RationalTime::from_int(rng.below(events) as i32)),
                    MusicalDuration::whole(),
                    &pitches,
                ),
            })
        }
        1 => OperationKind::DeleteEvent(DeleteEventOp {
            event: obj_event(rng.below(events)),
            tuplet_compensation: TupletCompensation::NotInTuplet,
        }),
        2 => OperationKind::RespellPitch(RespellPitchOp {
            pitch: obj_pitch(rng.below(pitches)),
            spelling: valuegen::spelling(rng.below(4) as u8 + 1),
        }),
        3 => OperationKind::CreateCrossCutting(CreateCrossCuttingOp {
            structure: CrossCuttingValue::Slur(valuegen::slur(
                SlurId::new(OBJ_REPLICA, rng.below(events)),
                obj_event(rng.below(events)),
                obj_event(rng.below(events)),
            )),
        }),
        4 => OperationKind::SetUserSystemBreak(SetUserSystemBreakOp {
            region: RegionId::new(OBJ_REPLICA, 0),
            anchor: valuegen::region_start_anchor(
                RegionId::new(OBJ_REPLICA, 0),
                MusicalPosition(RationalTime::from_int(rng.below(4) as i32)),
            ),
            present: rng.boolean(),
        }),
        5 => OperationKind::ChangeRegionTimeModel(ChangeRegionTimeModelOp {
            region: RegionId::new(OBJ_REPLICA, rng.below(2)),
            new_time_model: match rng.below(3) {
                0 => valuegen::metric_model(),
                1 => valuegen::proportional_model(),
                _ => valuegen::aleatoric_model(),
            },
            declared_incompatible: Vec::new(),
            remapping: PositionRemapping::PreserveTime,
        }),
        6 => OperationKind::DeclareTransaction(TransactionDescriptor {
            id: TransactionId::new(OBJ_REPLICA, rng.below(events)),
            label: String::from("edit"),
            category: Some(*rng.choose(&[
                TransactionCategory::NoteEntry,
                TransactionCategory::Structural,
                TransactionCategory::Layout,
            ])),
        }),
        // Group 1 (M2): event & pitch leaf-field ops over the shared id space.
        7 => OperationKind::ModifyEvent(ModifyEventOp {
            event: valuegen::insert_event_value(
                obj_event(rng.below(events)),
                VoiceId::new(OBJ_REPLICA, rng.below(4)),
                MusicalPosition(RationalTime::from_int(rng.below(events) as i32)),
                MusicalDuration::whole(),
                &[obj_pitch(rng.below(pitches))],
            ),
        }),
        8 => OperationKind::Transpose(TransposeOp {
            targets: vec![obj_pitch(rng.below(pitches))],
            chromatic_steps: rng.below(5) as i32 - 2,
        }),
        9 => OperationKind::InsertIdentifiedPitch(InsertIdentifiedPitchOp {
            event: obj_event(rng.below(events)),
            pitch: valuegen::identified_pitch(obj_pitch(rng.below(pitches))),
        }),
        10 => OperationKind::DeleteIdentifiedPitch(DeleteIdentifiedPitchOp {
            pitch: obj_pitch(rng.below(pitches)),
        }),
        11 => OperationKind::ModifyIdentifiedPitch(ModifyIdentifiedPitchOp {
            pitch: obj_pitch(rng.below(pitches)),
            value: valuegen::pitch_value_nth(rng.below(4) as u8 + 1),
        }),
        // Group 2 (M2): cross-cutting CRUD over the shared id space.
        12 => OperationKind::DeleteCrossCutting(DeleteCrossCuttingOp {
            structure: TypedObjectId::Slur(SlurId::new(OBJ_REPLICA, rng.below(events))),
        }),
        13 => OperationKind::ModifyCrossCutting(ModifyCrossCuttingOp {
            structure: CrossCuttingValue::Slur(valuegen::slur(
                SlurId::new(OBJ_REPLICA, rng.below(events)),
                obj_event(rng.below(events)),
                obj_event(rng.below(events)),
            )),
        }),
        // Group 3 (M2c): structural container CRUD over the shared id space.
        14 => OperationKind::CreateRegion(CreateRegionOp {
            region: valuegen::region(RegionId::new(OBJ_REPLICA, rng.below(2))),
        }),
        15 => OperationKind::DeleteRegion(DeleteRegionOp {
            region: RegionId::new(OBJ_REPLICA, rng.below(2)),
        }),
        16 => OperationKind::CreateStaffInstance(CreateStaffInstanceOp {
            region: RegionId::new(OBJ_REPLICA, rng.below(2)),
            instance: valuegen::staff_instance(
                StaffInstanceId::new(OBJ_REPLICA, rng.below(2)),
                StaffId::new(OBJ_REPLICA, 0),
            ),
        }),
        17 => OperationKind::DeleteStaffInstance(DeleteStaffInstanceOp {
            staff_instance: StaffInstanceId::new(OBJ_REPLICA, rng.below(2)),
        }),
        18 => OperationKind::CreateVoice(CreateVoiceOp {
            staff_instance: StaffInstanceId::new(OBJ_REPLICA, rng.below(2)),
            voice: valuegen::voice(VoiceId::new(OBJ_REPLICA, rng.below(4))),
        }),
        19 => OperationKind::DeleteVoice(DeleteVoiceOp {
            voice: VoiceId::new(OBJ_REPLICA, rng.below(4)),
        }),
        // Group 4 (M2d): score settings (LWW) over the shared id space.
        20 => OperationKind::SetMetadata(SetMetadataOp {
            metadata: valuegen::score_metadata(rng.below(3) as u8),
        }),
        21 => OperationKind::SetMetricGrid(SetMetricGridOp {
            region: RegionId::new(OBJ_REPLICA, rng.below(2)),
            grid: rng.boolean().then(valuegen::metric_grid),
        }),
        22 => OperationKind::SetUserPageBreak(SetUserPageBreakOp {
            region: RegionId::new(OBJ_REPLICA, 0),
            anchor: valuegen::region_start_anchor(
                RegionId::new(OBJ_REPLICA, 0),
                MusicalPosition(RationalTime::from_int(rng.below(4) as i32)),
            ),
            present: rng.boolean(),
        }),
        _ => OperationKind::Registered(
            OperationKindRegistryId(rng.next_u64() as u128),
            rng.byte_vec(0, 16),
        ),
    };
    OperationPayload::Primitive(kind)
}

/// A well-formed set of real [`OperationEnvelope`]s authored by `n_replicas`
/// replicas over a shared id space, **honoring the HLC authoring contract**
/// (spec §"Identifiers", the `OperationStamp` requirement): every operation's
/// stamp is strictly greater than the stamp of every operation in its causal
/// context. This is achieved with the standard hybrid-logical-clock send rule —
/// a new stamp is `max(local clock reading, this replica's previous stamp, the
/// maximum stamp over the declared causal predecessors)`, with the logical
/// counter bumped on ties — so the canonical reduction order (which sorts by the
/// HLC tuple) genuinely places causal predecessors before successors.
///
/// The DVV causal context references only **prior** operations: the author
/// always sees its own contiguous history, plus a random prefix of each other
/// replica's history. Concurrency across replicas is what makes delivery order
/// differ from the canonical reduction order.
///
/// [`assert_causal_order_respected`](crate::convergence::assert_causal_order_respected)
/// verifies the resulting histories actually satisfy the authoring contract.
/// A reusable authoring context: each [`Session::author`] call mints one
/// well-formed, causally-conformant envelope on a chosen replica via the HLC
/// send rule + prior-history DVV described on [`operation_envelopes`].
struct Session {
    counters: Vec<u64>,
    clocks: Vec<i64>,
    /// Per-replica stamp history `(physical, logical)`, indexed by counter.
    /// Stamps are monotonic within a replica, so `stamps[rr][k]` is the max over
    /// `0..=k`.
    stamps: Vec<Vec<(i64, u32)>>,
    out: Vec<OperationEnvelope>,
}

impl Session {
    fn new(n_replicas: usize) -> Self {
        let n = n_replicas.max(1);
        Session {
            counters: vec![0; n],
            clocks: vec![0; n],
            stamps: vec![Vec::new(); n],
            out: Vec::new(),
        }
    }

    fn n_replicas(&self) -> usize {
        self.counters.len()
    }

    /// Authors one operation on replica `r` carrying `payload`, honoring the HLC
    /// authoring contract (the new stamp strictly outranks every causal
    /// predecessor) with a prior-history DVV (own history always seen; others
    /// sampled).
    fn author(&mut self, rng: &mut Rng, r: usize, payload: OperationPayload) {
        let n_replicas = self.counters.len();
        let replica = ReplicaId(r as u64 + 1);
        let c = self.counters[r];
        let id = OperationId::new(replica, c);

        let mut ctx = CausalContext::new();
        let mut pred_max = (0i64, 0u32);
        if c > 0 {
            ctx = ctx.with_seen(replica, c - 1);
            pred_max = pred_max.max(self.stamps[r][(c - 1) as usize]);
        }
        for rr in 0..n_replicas {
            if rr == r {
                continue;
            }
            let known = self.counters[rr];
            if known > 0 && rng.boolean() {
                let k = rng.below(known);
                ctx = ctx.with_seen(ReplicaId(rr as u64 + 1), k);
                pred_max = pred_max.max(self.stamps[rr][k as usize]);
            }
        }

        self.clocks[r] += rng.below(3) as i64;
        let pt = self.clocks[r];
        let prev = self.stamps[r].last().copied().unwrap_or((0, 0));
        let l = pt.max(prev.0).max(pred_max.0);
        let logical = if l == prev.0 && l == pred_max.0 {
            prev.1.max(pred_max.1) + 1
        } else if l == prev.0 {
            prev.1 + 1
        } else if l == pred_max.0 {
            pred_max.1 + 1
        } else {
            0
        };
        self.stamps[r].push((l, logical));
        self.counters[r] += 1;

        let env = OperationEnvelope {
            id,
            author: AuthorId(replica.0 as u128),
            stamp: OperationStamp::new(HybridLogicalClock::new(WallClockTime(l), logical), id),
            causal_context: ctx,
            transaction: None,
            payload,
        };
        debug_assert!(epiphany_ops::well_formed(&env).is_ok());
        self.out.push(env);
    }
}

/// A well-formed set of real [`OperationEnvelope`]s authored by `n_replicas`
/// replicas over a shared id space, **honoring the HLC authoring contract**
/// (spec §"Identifiers", the `OperationStamp` requirement): every operation's
/// stamp is strictly greater than the stamp of every operation in its causal
/// context. This is achieved with the standard hybrid-logical-clock send rule —
/// a new stamp is `max(local clock reading, this replica's previous stamp, the
/// maximum stamp over the declared causal predecessors)`, with the logical
/// counter bumped on ties — so the canonical reduction order (which sorts by the
/// HLC tuple) genuinely places causal predecessors before successors.
///
/// The DVV causal context references only **prior** operations: the author
/// always sees its own contiguous history, plus a random prefix of each other
/// replica's history. Concurrency across replicas is what makes delivery order
/// differ from the canonical reduction order.
///
/// [`assert_causal_order_respected`](crate::convergence::assert_causal_order_respected)
/// verifies the resulting histories actually satisfy the authoring contract.
pub fn operation_envelopes(
    rng: &mut Rng,
    n_ops: usize,
    n_replicas: usize,
    events: u64,
    pitches: u64,
) -> Vec<OperationEnvelope> {
    let mut session = Session::new(n_replicas);
    for _ in 0..n_ops {
        let r = rng.below(session.n_replicas() as u64) as usize;
        let payload = operation_payload(rng, events, pitches);
        session.author(rng, r, payload);
    }
    session.out
}

/// A real materialized state produced by reducing a generated conformant
/// operation set. This is preferable to assembling reducer-owned invariants by
/// hand for ordinary property tests.
pub fn materialized_state(rng: &mut Rng, n_ops: usize) -> MaterializedState {
    let envelopes = operation_envelopes(rng, n_ops, 3, 8, 8);
    let mut set = OperationSet::new();
    set.accept_all(envelopes);
    set.reduce()
}

/// Bars per staff in [`two_staff_edit_session`].
pub const TWO_STAFF_BARS: u64 = 50;
/// Half-note events per 4/4 bar (so the session spans exactly [`TWO_STAFF_BARS`]).
const EVENTS_PER_BAR: u64 = 2;
/// Per-staff event count in [`two_staff_edit_session`]: [`TWO_STAFF_BARS`] bars ×
/// two half-notes per 4/4 bar.
pub const TWO_STAFF_EVENTS_PER_STAFF: u64 = TWO_STAFF_BARS * EVENTS_PER_BAR;

/// An `InsertEvent` payload minting `event` (with `pitch`) into the given
/// staff-instance / voice as the `index`-th half-note (position `index/2`
/// whole-notes, i.e. two per 4/4 bar).
fn insert_at(instance: u64, voice: u64, event: u64, pitch: u64, index: u64) -> OperationPayload {
    OperationPayload::Primitive(OperationKind::InsertEvent(InsertEventOp {
        staff_instance: StaffInstanceId::new(OBJ_REPLICA, instance),
        event: valuegen::insert_event_value(
            obj_event(event),
            VoiceId::new(OBJ_REPLICA, voice),
            MusicalPosition(RationalTime::new(index as i64, EVENTS_PER_BAR as i64).unwrap()),
            MusicalDuration(RationalTime::new(1, EVENTS_PER_BAR as i64).unwrap()),
            &[obj_pitch(pitch)],
        ),
    }))
}

/// The v0 criterion-1 scenario, **instantiated** (not merely modeled): a
/// two-staff score built by two replicas. Each staff (instance 0 / voice 0 and
/// instance 1 / voice 1) is filled with [`TWO_STAFF_EVENTS_PER_STAFF`] events
/// spanning ~50 bars, authored by *both* replicas (alternating), so both staves
/// are genuinely populated. A batch of overlapping deletes and respellings over
/// the shared id space then makes reduction order — not delivery order — decide
/// the materialized result. [`assert_two_staff_populated`] checks both staves
/// actually survive reduction.
pub fn two_staff_edit_session(rng: &mut Rng) -> Vec<OperationEnvelope> {
    let n = TWO_STAFF_EVENTS_PER_STAFF;
    let mut session = Session::new(2);
    for i in 0..n {
        session.author(rng, (i % 2) as usize, insert_at(0, 0, i, i, i));
        session.author(
            rng,
            ((i + 1) % 2) as usize,
            insert_at(1, 1, n + i, n + i, i),
        );
    }
    let total = 2 * n;
    for _ in 0..80 {
        let r = rng.below(2) as usize;
        let payload = if rng.boolean() {
            OperationPayload::Primitive(OperationKind::DeleteEvent(DeleteEventOp {
                event: obj_event(rng.below(total)),
                tuplet_compensation: TupletCompensation::NotInTuplet,
            }))
        } else {
            OperationPayload::Primitive(OperationKind::RespellPitch(RespellPitchOp {
                pitch: obj_pitch(rng.below(total)),
                spelling: valuegen::spelling(rng.below(4) as u8 + 1),
            }))
        };
        session.author(rng, r, payload);
    }
    session.out
}

/// Asserts both staves of a [`two_staff_edit_session`] are actually populated:
/// each staff's voice is live and at least one of its events survives reduction.
/// (The reducer materializes voices/events but not staff-instance objects, so
/// liveness is checked at the voice and event level.)
pub fn assert_two_staff_populated(envelopes: &[OperationEnvelope]) {
    let mut set = OperationSet::new();
    set.accept_all(envelopes.iter().cloned());
    let state = set.reduce();
    let n = TWO_STAFF_EVENTS_PER_STAFF;

    let voice_live = |v: u64| {
        matches!(
            state
                .objects
                .get(&TypedObjectId::Voice(VoiceId::new(OBJ_REPLICA, v))),
            Some(ObjectState::Live)
        )
    };
    assert!(voice_live(0), "staff 0 voice is not live after reduction");
    assert!(voice_live(1), "staff 1 voice is not live after reduction");

    let live_events = |lo: u64, hi: u64| {
        (lo..hi)
            .filter(|e| {
                matches!(
                    state.objects.get(&TypedObjectId::Event(obj_event(*e))),
                    Some(ObjectState::Live)
                )
            })
            .count()
    };
    assert!(live_events(0, n) > 0, "staff 0 has no live events");
    assert!(live_events(n, 2 * n) > 0, "staff 1 has no live events");
}

/// Whole-note position past which [`graph_edit_session`] inserts, chosen to
/// clear `valid_score`'s base content (quarter-note events in `[0, 1)`).
const GRAPH_SESSION_OFFSET: i64 = 4;

/// An `InsertEvent` payload targeting a **real** base voice (`staff_instance`
/// must be the voice's actual container), minting `event`/`pitch` under
/// [`OBJ_REPLICA`] as the `index`-th half-note past the base content. Unlike
/// [`insert_at`], this addresses ids that already exist in a base [`graph`]
/// score, so the payload survives [`OperationSet::reduce_onto`] (which rejects
/// inserts into unknown voices) rather than only the base-free reducer.
fn insert_into(
    staff_instance: StaffInstanceId,
    voice: VoiceId,
    event: u64,
    pitch: u64,
    index: u64,
) -> OperationPayload {
    OperationPayload::Primitive(OperationKind::InsertEvent(InsertEventOp {
        staff_instance,
        // position = GRAPH_SESSION_OFFSET + index/2 (two half-notes per 4/4 bar).
        event: valuegen::insert_event_value(
            obj_event(event),
            voice,
            MusicalPosition(
                RationalTime::new(
                    GRAPH_SESSION_OFFSET * EVENTS_PER_BAR as i64 + index as i64,
                    2,
                )
                .unwrap(),
            ),
            MusicalDuration(RationalTime::new(1, 2).unwrap()),
            &[obj_pitch(pitch)],
        ),
    }))
}

/// The graph-level twin of [`two_staff_edit_session`]: a real ~50-bar edit
/// session targeting the **actual** voices of `base`, for the
/// [`OperationSet::reduce_onto`] convergence gate (acceptance criterion 1).
///
/// Two replicas alternately insert [`TWO_STAFF_EVENTS_PER_STAFF`] half-note
/// events past the base content into the base's first two voices (so both
/// staves are genuinely edited), then concurrently respell and delete over the
/// shared minted id space, so the canonical reduction — not delivery order —
/// decides the materialized `Score`. Returns the targeted voices alongside the
/// envelopes (the convergence harness checks each one actually grew).
///
/// Requires a base with at least one voice; callers pass a base scanned for two
/// (see `epiphany_core::generators::valid_score`).
pub fn graph_edit_session(
    base: &epiphany_core::Score,
    rng: &mut Rng,
) -> (Vec<(StaffInstanceId, VoiceId)>, Vec<OperationEnvelope>) {
    let targets: Vec<(StaffInstanceId, VoiceId)> = base
        .voices()
        .map(|(_, instance, voice)| (instance, voice.id))
        .take(2)
        .collect();
    assert!(
        !targets.is_empty(),
        "graph_edit_session requires a base score with at least one voice"
    );

    let n = TWO_STAFF_EVENTS_PER_STAFF;
    let mut session = Session::new(2);
    for i in 0..n {
        for (ti, &(instance, voice)) in targets.iter().enumerate() {
            // Each target voice's consecutive half-notes alternate authoring
            // replicas; the two voices are edited by opposite replicas at any
            // given index. Positions are distinct within a voice (no
            // same-position collision → clean inserts), while cross-replica
            // causal sampling makes delivery order differ from reduction order.
            let r = (i as usize + ti) % 2;
            let object = ti as u64 * n + i;
            session.author(rng, r, insert_into(instance, voice, object, object, i));
        }
    }

    let total = targets.len() as u64 * n;

    // Create a handful of slurs over replica-0 events (the even object indices of
    // the first target voice — replica 0 authored and therefore sees them), so the
    // Group-2 cross-cutting CRUD below has live structures to delete and modify,
    // and the DeleteEvent edits exercise slur re-anchoring on the real graph.
    let n_slurs = (n / 4).clamp(1, 6);
    for k in 0..n_slurs {
        session.author(
            rng,
            0,
            OperationPayload::Primitive(OperationKind::CreateCrossCutting(CreateCrossCuttingOp {
                structure: CrossCuttingValue::Slur(valuegen::slur(
                    SlurId::new(OBJ_REPLICA, k),
                    obj_event(4 * k),
                    obj_event(4 * k + 2),
                )),
            })),
        );
    }

    for _ in 0..80 {
        let r = rng.below(2) as usize;
        // Mix the original edit kinds with the Group-1/2 (M2) ops so the real-Score
        // gate exercises their *graph* materialization (reduce_onto +
        // check_invariants), not just the bookkeeping projection. Each targets a
        // live object minted by the phases above.
        let kind = match rng.below(9) {
            0 => OperationKind::DeleteEvent(DeleteEventOp {
                event: obj_event(rng.below(total)),
                tuplet_compensation: TupletCompensation::NotInTuplet,
            }),
            1 => OperationKind::RespellPitch(RespellPitchOp {
                pitch: obj_pitch(rng.below(total)),
                spelling: valuegen::spelling(rng.below(4) as u8 + 1),
            }),
            2 => {
                // Rebuild the event at its *original* placement (so the graph
                // applies the modify rather than deferring it as a move).
                let idx = rng.below(total);
                let (_, voice) = targets[(idx / n) as usize];
                OperationKind::ModifyEvent(ModifyEventOp {
                    event: valuegen::insert_event_value(
                        obj_event(idx),
                        voice,
                        MusicalPosition(
                            RationalTime::new(
                                GRAPH_SESSION_OFFSET * EVENTS_PER_BAR as i64 + (idx % n) as i64,
                                2,
                            )
                            .unwrap(),
                        ),
                        MusicalDuration(RationalTime::new(1, 2).unwrap()),
                        &[obj_pitch(idx)],
                    ),
                })
            }
            3 => OperationKind::Transpose(TransposeOp {
                targets: vec![obj_pitch(rng.below(total))],
                chromatic_steps: rng.below(5) as i32 - 2,
            }),
            // Fresh pitch id (beyond the inserted 0..total range): adds a pitch to
            // a note, or turns a rest (left by a last-pitch delete) back into one.
            4 => OperationKind::InsertIdentifiedPitch(InsertIdentifiedPitchOp {
                event: obj_event(rng.below(total)),
                pitch: valuegen::identified_pitch(obj_pitch(total + rng.below(total))),
            }),
            // Deletes a single-pitch note's only pitch → exercises the note→rest
            // degradation path.
            5 => OperationKind::DeleteIdentifiedPitch(DeleteIdentifiedPitchOp {
                pitch: obj_pitch(rng.below(total)),
            }),
            6 => OperationKind::ModifyIdentifiedPitch(ModifyIdentifiedPitchOp {
                pitch: obj_pitch(rng.below(total)),
                value: valuegen::pitch_value_nth(rng.below(7) as u8),
            }),
            // Group 2 (M2): cross-cutting CRUD over the slurs created above.
            7 => OperationKind::DeleteCrossCutting(DeleteCrossCuttingOp {
                structure: TypedObjectId::Slur(SlurId::new(OBJ_REPLICA, rng.below(n_slurs))),
            }),
            _ => {
                let k = rng.below(n_slurs);
                OperationKind::ModifyCrossCutting(ModifyCrossCuttingOp {
                    // Re-point the slur's end to another even (replica-0) event.
                    structure: CrossCuttingValue::Slur(valuegen::slur(
                        SlurId::new(OBJ_REPLICA, k),
                        obj_event(4 * k),
                        obj_event(4 * ((k + 1) % n_slurs)),
                    )),
                })
            }
        };
        session.author(rng, r, OperationPayload::Primitive(kind));
    }
    (targets, session.out)
}

/// A `(base, mutated)` pair of operation sets whose operations have **identical
/// identities, stamps, and causal contexts** but differ in payload *content*
/// (one respelling's spelling). Both sets insert a pitch and respell it, so the
/// respelling takes effect; reducing them therefore yields *different* canonical
/// bytes — the exact rebuttal to an id-only "serializer" that would collapse
/// distinct scores. `mutated` is `base.clone()` with only one payload changed,
/// so the identity/ordering metadata is provably preserved.
pub fn content_mutation_pair() -> (Vec<OperationEnvelope>, Vec<OperationEnvelope>) {
    let mut rng = Rng::new(0x00C0_1117_E27E_5EED);
    let mut session = Session::new(1);
    // Insert event 0 carrying pitch 0 (so the pitch is live), then respell it.
    session.author(&mut rng, 0, insert_at(0, 0, 0, 0, 0));
    session.author(
        &mut rng,
        0,
        OperationPayload::Primitive(OperationKind::RespellPitch(RespellPitchOp {
            pitch: obj_pitch(0),
            spelling: valuegen::spelling(0xAA),
        })),
    );
    let base = session.out;

    let mut mutated = base.clone();
    if let OperationPayload::Primitive(OperationKind::RespellPitch(op)) = &mut mutated[1].payload {
        op.spelling = valuegen::spelling(0xBB);
    } else {
        unreachable!("the second op is the respelling");
    }
    (base, mutated)
}

/// The canonical [`CausalContext`] bytes for exactly the structurally accepted
/// operation identifiers in `envelopes`.
///
/// Each replica's contiguous prefix beginning at counter zero is represented in
/// the DVV vector; identifiers after a gap are represented as dots. Malformed
/// envelopes are excluded because [`OperationSet`] excludes them from the
/// materialized snapshot as well. The returned bytes are the operation layer's
/// real canonical encoding, not a bundle-local approximation.
pub fn frontier_bytes(envelopes: &[OperationEnvelope]) -> Vec<u8> {
    use std::collections::{BTreeMap, BTreeSet};

    let mut by_replica: BTreeMap<ReplicaId, BTreeSet<u64>> = BTreeMap::new();
    for envelope in envelopes {
        if epiphany_ops::well_formed(envelope).is_ok() {
            by_replica
                .entry(envelope.id.replica)
                .or_default()
                .insert(envelope.id.counter);
        }
    }

    let mut frontier = CausalContext::new();
    for (replica, counters) in by_replica {
        let mut next = 0u64;
        while counters.contains(&next) {
            if next == u64::MAX {
                break;
            }
            next += 1;
        }
        if next > 0 {
            frontier = frontier.with_seen(replica, next - 1);
        }
        for counter in counters.range(next..) {
            frontier = frontier.with_dot(OperationId::new(replica, *counter));
        }
    }
    frontier.to_canonical_bytes()
}

/// A second envelope addressing the same [`OperationId`] as `env` but with
/// different canonical bytes — the input that drives a `Single` slot to
/// `Equivocated`. The stamp's id stays pinned to `env.id`, so it is still
/// well-formed.
pub fn equivocating_twin(env: &OperationEnvelope) -> OperationEnvelope {
    let mut twin = env.clone();
    twin.payload = OperationPayload::Primitive(OperationKind::RespellPitch(RespellPitchOp {
        pitch: obj_pitch(0),
        spelling: valuegen::spelling(0xEE),
    }));
    if twin.envelope_hash() == env.envelope_hash() {
        twin.payload = OperationPayload::Primitive(OperationKind::RespellPitch(RespellPitchOp {
            pitch: obj_pitch(1),
            spelling: valuegen::spelling(0x11),
        }));
    }
    twin
}

// ===========================================================================
// Agent D — epiphany-bundle: refs, snapshots, blobs, a rich manifest
// ===========================================================================

/// One of the chunk kinds.
pub fn chunk_kind(rng: &mut Rng) -> ChunkKind {
    *rng.choose(&[
        ChunkKind::OperationEnvelopeBlock,
        ChunkKind::OperationIndex,
        ChunkKind::Snapshot,
        ChunkKind::Blob,
        ChunkKind::ExtensionData,
        ChunkKind::TextProjection,
        ChunkKind::LayoutCache,
        ChunkKind::IntegrityIndex,
        ChunkKind::Manifest,
    ])
}

/// A compression algorithm (every variant). Compression is metadata, not
/// identity, so any value is valid in a [`ChunkRef`].
pub fn compression_algorithm(rng: &mut Rng) -> CompressionAlgorithm {
    match rng.below(3) {
        0 => CompressionAlgorithm::None,
        1 => CompressionAlgorithm::Zstd {
            level: rng.range(1, 22) as u8,
        },
        _ => CompressionAlgorithm::Reserved(rng.range(2, 255) as u8),
    }
}

/// A content-addressed chunk reference of the given kind, derived from a random
/// payload so its id and hash are internally consistent (content identity is the
/// uncompressed payload, independent of the `compression` metadata).
pub fn chunk_ref(rng: &mut Rng, kind: ChunkKind) -> ChunkRef {
    let payload = rng.byte_vec(1, 64);
    let schema = SchemaVersion::V0;
    ChunkRef {
        id: chunk_id(kind, schema, &payload),
        kind,
        schema_version: schema,
        offset: rng.range(0, 1 << 32),
        compressed_length: payload.len() as u64,
        uncompressed_length: payload.len() as u64,
        compression: compression_algorithm(rng),
        hash: content_hash_for(kind, schema, &payload),
    }
}

/// A committed superblock with varied fields. Committed so it parses as
/// [`epiphany_bundle::SlotParse::Valid`] for ordinary selection.
pub fn superblock(rng: &mut Rng) -> Superblock {
    Superblock {
        generation: rng.range(0, 1_000),
        manifest_offset: rng.range(0, 1 << 32),
        manifest_length: rng.range(1, 1 << 20),
        manifest_hash: content_hash(rng),
        manifest_schema_version: SchemaVersion::V0,
        reduction_algorithm_version: ReductionAlgorithmVersion(rng.range(0, 8) as u32),
        profile_id: profile_id(rng),
        commit_state: CommitState::Committed,
        commit_timestamp: BundleWallClockTime(rng.range(0, 1 << 40) as i64),
    }
}

/// A snapshot reference (a canonical base or acceleration snapshot) under a
/// varied profile. The `hash` is the snapshot root chunk's hash (the field's
/// documented meaning: "hash of the snapshot's root chunk, for fast verification").
pub fn snapshot_ref(rng: &mut Rng) -> SnapshotRef {
    let profile = profile_id(rng);
    snapshot_ref_with_profile(rng, profile)
}

/// A snapshot reference whose `profile_id` is the caller's choice (so a manifest
/// can keep its snapshots' profiles among its declared profiles).
pub fn snapshot_ref_with_profile(rng: &mut Rng, profile: ProfileId) -> SnapshotRef {
    let root = chunk_ref(rng, ChunkKind::Snapshot);
    let hash = root.hash;
    SnapshotRef {
        snapshot_id: SnapshotId(rng.array16()),
        covers_causal_frontier: FrontierBytes::from_bytes(rng.byte_vec(0, 24)),
        reduction_algorithm_version: ReductionAlgorithmVersion(rng.range(0, 8) as u32),
        profile_id: profile,
        root,
        hash,
    }
}

/// A profile identifier (every variant).
pub fn profile_id(rng: &mut Rng) -> ProfileId {
    match rng.below(4) {
        0 => ProfileId::Full,
        1 => ProfileId::ReadOnly,
        2 => ProfileId::Lite,
        _ => ProfileId::Custom(ProfileRegistryId(rng.array16())),
    }
}

/// A profile identifier that is never [`ProfileId::Full`] — for a *second*
/// declaration alongside a Full one, so the two ids do not collide.
pub fn non_full_profile_id(rng: &mut Rng) -> ProfileId {
    match rng.below(3) {
        0 => ProfileId::ReadOnly,
        1 => ProfileId::Lite,
        _ => ProfileId::Custom(ProfileRegistryId(rng.array16())),
    }
}

/// A blob reference with a valid RFC 6838 media type. The `hash` is the payload's
/// content hash, consistent with the content-derived `blob_id`.
pub fn blob_ref(rng: &mut Rng) -> BlobRef {
    let payload = rng.byte_vec(1, 64);
    let len = payload.len() as u64;
    let blob_id = BlobId::of_payload(&payload);
    BlobRef {
        // The verification hash is the blob's content hash, consistent with its
        // content-derived id.
        hash: blob_id.0,
        blob_id,
        media_type: String::from("application/octet-stream"),
        offset: rng.range(0, 1 << 32),
        compressed_length: len,
        uncompressed_length: len,
        compression: CompressionAlgorithm::None,
        declared_max_uncompressed_length: if rng.boolean() {
            Some(len.max(1))
        } else {
            None
        },
    }
}

/// An extension declaration. `preserved_chunk_roots` is kept to at most one
/// entry so the canonical sort/dedup `encode` applies is a no-op and the decoded
/// value compares equal to the original.
pub fn extension_declaration(rng: &mut Rng) -> ExtensionDeclaration {
    ExtensionDeclaration {
        extension_id: ExtensionId(rng.array16()),
        version: SemVer::new(
            rng.range(0, 3) as u32,
            rng.range(0, 9) as u32,
            rng.range(0, 9) as u32,
        ),
        required: rng.boolean(),
        preserved_chunk_roots: if rng.boolean() {
            vec![chunk_ref(rng, ChunkKind::ExtensionData)]
        } else {
            vec![]
        },
        affected_object_kinds: rng.byte_vec(0, 8),
        edit_barriers: rng.byte_vec(0, 8),
    }
}

/// A retention policy with varied (non-default) fields.
pub fn retention_policy(rng: &mut Rng) -> RetentionPolicy {
    RetentionPolicy {
        retain_previous_manifests: rng.range(0, 8) as u32,
        retain_duration: if rng.boolean() {
            Some(BundleWallClockDuration(rng.range(0, 1 << 40) as i64))
        } else {
            None
        },
        retain_named_checkpoints: rng.boolean(),
    }
}

/// A profile declaration with a varied profile id and constraints (a varied
/// [`retention_policy`] and block-size limit).
pub fn profile_declaration(rng: &mut Rng) -> ProfileDeclaration {
    let profile = profile_id(rng);
    profile_declaration_with(rng, profile)
}

/// A profile declaration with the caller's `profile_id` and varied constraints.
pub fn profile_declaration_with(rng: &mut Rng, profile: ProfileId) -> ProfileDeclaration {
    ProfileDeclaration {
        profile_id: profile,
        version: SemVer::new(rng.range(0, 2) as u32, rng.range(0, 9) as u32, 0),
        constraints: ProfileConstraints {
            max_uncompressed_block_size: rng.range(1 << 16, 64 << 20),
            retention_policy: retention_policy(rng),
        },
    }
}

/// A **rich** manifest exercising every optional field and reference vector:
/// lineage, multiple operation roots, an operation index, a canonical base,
/// acceleration snapshots, blobs, extension and (two, distinct-id) profile
/// declarations, and the text-projection / integrity optional roots. Snapshots
/// reference only declared profiles, and the two profile declarations have
/// distinct ids (no `Full`/`Full` collision).
///
/// **Scope.** This is a *codec / structural* manifest generator — it exercises
/// `Manifest::encode`/`decode` byte-stability and the decoder's validation. It is
/// not a fully bundle-valid manifest: its `ChunkRef`/`BlobRef` references are not
/// backed by chunks in any store (so `Bundle::open` + `verify_canonical_chunks`
/// would not accept it). Bundle *lifecycle* validity is covered by
/// [`crate::roundtrip::committed_manifest`] (a manifest produced by real commits)
/// and the bundle's own crash/manifest-selection harnesses.
pub fn rich_manifest(rng: &mut Rng) -> Manifest {
    let mut m = Manifest::empty(DocumentId(rng.array16()));
    m.generation = rng.range(0, 1_000);
    m.lineage_id = if rng.boolean() {
        Some(LineageId(rng.array16()))
    } else {
        None
    };
    for _ in 0..rng.range_usize(0, 3) {
        m.operation_roots
            .push(chunk_ref(rng, ChunkKind::OperationEnvelopeBlock));
    }
    m.operation_index_root = if rng.boolean() {
        Some(chunk_ref(rng, ChunkKind::OperationIndex))
    } else {
        None
    };

    // Two profile declarations with distinct ids (Full + a non-Full one).
    let extra_id = non_full_profile_id(rng);
    let extra = profile_declaration_with(rng, extra_id);
    m.profile_declarations = vec![ProfileDeclaration::full(), extra];
    // Snapshots reference only declared profiles (Full or the extra).
    let declared = [ProfileId::Full, extra_id];
    m.canonical_base = if rng.boolean() {
        let p = *rng.choose(&declared);
        Some(snapshot_ref_with_profile(rng, p))
    } else {
        None
    };
    for _ in 0..rng.range_usize(0, 2) {
        let p = *rng.choose(&declared);
        m.acceleration_snapshots
            .push(snapshot_ref_with_profile(rng, p));
    }
    for _ in 0..rng.range_usize(0, 2) {
        m.blob_roots.push(blob_ref(rng));
    }
    for _ in 0..rng.range_usize(0, 2) {
        m.extension_declarations.push(extension_declaration(rng));
    }
    m.text_projection_root = if rng.boolean() {
        Some(chunk_ref(rng, ChunkKind::TextProjection))
    } else {
        None
    };
    m.integrity_root = if rng.boolean() {
        Some(chunk_ref(rng, ChunkKind::IntegrityIndex))
    } else {
        None
    };
    m
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn envelope_set_is_well_formed_and_deterministic() {
        let mut a = Rng::new(42);
        let mut b = Rng::new(42);
        let sa = operation_envelopes(&mut a, 64, 3, 6, 6);
        let sb = operation_envelopes(&mut b, 64, 3, 6, 6);
        assert_eq!(sa.len(), 64);
        assert_eq!(sa, sb, "generation must be reproducible from the seed");
        assert!(sa.iter().all(|e| epiphany_ops::well_formed(e).is_ok()));
    }

    #[test]
    fn equivocating_twin_differs_in_bytes_but_not_id() {
        let mut rng = Rng::new(7);
        let set = operation_envelopes(&mut rng, 4, 2, 6, 6);
        let twin = equivocating_twin(&set[0]);
        assert_eq!(twin.id, set[0].id);
        assert_eq!(twin.stamp.id, set[0].id);
        assert_ne!(twin.envelope_hash(), set[0].envelope_hash());
    }

    #[test]
    fn rational_large_is_actually_promoted() {
        let mut rng = Rng::new(3);
        for _ in 0..100 {
            let r = rational_time_large(&mut rng);
            assert!(
                matches!(r, RationalTime::Large(_)),
                "rational_time_large must produce the promoted arm, got {r:?}"
            );
        }
    }

    #[test]
    fn rich_manifest_is_deterministic() {
        let mut a = Rng::new(99);
        let mut b = Rng::new(99);
        assert_eq!(rich_manifest(&mut a), rich_manifest(&mut b));
    }

    #[test]
    fn zero_sized_operation_spaces_are_supported() {
        let mut rng = Rng::new(5);
        for _ in 0..128 {
            let _ = operation_payload(&mut rng, 0, 0);
        }
    }

    #[test]
    fn frontier_uses_the_real_dvv_encoding_and_preserves_gaps() {
        let mut rng = Rng::new(11);
        let mut envelope = operation_envelopes(&mut rng, 1, 1, 1, 1).remove(0);
        envelope.id = OperationId::new(ReplicaId(1), 5);
        envelope.stamp.id = envelope.id;
        let expected = CausalContext::new()
            .with_dot(envelope.id)
            .to_canonical_bytes();
        assert_eq!(frontier_bytes(std::slice::from_ref(&envelope)), expected);

        envelope.stamp.hlc.physical_time = WallClockTime(-1);
        assert_eq!(
            frontier_bytes(&[envelope]),
            CausalContext::new().to_canonical_bytes(),
            "rejected envelopes must not enter the snapshot frontier"
        );
    }

    #[test]
    fn generated_reduction_outputs_decode_canonically() {
        for seed in 0..256 {
            let mut rng = Rng::new(seed);
            let mut state = MaterializedState::default();
            state
                .effects
                .push((operation_id(&mut rng), operation_effect(&mut rng)));
            state.conflicts = conflict_registry(&mut rng);
            state.anomalies.push(integrity_anomaly(&mut rng));
            state
                .objects
                .insert(typed_object_id(&mut rng), object_state(&mut rng));
            state
                .spellings
                .insert(pitch_id(&mut rng), valuegen::spelling(rng.below(7) as u8));
            state.breaks.insert(
                (region_id(&mut rng), musical_position(&mut rng)),
                rng.boolean(),
            );
            state.page_breaks.insert(
                (region_id(&mut rng), musical_position(&mut rng)),
                rng.boolean(),
            );
            state
                .pending
                .push((operation_id(&mut rng), pending_reason(&mut rng)));

            let bytes = state.canonical_bytes();
            let decoded = MaterializedState::decode_canonical(&bytes).unwrap();
            assert_eq!(decoded.canonical_bytes(), bytes);

            let _ = reanchor_result(&mut rng);
            let _ = anomalous_replica_segment(&mut rng);
        }
    }
}
