//! `schedule` (jwp-receipts §2.4.16; `extensions/carousel.md` §4, §5): a publisher's plan of
//! when each object of a group passes on a bearer instance, and with which forward error
//! correction.
//!
//! This file holds steps 2 and 4 of jwp-receipts §3.1 for the class — its body's members and
//! types, and carousel §4.2 with §5's admissibility of each code's parameters — and step 6,
//! its checks against the group receipt and the object receipts (§4.3, and cache-authority §5.6
//! for a node's own schedule), and when one schedule extends another (§4.5). What a verifier
//! derives from a schedule, and what a receiver plans with one, is `crate::carousel`'s.

use super::*;
use crate::jwp_delivery::{GroupReceipt, ObjectReceipt};

/// What a schedule claim attests, in-band (carousel §4.2 rule 1).
pub const ATTESTS_SCHEDULED: &str = "scheduled";
/// RFC 6330 §5.1.2: the most source symbols one source block has.
pub const K_PRIME_MAX: u64 = 56_403;
/// RFC 6330 §3.3.2 as its verified erratum 5548 corrects it: the greatest transfer length,
/// 56,403 · 65,535 · 255.
pub const RAPTORQ_MAX_TRANSFER: u64 = 942_574_504_275;
/// RFC 6330 §3.2: ESIs are 24 bits.
pub const RAPTORQ_ESIS: u64 = 1 << 24;
/// RFC 5053 §5.1.2: the most source symbols one source block has.
pub const RAPTOR_K_MAX: u64 = 8_192;
/// RFC 5053 §5.7: the fewest source symbols a block has a systematic index for.
pub const RAPTOR_K_MIN: u64 = 4;
/// RFC 5053 §3.2.2: a transfer length is less than this.
pub const RAPTOR_TRANSFER_LIMIT: u64 = 1 << 45;
/// RFC 5053 §3.1: ESIs are 16 bits.
pub const RAPTOR_ESIS: u64 = 1 << 16;
/// RFC 5445 §3.2.1: source block numbers and ESIs are 16 bits.
pub const NO_CODE_FIELD: u64 = 1 << 16;
/// 9999-12-31T23:59:59.999Z, the last instant a pass may end at (carousel §4.2 rule 3.3).
pub const LAST_INSTANT_MS: u64 = 253_402_300_799_999;

/// A schedule body's members (carousel §4.1), in the order step 2 reads them.
const SCHEDULE_MEMBERS: &[&str] = &["bearer_id", "objects", "attests"];
/// An entry's members, in the order step 2 reads them.
const ENTRY_MEMBERS: &[&str] =
    &["leaf_index", "content_hash", "fec", "oti", "repair", "esi", "first_ms", "period_ms", "duration_ms", "count"];
const RAPTORQ_OTI: &[&str] = &["transfer_length", "symbol_size", "source_blocks", "sub_blocks", "alignment"];
const NO_CODE_OTI: &[&str] = &["transfer_length", "symbol_size", "max_block_length"];

/// The code an entry's symbols are made by (carousel §4.1 `fec`).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Fec {
    /// RFC 6330, FEC Encoding ID 6.
    RaptorQ,
    /// RFC 5053, FEC Encoding ID 1.
    Raptor,
    /// RFC 5445, FEC Encoding ID 0.
    NoCode,
}

impl Fec {
    pub fn label(self) -> &'static str {
        match self {
            Fec::RaptorQ => "raptorq",
            Fec::Raptor => "raptor",
            Fec::NoCode => "no-code",
        }
    }

    pub fn from_label(s: &str) -> Option<Fec> {
        [Fec::RaptorQ, Fec::Raptor, Fec::NoCode].into_iter().find(|f| f.label() == s)
    }
}

/// Which ESIs a pass sends (carousel §5.4).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Esi {
    /// Every pass sends ESIs 0 to m − 1.
    Repeat,
    /// Pass j sends ESIs (j − 1)·m to j·m − 1.
    Advance,
}

impl Esi {
    pub fn label(self) -> &'static str {
        match self {
            Esi::Repeat => "repeat",
            Esi::Advance => "advance",
        }
    }

    pub fn from_label(s: &str) -> Option<Esi> {
        [Esi::Repeat, Esi::Advance].into_iter().find(|e| e.label() == s)
    }
}

/// A code's object transmission information (carousel §4.1 `oti`).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Oti {
    /// RFC 6330 §3.3.2, §3.3.3: F, T, Z, N, Al.
    RaptorQ { transfer_length: u64, symbol_size: u64, source_blocks: u64, sub_blocks: u64, alignment: u64 },
    /// RFC 5053 §3.2.2, §3.2.3: F, T, Z, N, Al.
    Raptor { transfer_length: u64, symbol_size: u64, source_blocks: u64, sub_blocks: u64, alignment: u64 },
    /// RFC 5445 §3.2.2.2: the transfer length, the encoding symbol length, and the maximum
    /// source block length in symbols.
    NoCode { transfer_length: u64, symbol_size: u64, max_block_length: u64 },
}

/// Source blocks of one size: `blocks` of them, each of `k` source symbols.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct BlockSize {
    pub k: u64,
    pub blocks: u64,
}

/// RFC 6330 §4.4.1.2's Partition[I, J]: (IL, IS, JL, JS).
pub fn partition(i: u64, j: u64) -> (u64, u64, u64, u64) {
    let (il, is) = (i.div_ceil(j), i / j);
    let jl = i - is * j;
    (il, is, jl, j - jl)
}

impl Oti {
    pub fn fec(&self) -> Fec {
        match self {
            Oti::RaptorQ { .. } => Fec::RaptorQ,
            Oti::Raptor { .. } => Fec::Raptor,
            Oti::NoCode { .. } => Fec::NoCode,
        }
    }

    pub fn transfer_length(&self) -> u64 {
        match *self {
            Oti::RaptorQ { transfer_length, .. } | Oti::Raptor { transfer_length, .. } | Oti::NoCode { transfer_length, .. } => transfer_length,
        }
    }

    pub fn symbol_size(&self) -> u64 {
        match *self {
            Oti::RaptorQ { symbol_size, .. } | Oti::Raptor { symbol_size, .. } | Oti::NoCode { symbol_size, .. } => symbol_size,
        }
    }

    /// Carousel §5.1, §5.2 and §5.5: whether the code admits these parameters.
    pub fn admitted(&self) -> bool {
        match *self {
            Oti::RaptorQ { transfer_length: f, symbol_size: t, source_blocks: z, sub_blocks: n, alignment: al } => {
                (1..=RAPTORQ_MAX_TRANSFER).contains(&f)
                    && (1..=255).contains(&al)
                    && (1..=65_535).contains(&t)
                    && t.is_multiple_of(al)
                    && (1..=255).contains(&z)
                    && n >= 1
                    && n <= t / al
                    && z <= f.div_ceil(t)
                    && f.div_ceil(t).div_ceil(z) <= K_PRIME_MAX
            }
            Oti::Raptor { transfer_length: f, symbol_size: t, source_blocks: z, sub_blocks: n, alignment: al } => {
                (1..RAPTOR_TRANSFER_LIMIT).contains(&f)
                    && (1..=255).contains(&al)
                    && (1..=65_535).contains(&t)
                    && t.is_multiple_of(al)
                    && (1..=65_535).contains(&z)
                    && (1..=255).contains(&n)
                    && n <= t / al
                    && f.div_ceil(t) / z >= RAPTOR_K_MIN
                    && f.div_ceil(t).div_ceil(z) <= RAPTOR_K_MAX
            }
            Oti::NoCode { transfer_length: l, symbol_size: e, max_block_length: b } => {
                if !((1..1 << 48).contains(&l) && (1..=65_535).contains(&e) && (1..1 << 32).contains(&b)) {
                    return false;
                }
                let ts = l.div_ceil(e);
                let nb = ts.div_ceil(b);
                nb <= NO_CODE_FIELD && ts.div_ceil(nb) <= NO_CODE_FIELD
            }
        }
    }

    /// The source blocks by size, in the order the object is divided into them: the larger
    /// first, a size no block has left out (carousel §5.1, §5.2, §5.5). Only for parameters
    /// [`Oti::admitted`] admits.
    pub fn blocks(&self) -> Vec<BlockSize> {
        let (large, small, n_large, n_small) = match *self {
            Oti::RaptorQ { transfer_length: f, symbol_size: t, source_blocks: z, .. }
            | Oti::Raptor { transfer_length: f, symbol_size: t, source_blocks: z, .. } => partition(f.div_ceil(t), z),
            Oti::NoCode { transfer_length: l, symbol_size: e, max_block_length: b } => {
                let ts = l.div_ceil(e);
                let nb = ts.div_ceil(b);
                // RFC 5052 §9.1.2: A_large, A_small, I.
                let (a_large, a_small) = (ts.div_ceil(nb), ts / nb);
                let i = ts - a_small * nb;
                (a_large, a_small, i, nb - i)
            }
        };
        [BlockSize { k: large, blocks: n_large }, BlockSize { k: small, blocks: n_small }]
            .into_iter()
            .filter(|s| s.blocks > 0)
            .collect()
    }

    fn to_value(self) -> Value {
        match self {
            Oti::RaptorQ { transfer_length, symbol_size, source_blocks, sub_blocks, alignment }
            | Oti::Raptor { transfer_length, symbol_size, source_blocks, sub_blocks, alignment } => json_obj(&[
                ("transfer_length", transfer_length),
                ("symbol_size", symbol_size),
                ("source_blocks", source_blocks),
                ("sub_blocks", sub_blocks),
                ("alignment", alignment),
            ]),
            Oti::NoCode { transfer_length, symbol_size, max_block_length } => json_obj(&[
                ("transfer_length", transfer_length),
                ("symbol_size", symbol_size),
                ("max_block_length", max_block_length),
            ]),
        }
    }

    fn values(&self) -> Vec<u64> {
        match *self {
            Oti::RaptorQ { transfer_length, symbol_size, source_blocks, sub_blocks, alignment }
            | Oti::Raptor { transfer_length, symbol_size, source_blocks, sub_blocks, alignment } => {
                vec![transfer_length, symbol_size, source_blocks, sub_blocks, alignment]
            }
            Oti::NoCode { transfer_length, symbol_size, max_block_length } => {
                vec![transfer_length, symbol_size, max_block_length]
            }
        }
    }
}

fn json_obj(pairs: &[(&str, u64)]) -> Value {
    Value::Object(pairs.iter().map(|(k, v)| ((*k).to_owned(), Value::from(*v))).collect())
}

/// One object's passes (carousel §4.1).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ScheduleEntry {
    pub leaf_index: u64,
    pub content_hash: [u8; 32],
    pub oti: Oti,
    /// Repair symbols of each source block in each pass.
    pub repair: u64,
    pub esi: Esi,
    pub first_ms: u64,
    pub period_ms: u64,
    pub duration_ms: u64,
    pub count: u64,
}

impl ScheduleEntry {
    /// The instant pass `j` (from 1) starts. Only for an entry that passed step 4, whose last
    /// pass ends within [`LAST_INSTANT_MS`].
    pub fn pass_start(&self, j: u64) -> u64 {
        self.first_ms + (j - 1) * self.period_ms
    }

    /// The instant pass `j` (from 1) ends.
    pub fn pass_end(&self, j: u64) -> u64 {
        self.pass_start(j) + self.duration_ms
    }

    /// The octets the entry's passes send (carousel §5.4). Only for an entry that passed step 4,
    /// whose figure rule 3.3 bounds.
    pub fn bytes_sent(&self) -> u64 {
        self.checked_bytes_sent().expect("rule 3.3 bounds it")
    }

    /// Carousel §5.4: whether the symbols are ones the code can name.
    fn symbols_named(&self) -> bool {
        match self.oti {
            Oti::NoCode { .. } => self.repair == 0 && self.esi == Esi::Repeat,
            Oti::RaptorQ { .. } | Oti::Raptor { .. } => {
                let kl = self.oti.blocks()[0].k;
                let Some(m) = kl.checked_add(self.repair) else { return false };
                let greatest = match self.esi {
                    Esi::Repeat => Some(m),
                    Esi::Advance => self.count.checked_mul(m),
                };
                let esis = if self.oti.fec() == Fec::Raptor { RAPTOR_ESIS } else { RAPTORQ_ESIS };
                greatest.is_some_and(|g| g <= esis)
            }
        }
    }

    /// Carousel §4.2 rule 3.3: whether the passes are well formed. Only for an entry whose
    /// parameters its code admits.
    fn passes_formed(&self) -> bool {
        if self.count == 0 || self.duration_ms == 0 || (self.count >= 2 && self.period_ms < self.duration_ms) {
            return false;
        }
        let ends = (self.count - 1)
            .checked_mul(self.period_ms)
            .and_then(|s| s.checked_add(self.first_ms))
            .and_then(|s| s.checked_add(self.duration_ms))
            .is_some_and(|end| end <= LAST_INSTANT_MS);
        ends && self.checked_bytes_sent().is_some_and(|b| b <= MAX_INTEGER)
    }

    /// [`ScheduleEntry::bytes_sent`], `None` past `u64`.
    fn checked_bytes_sent(&self) -> Option<u64> {
        match self.oti {
            Oti::RaptorQ { symbol_size, .. } | Oti::Raptor { symbol_size, .. } => {
                let per_pass = self.oti.blocks().iter().try_fold(0u64, |a, s| {
                    s.k.checked_add(self.repair).and_then(|m| m.checked_mul(s.blocks)).and_then(|n| a.checked_add(n))
                })?;
                per_pass.checked_mul(symbol_size)?.checked_mul(self.count)
            }
            Oti::NoCode { transfer_length, .. } => transfer_length.checked_mul(self.count),
        }
    }

    fn to_value(&self) -> Value {
        let mut m = Map::new();
        m.insert("leaf_index".into(), self.leaf_index.into());
        m.insert("content_hash".into(), hex(&self.content_hash).into());
        m.insert("fec".into(), self.oti.fec().label().into());
        m.insert("oti".into(), self.oti.to_value());
        m.insert("repair".into(), self.repair.into());
        m.insert("esi".into(), self.esi.label().into());
        m.insert("first_ms".into(), self.first_ms.into());
        m.insert("period_ms".into(), self.period_ms.into());
        m.insert("duration_ms".into(), self.duration_ms.into());
        m.insert("count".into(), self.count.into());
        Value::Object(m)
    }

    /// One entry as a receiver holds it apart from its claim (carousel §7): step 2's reading
    /// of it, then §4.2 rule 3.
    pub fn from_value(v: &Value) -> Result<ScheduleEntry, Reason> {
        let e = ScheduleEntry::read(v)?;
        e.check()?;
        Ok(e)
    }

    /// Carousel §4.2 rule 3 for this entry, in its order.
    pub fn check(&self) -> Result<(), Reason> {
        if !self.oti.admitted() {
            return Err(Reason::ScheduleOtiInvalid);
        }
        if !self.symbols_named() {
            return Err(Reason::ScheduleSymbolsInvalid);
        }
        if !self.passes_formed() {
            return Err(Reason::SchedulePassesInvalid);
        }
        Ok(())
    }

    /// The entry in its JSON form (carousel §4.1).
    pub fn to_json(&self) -> Value {
        self.to_value()
    }

    /// Carousel §4.5: whether this entry's passes begin with `earlier`'s, at the same instants and
    /// sending the same symbols. Its members are `earlier`'s but `count`, which is at least
    /// `earlier`'s, and `period_ms`, which may differ only where `earlier` has one pass, whose
    /// period is not read.
    pub fn extends(&self, earlier: &ScheduleEntry) -> bool {
        self.count >= earlier.count
            && (earlier.count == 1 || self.period_ms == earlier.period_ms)
            && ScheduleEntry { count: earlier.count, period_ms: earlier.period_ms, ..self.clone() } == *earlier
    }

    /// Step 2: the members, then each one's type in the table's order, `oti`'s members as the
    /// code `fec` names reads them.
    fn read(v: &Value) -> Result<ScheduleEntry, Reason> {
        let m = v.as_object().ok_or(Reason::FieldType)?;
        members(m, ENTRY_MEMBERS)?;
        let leaf_index = get_int(m, "leaf_index")?;
        let content_hash = get_hash(m, "content_hash")?;
        let fec = Fec::from_label(&get_str(m, "fec")?).ok_or(Reason::FieldType)?;
        let o = m["oti"].as_object().ok_or(Reason::FieldType)?;
        let oti = match fec {
            Fec::RaptorQ => {
                members(o, RAPTORQ_OTI)?;
                Oti::RaptorQ {
                    transfer_length: get_int(o, "transfer_length")?,
                    symbol_size: get_int(o, "symbol_size")?,
                    source_blocks: get_int(o, "source_blocks")?,
                    sub_blocks: get_int(o, "sub_blocks")?,
                    alignment: get_int(o, "alignment")?,
                }
            }
            Fec::Raptor => {
                members(o, RAPTORQ_OTI)?;
                Oti::Raptor {
                    transfer_length: get_int(o, "transfer_length")?,
                    symbol_size: get_int(o, "symbol_size")?,
                    source_blocks: get_int(o, "source_blocks")?,
                    sub_blocks: get_int(o, "sub_blocks")?,
                    alignment: get_int(o, "alignment")?,
                }
            }
            Fec::NoCode => {
                members(o, NO_CODE_OTI)?;
                Oti::NoCode {
                    transfer_length: get_int(o, "transfer_length")?,
                    symbol_size: get_int(o, "symbol_size")?,
                    max_block_length: get_int(o, "max_block_length")?,
                }
            }
        };
        Ok(ScheduleEntry {
            leaf_index,
            content_hash,
            oti,
            repair: get_int(m, "repair")?,
            esi: Esi::from_label(&get_str(m, "esi")?).ok_or(Reason::FieldType)?,
            first_ms: get_int(m, "first_ms")?,
            period_ms: get_int(m, "period_ms")?,
            duration_ms: get_int(m, "duration_ms")?,
            count: get_int(m, "count")?,
        })
    }

    fn check_values(&self) -> Result<(), Reason> {
        for n in self.oti.values() {
            int_ok(n)?;
        }
        for n in [self.leaf_index, self.repair, self.first_ms, self.period_ms, self.duration_ms, self.count] {
            int_ok(n)?;
        }
        Ok(())
    }
}

/// `schedule` (carousel §4): the publisher's plan of the passes of the objects of the group its
/// subject names, on one bearer instance.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ScheduleClaim {
    /// The bearer instance, an absolute URI compared byte for byte.
    pub bearer_id: String,
    /// One entry for each object scheduled, in increasing `leaf_index`.
    pub objects: Vec<ScheduleEntry>,
    /// MUST be `"scheduled"`.
    pub attests: String,
}

impl ScheduleClaim {
    pub(super) fn to_value(&self) -> Value {
        let mut m = Map::new();
        m.insert("bearer_id".into(), self.bearer_id.clone().into());
        m.insert("objects".into(), Value::Array(self.objects.iter().map(ScheduleEntry::to_value).collect()));
        m.insert("attests".into(), self.attests.clone().into());
        Value::Object(m)
    }

    /// Step 2: the members — missing, then unknown — then each member's type in the table's
    /// order, each entry's in its own.
    pub(super) fn read(m: &Map<String, Value>) -> Result<ScheduleClaim, Reason> {
        members(m, SCHEDULE_MEMBERS)?;
        let s = get_str(m, "bearer_id")?;
        uri_ok(&s)?;
        let objects = m["objects"].as_array().ok_or(Reason::FieldType)?.iter().map(ScheduleEntry::read).collect::<Result<_, _>>()?;
        Ok(ScheduleClaim { bearer_id: s, objects, attests: get_str(m, "attests")? })
    }

    /// Step 2's checks for a body built field by field.
    pub(super) fn check_values(&self) -> Result<(), Reason> {
        uri_ok(&self.bearer_id)?;
        self.objects.iter().try_for_each(ScheduleEntry::check_values)
    }

    /// Step 4: carousel §4.2, in its order.
    pub(super) fn rules(&self) -> Result<(), Reason> {
        if self.attests != ATTESTS_SCHEDULED {
            return Err(Reason::ScheduleNotScheduled);
        }
        if self.objects.is_empty() || self.objects.windows(2).any(|w| w[0].leaf_index >= w[1].leaf_index) {
            return Err(Reason::ScheduleObjectsMalformed);
        }
        self.objects.iter().try_for_each(ScheduleEntry::check)
    }

    /// The entry for object `i`, if the schedule has one.
    pub fn entry(&self, i: u64) -> Option<&ScheduleEntry> {
        self.objects.iter().find(|e| e.leaf_index == i)
    }

    /// Carousel §4.5: whether this schedule extends `earlier`, of the same instance: for every
    /// entry of `earlier` it has an entry of that object that extends it
    /// ([`ScheduleEntry::extends`]). It may have entries for objects `earlier` has none for.
    /// Every schedule extends itself.
    pub fn extends(&self, earlier: &ScheduleClaim) -> bool {
        self.bearer_id == earlier.bearer_id
            && self.attests == earlier.attests
            && earlier.objects.iter().all(|a| self.entry(a.leaf_index).is_some_and(|b| b.extends(a)))
    }
}

impl ClaimReceipt {
    /// Step 6 (carousel §4.3): a `schedule` claim against the group receipt its subject names,
    /// the object receipts the verifier holds under it, and the envelope bytes of those it
    /// holds, each object passing jwp-receipts §3 step 5 against the group (with its bytes, the
    /// object's own) or taking no part. In order: the claim is a schedule
    /// (`context_mismatch`); the group passes §3 steps 1 and 2 and its `group_start` reads as
    /// an RFC 3339 date-time (`context_invalid`); its link hash is the subject
    /// (`schedule_subject_mismatch`); its key signed the claim (`schedule_signer_mismatch`);
    /// every entry's object is in it (`schedule_leaf_out_of_range`); no first pass starts
    /// before it does (`schedule_before_group`); and each entry's object, where held, has the
    /// entry's content hash (`schedule_content_mismatch`) and, where its bytes are held, the
    /// entry's transfer length (`schedule_length_mismatch`).
    pub fn check_schedule_against(&self, group: &GroupReceipt, objects: &[(&ObjectReceipt, Option<&[u8]>)]) -> Result<(), Rejection> {
        self.check_schedule_context(group, objects, true)
    }

    /// Step 6 for a node's schedule of its own instance (cache-authority §5.6): carousel §4.3
    /// but for its step 2, since a node's key, not the group's, signs it. Whose schedule it is,
    /// and where it stands, is `crate::cache_authority`'s; this checks it against the group as
    /// [`ClaimReceipt::check_schedule_against`] does in every other step.
    pub fn check_node_schedule_against(&self, group: &GroupReceipt, objects: &[(&ObjectReceipt, Option<&[u8]>)]) -> Result<(), Rejection> {
        self.check_schedule_context(group, objects, false)
    }

    fn check_schedule_context(&self, group: &GroupReceipt, objects: &[(&ObjectReceipt, Option<&[u8]>)], publisher: bool) -> Result<(), Rejection> {
        let ClaimBody::Schedule(s) = &self.body else {
            return Err(Rejection::at(6, Reason::ContextMismatch));
        };
        let at = |reason| Err(Rejection::at(6, reason));
        let start = match group.verify() {
            Ok(()) => rfc3339_seconds(&group.group_start),
            Err(_) => None,
        };
        let Some(start) = start else { return at(Reason::ContextInvalid) };
        if group.link_hash() != self.subject {
            return at(Reason::ScheduleSubjectMismatch);
        }
        if publisher && group.signer_pubkey != self.signer_pubkey {
            return at(Reason::ScheduleSignerMismatch);
        }
        if s.objects.iter().any(|e| e.leaf_index >= group.leaf_count) {
            return at(Reason::ScheduleLeafOutOfRange);
        }
        if s.objects.iter().any(|e| ((e.first_ms / 1000) as i64) < start) {
            return at(Reason::ScheduleBeforeGroup);
        }
        for e in &s.objects {
            for (o, bytes) in objects.iter().filter(|(o, _)| o.object_id == e.leaf_index) {
                if o.verify_inclusion(group).is_err() {
                    continue;
                }
                if o.content_hash != e.content_hash {
                    return at(Reason::ScheduleContentMismatch);
                }
                if let Some(b) = bytes
                    && o.verify_against(group, Some(b)).is_ok()
                    && b.len() as u64 != e.oti.transfer_length()
                {
                    return at(Reason::ScheduleLengthMismatch);
                }
            }
        }
        Ok(())
    }

    /// The body of a schedule claim, if this is one.
    pub fn schedule(&self) -> Option<&ScheduleClaim> {
        match &self.body {
            ClaimBody::Schedule(s) => Some(s),
            _ => None,
        }
    }
}

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

    fn rq(f: u64, t: u64, z: u64, n: u64, al: u64) -> Oti {
        Oti::RaptorQ { transfer_length: f, symbol_size: t, source_blocks: z, sub_blocks: n, alignment: al }
    }

    fn rp(f: u64, t: u64, z: u64, n: u64, al: u64) -> Oti {
        Oti::Raptor { transfer_length: f, symbol_size: t, source_blocks: z, sub_blocks: n, alignment: al }
    }

    fn nc(l: u64, e: u64, b: u64) -> Oti {
        Oti::NoCode { transfer_length: l, symbol_size: e, max_block_length: b }
    }

    #[test]
    fn partition_is_rfc_6330s() {
        assert_eq!(partition(13, 2), (7, 6, 1, 1));
        assert_eq!(partition(12, 3), (4, 4, 0, 3));
        assert_eq!(partition(1, 1), (1, 1, 0, 1));
    }

    #[test]
    fn raptorq_blocks_follow_the_partition() {
        // 15 symbols in two blocks: 8 and 7.
        assert_eq!(rq(960, 64, 2, 1, 4).blocks(), vec![BlockSize { k: 8, blocks: 1 }, BlockSize { k: 7, blocks: 1 }]);
        // 16 in two: 8 and 8, one size.
        assert_eq!(rq(1000, 64, 2, 1, 4).blocks(), vec![BlockSize { k: 8, blocks: 2 }]);
        assert_eq!(rq(1024, 64, 4, 1, 4).blocks(), vec![BlockSize { k: 4, blocks: 4 }]);
    }

    #[test]
    fn no_code_blocks_follow_rfc_5052() {
        // 10 symbols, at most 4 a block: 3 blocks of 4, 3, 3.
        assert_eq!(nc(100, 10, 4).blocks(), vec![BlockSize { k: 4, blocks: 1 }, BlockSize { k: 3, blocks: 2 }]);
        assert_eq!(nc(95, 10, 100).blocks(), vec![BlockSize { k: 10, blocks: 1 }]);
    }

    #[test]
    fn raptorq_parameters_are_held_to_rfc_6330() {
        assert!(rq(1000, 64, 2, 1, 4).admitted());
        assert!(!rq(0, 64, 1, 1, 4).admitted(), "no octets");
        assert!(!rq(RAPTORQ_MAX_TRANSFER + 1, 65_535, 255, 1, 1).admitted(), "past the greatest F");
        assert!(!rq(1000, 66, 1, 1, 4).admitted(), "T not a multiple of Al");
        assert!(!rq(1000, 64, 1, 1, 0).admitted(), "no alignment");
        assert!(!rq(1000, 64, 0, 1, 4).admitted(), "no block");
        assert!(!rq(1000, 64, 256, 1, 4).admitted(), "past Z's field");
        assert!(!rq(1000, 64, 1, 0, 4).admitted(), "no sub-block");
        assert!(!rq(1000, 64, 1, 17, 4).admitted(), "a sub-symbol under Al");
        assert!(rq(1000, 64, 1, 16, 4).admitted(), "sub-symbols of Al each");
        assert!(!rq(1000, 64, 17, 1, 4).admitted(), "an empty block");
        assert!(rq(1000, 64, 16, 1, 4).admitted(), "a block a symbol");
        assert!(rq(K_PRIME_MAX * 4, 4, 1, 1, 4).admitted(), "one block of K'max");
        assert!(!rq(K_PRIME_MAX * 4 + 1, 4, 1, 1, 4).admitted(), "one block past K'max");
    }

    #[test]
    fn raptor_blocks_follow_the_partition() {
        // 15 symbols in two blocks: 8 and 7.
        assert_eq!(rp(960, 64, 2, 1, 4).blocks(), vec![BlockSize { k: 8, blocks: 1 }, BlockSize { k: 7, blocks: 1 }]);
    }

    #[test]
    fn raptor_parameters_are_held_to_rfc_5053() {
        assert!(rp(1000, 64, 2, 1, 4).admitted());
        assert!(!rp(0, 64, 1, 1, 4).admitted(), "no octets");
        assert!(rp(65_535 * 65_535 * 8_192, 65_535, 65_535, 1, 1).admitted(), "the most blocks of the most symbols");
        assert!(!rp(65_535 * 65_535 * 8_192 + 1, 65_535, 65_535, 1, 1).admitted(), "a block past Kmax");
        assert!(!rp(1 << 45, 65_535, 65_535, 1, 1).admitted(), "past the greatest F");
        assert!(!rp(1000, 66, 1, 1, 4).admitted(), "T not a multiple of Al");
        assert!(!rp(1000, 64, 1, 1, 0).admitted(), "no alignment");
        assert!(!rp(1000, 256, 1, 1, 256).admitted(), "past Al's field");
        assert!(!rp(1000, 64, 0, 1, 4).admitted(), "no block");
        assert!(rp(1 << 20, 4, 65_535, 1, 4).admitted(), "Z at its field's greatest");
        assert!(!rp(1 << 20, 4, 65_536, 1, 4).admitted(), "past Z's field");
        assert!(!rp(1000, 64, 1, 0, 4).admitted(), "no sub-block");
        assert!(!rp(1000, 64, 1, 17, 4).admitted(), "a sub-symbol under Al");
        assert!(rp(1000, 64, 1, 16, 4).admitted(), "sub-symbols of Al each");
        assert!(rp(8_160, 1_020, 1, 255, 4).admitted(), "N at its field's greatest");
        assert!(!rp(8_192, 2_048, 1, 256, 4).admitted(), "past N's field");
        assert!(rp(256, 64, 1, 1, 4).admitted(), "a block of the fewest symbols");
        assert!(!rp(192, 64, 1, 1, 4).admitted(), "a block under the fewest symbols");
        assert!(!rp(448, 64, 2, 1, 4).admitted(), "blocks of 4 and 3: the smaller under the fewest");
        assert!(rp(8_192 * 4, 4, 1, 1, 4).admitted(), "one block of Kmax");
        assert!(!rp(8_192 * 4 + 1, 4, 1, 1, 4).admitted(), "one block past Kmax");
    }

    #[test]
    fn raptor_symbols_fit_sixteen_bits() {
        let e = |repair, esi, count| ScheduleEntry { oti: rp(1_000, 64, 1, 1, 4), repair, esi, count, ..entry(0, 1, 10_000) };
        // 16 source symbols.
        assert!(e((1 << 16) - 16, Esi::Repeat, 1).symbols_named());
        assert!(!e((1 << 16) - 15, Esi::Repeat, 1).symbols_named());
        assert!(e((1 << 12) - 16, Esi::Advance, 16).symbols_named());
        assert!(!e((1 << 12) - 15, Esi::Advance, 16).symbols_named());
    }

    #[test]
    fn no_code_parameters_are_held_to_rfc_5445() {
        assert!(nc(100, 10, 4).admitted());
        assert!(!nc(0, 10, 4).admitted(), "no octets");
        assert!(!nc(1 << 48, 10, 4).admitted(), "past the transfer length's field");
        assert!(!nc(100, 0, 4).admitted(), "no symbol");
        assert!(!nc(100, 65_536, 4).admitted(), "past the symbol length's field");
        assert!(!nc(100, 10, 0).admitted(), "no block length");
        assert!(!nc(100, 10, 1 << 32).admitted(), "past the block length's field");
        assert!(nc(NO_CODE_FIELD, 1, 1 << 16).admitted(), "one block of 65,536 symbols");
        assert!(!nc(NO_CODE_FIELD + 1, 1, 1 << 17).admitted(), "a block past the ESI field");
        assert!(nc(NO_CODE_FIELD, 1, 1).admitted(), "65,536 blocks");
        assert!(!nc(NO_CODE_FIELD + 1, 1, 1).admitted(), "a block past the SBN field");
    }

    fn entry(leaf_index: u64, count: u64, period_ms: u64) -> ScheduleEntry {
        ScheduleEntry {
            leaf_index,
            content_hash: [leaf_index as u8; 32],
            oti: rq(1000, 64, 2, 1, 4),
            repair: 4,
            esi: Esi::Advance,
            first_ms: 1_000_000,
            period_ms,
            duration_ms: 2_000,
            count,
        }
    }

    fn sched(objects: Vec<ScheduleEntry>) -> ScheduleClaim {
        ScheduleClaim { bearer_id: "urn:example:b".into(), objects, attests: ATTESTS_SCHEDULED.into() }
    }

    #[test]
    fn an_entry_extends_one_whose_passes_it_begins_with() {
        let a = entry(0, 3, 10_000);
        assert!(a.extends(&a), "itself");
        assert!(entry(0, 5, 10_000).extends(&a), "more passes");
        assert!(!a.extends(&entry(0, 5, 10_000)), "fewer passes");
        assert!(!entry(0, 5, 12_000).extends(&a), "a later pass moved");
        assert!(entry(0, 4, 12_000).extends(&entry(0, 1, 0)), "one pass: its period is not read");
        assert!(!entry(0, 4, 12_000).extends(&entry(0, 2, 0)), "two passes: the period is read");
        let moved = ScheduleEntry { first_ms: 1_000_001, ..entry(0, 5, 10_000) };
        assert!(!moved.extends(&a), "the first pass moved");
        let more_repair = ScheduleEntry { repair: 5, ..entry(0, 5, 10_000) };
        assert!(!more_repair.extends(&a), "other symbols");
        let longer = ScheduleEntry { duration_ms: 2_001, ..entry(0, 5, 10_000) };
        assert!(!longer.extends(&a), "a longer pass");
    }

    #[test]
    fn a_schedule_extends_one_whose_every_entry_it_extends() {
        let a = sched(vec![entry(0, 3, 10_000)]);
        assert!(a.extends(&a));
        assert!(sched(vec![entry(0, 3, 10_000), entry(2, 1, 0)]).extends(&a), "an object added");
        assert!(sched(vec![entry(0, 4, 10_000), entry(2, 1, 0)]).extends(&a), "an object added and a pass");
        assert!(!sched(vec![entry(2, 1, 0)]).extends(&a), "an object dropped");
        assert!(!sched(vec![entry(0, 3, 10_000), entry(2, 1, 0)]).extends(&sched(vec![entry(0, 4, 10_000)])), "a pass withdrawn");
        let other = ScheduleClaim { bearer_id: "urn:example:c".into(), ..a.clone() };
        assert!(!other.extends(&a), "another instance");
    }
}
