//! mgf-hrb fairness / anti-starvation (Tier-1 #10, survivability C2/C4).
//!
//! Atomicity (Part 3.4) guarantees *a* winner of a contended claim, not a *fair*
//! one. Plain first-come-first-served has no anti-starvation: a tenant that
//! repeatedly claims+releases a contended device can lock a rival out
//! indefinitely. For "many customers competing," a tenant that never makes
//! progress is — to that tenant — an outage, so fairness is a reliability
//! property (priority #1), not a nicety.
//!
//! This is the cold-path contention policy the core consults at the grant +
//! release seams. The mechanism is a **per-resource FIFO wait queue** plus a
//! **reservation-on-release**: when a contended resource frees, it is reserved
//! for the longest-waiting tenant for a grace window, so a spammer re-claiming
//! immediately is denied (the reservation holds) and the fair waiter wins on its
//! next retry. The wire protocol is unchanged — a denied claim still returns
//! `Conflict` and the client retries; fairness just decides *whose* retry wins.
//!
//! Identity is the unforgeable SO_PEERCRED **uid** (a tenant can't jump the
//! queue by reconnecting), the same identity the #9 per-tenant ceilings use.
//!
//! ## What this is, and the gated remainder
//!
//! This ships the FIFO anti-starvation closure for C2/C4 AND the priority-ordered
//! admission half of the §5.4 priority tier (C3 — priority inversion among
//! WAITERS). Each waiter carries a **policy-assigned** priority (an injected
//! `PrioritySource` keyed on the unforgeable SO_PEERCRED uid — a tenant can't
//! self-escalate by asking), and a freed contended resource is reserved for the
//! HIGHEST-priority waiter, longest-waiting within a priority. With no source
//! configured every waiter is priority 0, so the selection collapses to the
//! original pure FIFO — back-compat is exact (the C2/C4 pins below are unchanged).
//!
//! It ALSO ships the §5.4 active holder-preemption MECHANISM (C3's other half /
//! C9): when a denied claim strictly outranks the live holder, `shouldPreempt`
//! says so and `markPreemption` opens a `GRACE_MS` window (the §3.4 graceful
//! signal + grace period); if the holder hasn't released by then,
//! `Core.runDuePreemptions` forces the IOMMU/VFIO teardown (`backend.revoke`
//! below the fd) and hands the resource to the waiter. The holder-signal is the
//! queryable `isPreemptionPending` (soft, pre-teardown) plus the existing
//! `Err.Revoked` disclosure (post-teardown). With no `PrioritySource` set
//! `shouldPreempt` is always false, so none of this engages — exact FIFO.
//!
//! Gated remainder — each needs real multi-tenant traffic to set well, not a
//! mechanism (the mechanisms above are all built + pinned):
//!   * weighted-fair queues + hold-time limits on contended leases.
//!   * the actual uid→priority policy VALUES, the preemption threshold, and
//!     `GRACE_MS` tuning.
//!   * a wire op / SDK call to TRIGGER `runDuePreemptions` (operator/loop-driven
//!     today, like `reconcileAfterRestart`).
//! `Fairness` stays the single surface the core calls through, so all of those
//! slot in here without touching the claim path.
//!
//! Callers hold `Core.lock` (these methods touch only this state, never the
//! backend), and pass `now_ms` so the policy is deterministically testable.

const std = @import("std");
const model = @import("mgf_hrb_model");

const Urn = model.Urn;

/// How long a freed contended resource is reserved for its longest-waiting
/// tenant before the reservation lapses (a no-show forfeits its turn). Provisional
/// — the value to tune against real contention traffic is part of the gated
/// remainder; 3 s is a sane default for human/operator-scale contention.
pub const GRACE_MS: u64 = 3000;

/// S5 bounds — the policy state can't grow without limit under claim spam.
pub const MAX_TRACKED_URNS: usize = 256;
pub const MAX_WAITERS_PER_URN: usize = 64;

/// The result of consulting the policy at the top of a claim.
pub const Verdict = enum {
    /// No reservation blocks this tenant — continue the normal claim path.
    proceed,
    /// The resource is reserved for a different (longer-waiting) tenant — deny
    /// with `Conflict` so the reserved tenant gets its turn.
    reserved_for_other,
};

/// Priority policy seam (§5.4). Maps the unforgeable SO_PEERCRED `uid` to a
/// priority — higher = more urgent. Injected so the priority VALUES (operator
/// policy that needs real deployment to set well) stay OUT of this mechanism;
/// the mechanism — priority-ordered reservation — is what ships and is pinned
/// here. With no source set, `priorityFor` is 0 for everyone (pure FIFO).
pub const PrioritySource = struct {
    ctx: *anyopaque,
    get: *const fn (ctx: *anyopaque, uid: u32) u8,
};

const Waiter = struct { uid: u32, since_ms: u64, prio: u8 = 0 };
const Reservation = struct { uid: u32, until_ms: u64 };

/// A live holder marked for GRACEFUL preemption (§5.4 / §3.4): its hold on `urn`
/// will be force-revoked at `deadline_ms` in favour of the higher-priority
/// `for_uid` — UNLESS the holder releases voluntarily first (which cancels it,
/// the graceful path). The grace window is the "graceful signal + grace period"
/// before the forced IOMMU/VFIO teardown.
const Preemption = struct {
    urn: Urn,
    holder_uid: u32,
    for_uid: u32,
    deadline_ms: u64,
};

/// A preemption whose grace window has expired — returned by `takeDuePreemption`
/// for the core to execute the forced teardown against. Carries the holder it
/// targets (so the core can confirm that same holder is still holding) and the
/// beneficiary it frees the resource for.
pub const DuePreemption = struct {
    urn: Urn,
    holder_uid: u32,
    for_uid: u32,
};

const Entry = struct {
    urn: Urn,
    waiters: std.ArrayListUnmanaged(Waiter) = .empty,
    reservation: ?Reservation = null,
};

pub const Fairness = struct {
    entries: std.ArrayListUnmanaged(Entry) = .empty,
    alloc: std.mem.Allocator,
    /// The §5.4 priority policy (the gated `ContentionPolicy` wiring point).
    /// null → every tenant is priority 0 → pure FIFO (the shipped default).
    prio_source: ?PrioritySource = null,
    /// §5.4 active holder-preemption — holders marked for graceful eviction in
    /// favour of a strictly-higher-priority waiter. Each carries a grace deadline;
    /// `Core.runDuePreemptions` executes the forced teardown once it lapses. Empty
    /// unless a `PrioritySource` ranks a waiter above a live holder, so the FIFO
    /// default never populates it. Touched only under `Core.lock`.
    preemptions: std.ArrayListUnmanaged(Preemption) = .empty,

    pub fn init(alloc: std.mem.Allocator) Fairness {
        return .{ .alloc = alloc };
    }

    /// Install the priority policy. The core leaves this unset today (no priority
    /// policy is configured yet — that's the gated remainder); a future
    /// `ContentionPolicy` wires a real uid→priority source here without any
    /// change to the claim path or this module's mechanism.
    pub fn setPrioritySource(self: *Fairness, src: PrioritySource) void {
        self.prio_source = src;
    }

    fn priorityFor(self: *const Fairness, uid: u32) u8 {
        const p = self.prio_source orelse return 0;
        return p.get(p.ctx, uid);
    }

    pub fn deinit(self: *Fairness) void {
        for (self.entries.items) |*e| e.waiters.deinit(self.alloc);
        self.entries.deinit(self.alloc);
        self.preemptions.deinit(self.alloc);
    }

    fn find(self: *Fairness, urn: *const Urn) ?*Entry {
        for (self.entries.items) |*e| {
            if (e.urn.eql(urn)) return e;
        }
        return null;
    }

    fn findOrCreate(self: *Fairness, urn: *const Urn) ?*Entry {
        if (self.find(urn)) |e| return e;
        if (self.entries.items.len >= MAX_TRACKED_URNS) return null; // S5 — shed silently
        self.entries.append(self.alloc, .{ .urn = urn.* }) catch return null;
        return &self.entries.items[self.entries.items.len - 1];
    }

    fn dropWaiter(_: *Fairness, e: *Entry, uid: u32) void {
        for (e.waiters.items, 0..) |w, i| {
            if (w.uid == uid) {
                _ = e.waiters.orderedRemove(i); // orderedRemove preserves FIFO
                return;
            }
        }
    }

    /// The waiter that should get the resource next: highest priority, and the
    /// longest-waiting (earliest `since_ms`) within that priority. The queue is
    /// kept in arrival order, so with all priorities equal — the default, no
    /// `PrioritySource` — this is index 0, the FIFO front: the original
    /// anti-starvation behavior, preserved exactly.
    fn bestWaiterIndex(_: *Fairness, e: *Entry) ?usize {
        if (e.waiters.items.len == 0) return null;
        var best: usize = 0;
        for (e.waiters.items[1..], 1..) |w, i| {
            const b = e.waiters.items[best];
            if (w.prio > b.prio or (w.prio == b.prio and w.since_ms < b.since_ms)) best = i;
        }
        return best;
    }

    /// Consulted at the top of a claim. Returns `reserved_for_other` when a live
    /// reservation is held for a DIFFERENT tenant (deny). A reservation held for
    /// `uid` itself, or none, or an EXPIRED one, yields `proceed` — and an
    /// expired reservation is cleared here, with its no-show waiter dropped (it
    /// had its grace window and forfeited, so it can't block the queue forever).
    pub fn reservationVerdict(self: *Fairness, urn: *const Urn, uid: u32, now_ms: u64) Verdict {
        const e = self.find(urn) orelse return .proceed;
        const r = e.reservation orelse return .proceed;
        if (now_ms > r.until_ms) {
            self.dropWaiter(e, r.uid); // no-show forfeits its turn
            e.reservation = null;
            return .proceed;
        }
        if (r.uid == uid) return .proceed; // reserved FOR them
        return .reserved_for_other;
    }

    /// A claim was DENIED (incompatible holder, or reserved-for-other) — enqueue
    /// `uid` as a waiter (FIFO, deduped, bounded). Records "this tenant wants it"
    /// so a later release reserves the resource for the longest-waiting tenant.
    pub fn onConflict(self: *Fairness, urn: *const Urn, uid: u32, now_ms: u64) void {
        const e = self.findOrCreate(urn) orelse return; // overflow → best-effort skip
        for (e.waiters.items) |w| {
            if (w.uid == uid) return; // already queued — keep its original FIFO position
        }
        if (e.waiters.items.len >= MAX_WAITERS_PER_URN) return; // S5
        // Priority is a stable function of the (unforgeable) uid, captured at
        // enqueue. A re-queue of an already-waiting uid is deduped above, so its
        // original position AND priority stand.
        e.waiters.append(self.alloc, .{
            .uid = uid,
            .since_ms = now_ms,
            .prio = self.priorityFor(uid),
        }) catch {};
    }

    /// A claim SUCCEEDED for `uid` on `urn` — clear the reservation if it was
    /// theirs and drop them from the wait queue (they got it).
    pub fn onGranted(self: *Fairness, urn: *const Urn, uid: u32) void {
        const e = self.find(urn) orelse return;
        if (e.reservation) |r| {
            if (r.uid == uid) e.reservation = null;
        }
        self.dropWaiter(e, uid);
    }

    /// `urn` became free (its last lease was released). If anyone is waiting,
    /// reserve the resource for the BEST waiter (highest priority, then
    /// longest-waiting — see `bestWaiterIndex`) for `GRACE_MS`, so a spammer
    /// re-claiming immediately is denied and the fair waiter wins on its next
    /// retry. With no priority policy set, "best" is the FIFO front. With no
    /// waiters, drop any stale reservation.
    pub fn onRelease(self: *Fairness, urn: *const Urn, now_ms: u64) void {
        // §5.4 — a voluntary release within the grace window cancels any pending
        // preemption on this resource: the holder yielded gracefully, so no forced
        // teardown is owed. (When `runDuePreemptions` calls `onRelease` AFTER a
        // forced teardown, the preemption was already taken, so this is a no-op.)
        self.cancelPreemption(urn);
        const e = self.find(urn) orelse return;
        const bi = self.bestWaiterIndex(e) orelse {
            e.reservation = null; // nobody waiting — drop any stale reservation
            return;
        };
        e.reservation = .{ .uid = e.waiters.items[bi].uid, .until_ms = now_ms + GRACE_MS };
    }

    /// Introspection (tests): how many tenants are waiting on `urn`.
    pub fn waiterCount(self: *Fairness, urn: *const Urn) usize {
        const e = self.find(urn) orelse return 0;
        return e.waiters.items.len;
    }

    // -- §5.4 active holder-preemption (the C3/C9 other half) ----------------
    //
    // DECISION + grace-window record live here (the single policy surface); the
    // forced teardown EXECUTION lives in `Core.runDuePreemptions` (it needs the
    // backend + lease table). The graceful contract: a higher-priority waiter
    // marks the live holder, a `GRACE_MS` window opens, and only if the holder
    // hasn't released by then is the hold force-revoked. With no `PrioritySource`
    // set `shouldPreempt` is always false, so none of this engages — exact FIFO.

    /// Should a claim blocked by a live HOLDER forcibly evict it for the blocked
    /// (waiting) tenant? True ONLY when the waiter's policy priority STRICTLY
    /// exceeds the holder's. A tenant never preempts an equal- or lower-priority
    /// peer (no thrash, no same-tier eviction) and never itself. With no
    /// `PrioritySource` every uid is priority 0, so this is ALWAYS false — pure
    /// FIFO, no preemption (exact back-compat).
    pub fn shouldPreempt(self: *const Fairness, waiter_uid: u32, holder_uid: u32) bool {
        if (waiter_uid == holder_uid) return false;
        return self.priorityFor(waiter_uid) > self.priorityFor(holder_uid);
    }

    /// Mark `holder_uid`'s hold on `urn` for graceful preemption in favour of the
    /// higher-priority `for_uid`: a `GRACE_MS` window opens now. Deduped per urn —
    /// a re-mark keeps the ORIGINAL deadline, so a stream of high-priority retries
    /// can't keep pushing the holder's grace window back. Bounded (S5).
    pub fn markPreemption(self: *Fairness, urn: *const Urn, holder_uid: u32, for_uid: u32, now_ms: u64) void {
        for (self.preemptions.items) |p| {
            if (p.urn.eql(urn)) return; // already pending — original deadline stands
        }
        if (self.preemptions.items.len >= MAX_TRACKED_URNS) return; // S5 — shed silently
        self.preemptions.append(self.alloc, .{
            .urn = urn.*,
            .holder_uid = holder_uid,
            .for_uid = for_uid,
            .deadline_ms = now_ms + GRACE_MS,
        }) catch {};
    }

    /// Is a graceful preemption currently pending on `urn`? The disclosure hook a
    /// wire op / SDK uses to tell a holder it's being preempted (the soft signal
    /// before the hard teardown), and the tests' observation point.
    pub fn isPreemptionPending(self: *const Fairness, urn: *const Urn) bool {
        for (self.preemptions.items) |p| if (p.urn.eql(urn)) return true;
        return false;
    }

    /// Cancel any pending preemption on `urn` (the holder yielded gracefully).
    /// Idempotent; called from `onRelease`.
    pub fn cancelPreemption(self: *Fairness, urn: *const Urn) void {
        for (self.preemptions.items, 0..) |p, i| {
            if (p.urn.eql(urn)) {
                _ = self.preemptions.swapRemove(i);
                return;
            }
        }
    }

    /// Remove and return one preemption whose grace window has expired (`now_ms`
    /// at/after its deadline), or null if none is due. Removing it here means each
    /// due preemption is handed to the core for teardown exactly once.
    pub fn takeDuePreemption(self: *Fairness, now_ms: u64) ?DuePreemption {
        for (self.preemptions.items, 0..) |p, i| {
            if (now_ms >= p.deadline_ms) {
                const out = DuePreemption{ .urn = p.urn, .holder_uid = p.holder_uid, .for_uid = p.for_uid };
                _ = self.preemptions.swapRemove(i);
                return out;
            }
        }
        return null;
    }
};

// ===========================================================================
// C2/C4 — anti-starvation policy pins.
// ===========================================================================

const testing = std.testing;

test "C2/C4: a freed contended resource is reserved for the longest-waiting tenant" {
    var f = Fairness.init(testing.allocator);
    defer f.deinit();
    const x = Urn.from("hw:/pci/0000:01:00.0/fn0");
    const bob: u32 = 1001;
    const spammer: u32 = 1002;

    // (Alice holds X, in the core.) Bob then the spammer try + are denied → queued FIFO.
    f.onConflict(&x, bob, 100);
    f.onConflict(&x, spammer, 110);
    try testing.expectEqual(@as(usize, 2), f.waiterCount(&x));

    // Alice releases X → reserved for Bob (the front of the queue).
    f.onRelease(&x, 200);

    // The spammer re-claims immediately — DENIED, the reservation is Bob's.
    try testing.expectEqual(Verdict.reserved_for_other, f.reservationVerdict(&x, spammer, 210));
    // Bob claims — allowed (the reservation is his).
    try testing.expectEqual(Verdict.proceed, f.reservationVerdict(&x, bob, 210));
    // …and on grant Bob leaves the queue + the reservation clears.
    f.onGranted(&x, bob);
    try testing.expectEqual(@as(usize, 1), f.waiterCount(&x)); // only the spammer remains
    try testing.expectEqual(Verdict.proceed, f.reservationVerdict(&x, spammer, 220)); // now free for it
}

test "C2/C4: FIFO order — the earlier waiter is reserved first" {
    var f = Fairness.init(testing.allocator);
    defer f.deinit();
    const x = Urn.from("hw:/pci/0000:01:00.0/fn0");
    f.onConflict(&x, 1, 100); // first
    f.onConflict(&x, 2, 101); // second
    f.onConflict(&x, 1, 102); // dup — keeps uid 1's original position, no double-enqueue
    try testing.expectEqual(@as(usize, 2), f.waiterCount(&x));

    f.onRelease(&x, 200);
    try testing.expectEqual(Verdict.proceed, f.reservationVerdict(&x, 1, 210)); // uid 1 (front) reserved
    try testing.expectEqual(Verdict.reserved_for_other, f.reservationVerdict(&x, 2, 210));
}

test "C2/C4: a reservation lapses after the grace window — the no-show forfeits its turn" {
    var f = Fairness.init(testing.allocator);
    defer f.deinit();
    const x = Urn.from("hw:/pci/0000:01:00.0/fn0");
    f.onConflict(&x, 1, 100); // front
    f.onConflict(&x, 2, 101);
    f.onRelease(&x, 200); // reserved for uid 1 until 200 + GRACE_MS

    // uid 2 claims AFTER the grace window — uid 1 never showed → its reservation
    // expires, uid 1 forfeits (dropped from the queue), and uid 2 proceeds.
    const after_grace = 200 + GRACE_MS + 1;
    try testing.expectEqual(Verdict.proceed, f.reservationVerdict(&x, 2, after_grace));
    try testing.expectEqual(@as(usize, 1), f.waiterCount(&x)); // uid 1 forfeited; only uid 2 left
}

test "C2/C4: no waiters → no reservation; release is a clean no-op" {
    var f = Fairness.init(testing.allocator);
    defer f.deinit();
    const x = Urn.from("hw:/usb/bus1/dev4/if0");
    f.onRelease(&x, 100); // nobody waiting
    try testing.expectEqual(Verdict.proceed, f.reservationVerdict(&x, 7, 110));
    try testing.expectEqual(@as(usize, 0), f.waiterCount(&x));
}

test "C2/C4: waiter queue is bounded (S5)" {
    var f = Fairness.init(testing.allocator);
    defer f.deinit();
    const x = Urn.from("hw:/pci/0000:01:00.0/fn0");
    var uid: u32 = 0;
    while (uid < MAX_WAITERS_PER_URN + 50) : (uid += 1) f.onConflict(&x, uid, 100);
    try testing.expectEqual(MAX_WAITERS_PER_URN, f.waiterCount(&x)); // capped, no unbounded growth
}

// ===========================================================================
// C3 §5.4 — priority-ordered admission pins (the waiter-priority half of the
// priority tier; active holder-preemption is the gated remainder).
// ===========================================================================

/// Test priority policy: one designated uid is high-priority, everyone else 0.
const TwoTier = struct {
    hi_uid: u32,
    fn get(ctx: *anyopaque, uid: u32) u8 {
        const self: *TwoTier = @ptrCast(@alignCast(ctx));
        return if (uid == self.hi_uid) 10 else 0;
    }
    fn source(self: *TwoTier) PrioritySource {
        return .{ .ctx = self, .get = TwoTier.get };
    }
};

test "C3 §5.4: a higher-priority waiter is reserved ahead of an earlier low-priority one" {
    var f = Fairness.init(testing.allocator);
    defer f.deinit();
    var tier = TwoTier{ .hi_uid = 2 };
    f.setPrioritySource(tier.source());
    const x = Urn.from("hw:/pci/0000:01:00.0/fn0");

    // uid 1 (low) waits FIRST; uid 2 (high) waits SECOND.
    f.onConflict(&x, 1, 100);
    f.onConflict(&x, 2, 110);

    // On release the HIGH-priority uid 2 is reserved even though uid 1 waited
    // longer — priority beats arrival order. That's the C3 inversion fix: a
    // high-priority tenant no longer queues behind low-priority ones.
    f.onRelease(&x, 200);
    try testing.expectEqual(Verdict.proceed, f.reservationVerdict(&x, 2, 210));
    try testing.expectEqual(Verdict.reserved_for_other, f.reservationVerdict(&x, 1, 210));

    // uid 2 takes it; uid 1 (still queued) then wins the next round — no
    // starvation of the low-priority tenant, just correct ordering.
    f.onGranted(&x, 2);
    f.onRelease(&x, 300);
    try testing.expectEqual(Verdict.proceed, f.reservationVerdict(&x, 1, 310));
}

test "C3 §5.4: within one priority, the longest-waiting tenant still wins (FIFO tie-break)" {
    var f = Fairness.init(testing.allocator);
    defer f.deinit();
    var tier = TwoTier{ .hi_uid = 99 }; // nobody present is high → all priority 0
    f.setPrioritySource(tier.source());
    const x = Urn.from("hw:/pci/0000:01:00.0/fn0");
    f.onConflict(&x, 1, 100);
    f.onConflict(&x, 2, 110);
    f.onConflict(&x, 3, 120);
    f.onRelease(&x, 200);
    // All equal priority → earliest arrival (uid 1) reserved, exactly as pure FIFO.
    try testing.expectEqual(Verdict.proceed, f.reservationVerdict(&x, 1, 210));
    try testing.expectEqual(Verdict.reserved_for_other, f.reservationVerdict(&x, 2, 210));
}

test "C3 §5.4: the highest priority wins among several tiers" {
    var f = Fairness.init(testing.allocator);
    defer f.deinit();
    // A graded policy: priority == uid, so a higher uid is more urgent.
    const Graded = struct {
        fn get(_: *anyopaque, uid: u32) u8 {
            return @intCast(uid);
        }
    };
    var dummy: u8 = 0;
    f.setPrioritySource(.{ .ctx = &dummy, .get = Graded.get });
    const x = Urn.from("hw:/pci/0000:01:00.0/fn0");
    f.onConflict(&x, 1, 100); // prio 1
    f.onConflict(&x, 3, 110); // prio 3 — highest, though it arrived in the middle
    f.onConflict(&x, 2, 120); // prio 2
    f.onRelease(&x, 200);
    try testing.expectEqual(Verdict.proceed, f.reservationVerdict(&x, 3, 210));
    try testing.expectEqual(Verdict.reserved_for_other, f.reservationVerdict(&x, 1, 210));
    try testing.expectEqual(Verdict.reserved_for_other, f.reservationVerdict(&x, 2, 210));
}

test "C3 §5.4: with no priority source set, behavior is exactly FIFO (back-compat)" {
    var f = Fairness.init(testing.allocator); // no setPrioritySource → all priority 0
    defer f.deinit();
    const x = Urn.from("hw:/pci/0000:01:00.0/fn0");
    f.onConflict(&x, 5, 100); // earliest
    f.onConflict(&x, 6, 110);
    f.onRelease(&x, 200);
    // The FIFO front (uid 5) is reserved, identical to the pre-priority core.
    try testing.expectEqual(Verdict.proceed, f.reservationVerdict(&x, 5, 210));
    try testing.expectEqual(Verdict.reserved_for_other, f.reservationVerdict(&x, 6, 210));
}

// ===========================================================================
// §5.4 active holder-preemption — the DECISION + grace-window record. The
// forced-teardown EXECUTION needs the backend + lease table and is pinned end-
// to-end in core_test.zig (`runDuePreemptions`). These pin the policy mechanism:
// who may preempt, the grace window, the graceful-release cancel, and dedup.
// ===========================================================================

test "§5.4 preempt-decision: only a STRICTLY-higher-priority waiter may preempt a holder" {
    var f = Fairness.init(testing.allocator);
    defer f.deinit();
    var tier = TwoTier{ .hi_uid = 2 }; // uid 2 → prio 10, everyone else 0
    f.setPrioritySource(tier.source());
    try testing.expect(f.shouldPreempt(2, 1)); // high waiter vs low holder → yes
    try testing.expect(!f.shouldPreempt(1, 2)); // low waiter vs high holder → no
    try testing.expect(!f.shouldPreempt(1, 3)); // equal (both 0) → no (no same-tier eviction)
    try testing.expect(!f.shouldPreempt(2, 2)); // self never preempts self
}

test "§5.4 preempt back-compat: with no priority source, NOBODY may preempt (pure FIFO)" {
    var f = Fairness.init(testing.allocator); // no source → all prio 0
    defer f.deinit();
    try testing.expect(!f.shouldPreempt(2, 1));
    try testing.expect(!f.shouldPreempt(1, 2));
}

test "§5.4 grace window: a marked preemption is pending, and due ONLY after GRACE_MS" {
    var f = Fairness.init(testing.allocator);
    defer f.deinit();
    const x = Urn.from("hw:/pci/0000:01:00.0/fn0");
    f.markPreemption(&x, 1, 2, 1000); // holder uid1, for uid2, at t=1000 → due at 1000+GRACE_MS
    try testing.expect(f.isPreemptionPending(&x));
    // Before the window passes: nothing due, still pending.
    try testing.expect(f.takeDuePreemption(1000) == null);
    try testing.expect(f.takeDuePreemption(1000 + GRACE_MS - 1) == null);
    try testing.expect(f.isPreemptionPending(&x));
    // At/after the deadline: due exactly once, carrying the right holder + waiter.
    const due = f.takeDuePreemption(1000 + GRACE_MS).?;
    try testing.expectEqual(@as(u32, 1), due.holder_uid);
    try testing.expectEqual(@as(u32, 2), due.for_uid);
    try testing.expect(!f.isPreemptionPending(&x)); // consumed
    try testing.expect(f.takeDuePreemption(1000 + GRACE_MS + 9999) == null); // not handed out twice
}

test "§5.4 graceful release: a voluntary release cancels a pending preemption (no teardown owed)" {
    var f = Fairness.init(testing.allocator);
    defer f.deinit();
    const x = Urn.from("hw:/pci/0000:01:00.0/fn0");
    f.onConflict(&x, 2, 500); // the high-priority waiter is queued
    f.markPreemption(&x, 1, 2, 1000);
    try testing.expect(f.isPreemptionPending(&x));
    // The holder releases within its grace window → the preemption is cancelled.
    f.onRelease(&x, 1500);
    try testing.expect(!f.isPreemptionPending(&x));
    // …and nothing is EVER due, even far past the original deadline (no late teardown).
    try testing.expect(f.takeDuePreemption(1000 + GRACE_MS + 1) == null);
}

test "§5.4 dedup: a re-mark keeps the ORIGINAL deadline (a retry can't extend the holder's grace)" {
    var f = Fairness.init(testing.allocator);
    defer f.deinit();
    const x = Urn.from("hw:/pci/0000:01:00.0/fn0");
    f.markPreemption(&x, 1, 2, 1000); // due at 1000 + GRACE_MS
    f.markPreemption(&x, 1, 2, 1000 + GRACE_MS); // a later retry — must NOT push the deadline back
    // The ORIGINAL deadline still governs: due at 1000 + GRACE_MS, not later.
    const due = f.takeDuePreemption(1000 + GRACE_MS).?;
    try testing.expectEqual(@as(u32, 2), due.for_uid);
}
