//! The integer video encoder: RGB8 frames to a `WIV1` keyframe-0 payload
//! (`wai.video.int_motion`, `extensions/integer-payloads.md` §3), whose decode is
//! [`crate::int_video`]'s. It is the one encoder of the format in this crate:
//! `int_payload::encode_wiv1_zero_motion` is its zero-motion case.
//!
//! All integer, so a clip, its parameters and this module's revision fix the payload's
//! every byte:
//!
//! - **Motion search** (frames 1 on): blocks in raster order; for each, every vector
//!   `(dy, dx)` with both components in `−R..=R`, `R = search · mv_precision`, `dy`
//!   outer and `dx` inner, each ascending; its cost is the sum, over the block's pixels
//!   inside the frame and the three channels, of `|source − prediction|`, the
//!   prediction being the decoder's ([`crate::int_video::predict`]) from the previous
//!   frame **as decoded**. The least cost wins; a tie goes to the smaller
//!   `|dy| + |dx|`, then to the earlier vector.
//! - **Residual**: for each frame, each pixel in raster order, each of R, G, B, the
//!   prediction (128 for frame 0) is subtracted from the source and quantized at step
//!   `q`, rounding halves away from zero ([`quantize`]); the frame as decoded is
//!   `clamp(prediction + r·q, 0, 255)`, and the next frame predicts from it, so the
//!   encoder and every decoder hold the same reference (a closed loop).
//! - **Table**: one residual table for the clip ([`residual_table`]): the histogram of
//!   the residuals from the least, at most 4,095 symbols wide, through
//!   `int_entropy::hist_to_cdf` at precision 16; a residual past it would be coded
//!   through the bypass escape (`extensions/integer-payloads.md` §2), and none is.
//!
//! It runs in two passes, so it holds no residual and no symbol per sample: the first
//! finds the vectors and builds the reconstruction, its SSE and the residuals' counts;
//! the second derives each residual again and codes it, last first, into
//! `rans::RansReverseEncoder`. [`working_bytes`] states what it holds.
//!
//! A clip of one frame is written with `block` 1 and `mv_precision` 1, as the format
//! requires. The caps (a frame, the clip, the frame count, the motion-vector field) are
//! checked before anything is allocated.

use crate::int_payload::{wire, IntPayloadError, Refusal};
use crate::int_video::predict;

/// The most `search · mv_precision` this encoder searches: a vector component's range.
///
/// cbindgen:ignore
pub const MAX_SEARCH_RANGE: u32 = 127;

/// What the encoder is asked for.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EncParams {
    /// The block edge in pixels, at least 1.
    pub block: u32,
    /// The search radius in whole pixels; 0 searches only the zero vector.
    pub search: u32,
    /// 1, whole-pel vectors; 2, half-pel.
    pub mv_precision: u8,
    /// The quantizer step, `1..=65535`.
    pub q: u32,
}

/// An encoded clip.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Encoded {
    /// The `WIV1` payload.
    pub wiv1: Vec<u8>,
    /// Each frame as every decoder reconstructs it.
    pub recon: Vec<Vec<u8>>,
    /// `Σ (source − recon)²` over every sample of every frame.
    pub sse: u64,
}

/// `r / q` rounded to the nearest integer, halves away from zero: `sign(r)·⌊(2|r| +
/// q) / 2q⌋`, for `q ≥ 1` (the rounding of `staged::predict::rdiv`).
pub fn quantize(r: i64, q: u32) -> i64 {
    let (m, d) = (r.unsigned_abs(), q.max(1) as u64);
    let (n, rem) = (m / d, m % d);
    let mag = (n + (rem >= d - rem) as u64) as i64;
    if r < 0 { -mag } else { mag }
}

/// The largest residual magnitude: a source and a prediction are both in 0..=255 and
/// `q ≥ 1`, so every residual is in `−255..=255`.
///
/// cbindgen:ignore
pub const RESIDUAL_MAX: i64 = 255;

/// The count of each residual `−255..=255`, at index `r + 255`.
pub type ResidualCounts = [u64; 2 * RESIDUAL_MAX as usize + 1];

/// The clip's residual table and offset from its residual counts: the histogram of the
/// residuals from the least (`offset`) to the greatest, at most 4,095 symbols wide,
/// fitted by `int_entropy::hist_to_cdf` at precision 16. A residual past its span would
/// be coded through the bypass escape; the residuals' span, at most 511, never is.
pub fn residual_table(counts: &ResidualCounts) -> Result<(Vec<i32>, i32), IntPayloadError> {
    let field = |why: &str| IntPayloadError::new(Refusal::FieldRange, why.to_string());
    let lo = counts.iter().position(|&c| c != 0).ok_or_else(|| field("no residual to fit a table to"))?;
    let hi = counts.iter().rposition(|&c| c != 0).unwrap_or(lo);
    let width = (hi - lo + 1).min(4095);
    let cdf = crate::int_entropy::hist_to_cdf(&counts[lo..lo + width], 16).ok_or_else(|| field("no residual table fits"))?;
    Ok((cdf, lo as i32 - RESIDUAL_MAX as i32))
}

/// The widest residual table: one symbol for each residual `−255..=255`.
const TABLE_SYMBOLS: u64 = 2 * RESIDUAL_MAX as u64 + 1;

/// What fitting the widest table holds at once (`int_entropy::hist_to_cdf`): a 16-byte
/// frequency for each symbol and the escape, an 8-byte index for each symbol and as much
/// again for the stable sort's scratch, and the table, 4 bytes for each of its
/// `symbols + 2` entries.
const TABLE_FIT_BYTES: u64 = 16 * (TABLE_SYMBOLS + 1) + 2 * 8 * TABLE_SYMBOLS + 4 * (TABLE_SYMBOLS + 2);

/// The widest table as `encode` keeps it, as the payload carries it and as the payload's
/// check parses it, and the payload's fixed header with its stream length.
const TABLE_HELD_BYTES: u64 = 3 * 4 * (TABLE_SYMBOLS + 2) + 64;

/// The most bytes [`encode`] holds at once beyond the frames it is given, for a clip of
/// `n_frames` frames of `h × w` at block `block` (any, for one frame). The decoder's
/// counterpart is `int_payload::DeclaredCost`.
///
/// It holds the reconstruction it returns and its frame list (`n·h·w·3` and 24 bytes a
/// frame), the motion vectors (4 bytes each), and the stream: its words as emitted, in a
/// vector that may hold twice their length, then the stream and the payload that copies
/// it, the payload's own copy of the vectors and the check's parse of them. The stream
/// is at most 2 bytes a sample and 16 more: every residual is a table symbol (no
/// escape), and a symbol's frequency is at least 1 in 2^16. The fixed term is the widest
/// table's: its fit (`TABLE_FIT_BYTES`) and the copies held of it
/// (`TABLE_HELD_BYTES`).
///
/// A clip past the format's caps (a frame of more than `2^26` samples, more than `2^20`
/// frames, a clip of more than `2^30` bytes, or a motion-vector field of more than `2^26`
/// elements) is one `encode` refuses before it allocates anything sized from it; for one,
/// this is `u64::MAX`. Every other figure is formed in `u128` and saturates at
/// `u64::MAX`, so no input wraps.
pub fn working_bytes(n_frames: u64, h: u32, w: u32, block: u32) -> u64 {
    let (n, h, w, b) = (n_frames as u128, h as u128, w as u128, block.max(1) as u128);
    let frame = h * w * 3;
    let clip = n * frame;
    let n_mv = if n <= 1 { 0 } else { h.div_ceil(b) * w.div_ceil(b) * (n - 1) };
    if frame > wire::MAX_TENSOR as u128 || n > wire::MAX_FRAMES as u128 || clip > wire::MAX_CLIP as u128 || 2 * n_mv > wire::MAX_TENSOR as u128 {
        return u64::MAX;
    }
    let stream = 2 * clip + 16;
    let total = clip + 24 * n + 12 * n_mv + 3 * stream + (TABLE_FIT_BYTES + TABLE_HELD_BYTES) as u128;
    u64::try_from(total).unwrap_or(u64::MAX)
}

/// The vector of one block: the least-cost candidate under the search order and
/// tie-break of the module documentation.
#[allow(clippy::too_many_arguments)]
fn search_block(src: &[u8], rf: &[u8], h: usize, w: usize, y0: usize, x0: usize, block: usize, range: i64, prec: i64) -> (i16, i16) {
    let (y1, x1) = ((y0 + block).min(h), (x0 + block).min(w));
    let mut best: (u64, i64, i64, i64) = (u64::MAX, 0, 0, 0); // cost, |dy|+|dx|, dy, dx
    for dy in -range..=range {
        for dx in -range..=range {
            let mut cost = 0u64;
            'px: for y in y0..y1 {
                for x in x0..x1 {
                    for c in 0..3 {
                        let p = predict(rf, y, x, dy, dx, c, prec, h, w);
                        cost += (src[(y * w + x) * 3 + c] as i64 - p).unsigned_abs();
                    }
                    // A candidate already past the best cannot win; stopping early
                    // changes no outcome.
                    if cost > best.0 {
                        break 'px;
                    }
                }
            }
            let l1 = dy.abs() + dx.abs();
            if cost < best.0 || (cost == best.0 && l1 < best.1) {
                best = (cost, l1, dy, dx);
            }
        }
    }
    (best.2 as i16, best.3 as i16)
}

/// Where the motion search of [`encode_with`] runs: something that gives, for one frame,
/// the vector of every block in raster order, each the least-cost candidate under the
/// search order and tie-break of the module documentation. The search is a pure
/// function of the frame, the reference and the parameters, so every implementation
/// that keeps that rule gives the same vectors, and so the same payload; [`CpuSearch`]
/// is the reference. An implementation that cannot search a frame (a device lost, a
/// size it does not take) searches it with [`CpuSearch`] rather than fail, so a payload
/// never depends on where its search ran.
pub trait MotionSearch {
    /// Append to `out` the vectors of the `⌈h / block⌉ · ⌈w / block⌉` blocks of `src`
    /// (`h × w` RGB8), predicted from `rf` (the previous frame as decoded), with vector
    /// components in `−range..=range` in units of `1 / prec` pel.
    #[allow(clippy::too_many_arguments)]
    fn frame(&mut self, src: &[u8], rf: &[u8], h: usize, w: usize, block: usize, range: i64, prec: i64, out: &mut Vec<(i16, i16)>);
}

/// The reference motion search, on the CPU: [`search_block`] for each block, in raster
/// order.
#[derive(Debug, Clone, Copy, Default)]
pub struct CpuSearch;

impl MotionSearch for CpuSearch {
    fn frame(&mut self, src: &[u8], rf: &[u8], h: usize, w: usize, block: usize, range: i64, prec: i64, out: &mut Vec<(i16, i16)>) {
        for by in 0..h.div_ceil(block) {
            for bx in 0..w.div_ceil(block) {
                out.push(search_block(src, rf, h, w, by * block, bx * block, block, range, prec));
            }
        }
    }
}

/// Encode `frames`, each `h × w` RGB8 (rows top to bottom), as a `WIV1` keyframe-0
/// payload under `p`. Refuses `field_range` for a parameter outside its range (`q`
/// outside `1..=65535`, `block` 0, `mv_precision` other than 1 or 2, `search ·
/// mv_precision` past [`MAX_SEARCH_RANGE`]), an empty clip, a zero dimension or a frame
/// that is not `h·w·3` bytes; `too_large` for a clip past the format's caps, before
/// anything is allocated. The motion search runs on the CPU ([`CpuSearch`]).
pub fn encode(frames: &[Vec<u8>], h: u32, w: u32, p: &EncParams) -> Result<Encoded, IntPayloadError> {
    encode_with(frames, h, w, p, &mut CpuSearch)
}

/// [`encode`], with the motion search of frames 1 on run by `search`. Every other step
/// (the residuals, the reconstruction, its SSE, the table and the stream) runs here, so a
/// `search` that keeps [`MotionSearch`]'s rule gives [`encode`]'s payload byte for byte.
pub fn encode_with(frames: &[Vec<u8>], h: u32, w: u32, p: &EncParams, search: &mut dyn MotionSearch) -> Result<Encoded, IntPayloadError> {
    let field = |why: &str| IntPayloadError::new(Refusal::FieldRange, why.to_string());
    if !(1..=wire::MAX_Q).contains(&p.q) {
        return Err(field("q is outside 1..=65535"));
    }
    if p.block == 0 || !matches!(p.mv_precision, 1 | 2) || p.search.saturating_mul(p.mv_precision as u32) > MAX_SEARCH_RANGE {
        return Err(field("block is 0, mv_precision is neither 1 nor 2, or the search range is past 127"));
    }
    if frames.is_empty() || h == 0 || w == 0 {
        return Err(field("a clip needs a frame of at least one pixel"));
    }
    // The format's caps, checked before anything is allocated: a frame of at most
    // MAX_TENSOR elements, at most MAX_FRAMES frames and MAX_CLIP elements in the clip,
    // and a motion-vector field of at most MAX_TENSOR elements.
    let too_large = |why: &str| IntPayloadError::new(Refusal::TooLarge, why.to_string());
    let len = (h as u64)
        .checked_mul(w as u64)
        .and_then(|p| p.checked_mul(3))
        .filter(|&l| l <= wire::MAX_TENSOR)
        .ok_or_else(|| too_large("h × w × 3 exceeds the tensor cap"))?;
    if frames.len() as u64 > wire::MAX_FRAMES {
        return Err(too_large("more frames than the frame cap"));
    }
    if (frames.len() as u64).checked_mul(len).is_none_or(|c| c > wire::MAX_CLIP) {
        return Err(too_large("the clip exceeds the clip cap"));
    }
    let n = frames.len();
    let one = n == 1;
    let (block, prec) = if one { (1, 1) } else { (p.block, p.mv_precision) };
    let (nby, nbx) = ((h as u64).div_ceil(block as u64), (w as u64).div_ceil(block as u64));
    let nb = nby * nbx;
    if nb.checked_mul(2 * (n as u64 - 1)).is_none_or(|m| m > wire::MAX_TENSOR) {
        return Err(too_large("the motion vectors exceed the tensor cap"));
    }
    if frames.iter().any(|f| f.len() as u64 != len) {
        return Err(field("a frame is not h × w × 3 bytes"));
    }
    let (h, w, block, nbx) = (h as usize, w as usize, block as usize, nbx as usize);
    let range = if one { 0 } else { (p.search * prec as u32) as i64 };
    let mut recon: Vec<Vec<u8>> = Vec::with_capacity(n);
    let mut mvs: Vec<(i16, i16)> = Vec::with_capacity(nb as usize * (n - 1));
    let mut counts: ResidualCounts = [0; 2 * RESIDUAL_MAX as usize + 1];
    let mut sse = 0u64;
    let residual = |src: &[u8], recon: &[Vec<u8>], mvs: &[(i16, i16)], f: usize, y: usize, x: usize, c: usize| {
        let pred = match f {
            0 => 128,
            _ => {
                let (dy, dx) = mvs[(f - 1) * nb as usize + (y / block) * nbx + x / block];
                predict(&recon[f - 1], y, x, dy as i64, dx as i64, c, prec as i64, h, w)
            }
        };
        let t = src[(y * w + x) * 3 + c] as i64;
        (pred, t, quantize(t - pred, p.q))
    };
    // Pass 1: the vectors, the reconstruction and its SSE, and the residuals' counts.
    for (f, src) in frames.iter().enumerate() {
        if f > 0 {
            let rf = &recon[f - 1];
            let before = mvs.len();
            search.frame(src, rf, h, w, block, range, prec as i64, &mut mvs);
            if mvs.len() - before != nb as usize {
                return Err(field("the motion search gave another number of vectors than the frame has blocks"));
            }
            // A vector past the window still decodes, but it is not §5.5's search, so a
            // package made with it would not be the reference's (packaging §11.2).
            if mvs[before..].iter().any(|&(dy, dx)| i64::from(dy).abs() > range || i64::from(dx).abs() > range) {
                return Err(field("the motion search gave a vector outside the search window"));
            }
        }
        let mut cur = Vec::with_capacity(src.len());
        for y in 0..h {
            for x in 0..w {
                for c in 0..3 {
                    let (pred, t, r) = residual(src, &recon, &mvs, f, y, x, c);
                    counts[(r + RESIDUAL_MAX) as usize] += 1;
                    let v = (pred + r * p.q as i64).clamp(0, 255);
                    sse += ((t - v) * (t - v)) as u64;
                    cur.push(v as u8);
                }
            }
        }
        recon.push(cur);
    }
    let (cdf, offset) = residual_table(&counts)?;
    // Pass 2: each residual again, from the source, the reconstruction and the vectors,
    // last first, into a writer that holds only the words it emits.
    let mut e = crate::rans::RansReverseEncoder::new();
    for f in (0..n).rev() {
        for y in (0..h).rev() {
            for x in (0..w).rev() {
                for c in (0..3).rev() {
                    let (_, _, r) = residual(&frames[f], &recon, &mvs, f, y, x, c);
                    if !e.put(r as i32, &cdf, cdf.len(), offset) {
                        return Err(field("a residual the stream cannot code"));
                    }
                }
            }
        }
    }
    let stream = e.finish();
    let v = wire::Wiv1 {
        mv_precision: prec,
        keyframe: 0,
        n_frames: n as u32,
        h: h as u32,
        w: w as u32,
        block: block as u32,
        q: p.q,
        offset,
        cdf,
        mvs,
        rans: &stream,
        wih1: None,
    };
    let wiv1 = wire::write_wiv1(&v);
    wire::parse_wiv1(&wiv1, Some(0))?;
    Ok(Encoded { wiv1, recon, sse })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::int_payload::{decode_int, IntDecoded, IntParams};

    /// A clip with motion: a textured square drifting over a gradient, with a
    /// brightness step, so blocks have vectors to find and residuals to code.
    fn clip(h: usize, w: usize, n: usize) -> Vec<Vec<u8>> {
        (0..n)
            .map(|t| {
                let mut f = Vec::with_capacity(h * w * 3);
                for y in 0..h {
                    for x in 0..w {
                        let inside = (y + 7 * t / 3) % h < h / 2 && (x + 2 * t) % w < w / 2;
                        let g = ((x * 9 + y * 5) % 200) as u8;
                        let v = if inside { 255 - ((x * 31 + y * 17 + t) % 90) as u8 } else { g.saturating_add(t as u8) };
                        f.extend_from_slice(&[v, v / 2 + 40, 255 - v]);
                    }
                }
                f
            })
            .collect()
    }

    fn decode(wiv1: &[u8]) -> Vec<Vec<u8>> {
        match decode_int(crate::codecs::CAP_VIDEO_INT_MOTION, wiv1, &IntParams::none()).unwrap() {
            IntDecoded::Video { frames, .. } => frames,
            other => panic!("not video: {other:?}"),
        }
    }

    /// Every decoder reconstructs exactly the frames the encoder predicted from, and the
    /// stated SSE is the one recomputed from them, at every block size, precision,
    /// search radius and step.
    #[test]
    fn the_decode_is_the_encoders_reconstruction() {
        let (h, w) = (19, 23);
        let src = clip(h, w, 5);
        for &(block, search, prec, q) in &[(4, 2, 2, 1), (8, 3, 1, 4), (16, 1, 2, 9), (7, 0, 1, 3), (1, 1, 1, 2), (32, 2, 2, 40)] {
            let p = EncParams { block, search, mv_precision: prec, q };
            let e = encode(&src, h as u32, w as u32, &p).unwrap();
            assert_eq!(decode(&e.wiv1), e.recon, "{p:?}");
            let sse: u64 = src.iter().flatten().zip(e.recon.iter().flatten()).map(|(&a, &b)| (a as i64 - b as i64).pow(2) as u64).sum();
            assert_eq!(sse, e.sse, "{p:?}");
            if q == 1 {
                assert_eq!(e.recon, src, "q 1 is lossless");
            }
        }
    }

    /// A frame that is the previous one moved by a whole number of pixels: every
    /// interior block finds that vector, at both precisions (in units of 1/prec pel).
    #[test]
    fn motion_search_finds_a_known_shift() {
        let (h, w) = (32usize, 32usize);
        let f0: Vec<u8> = (0..h * w)
            .flat_map(|i| {
                let (y, x) = (i / w, i % w);
                let v = ((x * x * 7 + y * 13 + x * y * 3) % 251) as u8;
                [v, v.wrapping_mul(3), v ^ 0x5a]
            })
            .collect();
        // frame 1 at (y, x) is frame 0 at (y + 1, x − 2).
        let f1: Vec<u8> = (0..h * w)
            .flat_map(|i| {
                let (y, x) = ((i / w + 1).min(h - 1), (i % w).saturating_sub(2));
                let j = (y * w + x) * 3;
                [f0[j], f0[j + 1], f0[j + 2]]
            })
            .collect();
        for prec in [1u8, 2] {
            let p = EncParams { block: 8, search: 3, mv_precision: prec, q: 1 };
            let e = encode(&[f0.clone(), f1.clone()], h as u32, w as u32, &p).unwrap();
            let v = wire::parse_wiv1(&e.wiv1, Some(0)).unwrap();
            let k = prec as i16;
            // Interior blocks: neither the bottom row (clamped) nor the left column.
            for by in 0..3 {
                for bx in 1..4 {
                    assert_eq!(v.mvs[by * 4 + bx], (k, -2 * k), "block ({by}, {bx}) at precision {prec}");
                }
            }
        }
    }

    /// A search run elsewhere is handed each frame from 1 on with the previous frame as
    /// decoded; one that keeps the rule gives `encode`'s payload byte for byte, one that
    /// moves a vector gives another payload that still decodes to its own
    /// reconstruction, and one that gives another number of vectors is refused.
    #[test]
    fn encode_with_runs_only_the_search_elsewhere() {
        struct Seen<'a> {
            calls: Vec<(usize, Vec<u8>)>,
            nudge: Option<usize>,
            short: bool,
            past: bool,
            recon: &'a [Vec<u8>],
        }
        impl MotionSearch for Seen<'_> {
            fn frame(&mut self, src: &[u8], rf: &[u8], h: usize, w: usize, block: usize, range: i64, prec: i64, out: &mut Vec<(i16, i16)>) {
                let f = self.calls.len() + 1;
                self.calls.push((f, rf.to_vec()));
                let start = out.len();
                CpuSearch.frame(src, rf, h, w, block, range, prec, out);
                if self.nudge == Some(f) {
                    out[start].1 = if out[start].1 == range as i16 { out[start].1 - 1 } else { out[start].1 + 1 };
                }
                if self.short {
                    out.pop();
                }
                if self.past {
                    out[start].0 = range as i16 + 1;
                }
            }
        }
        let (h, w) = (19, 23);
        let src = clip(h, w, 5);
        for &(block, search, prec, q) in &[(4, 2, 2, 3), (8, 3, 1, 4), (16, 1, 2, 9)] {
            let p = EncParams { block, search, mv_precision: prec, q };
            let want = encode(&src, h as u32, w as u32, &p).unwrap();
            let mut s = Seen { calls: Vec::new(), nudge: None, short: false, past: false, recon: &want.recon };
            assert_eq!(encode_with(&src, h as u32, w as u32, &p, &mut s).unwrap(), want, "{p:?}");
            assert_eq!(s.calls.len(), 4, "{p:?}: frames 1 to 4");
            for (f, rf) in &s.calls {
                assert_eq!(rf, &s.recon[f - 1], "{p:?}: frame {f} searched from the previous frame as decoded");
            }
            let mut moved = Seen { calls: Vec::new(), nudge: Some(2), short: false, past: false, recon: &want.recon };
            let other = encode_with(&src, h as u32, w as u32, &p, &mut moved).unwrap();
            assert_ne!(other.wiv1, want.wiv1, "{p:?}: a moved vector is another payload");
            assert_eq!(decode(&other.wiv1), other.recon, "{p:?}");
            let mut short = Seen { calls: Vec::new(), nudge: None, short: true, past: false, recon: &want.recon };
            assert_eq!(encode_with(&src, h as u32, w as u32, &p, &mut short).map(|_| ()).unwrap_err().code(), "field_range");
            // A vector one step past the window: it would decode, but it is not §5.5's search.
            let mut past = Seen { calls: Vec::new(), nudge: None, short: false, past: true, recon: &want.recon };
            assert_eq!(encode_with(&src, h as u32, w as u32, &p, &mut past).map(|_| ()).unwrap_err().code(), "field_range", "{p:?}");
        }
    }

    #[test]
    fn quantize_rounds_halves_away_from_zero() {
        let cases = [(5, 2, 3), (-5, 2, -3), (3, 2, 2), (1, 2, 1), (-1, 2, -1), (7, 4, 2), (6, 4, 2), (2, 4, 1), (-2, 4, -1), (1, 4, 0), (0, 9, 0), (255, 1, 255), (-255, 255, -1)];
        for (r, q, want) in cases {
            assert_eq!(quantize(r, q), want, "quantize({r}, {q})");
        }
    }

    /// A one-frame clip is written as the format requires, whatever was asked.
    #[test]
    fn one_frame_is_block_1_whole_pel() {
        let p = EncParams { block: 16, search: 4, mv_precision: 2, q: 3 };
        let e = encode(&clip(5, 6, 1), 5, 6, &p).unwrap();
        let v = wire::parse_wiv1(&e.wiv1, Some(0)).unwrap();
        assert_eq!((v.block, v.mv_precision, v.mvs.len()), (1, 1, 0));
        assert_eq!(decode(&e.wiv1), e.recon);
    }

    /// Past the format's caps the bound is `u64::MAX`, never a wrapped figure: the
    /// review's three inputs (2^62 one-pixel frames; 2^40 frames of 2^16 × 2^16; one frame
    /// past the frame cap at the tensor cap's size).
    #[test]
    fn working_bytes_saturates_past_the_caps() {
        assert_eq!(working_bytes(1 << 62, 1, 1, 1), u64::MAX);
        assert_eq!(working_bytes(1 << 40, 1 << 16, 1 << 16, 1), u64::MAX);
        assert_eq!(working_bytes((1 << 20) + 1, 4096, 5461, 1), u64::MAX);
        assert_eq!(working_bytes(1, u32::MAX, u32::MAX, u32::MAX), u64::MAX);
        // At the caps it is a figure: the largest clip, one frame.
        let at = working_bytes(1, 4096, 5461, 1);
        assert!(at < u64::MAX && at > 4096 * 5461 * 3, "{at}");
        // The fixed term is derived from the widest table.
        assert_eq!(TABLE_FIT_BYTES + TABLE_HELD_BYTES, 24_640);
    }

    #[test]
    fn refusals_before_allocation() {
        let code = |r: Result<Encoded, IntPayloadError>| r.map(|_| "ok").unwrap_or_else(|e| e.code());
        let f = clip(4, 4, 2);
        let p = |block, search, mv_precision, q| EncParams { block, search, mv_precision, q };
        assert_eq!(code(encode(&f, 4, 4, &p(4, 1, 1, 0))), "field_range");
        assert_eq!(code(encode(&f, 4, 4, &p(4, 1, 1, 65_536))), "field_range");
        assert_eq!(code(encode(&f, 4, 4, &p(0, 1, 1, 4))), "field_range");
        assert_eq!(code(encode(&f, 4, 4, &p(4, 1, 3, 4))), "field_range");
        assert_eq!(code(encode(&f, 4, 4, &p(4, 64, 2, 4))), "field_range");
        assert_eq!(code(encode(&f, 4, 4, &p(4, 127, 1, 4))), "ok");
        assert_eq!(code(encode(&f, 4, 5, &p(4, 1, 1, 4))), "field_range");
        assert_eq!(code(encode(&[], 4, 4, &p(4, 1, 1, 4))), "field_range");
        // A frame past the tensor cap is refused before its bytes are read.
        assert_eq!(code(encode(&f, 4097, 5462, &p(4, 1, 1, 4))), "too_large");
        // Block 1 on a 4096 × 4096 frame is 2^24 vectors a frame: three P-frames pass the
        // field's cap (2·nb·(n − 1) > 2^26), refused before a frame's bytes are read.
        let big = vec![Vec::new(); 4];
        assert_eq!(code(encode(&big, 4096, 4096, &p(1, 0, 1, 4))), "too_large");
    }
}
