//! The slot-family RFI (reference-frame invalidation) recovery **policy**, shared by the three
//! backends that answer a loss with a re-reference to a known-good older frame instead of an IDR:
//! native AMF (user-LTR bitfield), native QSV (`mfxExtRefListCtrl` LTR) and Vulkan Video (the
//! app-owned DPB slot table). One decision, three mechanisms — the policy lived as three
//! hand-copies and had already diverged once (the fecbec2d taint sweep reached AMF/QSV a commit
//! before the Vulkan backend was carved out, and Vulkan shipped without it until a later fix).
//! The NVENC twins' *range* policy is the other half of WP7.2, in
//! [`super::nvenc_core::plan_range_recovery`].
//!
//! Policy only. Every mechanism — how a force is applied, how distrust is *persisted* (AMF clears
//! its mirror slot to `None`, QSV sets a separate `ltr_tainted` flag because its mirror must keep
//! naming the slot for the RejectedRefList, Vulkan blanks `slot_wire` to `-1` while `slot_poc`
//! keeps the picture resident for the RPS) — stays in its backend. Callers feed their
//! **currently-trusted** references and apply the returned taints through their own marker; that
//! caller-side filter is exactly what makes the three persistence schemes equivalent under one
//! pure function.
//!
//! The decline arm is also the backend's: AMF/QSV clear an un-consumed `pending_force` (the sweep
//! may have emptied the slot it points at), while Vulkan deliberately leaves `pending_loss` armed
//! (a stale arm is re-resolved at frame-build, where a failed re-pick forces the IDR that heals
//! the stream). Do not harmonize them here.

/// One loss event's recovery decision over a slot table: which trusted references become
/// untrustworthy, and which one anchors the recovery.
pub(super) struct SlotPlan {
    /// Bitmask of slots whose reference was encoded **at or after** the loss start — inside the
    /// client's corrupt window, so the client either never received it or decoded it against a
    /// broken chain. Serving one as "known-good" on a LATER loss ships corruption tagged as the
    /// recovery anchor; the backend must record the distrust in its own persistent marker, because
    /// these wires would otherwise look like valid pre-loss anchors to the next loss event.
    pub(super) tainted: u32,
    /// The recovery anchor: the newest trusted reference **strictly older** than the loss —
    /// `(slot, wire)` — i.e. the most recent picture the client still holds intact, so
    /// re-referencing it costs the smallest residual. `None`: every candidate is inside or after
    /// the corrupt window — the caller declines and its (coalesced) keyframe path recovers.
    pub(super) anchor: Option<(usize, i64)>,
}

/// Plan the recovery for a loss starting at wire frame `loss_first`, over the backend's
/// currently-trusted references (`(slot, wire)`; previously-distrusted entries must already be
/// filtered out by the caller — see the module doc). Sweep and pick are one call so the anchor is
/// chosen from the same snapshot the taints are computed from, by construction: the anchor
/// delegates to [`pick_anchor`], and `wire >= loss_first` (taint) and `wire < loss_first`
/// (anchor) are disjoint, so a slot tainted by this call can never be this call's anchor.
pub(super) fn plan_slot_recovery(refs: &[(usize, i64)], loss_first: i64) -> SlotPlan {
    // The callers' validity gate (`first < 0 → decline`) is what makes their sentinel filters
    // (-1 / `None`) exact views of "trusted"; this assert keeps the contract visible from inside
    // the extracted code. Plain assert: the lint legs run --release, and a compiled-out check
    // here would silently drop taints instead of failing loudly.
    assert!(
        loss_first >= 0,
        "loss_first must be validity-gated by the caller"
    );
    let mut tainted = 0u32;
    for &(slot, wire) in refs {
        if wire >= loss_first {
            assert!(slot < 32, "slot table exceeds the u32 taint mask");
            tainted |= 1 << slot;
        }
    }
    SlotPlan {
        tainted,
        anchor: pick_anchor(refs, loss_first),
    }
}

/// The pick half alone: newest trusted reference strictly older than the loss. Ties break to the
/// first entry in `refs` (every caller feeds ascending slot order, so the lowest slot wins —
/// the strict `>` all three backends used). Standalone because Vulkan re-picks at frame-build
/// time: its arm carries the loss start, not the slot, so the slot is resolved against the table
/// as it stands when the recovery frame is actually encoded.
pub(super) fn pick_anchor(refs: &[(usize, i64)], loss_first: i64) -> Option<(usize, i64)> {
    let mut best: Option<(usize, i64)> = None;
    for &(slot, wire) in refs {
        if wire < loss_first && best.is_none_or(|(_, b)| wire > b) {
            best = Some((slot, wire));
        }
    }
    best
}

#[cfg(test)]
mod tests {
    use super::{pick_anchor, plan_slot_recovery};

    /// Adapt a raw slot table (the Vulkan `slot_wire` shape: `-1` = empty) into the trusted view
    /// the policy takes — the same filter the backend adapters apply.
    fn view(wires: &[i64]) -> Vec<(usize, i64)> {
        wires
            .iter()
            .enumerate()
            .filter_map(|(s, &w)| (w >= 0).then_some((s, w)))
            .collect()
    }

    /// Apply a plan's taints the way the Vulkan adapter does (blank the wire) — the persistence
    /// half the pure fn hands back to the caller.
    fn apply(wires: &mut [i64], tainted: u32) {
        for (s, w) in wires.iter_mut().enumerate() {
            if tainted & (1 << s) != 0 {
                *w = -1;
            }
        }
    }

    /// The RFI anchor picker: newest resident wire strictly older than the loss; empty/newer
    /// slots never qualify. (Migrated 1:1 from `vulkan_video.rs`'s `pick_recovery_slot` tests —
    /// same vectors, now `(slot, wire)`-valued.)
    #[test]
    fn picks_newest_pre_loss() {
        // slots hold wires 5..12 (ring position arbitrary); loss starts at 9 → anchor = wire 8.
        let wires = [8i64, 9, 10, 11, 12, 5, 6, 7];
        assert_eq!(pick_anchor(&view(&wires), 9), Some((0, 8)));
        // loss older than everything resident → no anchor (caller keyframes).
        assert_eq!(pick_anchor(&view(&wires), 5), None);
        // empty slots (-1) are skipped by the view filter and never anchor.
        assert_eq!(pick_anchor(&view(&[-1, 3, -1, 4]), 5), Some((3, 4)));
        assert_eq!(pick_anchor(&view(&[-1; 8]), 5), None);
        // wire == loss_first is INSIDE the corrupt window: strictly-older only.
        assert_eq!(pick_anchor(&view(&[9, 8]), 9), Some((1, 8)));
        // tie on the wire → first entry (= lowest slot) wins, the strict `>` all three backends
        // used.
        assert_eq!(pick_anchor(&[(2, 7), (5, 7)], 9), Some((2, 7)));
        // empty view.
        assert_eq!(pick_anchor(&[], 9), None);
    }

    /// The taint sweep (fecbec2d's fix): a slot encoded inside an EARLIER, still unrepaired loss
    /// window must not become the "known-good" anchor of a LATER loss. Without persisted
    /// distrust the picker accepts it — it is resident and its wire is below the second loss
    /// start — and the frame ships tagged `recovery_anchor`, lifting the client's freeze onto a
    /// reference it never decoded. (Migrated 1:1 from `vulkan_video.rs`; the hand-replicated
    /// sweep is now `plan_slot_recovery` itself.)
    #[test]
    fn taint_sweep_excludes_slots_from_an_earlier_loss() {
        // Slots hold wires 0..7. Loss 1 starts at wire 4, so wires 4..7 are undecodable at the
        // client. A second loss report arrives at wire 6 while they are all still resident.
        let tainted_wires = [4i64, 5, 6, 7];

        // WITHOUT the sweep this is the bug: the newest wire below 6 is wire 5 — squarely inside
        // loss 1's unrepaired window — and it would be served as the "known-good" anchor.
        let unswept = [0i64, 1, 2, 3, 4, 5, 6, 7];
        let (_, picked_wire) = pick_anchor(&view(&unswept), 6).expect("unswept picks something");
        assert!(
            tainted_wires.contains(&picked_wire),
            "precondition: without the sweep the anchor comes from the earlier loss window"
        );

        // WITH the plan, loss 1 taints 4..7 (and anchors on wire 3 — never a tainted wire, by
        // the disjoint predicates), so loss 2 can only reach genuinely clean wires.
        let mut wires = unswept;
        let plan = plan_slot_recovery(&view(&wires), 4);
        assert_eq!(plan.tainted, 0b1111_0000);
        assert_eq!(plan.anchor, Some((3, 3)));
        apply(&mut wires, plan.tainted);
        assert_eq!(wires, [0, 1, 2, 3, -1, -1, -1, -1]);
        let (slot, wire) = pick_anchor(&view(&wires), 6).expect("clean wires remain");
        assert_eq!((slot, wire), (3, 3), "newest clean survivor is wire 3");

        // Encoding resumes after recovery; wires 8..11 refill the swept slots and are clean. A
        // later loss at wire 10 legitimately anchors on wire 9 — the sweep must not over-reject.
        wires[4] = 8;
        wires[5] = 9;
        wires[6] = 10;
        wires[7] = 11;
        let plan = plan_slot_recovery(&view(&wires), 10);
        assert_eq!(plan.anchor, Some((5, 9)), "wire 9 is post-recovery, clean");
        apply(&mut wires, plan.tainted);

        // A loss covering every live wire leaves nothing clean → decline, caller serves an IDR.
        let mut all = [5i64, 6, 7, 8, 9, 10, 11, 12];
        let plan = plan_slot_recovery(&view(&all), 5);
        assert_eq!(plan.tainted, 0b1111_1111);
        assert_eq!(plan.anchor, None);
        apply(&mut all, plan.tainted);
        assert_eq!(pick_anchor(&view(&all), 5), None);
    }

    /// `Encoder::distrust_references` — the OTHER way trust is withdrawn, and the one that needs no
    /// loss range. The host calls it when the client reports damage the host did not repair (a
    /// coalesced keyframe request, or an RFI anchor the client kept asking past): the sweep cannot
    /// run there because a keyframe request carries no range, so every resident reference is
    /// withdrawn wholesale instead. All three backends persist that through their own marker; what
    /// the shared policy must guarantee is the consequence — the next pick finds nothing and
    /// declines, so the caller keyframes instead of serving an anchor over unrepaired damage.
    #[test]
    fn distrusting_every_reference_makes_the_next_anchor_pick_decline() {
        // A table with plenty of pre-loss candidates: without the withdrawal, wire 7 anchors.
        let mut wires = [4i64, 5, 6, 7, -1, -1, -1, -1];
        assert_eq!(
            pick_anchor(&view(&wires), 9),
            Some((3, 7)),
            "precondition: this table would happily anchor"
        );

        // The Vulkan mechanism (blank the wire) stands in for all three: AMF clears its mirror and
        // QSV raises `ltr_tainted`, but each is filtered out of the trusted view identically —
        // which is exactly what makes one pure policy serve three persistence schemes.
        apply(&mut wires, u32::MAX);
        assert_eq!(
            pick_anchor(&view(&wires), 9),
            None,
            "every reference withdrawn → no anchor, caller falls through to its keyframe path"
        );
        // And it holds for ANY later loss, not just this one — the point of persisting distrust.
        assert_eq!(pick_anchor(&view(&wires), 100), None);
    }

    /// The withdrawal must be temporary, or one coalesced keyframe request would cost a session its
    /// RFI recovery for good and every later loss would ride the 20-40× IDR path. Each backend
    /// restores trust the same way it always did — a slot re-marked with a fresh frame (and an IDR
    /// flush, which empties the table first) — so a refilled slot anchors again.
    #[test]
    fn a_re_marked_slot_restores_anchor_trust_after_a_full_withdrawal() {
        let mut wires = [4i64, 5, 6, 7, -1, -1, -1, -1];
        apply(&mut wires, u32::MAX);
        assert_eq!(pick_anchor(&view(&wires), 20), None);

        // Encoding continues; the ring refills two slots with post-withdrawal frames. Those really
        // are clean — the client's damage was repaired by the IDR the withdrawal forced — so they
        // are legitimate anchors and the sweep must not keep rejecting them.
        wires[0] = 14;
        wires[1] = 15;
        assert_eq!(
            pick_anchor(&view(&wires), 20),
            Some((1, 15)),
            "a re-marked slot is trusted again — the suppression is a few frames, not the session"
        );
    }
}
