//! The simulator against hand-computed timelines, its conservation and cap rules, and
//! §10.4's stall bound on generated plans, with a counter-plan for each part of the
//! bound a schedule can lose.

use super::*;

/// SplitMix64, for generated models and plans.
struct Rng(u64);

impl Rng {
    fn next(&mut self) -> u64 {
        self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
        let mut z = self.0;
        z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
        z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
        z ^ (z >> 31)
    }
    fn range(&mut self, lo: u64, hi: u64) -> u64 {
        lo + self.next() % (hi - lo + 1)
    }
    fn coin(&mut self) -> bool {
        self.next() & 1 == 1
    }
}

/// §10.4's bound of a plan over a model whose refinements carry no timeout of their own.
fn plan_only_bound(p: &Plan) -> Option<u64> {
    bound_with(p, p.timeout_refinement_us)
}

fn obj(id: &str, base: u32, layer: u8, from: u64, count: u64, bytes: u64, capture_us: Option<u64>) -> SimObject {
    SimObject { id: id.into(), base, layer, from, count, bytes, capture_us, timeout_us: None }
}

/// `bases` groups of `per` units, each a base and `layers` refinement layers, each
/// layer one object per unit or one for the group; capture at the end of each group.
fn model(r: &mut Rng, bases: u32, per: u64, layers: u8, per_unit: bool) -> Model {
    let unit_us = 40_000;
    let mut objects = Vec::new();
    for b in 0..bases {
        let from = b as u64 * per;
        let cap = Some((from + per) * unit_us);
        objects.push(obj(&format!("b{b}"), b, 0, from, per, r.range(1500, 9000), cap));
        for k in 1..=layers {
            if per_unit {
                for u in 0..per {
                    objects.push(obj(&format!("b{b}l{k}u{u}"), b, k, from + u, 1, r.range(200, 2500), cap));
                }
            } else {
                objects.push(obj(&format!("b{b}l{k}"), b, k, from, per, r.range(800, 7000), cap));
            }
        }
    }
    Model { unit_us, objects }
}

/// A model drawn with nothing in order: groups take their unit ranges in a random
/// order, capture times are random (not rising with the units), object sizes span
/// tiny to large, and the objects are listed in a random order.
fn wild_model(r: &mut Rng) -> Model {
    let unit_us = r.range(1, 80_000);
    let bases = r.range(1, 5) as u32;
    let per = r.range(1, 6);
    let layers = r.range(0, 3) as u8;
    let mut slots: Vec<u64> = (0..bases as u64).collect();
    for i in (1..slots.len()).rev() {
        slots.swap(i, r.range(0, i as u64) as usize);
    }
    let mut objects = Vec::new();
    for b in 0..bases {
        let from = slots[b as usize] * per;
        let cap = Some(r.range(0, 600_000));
        let size = |r: &mut Rng| if r.coin() { r.range(1, 400) } else { r.range(400, 60_000) };
        objects.push(obj(&format!("b{b}"), b, 0, from, per, size(r), cap));
        for k in 1..=layers {
            if r.coin() {
                for u in 0..per {
                    objects.push(obj(&format!("b{b}l{k}u{u}"), b, k, from + u, 1, size(r), cap));
                }
            } else {
                objects.push(obj(&format!("b{b}l{k}"), b, k, from, per, size(r), cap));
            }
        }
    }
    for i in (1..objects.len()).rev() {
        objects.swap(i, r.range(0, i as u64) as usize);
    }
    Model { unit_us, objects }
}

/// A bounded plan drawn over the whole range: either bounded link schedule (a `fifo`
/// link always with a refinement timeout), either carriage, any window, live or on
/// demand, one to four workers, decode times from 1 µs, slow and fast links.
fn wild_plan(r: &mut Rng, m: &Model) -> Plan {
    let schedule = if r.coin() { Schedule::Fifo } else { Schedule::StrictPriority };
    let carriage = if r.coin() { Carriage::Datagram } else { Carriage::Subgroup };
    let largest = m.objects.iter().map(|o| o.bytes).max().unwrap_or(1);
    let timeout = |r: &mut Rng| match r.range(0, 3) {
        0 => 0,
        1 => r.range(1, 2000),
        _ => r.range(2000, 400_000),
    };
    Plan {
        mode: if r.coin() { Mode::Vod } else { Mode::Live },
        rtt_us: if r.coin() { 0 } else { r.range(1, 200_000) },
        bytes_per_ms: r.range(1, 20_000),
        chunk_bytes: if carriage == Carriage::Datagram { largest + r.range(0, 2000) } else { r.range(1, 3000) },
        schedule,
        carriage,
        layer_cap: if r.range(0, 4) == 0 { Some(r.range(0, 2) as u8) } else { None },
        timeout_base_us: None,
        timeout_refinement_us: if schedule == Schedule::Fifo || r.coin() { Some(timeout(r)) } else { None },
        vod_window_us: if r.coin() { Some(r.range(0, 300_000)) } else { None },
        workers: r.range(1, 4) as u32,
        decode_order: DecodeOrder::BaseFirst,
        decode_base_us: r.range(1, 20_000),
        decode_refinement_us: r.range(1, 10_000),
        playout_start_us: if r.coin() { Some(r.range(0, 400_000)) } else { None },
        late_refinement: if r.coin() { LateRefinement::Discard } else { LateRefinement::Present },
    }
}

fn plan(r: &mut Rng, schedule: Schedule, carriage: Carriage, timeout_ref: Option<u64>, decode_order: DecodeOrder) -> Plan {
    Plan {
        mode: if r.coin() { Mode::Vod } else { Mode::Live },
        rtt_us: r.range(0, 120_000),
        bytes_per_ms: r.range(150, 6000),
        chunk_bytes: if carriage == Carriage::Datagram { 10_000 } else { r.range(300, 1500) },
        schedule,
        carriage,
        layer_cap: None,
        timeout_base_us: None,
        timeout_refinement_us: timeout_ref,
        vod_window_us: if r.coin() { Some(r.range(0, 200_000)) } else { None },
        workers: r.range(1, 3) as u32,
        decode_order,
        decode_base_us: r.range(1000, 9000),
        decode_refinement_us: r.range(300, 5000),
        playout_start_us: if r.coin() { Some(r.range(0, 400_000)) } else { None },
        late_refinement: if r.coin() { LateRefinement::Discard } else { LateRefinement::Present },
    }
}

fn conserved(r: &SimResult) {
    assert_eq!(r.offered, r.delivered + r.dropped + r.abandoned, "offered = delivered + dropped + abandoned");
}

#[test]
fn a_hand_computed_timeline() {
    // One unit: a 2500-byte base and a 1250-byte layer; h = 20 ms, 2500 bytes/ms,
    // 1250-byte chunks (500 µs each). Base chunks leave 20.0–20.5 and 20.5–21.0 ms and
    // arrive 20 ms later; the layer's chunk leaves 21.0–21.5 ms. The base decodes 41.0–
    // 44.0 ms, the layer 44.0–45.5 ms.
    let m = Model { unit_us: 40_000, objects: vec![obj("b", 0, 0, 0, 1, 2500, None), obj("l", 0, 1, 0, 1, 1250, None)] };
    let p = Plan {
        mode: Mode::Vod,
        rtt_us: 40_000,
        bytes_per_ms: 2500,
        chunk_bytes: 1250,
        schedule: Schedule::StrictPriority,
        carriage: Carriage::Subgroup,
        layer_cap: None,
        timeout_base_us: None,
        timeout_refinement_us: None,
        vod_window_us: None,
        workers: 1,
        decode_order: DecodeOrder::BaseFirst,
        decode_base_us: 3000,
        decode_refinement_us: 1500,
        playout_start_us: None,
        late_refinement: LateRefinement::Discard,
    };
    let r = simulate(&m, &p).unwrap();
    assert_eq!(r.objects[0], ObjEvent { id: "b".into(), sent_first_us: Some(20_000), arrived_us: Some(41_000), state: ObjState::Delivered });
    assert_eq!(r.objects[1], ObjEvent { id: "l".into(), sent_first_us: Some(21_000), arrived_us: Some(41_500), state: ObjState::Delivered });
    assert_eq!(r.presentations, vec![(0, 1, 44_000), (0, 2, 45_500)]);
    assert_eq!((r.tfpf_us, r.tffs_us.clone(), r.bfpf_sched), (Some(44_000), vec![Some(44_000), Some(45_500)], Some(3750)));
    assert_eq!((r.offered, r.delivered, r.dropped, r.abandoned, r.missed), (3750, 3750, 0, 0, 0));
    assert_eq!(r.stall_us, vec![(0, Some(0))]);
    // With playout at 44.5 ms the layer is late: discarded and missed, or presented and missed.
    let late = Plan { playout_start_us: Some(44_500), ..p.clone() };
    let r = simulate(&m, &late).unwrap();
    assert_eq!((r.presentations.clone(), r.missed), (vec![(0, 1, 44_000)], 1));
    let r = simulate(&m, &Plan { late_refinement: LateRefinement::Present, ..late }).unwrap();
    assert_eq!((r.presentations.clone(), r.missed), (vec![(0, 1, 44_000), (0, 2, 45_500)], 1));
    // A 400 µs refinement timeout: the layer's deadline is 20.4 ms, and at the next chunk
    // boundary (20.5 ms) it has not started, so its subgroup is reset and its bytes
    // dropped; as a datagram, not started by its deadline, it is dropped too.
    let t = Plan { timeout_refinement_us: Some(400), ..p.clone() };
    let r = simulate(&m, &t).unwrap();
    assert_eq!((r.objects[1].state, r.objects[1].sent_first_us, r.abandoned, r.dropped, r.presentations.len()), (ObjState::Dropped, None, 0, 1250, 1));
    let dg = Plan { carriage: Carriage::Datagram, chunk_bytes: 2500, ..t };
    let r = simulate(&m, &dg).unwrap();
    assert_eq!((r.objects[1].state, r.objects[1].sent_first_us, r.dropped), (ObjState::Dropped, None, 1250));
    conserved(&r);
    // A 2500-byte layer and a 1200 µs timeout (deadline 21.2 ms): its first chunk leaves
    // 21.0–21.5 ms, and at 21.5 ms the subgroup is reset: 1250 bytes abandoned, 1250
    // dropped, nothing presented above the base.
    let m2 = Model { unit_us: 40_000, objects: vec![obj("b", 0, 0, 0, 1, 2500, None), obj("l", 0, 1, 0, 1, 2500, None)] };
    let r = simulate(&m2, &Plan { timeout_refinement_us: Some(1200), ..p.clone() }).unwrap();
    assert_eq!((r.objects[1].state, r.objects[1].sent_first_us, r.abandoned, r.dropped), (ObjState::Abandoned, Some(21_000), 1250, 1250));
    assert_eq!(r.presentations, vec![(0, 1, 44_000)]);
    conserved(&r);
    // A reset takes the subgroup's tail with it: two units' layers in one subgroup,
    // the second available 40 ms later (a window of 0) with a deadline far off, is
    // dropped when the first misses its deadline at 21.5 ms.
    let m3 = Model {
        unit_us: 40_000,
        objects: vec![obj("b", 0, 0, 0, 2, 2500, None), obj("l0", 0, 1, 0, 1, 2500, None), obj("l1", 0, 1, 1, 1, 1250, None)],
    };
    let r = simulate(&m3, &Plan { timeout_refinement_us: Some(1200), vod_window_us: Some(0), ..p.clone() }).unwrap();
    assert_eq!((r.objects[1].state, r.objects[2].state, r.objects[2].sent_first_us), (ObjState::Abandoned, ObjState::Dropped, None));
    assert_eq!((r.abandoned, r.dropped), (1250, 2500));
}

#[test]
fn strict_priority_bound() {
    let mut r = Rng(1);
    for i in 0..12 {
        let m = model(&mut r, 3, 4, 2, i % 2 == 0);
        let carriage = if r.coin() { Carriage::Datagram } else { Carriage::Subgroup };
        let t = if r.coin() { Some(r.range(0, 300_000)) } else { None };
        let p = plan(&mut r, Schedule::StrictPriority, carriage, t, DecodeOrder::BaseFirst);
        assert!(plan_only_bound(&p).is_some());
        conserved(&simulate(&m, &p).unwrap());
        check_stall(&m, &p).unwrap_or_else(|v| panic!("plan {i}: {v:?} {p:?}"));
    }
}

#[test]
fn fifo_timeout_bound() {
    let mut r = Rng(2);
    for carriage in [Carriage::Datagram, Carriage::Subgroup] {
        for i in 0..12 {
            let m = model(&mut r, 3, 4, 2, i % 2 == 1);
            let t = r.range(0, 300_000);
            let p = plan(&mut r, Schedule::Fifo, carriage, Some(t), DecodeOrder::BaseFirst);
            conserved(&simulate(&m, &p).unwrap());
            check_stall(&m, &p).unwrap_or_else(|v| panic!("{carriage:?} plan {i}: {v:?} {p:?}"));
        }
    }
}

#[test]
fn bound_holds_on_mixed_plans() {
    // Groups, units per group and layers drawn too, under either bounded schedule and
    // either carriage: the stall stays within §10.4's bound and the bytes are conserved.
    for seed in 0..300u64 {
        let mut r = Rng(1000 + seed);
        let (nb, per, nl, per_unit) = (r.range(1, 4) as u32, r.range(1, 6), r.range(1, 3) as u8, r.coin());
        let m = model(&mut r, nb, per, nl, per_unit);
        let schedule = if r.coin() { Schedule::Fifo } else { Schedule::StrictPriority };
        let carriage = if r.coin() { Carriage::Datagram } else { Carriage::Subgroup };
        let t = if schedule == Schedule::Fifo || r.coin() { Some(r.range(0, 300_000)) } else { None };
        let p = plan(&mut r, schedule, carriage, t, DecodeOrder::BaseFirst);
        conserved(&simulate(&m, &p).unwrap());
        check_stall(&m, &p).unwrap_or_else(|v| panic!("seed {seed}: {v:?} {p:?}"));
    }
}

/// §10.4's bound, checked on models and plans drawn over the whole range
/// ([`wild_model`], [`wild_plan`]). An exploratory run of the same draw over far more
/// seeds is reported with the change that added it; this is its bounded part.
#[test]
fn bound_holds_on_wild_plans() {
    for seed in 0..2000u64 {
        let mut r = Rng(0x5eed_0000 + seed);
        let m = wild_model(&mut r);
        let p = wild_plan(&mut r, &m);
        assert!(plan_only_bound(&p).is_some());
        let res = simulate(&m, &p).unwrap_or_else(|e| panic!("seed {seed}: {e}"));
        conserved(&res);
        check_stall(&m, &p).unwrap_or_else(|v| panic!("seed {seed}: {v:?}\n{p:?}\n{}", m.to_json()));
    }
}

/// A model and plan drawn toward the cases where send and decode order matter: capture
/// times on, just before and just after chunk boundaries, repeated and out of order;
/// a base split into two layer-0 objects with their own captures; objects listed in any
/// order; chunks of one byte to a datagram's size; slow links; zero, one-microsecond and
/// chunk-length timeouts; one to four workers. Every plan is inside §10.4's
/// preconditions.
fn targeted(r: &mut Rng) -> (Model, Plan) {
    fn pick<T: Copy>(r: &mut Rng, v: &[T]) -> T {
        v[r.range(0, v.len() as u64 - 1) as usize]
    }
    let chunk = pick(r, &[1u64, 2, 3, 7, 1200]);
    let bpm = pick(r, &[1u64, 3, 1000, 2500, 7000]);
    let c = (chunk * 1000).div_ceil(bpm);
    let unit_us = pick(r, &[1u64, 7, c, c + 1, 40_000]);
    let datagram = r.range(0, 2) == 0;
    let size = |r: &mut Rng| -> u64 {
        if datagram {
            pick(r, &[1, chunk.max(2) - 1, chunk]).max(1)
        } else {
            pick(r, &[1, chunk.max(2) - 1, chunk, chunk + 1, 2 * chunk + 1, 5 * chunk]).max(1)
        }
    };
    let live = r.coin();
    let caps = [0, 1, c.saturating_sub(1), c, c + 1, 2 * c, 2 * c + 1, 3 * c + 2, 7 * c];
    let nb = r.range(1, 4) as u32;
    let per = r.range(1, 3);
    let mut objects = Vec::new();
    for b in 0..nb {
        let from = b as u64 * per;
        let cap0 = live.then(|| pick(r, &caps));
        if per >= 2 && r.range(0, 2) == 0 {
            let split = r.range(1, per - 1);
            objects.push(obj(&format!("b{b}a"), b, 0, from, split, size(r), cap0));
            let cap1 = live.then(|| pick(r, &caps));
            objects.push(obj(&format!("b{b}b"), b, 0, from + split, per - split, size(r), cap1));
        } else {
            objects.push(obj(&format!("b{b}"), b, 0, from, per, size(r), cap0));
        }
        for k in 1..=r.range(0, 3) as u8 {
            if r.coin() {
                for u in 0..per {
                    let cap = live.then(|| pick(r, &caps));
                    objects.push(obj(&format!("b{b}l{k}u{u}"), b, k, from + u, 1, size(r), cap));
                }
            } else {
                let cap = live.then(|| pick(r, &caps));
                objects.push(obj(&format!("b{b}l{k}"), b, k, from, per, size(r), cap));
            }
        }
    }
    for i in (1..objects.len()).rev() {
        objects.swap(i, r.range(0, i as u64) as usize);
    }
    let strict = r.coin();
    let plan = Plan {
        mode: if live { Mode::Live } else { Mode::Vod },
        rtt_us: pick(r, &[0, 1, 2, 7, 40_000]),
        bytes_per_ms: bpm,
        chunk_bytes: chunk,
        schedule: if strict { Schedule::StrictPriority } else { Schedule::Fifo },
        carriage: if datagram { Carriage::Datagram } else { Carriage::Subgroup },
        layer_cap: pick(r, &[None, Some(0), Some(1), Some(2)]),
        timeout_base_us: None,
        timeout_refinement_us: if strict {
            pick(r, &[None, Some(0), Some(1), Some(c), Some(3 * c)])
        } else {
            Some(pick(r, &[0, 1, c.saturating_sub(1), c, c + 1, 3 * c, 10 * c]))
        },
        vod_window_us: if live { None } else { pick(r, &[None, Some(0), Some(1), Some(unit_us / 2), Some(unit_us * 2), Some(unit_us * 100)]) },
        workers: r.range(1, 4) as u32,
        decode_order: DecodeOrder::BaseFirst,
        decode_base_us: pick(r, &[1, c.max(1), 10 * c.max(1), 1000, 3]),
        decode_refinement_us: pick(r, &[1, c.saturating_sub(1).max(1), c.max(1), 5 * c.max(1), 2]),
        playout_start_us: pick(r, &[None, Some(0), Some(c), Some(unit_us)]),
        late_refinement: if r.coin() { LateRefinement::Discard } else { LateRefinement::Present },
    };
    (Model { unit_us, objects }, plan)
}

/// §10.4's bound on [`targeted`] draws, which reach the orders the bound depends on: with
/// the send or decode order of bases reverted to listing order, this test fails on its
/// own, without [`bases_keep_their_order`]'s two models.
#[test]
fn bound_holds_on_targeted_plans() {
    let mut r = Rng(0x7a76_e7ed);
    for i in 0..TARGETED_DRAWS {
        let (m, p) = targeted(&mut r);
        assert!(plan_only_bound(&p).is_some());
        let res = simulate(&m, &p).unwrap_or_else(|e| panic!("draw {i}: {e}\n{p:?}\n{}", m.to_json()));
        conserved(&res);
        check_stall(&m, &p).unwrap_or_else(|v| panic!("draw {i}: {v:?}\n{p:?}\n{}", m.to_json()));
    }
}

const TARGETED_DRAWS: u64 = 20_000;

/// The two models a review found past the bound before bases were sent and decoded in
/// the order they became available: a later-captured base ahead of an earlier one under
/// strict priority, and a base whose decode a smaller one overtook under base-first.
#[test]
fn bases_keep_their_order() {
    let a = Model {
        unit_us: 40_000,
        objects: vec![
            obj("B0", 0, 0, 0, 1, 10, Some(0)),
            obj("R0", 0, 1, 0, 1, 1000, Some(0)),
            obj("Bb", 2, 0, 2, 1, 10, Some(15)),
            obj("Ba", 1, 0, 1, 1, 50_000, Some(20)),
        ],
    };
    let pa = Plan {
        mode: Mode::Live,
        rtt_us: 0,
        bytes_per_ms: 1000,
        chunk_bytes: 1000,
        schedule: Schedule::StrictPriority,
        carriage: Carriage::Subgroup,
        layer_cap: None,
        timeout_base_us: None,
        timeout_refinement_us: None,
        vod_window_us: None,
        workers: 1,
        decode_order: DecodeOrder::BaseFirst,
        decode_base_us: 100,
        decode_refinement_us: 100,
        playout_start_us: None,
        late_refinement: LateRefinement::Present,
    };
    check_stall(&a, &pa).unwrap();
    let b = Model {
        unit_us: 40_000,
        objects: vec![
            obj("B0", 0, 0, 0, 1, 10, None),
            obj("R0", 0, 1, 0, 1, 10, None),
            obj("Bb", 2, 0, 2, 1, 100_000, None),
            obj("Ba", 1, 0, 1, 1, 10, None),
        ],
    };
    let pb = Plan { mode: Mode::Vod, schedule: Schedule::Fifo, timeout_refinement_us: Some(20), decode_base_us: 100_000, ..pa };
    check_stall(&b, &pb).unwrap();
}

/// The counter-plans' model: three groups of five units, two layers per unit, all
/// available at once, on a slow link.
fn counter_model() -> Model {
    let mut objects = Vec::new();
    for b in 0..3u32 {
        let from = b as u64 * 5;
        objects.push(obj(&format!("b{b}"), b, 0, from, 5, 4000, None));
        for k in 1..=2u8 {
            for u in 0..5 {
                objects.push(obj(&format!("b{b}l{k}u{u}"), b, k, from + u, 1, 1800, None));
            }
        }
    }
    Model { unit_us: 40_000, objects }
}

fn counter_plan(schedule: Schedule, timeout_ref: Option<u64>, decode_order: DecodeOrder) -> Plan {
    Plan {
        mode: Mode::Vod,
        rtt_us: 40_000,
        bytes_per_ms: 1000,
        chunk_bytes: 1200,
        schedule,
        carriage: Carriage::Subgroup,
        layer_cap: None,
        timeout_base_us: None,
        timeout_refinement_us: timeout_ref,
        vod_window_us: None,
        workers: 1,
        decode_order,
        decode_base_us: 4000,
        decode_refinement_us: 3000,
        playout_start_us: None,
        late_refinement: LateRefinement::Discard,
    }
}

#[test]
fn fifo_no_timeout_violates() {
    // FIFO without a timeout sends every layer of a group before the next group's base:
    // its stall passes the bound of the same plan under strict priority.
    let m = counter_model();
    let fifo = counter_plan(Schedule::Fifo, None, DecodeOrder::BaseFirst);
    let strict = counter_plan(Schedule::StrictPriority, None, DecodeOrder::BaseFirst);
    assert_eq!(plan_only_bound(&fifo), None);
    let bound = plan_only_bound(&strict).unwrap();
    check_stall(&m, &strict).unwrap();
    let worst = simulate(&m, &fifo).unwrap().max_stall().unwrap();
    assert!(worst > bound as i64, "fifo with no timeout stalls {worst} µs, within the strict bound {bound}");
    // With a timeout it is bounded again.
    let timed = counter_plan(Schedule::Fifo, Some(30_000), DecodeOrder::BaseFirst);
    check_stall(&m, &timed).unwrap();
}

#[test]
fn fifo_decode_violates() {
    // One worker taking jobs in ready order decodes a group's layers before the next
    // base: its stall passes the bound base-first decode would have.
    // The next group becomes available 20 ms after the first (a 180 ms window over
    // 200 ms groups), while the first group's layers still wait to be decoded.
    let m = counter_model();
    let fifo = Plan { vod_window_us: Some(180_000), ..counter_plan(Schedule::StrictPriority, None, DecodeOrder::Fifo) };
    let base_first = Plan { decode_order: DecodeOrder::BaseFirst, ..fifo.clone() };
    check_stall(&m, &base_first).unwrap();
    assert_eq!(plan_only_bound(&fifo), None);
    let worst = simulate(&m, &fifo).unwrap().max_stall().unwrap();
    assert!(worst > plan_only_bound(&base_first).unwrap() as i64, "fifo decode stalls {worst} µs");
}

#[test]
fn conservation_and_layer_cap() {
    let mut r = Rng(3);
    for i in 0..24 {
        let m = model(&mut r, 2, 3, 3, i % 3 == 0);
        let schedule = if r.coin() { Schedule::Fifo } else { Schedule::StrictPriority };
        let carriage = if r.coin() { Carriage::Datagram } else { Carriage::Subgroup };
        let t = if r.coin() { Some(r.range(0, 100_000)) } else { None };
        let mut p = plan(&mut r, schedule, carriage, t, DecodeOrder::BaseFirst);
        if r.coin() {
            p.timeout_base_us = Some(r.range(0, 200_000));
        }
        let cap = (i % 4) as u8;
        p.layer_cap = Some(cap);
        let res = simulate(&m, &p).unwrap();
        conserved(&res);
        let capped: u64 = m.objects.iter().filter(|o| o.layer > cap).map(|o| o.bytes).sum();
        assert_eq!(res.never_sent_capped, capped);
        for (o, e) in m.objects.iter().zip(&res.objects) {
            assert_eq!(o.layer > cap, e.state == ObjState::Capped, "{}", o.id);
            if o.layer > cap {
                assert_eq!(e.sent_first_us, None, "{}: a capped object is never sent", o.id);
            }
        }
        assert!(res.presentations.iter().all(|p| p.1 <= cap as u16 + 1), "no presentation above the cap");
    }
}

#[test]
fn malformed_models_and_plans_are_refused() {
    let m = counter_model();
    let p = counter_plan(Schedule::StrictPriority, None, DecodeOrder::BaseFirst);
    // A datagram larger than a chunk; a live plan with no capture time.
    assert!(matches!(simulate(&m, &Plan { carriage: Carriage::Datagram, ..p.clone() }), Err(SimError::DatagramTooLarge(_))));
    assert!(matches!(simulate(&m, &Plan { mode: Mode::Live, ..p.clone() }), Err(SimError::NoCaptureTime(_))));
    // A unit covered twice at one layer; a layer with nothing below it.
    let mut twice = m.clone();
    twice.objects.push(obj("x", 0, 1, 0, 1, 10, None));
    assert!(matches!(simulate(&twice, &p), Err(SimError::Invalid(_))));
    let mut gap = m.clone();
    gap.objects.push(obj("y", 0, 4, 0, 1, 10, None));
    assert!(matches!(simulate(&gap, &p), Err(SimError::Invalid(_))));
    // The JSON forms round-trip, and an unknown member is refused.
    assert_eq!(Plan::from_json(&p.to_json()).unwrap(), p);
    assert_eq!(Model::from_json(&m.to_json()).unwrap(), m);
    let mut v = p.to_json();
    v["sink"]["extra"] = json!(1);
    assert!(Plan::from_json(&v).is_err());
}

/// A model and plan whose times and byte sums pass 64 bits are refused, the same in
/// every build; a figure past `2⁵³ − 1` is refused when read.
#[test]
fn sums_past_64_bits_are_refused() {
    let big = Model { unit_us: 40_000, objects: vec![obj("B0", 0, 0, 0, 1, u64::MAX / 2, None), obj("B1", 1, 0, 1, 1, u64::MAX / 2 + 2, None)] };
    let p = Plan {
        mode: Mode::Vod,
        rtt_us: 0,
        bytes_per_ms: 1,
        chunk_bytes: u64::MAX,
        schedule: Schedule::StrictPriority,
        carriage: Carriage::Subgroup,
        layer_cap: None,
        timeout_base_us: None,
        timeout_refinement_us: None,
        vod_window_us: None,
        workers: 1,
        decode_order: DecodeOrder::BaseFirst,
        decode_base_us: 1,
        decode_refinement_us: 1,
        playout_start_us: None,
        late_refinement: LateRefinement::Present,
    };
    assert_eq!(simulate(&big, &p), Err(SimError::Overflow));
    // Each chunk's time fits; the link's clock after two of them does not.
    let three = Model {
        unit_us: 40_000,
        objects: (0..3).map(|b| obj(&format!("B{b}"), b, 0, b as u64, 1, 1 << 53, None)).collect(),
    };
    assert_eq!(simulate(&three, &Plan { chunk_bytes: 1 << 53, ..p.clone() }), Err(SimError::Overflow));
    // A decode that would end past 64 bits.
    let small = Model { unit_us: 40_000, objects: vec![obj("B0", 0, 0, 0, 1, 10, None)] };
    assert_eq!(simulate(&small, &Plan { chunk_bytes: 1000, bytes_per_ms: 1000, decode_base_us: u64::MAX, ..p.clone() }), Err(SimError::Overflow));
    assert_eq!(plan_only_bound(&p), None, "a bound past 64 bits is none");
    // The review's model, read from its JSON: its byte counts pass 2^53 - 1.
    let o = serde_json::json!({"format": "wai-sim-model/1", "unit_us": 40000, "objects": [
        {"id": "B0", "base": 0, "layer": 0, "units": [0, 1], "bytes": 18446744073709551u64, "capture_us": null},
        {"id": "B1", "base": 1, "layer": 0, "units": [1, 1], "bytes": 18446744073709552u64, "capture_us": null}]});
    assert!(matches!(Model::from_json(&o), Err(SimError::Invalid(_))));
    // At the largest figures a JSON number holds, the sums still fit or are refused.
    let edge = Model { unit_us: MAX_FIGURE, objects: vec![obj("B0", 0, 0, MAX_FIGURE - 1, 1, MAX_FIGURE, None)] };
    let q = Plan { chunk_bytes: MAX_FIGURE, rtt_us: MAX_FIGURE, vod_window_us: Some(0), ..p };
    assert!(matches!(simulate(&edge, &q), Err(SimError::Overflow)));
}

/// A refinement's own timeout replaces the plan's for it alone (a MoQ track's
/// `DELIVERY_TIMEOUT`, packaging §8.6): of two refinements behind a base on a slow link,
/// the one whose own timeout passes first is dropped and the other delivered. A base
/// takes no timeout of its own, and the member reads and writes back.
#[test]
fn a_refinement_s_own_timeout() {
    let p = Plan {
        mode: Mode::Vod,
        rtt_us: 0,
        bytes_per_ms: 1000,
        chunk_bytes: 1000,
        schedule: Schedule::StrictPriority,
        carriage: Carriage::Datagram,
        layer_cap: None,
        timeout_base_us: None,
        timeout_refinement_us: Some(10_000),
        vod_window_us: None,
        workers: 1,
        decode_order: DecodeOrder::BaseFirst,
        decode_base_us: 1,
        decode_refinement_us: 1,
        playout_start_us: None,
        late_refinement: LateRefinement::Present,
    };
    // The base takes 1 ms; each refinement 1 ms after it.
    let mut m = Model { unit_us: 40_000, objects: vec![obj("b", 0, 0, 0, 2, 1000, None), obj("l0", 0, 1, 0, 1, 1000, None), obj("l1", 0, 1, 1, 1, 1000, None)] };
    m.objects[1].timeout_us = Some(500);
    let r = simulate(&m, &p).unwrap();
    assert_eq!((r.objects[1].state, r.objects[2].state), (ObjState::Dropped, ObjState::Delivered), "l0's own 0.5 ms passed; l1 has the plan's 10 ms");
    let back = Model::from_json(&m.to_json()).unwrap();
    assert_eq!(back, m);
    assert!(m.to_json()["objects"][2].get("timeout_us").is_none(), "written only where an object has its own");
    let mut bad = m.to_json();
    bad["objects"][0]["timeout_us"] = serde_json::json!(5);
    assert!(Model::from_json(&bad).is_err(), "a base takes the plan's timeout");
    // Under fifo, the bound's T is the longest refinement timeout, its own or the plan's;
    // with every refinement timed, a plan with none of its own is bounded.
    let fifo = Plan { schedule: Schedule::Fifo, timeout_refinement_us: None, ..p.clone() };
    assert_eq!(plan_only_bound(&fifo), None);
    m.objects[2].timeout_us = Some(7000);
    assert_eq!(stall_bound_for(&m, &fifo), Some(7000 + 1000 + 1));
    assert_eq!(stall_bound_for(&m, &Plan { timeout_refinement_us: Some(9000), ..fifo.clone() }), Some(7000 + 1000 + 1), "own timeouts replace the plan's");
}

/// §10.4's bound on [`targeted`] draws whose refinements take timeouts of their own,
/// some of them, under either schedule.
#[test]
fn bound_holds_with_object_timeouts() {
    let mut r = Rng(0x0b1e_c7ed);
    for i in 0..TARGETED_DRAWS / 4 {
        let (mut m, p) = targeted(&mut r);
        let c = p.serialise_us(p.chunk_bytes).unwrap();
        let all = r.coin();
        for o in m.objects.iter_mut().filter(|o| o.layer >= 1) {
            if all || r.coin() {
                o.timeout_us = Some([0, 1, c, 3 * c, 10 * c][r.range(0, 4) as usize]);
            }
        }
        let p = if all && r.coin() { Plan { timeout_refinement_us: None, ..p } } else { p };
        let res = simulate(&m, &p).unwrap_or_else(|e| panic!("draw {i}: {e}\n{p:?}\n{}", m.to_json()));
        conserved(&res);
        check_stall(&m, &p).unwrap_or_else(|v| panic!("draw {i}: {v:?}\n{p:?}\n{}", m.to_json()));
    }
}
