//! `timeliness` (jwp-receipts §2.4.17; `extensions/timeliness.md` §4, §5): a publisher's
//! statement of when each object of a group is of use — the instant it is useful from, the
//! deadline by which a receiver must hold it for its full value, how that value falls after the
//! deadline, and the renditions a receiver presents in its place, a ladder in order of falling
//! value.
//!
//! This file holds steps 2 and 4 of jwp-receipts §3.1 for the class — its body's members and
//! types, and timeliness §4.2 — an entry's value at an instant (§5.1), and step 6, its checks
//! against the group receipts, object receipts and envelopes a verifier holds (§4.3). What a
//! verifier judges and an asker plans from it is `crate::timeliness`'s.

use super::schedule::LAST_INSTANT_MS;
use super::*;
use crate::container::{DerivedFrom, Wai};
use crate::jwp_delivery::{GroupReceipt, ObjectReceipt};

/// What a timeliness claim attests, in-band (timeliness §4.2 rule 1).
pub const ATTESTS_VALUED: &str = "valued";
/// An object's full value, in parts per million (timeliness §2).
pub const FULL_PPM: u64 = 1_000_000;
/// The most points a decay has (timeliness §4.2 rule 3.2).
pub const MAX_POINTS: usize = 16;
/// The most rungs a ladder has (timeliness §4.2 rule 3.4).
pub const MAX_RUNGS: usize = 8;

/// A timeliness body's members (timeliness §4.1), in the order step 2 reads them.
const TIMELINESS_MEMBERS: &[&str] = &["objects", "attests"];
/// An entry's members, in the order step 2 reads them.
const ENTRY_MEMBERS: &[&str] = &["leaf_index", "content_hash", "from_ms", "by_ms", "decay", "fallback"];
const POINT_MEMBERS: &[&str] = &["at_ms", "value_ppm"];
const FALLBACK_MEMBERS: &[&str] = &["group", "leaf_index", "content_hash", "value_ppm"];

/// One point of a decay: the object's value at an instant after its deadline.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DecayPoint {
    pub at_ms: u64,
    pub value_ppm: u64,
}

/// One rung of a ladder: an object a receiver presents in the primary's place (timeliness §4.1
/// `fallback`).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Fallback {
    /// The link hash of its group receipt.
    pub group: [u8; 32],
    pub leaf_index: u64,
    pub content_hash: [u8; 32],
    /// The value of presenting it in the primary's place.
    pub value_ppm: u64,
}

/// An entry's fallback (timeliness §4.1): its ladder, the objects a receiver presents in the
/// primary's place, in order of falling value. Each form is kept as it was read, so a claim
/// writes back to its own bytes: one rung is the object, two or more an array, and an array of
/// one is refused (§4.2 rule 3.4), never read as the object.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub enum Ladder {
    /// `null`: no rung.
    #[default]
    None,
    /// One rung, written as the object: the form before ladders.
    One(Fallback),
    /// An array of rungs, two to eight where well formed.
    Rungs(Vec<Fallback>),
}

impl Ladder {
    /// The rungs, in the ladder's order: none, the one, or the array's.
    pub fn rungs(&self) -> &[Fallback] {
        match self {
            Ladder::None => &[],
            Ladder::One(f) => std::slice::from_ref(f),
            Ladder::Rungs(v) => v,
        }
    }

    /// Whether the fallback is written as an array.
    pub fn is_array(&self) -> bool {
        matches!(self, Ladder::Rungs(_))
    }

    /// Rule 3.4: an array has 2 to 8 rungs, their values strictly falling, and no two name one
    /// content hash or one position.
    fn formed(&self) -> bool {
        let Ladder::Rungs(v) = self else { return true };
        (2..=MAX_RUNGS).contains(&v.len())
            && v.windows(2).all(|w| w[1].value_ppm < w[0].value_ppm)
            && v.iter().enumerate().all(|(i, a)| {
                v[i + 1..].iter().all(|b| a.content_hash != b.content_hash && (a.group, a.leaf_index) != (b.group, b.leaf_index))
            })
    }
}

impl From<Option<Fallback>> for Ladder {
    /// One fallback or none: the form before ladders.
    fn from(f: Option<Fallback>) -> Ladder {
        f.map_or(Ladder::None, Ladder::One)
    }
}

impl Fallback {
    fn to_value(self) -> Value {
        let mut q = Map::new();
        q.insert("group".into(), hex(&self.group).into());
        q.insert("leaf_index".into(), self.leaf_index.into());
        q.insert("content_hash".into(), hex(&self.content_hash).into());
        q.insert("value_ppm".into(), self.value_ppm.into());
        Value::Object(q)
    }

    /// Step 2 for one rung: an object, its members, then their types in the table's order.
    fn read(v: &Value) -> Result<Fallback, Reason> {
        let f = v.as_object().ok_or(Reason::FieldType)?;
        members(f, FALLBACK_MEMBERS)?;
        Ok(Fallback {
            group: get_hash(f, "group")?,
            leaf_index: get_int(f, "leaf_index")?,
            content_hash: get_hash(f, "content_hash")?,
            value_ppm: get_int(f, "value_ppm")?,
        })
    }
}

/// One object's statement (timeliness §4.1).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TimelinessEntry {
    pub leaf_index: u64,
    pub content_hash: [u8; 32],
    /// The instant from which the object is of use.
    pub from_ms: u64,
    /// The last instant at which a receiver that first holds the object has it at full value.
    pub by_ms: u64,
    pub decay: Vec<DecayPoint>,
    /// The ladder: the objects presented in the primary's place, in order of falling value.
    pub fallback: Ladder,
}

impl TimelinessEntry {
    /// Timeliness §5.2: the `at_ms` of the last point, from which an object first held is of no
    /// value. For an entry with no point, its deadline's next instant.
    pub fn end_ms(&self) -> u64 {
        self.decay.last().map_or(self.by_ms.saturating_add(1), |p| p.at_ms)
    }

    /// Timeliness §5.1: the value at instant `h`, in parts per million, the line between the
    /// two points about `h` floored, with (`by_ms`, 1,000,000) before the first point. The
    /// products are taken in 128 bits: they reach 2^68.
    pub fn value_at(&self, h: u64) -> u64 {
        if h <= self.by_ms {
            return FULL_PPM;
        }
        let (mut ta, mut va) = (self.by_ms, FULL_PPM);
        for p in &self.decay {
            if h <= p.at_ms {
                // ta < h ≤ tb, so tb − ta is at least 1.
                let (tb, vb) = (p.at_ms, p.value_ppm);
                let num = u128::from(va) * u128::from(tb - h) + u128::from(vb) * u128::from(h - ta);
                return (num / u128::from(tb - ta)) as u64;
            }
            (ta, va) = (p.at_ms, p.value_ppm);
        }
        0
    }

    /// Timeliness §4.2 rule 3 for this entry, in its order, `subject` being the claim's.
    pub fn check(&self, subject: &[u8; 32]) -> Result<(), Reason> {
        if self.from_ms > self.by_ms || self.by_ms >= LAST_INSTANT_MS {
            return Err(Reason::TimelinessWindowInvalid);
        }
        if !self.decay_formed() {
            return Err(Reason::TimelinessDecayInvalid);
        }
        if self.fallback.rungs().iter().any(|f| {
            !(1..=FULL_PPM).contains(&f.value_ppm) || f.content_hash == self.content_hash || (f.group == *subject && f.leaf_index == self.leaf_index)
        }) {
            return Err(Reason::TimelinessFallbackInvalid);
        }
        if !self.fallback.formed() {
            return Err(Reason::TimelinessLadderInvalid);
        }
        Ok(())
    }

    /// Rule 3.2: 1 to 16 points, their instants strictly increasing from after `by_ms` to at
    /// most the last instant, their values below full, none above the one before it, the last 0
    /// and every other at least 1, and no point but the last on the line through its
    /// neighbours, with (`by_ms`, 1,000,000) before the first.
    fn decay_formed(&self) -> bool {
        let d = &self.decay;
        let (Some(first), Some(last)) = (d.first(), d.last()) else { return false };
        if d.len() > MAX_POINTS
            || first.at_ms <= self.by_ms
            || last.at_ms > LAST_INSTANT_MS
            || d.windows(2).any(|w| w[0].at_ms >= w[1].at_ms)
            || d.iter().any(|p| p.value_ppm >= FULL_PPM)
            || d.windows(2).any(|w| w[1].value_ppm > w[0].value_ppm)
            || last.value_ppm != 0
            || d[..d.len() - 1].iter().any(|p| p.value_ppm == 0)
        {
            return false;
        }
        let points: Vec<(u64, u64)> =
            std::iter::once((self.by_ms, FULL_PPM)).chain(d.iter().map(|p| (p.at_ms, p.value_ppm))).collect();
        // Times strictly increase and values do not, so every difference is non-negative.
        points.windows(3).all(|w| {
            let ((t0, v0), (t1, v1), (t2, v2)) = (w[0], w[1], w[2]);
            u128::from(v0 - v1) * u128::from(t2 - t1) != u128::from(v1 - v2) * u128::from(t1 - t0)
        })
    }

    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("from_ms".into(), self.from_ms.into());
        m.insert("by_ms".into(), self.by_ms.into());
        let points = self.decay.iter().map(|p| {
            let mut q = Map::new();
            q.insert("at_ms".into(), p.at_ms.into());
            q.insert("value_ppm".into(), p.value_ppm.into());
            Value::Object(q)
        });
        m.insert("decay".into(), Value::Array(points.collect()));
        let fallback = match &self.fallback {
            Ladder::None => Value::Null,
            Ladder::One(f) => f.to_value(),
            Ladder::Rungs(v) => Value::Array(v.iter().map(|f| f.to_value()).collect()),
        };
        m.insert("fallback".into(), fallback);
        Value::Object(m)
    }

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

    /// One entry as a reader holds it apart from its claim: step 2's reading of it, then §4.2
    /// rule 3 with `subject` the claim's.
    pub fn from_value(v: &Value, subject: &[u8; 32]) -> Result<TimelinessEntry, Reason> {
        let e = TimelinessEntry::read(v)?;
        e.check_values()?;
        e.check(subject)?;
        Ok(e)
    }

    /// Step 2: the members, then each one's type in the table's order, each point's and each
    /// rung's members and types where the table reaches them.
    fn read(v: &Value) -> Result<TimelinessEntry, 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 from_ms = get_int(m, "from_ms")?;
        let by_ms = get_int(m, "by_ms")?;
        let decay = m["decay"]
            .as_array()
            .ok_or(Reason::FieldType)?
            .iter()
            .map(|p| {
                let p = p.as_object().ok_or(Reason::FieldType)?;
                members(p, POINT_MEMBERS)?;
                Ok(DecayPoint { at_ms: get_int(p, "at_ms")?, value_ppm: get_int(p, "value_ppm")? })
            })
            .collect::<Result<_, Reason>>()?;
        let fallback = match &m["fallback"] {
            Value::Null => Ladder::None,
            f @ Value::Object(_) => Ladder::One(Fallback::read(f)?),
            Value::Array(v) => Ladder::Rungs(v.iter().map(Fallback::read).collect::<Result<_, _>>()?),
            _ => return Err(Reason::FieldType),
        };
        Ok(TimelinessEntry { leaf_index, content_hash, from_ms, by_ms, decay, fallback })
    }

    fn check_values(&self) -> Result<(), Reason> {
        for n in [self.leaf_index, self.from_ms, self.by_ms] {
            int_ok(n)?;
        }
        for p in &self.decay {
            int_ok(p.at_ms)?;
            int_ok(p.value_ppm)?;
        }
        for f in self.fallback.rungs() {
            int_ok(f.leaf_index)?;
            int_ok(f.value_ppm)?;
        }
        Ok(())
    }
}

/// `timeliness` (timeliness §4): the publisher's statement of when each object it names, of the
/// group its subject names, is of use.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct TimelinessClaim {
    /// One entry for each object stated, in increasing `leaf_index`.
    pub objects: Vec<TimelinessEntry>,
    /// MUST be `"valued"`.
    pub attests: String,
}

impl TimelinessClaim {
    pub(super) fn to_value(&self) -> Value {
        let mut m = Map::new();
        m.insert("objects".into(), Value::Array(self.objects.iter().map(TimelinessEntry::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<TimelinessClaim, Reason> {
        members(m, TIMELINESS_MEMBERS)?;
        let objects =
            m["objects"].as_array().ok_or(Reason::FieldType)?.iter().map(TimelinessEntry::read).collect::<Result<_, _>>()?;
        Ok(TimelinessClaim { 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> {
        self.objects.iter().try_for_each(TimelinessEntry::check_values)
    }

    /// Step 4: timeliness §4.2, in its order, with the claim's `subject` for rule 3.3.
    pub(super) fn rules(&self, subject: &[u8; 32]) -> Result<(), Reason> {
        if self.attests != ATTESTS_VALUED {
            return Err(Reason::TimelinessNotValued);
        }
        if self.objects.is_empty() || self.objects.windows(2).any(|w| w[0].leaf_index >= w[1].leaf_index) {
            return Err(Reason::TimelinessObjectsMalformed);
        }
        self.objects.iter().try_for_each(|e| e.check(subject))
    }

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

/// A group receipt a verifier holds, with the object receipts it holds under it and the
/// envelope bytes of those whose bytes it holds (timeliness §4.3).
#[derive(Clone, Debug)]
pub struct HeldGroup<'a> {
    pub group: &'a GroupReceipt,
    pub objects: Vec<(&'a ObjectReceipt, Option<&'a [u8]>)>,
}

impl ClaimReceipt {
    /// Step 6 (timeliness §4.3): a `timeliness` claim against the group receipt its subject
    /// names and what else the verifier holds: the groups in `held` that pass jwp-receipts §3
    /// steps 1 and 2, each with the object receipts held under it that pass §3 step 5, and every
    /// envelope held, known by its content hash. In order: the claim is a statement of
    /// timeliness (`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
    /// (`timeliness_subject_mismatch`); its key signed the claim (`timeliness_signer_mismatch`);
    /// every entry's object is in it (`timeliness_leaf_out_of_range`); none is of use before
    /// it begins (`timeliness_before_group`); each entry's object receipt, where held, has the
    /// entry's content hash (`timeliness_content_mismatch`); no envelope held of an entry or a
    /// rung is a layer (`timeliness_on_layer`); each rung is where it says, where its group and
    /// object receipts are held (`timeliness_fallback_mismatch`); and each rung's envelope held
    /// derives from its primary (`timeliness_fallback_not_derived`).
    pub fn check_timeliness_against(&self, group: &GroupReceipt, held: &[HeldGroup<'_>]) -> Result<(), Rejection> {
        let ClaimBody::Timeliness(t) = &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::TimelinessSubjectMismatch);
        }
        if group.signer_pubkey != self.signer_pubkey {
            return at(Reason::TimelinessSignerMismatch);
        }
        if t.objects.iter().any(|e| e.leaf_index >= group.leaf_count) {
            return at(Reason::TimelinessLeafOutOfRange);
        }
        if t.objects.iter().any(|e| ((e.from_ms / 1000) as i64) < start) {
            return at(Reason::TimelinessBeforeGroup);
        }
        // The object receipt held at position `i` of the group `g` whose link hash is `link`,
        // each passing §3 step 5 against it.
        let receipts = |link: &[u8; 32], g: &GroupReceipt, i: u64| -> Vec<[u8; 32]> {
            held.iter()
                .filter(|h| h.group.link_hash() == *link)
                .flat_map(|h| h.objects.iter().map(|(o, _)| *o))
                .filter(|o| o.object_id == i && o.verify_inclusion(g).is_ok())
                .map(|o| o.content_hash)
                .collect()
        };
        let link = group.link_hash();
        if t.objects.iter().any(|e| receipts(&link, group, e.leaf_index).iter().any(|h| *h != e.content_hash)) {
            return at(Reason::TimelinessContentMismatch);
        }
        // Every envelope held, by its content hash.
        let envelopes: Vec<([u8; 32], &[u8])> = held
            .iter()
            .flat_map(|h| h.objects.iter().filter_map(|(_, b)| *b))
            .map(|b| (crate::jwp_hash::content_hash(b), b))
            .collect();
        let bytes_of = |hash: &[u8; 32]| -> Vec<&[u8]> { envelopes.iter().filter(|(h, _)| h == hash).map(|(_, b)| *b).collect() };
        let is_layer = |b: &&[u8]| Wai::from_bytes(b).is_ok_and(|w| crate::codecs::is_refinement_only(w.capability()));
        for e in &t.objects {
            if bytes_of(&e.content_hash).iter().any(is_layer) || e.fallback.rungs().iter().any(|f| bytes_of(&f.content_hash).iter().any(is_layer)) {
                return at(Reason::TimelinessOnLayer);
            }
        }
        for f in t.objects.iter().flat_map(|e| e.fallback.rungs()) {
            let Some(g) = held.iter().map(|h| h.group).find(|g| g.link_hash() == f.group && g.verify().is_ok()) else { continue };
            if f.leaf_index >= g.leaf_count || receipts(&f.group, g, f.leaf_index).iter().any(|h| *h != f.content_hash) {
                return at(Reason::TimelinessFallbackMismatch);
            }
        }
        for e in &t.objects {
            let motif = hex(&e.content_hash);
            let derived = |b: &&[u8]| {
                Wai::from_bytes(b).ok().and_then(|w| w.manifest.derived_from.as_ref().and_then(|v| DerivedFrom::from_value(v).ok())).is_some_and(|d| d.motif == motif)
            };
            if e.fallback.rungs().iter().any(|f| bytes_of(&f.content_hash).iter().any(|b| !derived(b))) {
                return at(Reason::TimelinessFallbackNotDerived);
            }
        }
        Ok(())
    }

    /// The body of a timeliness claim, if this is one.
    pub fn timeliness(&self) -> Option<&TimelinessClaim> {
        match &self.body {
            ClaimBody::Timeliness(t) => Some(t),
            _ => None,
        }
    }
}

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

    const D: u64 = 1_793_664_060_000;

    fn entry(by_ms: u64, decay: &[(u64, u64)]) -> TimelinessEntry {
        TimelinessEntry {
            leaf_index: 0,
            content_hash: [1; 32],
            from_ms: by_ms.min(1_000),
            by_ms,
            decay: decay.iter().map(|&(at_ms, value_ppm)| DecayPoint { at_ms, value_ppm }).collect(),
            fallback: Ladder::None,
        }
    }

    /// §5.1 with exact rationals: ⌊(va·(tb − h) + vb·(h − ta)) / (tb − ta)⌋ by repeated
    /// subtraction of whole parts, not by the 128-bit product the reference takes.
    fn reference(e: &TimelinessEntry, h: u64) -> u64 {
        if h <= e.by_ms {
            return FULL_PPM;
        }
        let mut points = vec![(e.by_ms, FULL_PPM)];
        points.extend(e.decay.iter().map(|p| (p.at_ms, p.value_ppm)));
        for w in points.windows(2) {
            let ((ta, va), (tb, vb)) = (w[0], w[1]);
            if ta < h && h <= tb {
                // va − (va − vb)·(h − ta)/(tb − ta), the fraction's ceiling taken off.
                let (fall, span, gone) = (va - vb, tb - ta, h - ta);
                let whole = fall / span * gone + (fall % span) * (gone / span);
                let rest = (fall % span) as u128 * (gone % span) as u128;
                let lost = u128::from(whole) + rest.div_ceil(u128::from(span));
                return (u128::from(va) - lost) as u64;
            }
        }
        0
    }

    #[test]
    fn the_value_is_the_stated_line_floored() {
        let e = entry(D, &[(D + 2_000, 600_000), (D + 10_000, 0)]);
        for (h, v) in [(D, 1_000_000), (D + 1, 999_800), (D + 1_000, 800_000), (D + 2_000, 600_000), (D + 6_000, 300_000), (D + 9_999, 75), (D + 10_000, 0), (D + 10_001, 0)] {
            assert_eq!(e.value_at(h), v, "at D + {}", h - D);
        }
        assert_eq!(e.end_ms(), D + 10_000);
        let hard = entry(D, &[(D + 1, 0)]);
        assert_eq!((hard.value_at(D), hard.value_at(D + 1), hard.end_ms()), (1_000_000, 0, D + 1));
        let wide = entry(1, &[(LAST_INSTANT_MS, 0)]);
        assert_eq!(wide.value_at(LAST_INSTANT_MS / 2), 500_000, "products past 2^64");
        assert_eq!(wide.value_at(2), 999_999);
    }

    #[test]
    fn the_value_agrees_with_an_exact_reference_and_never_increases() {
        let entries = [
            entry(D, &[(D + 2_000, 600_000), (D + 10_000, 0)]),
            entry(D, &[(D + 1, 500_000), (D + 3_001, 0)]),
            entry(D, &[(D + 3, 2), (D + 13, 0)]),
            entry(D, &[(D + 1_000, 700_000), (D + 5_000, 700_000), (D + 6_000, 0)]),
            entry(1, &[(LAST_INSTANT_MS, 0)]),
            entry(1, &[(7, 999_999), (LAST_INSTANT_MS - 1, 1), (LAST_INSTANT_MS, 0)]),
        ];
        // A fixed linear congruential sequence of instants: no randomness in a test.
        let mut x: u64 = 0x2545_f491_4f6c_dd1d;
        for e in &entries {
            let mut previous = FULL_PPM;
            let span = e.end_ms() + 2 - e.by_ms;
            let mut hs: Vec<u64> = (0..2_000)
                .map(|_| {
                    x = x.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407);
                    e.by_ms - 1 + (x >> 11) % span
                })
                .collect();
            hs.extend(e.decay.iter().flat_map(|p| [p.at_ms - 1, p.at_ms, p.at_ms + 1]));
            hs.sort_unstable();
            for h in hs {
                let v = e.value_at(h);
                assert_eq!(v, reference(e, h), "at {h}");
                assert!(v <= previous, "rises at {h}");
                previous = v;
            }
        }
    }

    #[test]
    fn collinearity_is_exact() {
        let subject = [9; 32];
        let ok = |d: &[(u64, u64)]| entry(D, d).check(&subject);
        assert_eq!(ok(&[(D + 1_000, 500_000), (D + 2_000, 0)]), Err(Reason::TimelinessDecayInvalid), "on one line");
        assert_eq!(ok(&[(D + 1_000, 500_001), (D + 2_000, 0)]), Ok(()), "a part per million off it");
        assert_eq!(ok(&[(D + 1_000, 499_999), (D + 2_000, 0)]), Ok(()));
        assert_eq!(ok(&[(D + 1_000, 700_000), (D + 5_000, 700_000), (D + 6_000, 0)]), Ok(()), "a plateau");
        assert_eq!(ok(&[(D + 1_000, 700_000), (D + 3_000, 700_000), (D + 5_000, 700_000), (D + 6_000, 0)]), Err(Reason::TimelinessDecayInvalid));
        // Products past 2^64: half the value over 10^14 ms twice is one line, and a part per
        // million off it is not; a fall of 999,999 over nearly the widest span is no line.
        let s = 100_000_000_000_000;
        assert_eq!(entry(1, &[(1 + s, 500_000), (1 + 2 * s, 0)]).check(&subject), Err(Reason::TimelinessDecayInvalid));
        assert_eq!(entry(1, &[(1 + s, 500_001), (1 + 2 * s, 0)]).check(&subject), Ok(()));
        assert_eq!(entry(1, &[(LAST_INSTANT_MS - 1, 999_999), (LAST_INSTANT_MS, 0)]).check(&subject), Ok(()));
    }

    #[test]
    fn each_rule_refuses() {
        let subject = [9; 32];
        let base = entry(D, &[(D + 1, 0)]);
        let with = |f: &dyn Fn(&mut TimelinessEntry)| {
            let mut e = base.clone();
            f(&mut e);
            e.check(&subject)
        };
        assert_eq!(with(&|_| ()), Ok(()));
        assert_eq!(with(&|e| e.from_ms = D + 1), Err(Reason::TimelinessWindowInvalid));
        assert_eq!(with(&|e| e.by_ms = LAST_INSTANT_MS), Err(Reason::TimelinessWindowInvalid));
        assert_eq!(with(&|e| e.decay.clear()), Err(Reason::TimelinessDecayInvalid));
        assert_eq!(with(&|e| e.decay[0].at_ms = D), Err(Reason::TimelinessDecayInvalid));
        assert_eq!(with(&|e| e.decay[0].value_ppm = 1), Err(Reason::TimelinessDecayInvalid));
        assert_eq!(with(&|e| e.decay[0].at_ms = LAST_INSTANT_MS + 1), Err(Reason::TimelinessDecayInvalid));
        let fallback = Fallback { group: [7; 32], leaf_index: 0, content_hash: [2; 32], value_ppm: 1 };
        let one = |f: Fallback| Ladder::One(f);
        assert_eq!(with(&|e| e.fallback = one(fallback)), Ok(()));
        assert_eq!(with(&|e| e.fallback = one(Fallback { value_ppm: 0, ..fallback })), Err(Reason::TimelinessFallbackInvalid));
        assert_eq!(with(&|e| e.fallback = one(Fallback { value_ppm: FULL_PPM + 1, ..fallback })), Err(Reason::TimelinessFallbackInvalid));
        assert_eq!(with(&|e| e.fallback = one(Fallback { content_hash: [1; 32], ..fallback })), Err(Reason::TimelinessFallbackInvalid));
        assert_eq!(with(&|e| e.fallback = one(Fallback { group: subject, ..fallback })), Err(Reason::TimelinessFallbackInvalid));
        assert_eq!(with(&|e| e.fallback = one(Fallback { group: subject, leaf_index: 1, ..fallback })), Ok(()));
    }

    #[test]
    fn a_ladder_is_two_to_eight_rungs_falling_each_named_once() {
        let subject = [9; 32];
        let check = |rungs: Vec<Fallback>| {
            let mut e = entry(D, &[(D + 1, 0)]);
            e.fallback = Ladder::Rungs(rungs);
            e.check(&subject)
        };
        // Rung k: group [7], position k, content hash [10 + k], at value v.
        let rung = |k: u8, v: u64| Fallback { group: [7; 32], leaf_index: u64::from(k), content_hash: [10 + k; 32], value_ppm: v };
        let falling = |n: u8| (0..n).map(|k| rung(k, 900_000 - 100_000 * u64::from(k))).collect::<Vec<_>>();
        assert_eq!(check(falling(2)), Ok(()));
        assert_eq!(check(falling(8)), Ok(()));
        assert_eq!(check(vec![rung(0, 2), rung(1, 1)]), Ok(()), "a part per million apart");
        assert_eq!(check(vec![rung(0, FULL_PPM), rung(1, 1)]), Ok(()), "the first at full value");
        assert_eq!(check(falling(9)), Err(Reason::TimelinessLadderInvalid));
        assert_eq!(check(falling(1)), Err(Reason::TimelinessLadderInvalid), "an array of one: its form is the object");
        assert_eq!(check(vec![]), Err(Reason::TimelinessLadderInvalid));
        assert_eq!(check(vec![rung(0, 400_000), rung(1, 400_000)]), Err(Reason::TimelinessLadderInvalid), "equal");
        assert_eq!(check(vec![rung(0, 400_000), rung(1, 700_000)]), Err(Reason::TimelinessLadderInvalid), "rising");
        assert_eq!(check(vec![rung(0, 700_000), Fallback { leaf_index: 1, ..rung(0, 400_000) }]), Err(Reason::TimelinessLadderInvalid), "one content hash");
        assert_eq!(check(vec![rung(0, 700_000), Fallback { content_hash: [99; 32], ..rung(0, 400_000) }]), Err(Reason::TimelinessLadderInvalid), "one position");
        // Rule 3.3 is each rung's, before rule 3.4.
        assert_eq!(check(vec![rung(0, 700_000), rung(1, 0)]), Err(Reason::TimelinessFallbackInvalid), "a rung at 0");
        assert_eq!(check(vec![rung(0, 0)]), Err(Reason::TimelinessFallbackInvalid), "rule 3.3 before 3.4");
        assert_eq!(check(vec![rung(0, 700_000), Fallback { content_hash: [1; 32], ..rung(1, 400_000) }]), Err(Reason::TimelinessFallbackInvalid), "a rung that is the primary");
    }

    #[test]
    fn each_form_writes_back_to_itself() {
        let rung = Fallback { group: [7; 32], leaf_index: 0, content_hash: [2; 32], value_ppm: 400_000 };
        for ladder in [Ladder::None, Ladder::One(rung), Ladder::Rungs(vec![rung]), Ladder::Rungs(vec![Fallback { value_ppm: 700_000, content_hash: [3; 32], leaf_index: 1, ..rung }, rung])] {
            let mut e = entry(D, &[(D + 1, 0)]);
            e.fallback = ladder;
            let read = TimelinessEntry::read(&e.to_json()).expect("reads");
            assert_eq!(read, e, "the form is kept");
        }
        let mut v = entry(D, &[(D + 1, 0)]).to_json();
        v["fallback"] = serde_json::json!([5]);
        assert_eq!(TimelinessEntry::read(&v), Err(Reason::FieldType), "a rung that is not an object");
        v["fallback"] = serde_json::json!([{"group": hex(&[7; 32]), "leaf_index": 0, "content_hash": hex(&[2; 32])}]);
        assert_eq!(TimelinessEntry::read(&v), Err(Reason::MissingRequiredField));
    }
}
