---@diagnostic disable: undefined-field, undefined-global
---@class Private
local Private = select(2, ...)
local WTUI = Private.WildThingsUI

-- READY-05 cooldown window: rank-0-2 viewer-gated popup with cross-rank
-- broadcast. ANY guildmate in the raid (rank 0-2 AND rank 3+) broadcasts
-- their own per-spec enabled cooldown list as a new WT_MSG sub-event
-- ("READYCHECK_CD"); rank 0-2 viewers coalesce by sender into
-- Private.readycheckCooldownBySender and (when their own auto_popup toggle
-- is ON) auto-open a popup listing (sender, spellName, mm:ss) rows that
-- live-countdown via a receiver-stamped receivedAt timestamp. Rank 3+
-- viewers still receive the broadcast on the wire but their handler
-- returns early, so they neither store data nor open the popup -- the
-- UI capability gate (ViewerHasCapability inside the popup builder and
-- around the SettingsScreen toggle) hides the chrome too. The toggle
-- steuert nur ob [Auto-Popup], per CONTEXT.md.
--
-- The wire half here is intentionally thin: it only knows how to broadcast
-- and how to receive; the visible chrome (popup window, settings toggle)
-- lives in UI/Widgets/PopupWindows/ReadyCheckCooldownPopup.lua and the
-- Settings screen. Two independent gates keep the broadcast leg and the
-- popup-open leg separate:
--   Gate 1 (Private:ShouldBroadcastReadyCheckCooldown): in-raid + READY-05
--                                                    (no rank check)
--   Gate 2 (Private:ShouldShowReadyCheckCooldownPopup): in-raid + READY-05
--                                                    + viewer rank 0-2
--                                                    + auto_popup toggle
-- A rank-0-2 sender with the toggle OFF still broadcasts whenever the
-- READY-05 gate passes -- the toggle only suppresses the local popup.
-- A rank-3+ sender with the toggle ON-or-OFF broadcasts whenever the
-- READY-05 gate passes; their receiver-side handler returns early on
-- ViewerHasCapability so they neither store data nor open the popup.
--
-- Private.* deps: Private:Broadcast (Transmission.lua), Private:RegisterCommEvent
-- (Init.lua), Private:ViewerHasCapability (Access.lua), Private:CanonicalizeLiveName
-- (RosterKeys.lua), Private:SnapshotRaidStatus (RaidSnapshot.lua),
-- Private:ShouldRunReadyCheckCooldown (ReadyCheck.lua), Private:CollectOwnCooldownStatuses
-- (this file, defined below).

-- ============================================================
-- Sender-side cooldown read (Task 1 step (3)(a))
--
-- Mirrors ReadyCheck.lua's local GetSpellRemaining (charges-aware) and
-- GetCooldownStatuses (per-spec enabled-set iteration), but DROPS the
-- 30s threshold filter and the {onCD, name, remaining} wrapper -- the
-- receiver's window is for visual countdown of every enabled spell,
-- including short timers, so the sender must report the full list.
-- ============================================================

-- Charges-aware remaining read. Identical to ReadyCheck.lua's
-- GetSpellRemaining; inlined here so this file's load order does not
-- require ReadyCheck.lua to expose a public helper.
local function GetSpellRemaining(spellID)
    local chargeInfo = C_Spell.GetSpellCharges(spellID)
    if chargeInfo then
        if chargeInfo.currentCharges >= 1 then return 0 end
        if chargeInfo.isActive and chargeInfo.cooldownDuration > 0 then
            return math.max(0, (chargeInfo.cooldownStartTime + chargeInfo.cooldownDuration) - GetTime())
        end
        return 0
    end

    local cd = C_Spell.GetSpellCooldown(spellID)
    if cd and cd.isEnabled and cd.isActive and cd.duration > 0 then
        return math.max(0, (cd.startTime + cd.duration) - GetTime())
    end
    return 0
end

-- Returns an array of {spellID, name, remaining, specID} for every
-- enabled per-spec spell, with NO threshold filter. The popup's
-- receiver-side display sorts by live remaining desc; a 0-remaining
-- enabled spell still belongs in the list (the popup fades it after
-- a brief grace window).
--
-- specID here is the sender's CURRENT specialization ID -- stamped
-- on every per-spell entry so the receiver can render a spec icon
-- next to each row (per user feedback 2026-07-26 "Spec Icon fehlt"
-- -- the spec atlas distinguishes e.g. Frost vs Fire for a Mage, which
-- the class icon alone cannot). A single specID is shared across
-- all entries in the payload (every entry comes from the same spec)
-- -- but the per-entry shape lets a future enhancement broadcast
-- cross-spec entries (e.g. a Mage talent that switches to Arcane)
-- without a payload-shape change.
function Private:CollectOwnCooldownStatuses()
    local statuses = {}

    local specIndex = GetSpecialization()
    local currentSpecID = nil
    if specIndex then
        -- BUG-FIX 2026-07-26 + 2026-07-27:
        --   The modern C_SpecializationInfo.GetSpecializationInfo
        --   nominally returns a TABLE {id, name, description, ...},
        --   but on some retail WoW builds it returns a plain NUMBER
        --   (the specID) directly -- `local info = C_SpecializationInfo.GetSpecializationInfo(specIndex)`
        --   then leaves info as a number, and `info.id` throws
        --   "attempt to index a number value". The legacy global
        --   GetSpecializationInfo specIndex returns multi-values
        --   (id, name, description, icon, role, classFile), never a
        --   table. Handle all three forms with one defensive lookup:
        --   try C_SpecializationInfo first, branch on the return
        --   TYPE; fall back to select(1, ...) on the global.
        if C_SpecializationInfo and C_SpecializationInfo.GetSpecializationInfo then
            local info = C_SpecializationInfo.GetSpecializationInfo(specIndex)
            if type(info) == "table" and info.id then
                currentSpecID = info.id
            elseif type(info) == "number" then
                currentSpecID = info
            end
        end
        if not currentSpecID then
            currentSpecID = select(1, GetSpecializationInfo(specIndex))
        end
    end
    if not currentSpecID then return statuses end

    local rc = WildThingsSavedVars.readycheck
    local perSpec = rc and rc.cooldown and rc.cooldown.per_spec and rc.cooldown.per_spec[currentSpecID]
    if not perSpec then return statuses end

    for spellID, isEnabled in pairs(perSpec) do
        if isEnabled then
            local remaining = GetSpellRemaining(spellID)
            local name = C_Spell.GetSpellName(spellID) or ("Spell " .. tostring(spellID))
            table.insert(statuses, {
                spellID = spellID,
                name = name,
                remaining = remaining,
                specID = currentSpecID,
            })
        end
    end

    return statuses
end

-- ============================================================
-- Two independent gates (Task 1 steps (3)(b) and (3)(c))
-- ============================================================

-- GATE 1: broadcast eligibility -- INDEPENDENT of auto_popup AND of the
-- sender's own guild rank. Any guildmate in the raid who passes the
-- READY-05 gate broadcasts, full stop -- rank-3+ senders are explicitly
-- allowed (revised 2026-07-26 after user clarification: only the
-- VIEWER's rank gates the popup, not the sender's). The local
-- auto_popup toggle controls ONLY whether the local popup auto-opens
-- (GATE 2), not whether the local sender broadcasts. Sender-side guild
-- trust (same guild + same realm) is enforced on the RECEIVER side, not
-- here -- this gate has no GetGuildInfo lookup because anyone who is
-- "in the raid" and on WT-Blackhand passes the implicit guild check via
-- the broadcast's own UnitInRaid delivery path.
function Private:ShouldBroadcastReadyCheckCooldown()
    if not IsInRaid() then return false end
    if not Private:ShouldRunReadyCheckCooldown() then return false end
    return true
end

-- GATE 2: popup-open eligibility -- ADDS the viewer's rank (must be 0-2
-- in WT-Blackhand) AND the local auto_popup toggle on top of GATE 1.
-- A rank-3+ viewer who has the toggle ON still receives broadcasts on
-- the wire (the broadcast leg is in GATE 1) but the receiver handler
-- returns early on ViewerHasCapability so they neither store data nor
-- open the popup. A rank-0-2 viewer with the toggle OFF broadcasts but
-- does NOT auto-open the popup on READY_CHECK. The toggle is exclusively
-- a local display preference; the viewer-rank gate is the locked
-- CONTEXT.md "Nur Rank 0-2 sieht Toggle & Fenster" decision.
function Private:ShouldShowReadyCheckCooldownPopup()
    if not Private:ShouldBroadcastReadyCheckCooldown() then return false end
    if not Private:ViewerHasCapability("readycheck.cooldownwindow") then return false end
    local rc = WildThingsSavedVars.readycheck
    if not (rc and rc.cooldownwindow) then return false end
    if rc.cooldownwindow.auto_popup ~= true then return false end
    return true
end

-- ============================================================
-- Sender broadcast (Task 1 step (3)(d))
-- ============================================================

function Private:BroadcastOwnReadyCheckCooldowns()
    if not Private:ShouldBroadcastReadyCheckCooldown() then return end

    local payload = Private:CollectOwnCooldownStatuses()
    if #payload == 0 then return end

    Private:Broadcast("READYCHECK_CD", "RAID", payload)
end

-- ============================================================
-- Local-read path for the popup (Task 1 step (3)(f), per
-- RESOLVED Open Question 2 in RESEARCH.md)
--
-- The local player never appears in the wire payload because
-- Private:Broadcast over Transmission.lua:79 self-skips. The popup
-- must still show the local player's cooldowns, so this function
-- writes them into the same coalescing map under the explicit
-- "player" key. Re-fetched on every popup open so a freshly-used
-- cooldown shows up at 0 immediately, not stale at the previous
-- broadcast's value.
-- ============================================================

function Private:CollectAndStoreLocalPlayerCooldowns()
    local payload = Private:CollectOwnCooldownStatuses()
    -- BUG-FIX 2026-07-26 (countdown not ticking): stamp `receivedAt`
    -- on every per-spell entry so the popup's LiveRemaining formula
    -- has a non-nil timestamp to subtract from. The sender-level
    -- `receivedAt` set below is for the 30s GC, not the countdown.
    local now = GetTime()
    for _, e in ipairs(payload) do
        if type(e) == "table" then
            e.receivedAt = now
        end
    end
    Private.readycheckCooldownBySender = Private.readycheckCooldownBySender or {}
    Private.readycheckCooldownBySender["player"] = {
        senderName = UnitName("player"),
        senderClass = select(2, UnitClass("player")),
        entries = payload,
        lastSeen = now,
        receivedAt = now, -- freshly read; per-spell receivedAt == now so the live formula is a no-op on first tick, then decrements
    }
end

-- ============================================================
-- Coalescing map (Task 1 step (3)(g))
--
-- Receivers write under Private:CanonicalizeLiveName(sender) --
-- the cross-realm / same-realm canonical key from RosterSnapshot.lua
-- so two clients on the same connected realm share a key. Each
-- receiver-written entry is stamped with receivedAt = GetTime()
-- at parse time so the popup's live countdown subtracts wall-clock
-- elapsed (B3 fix). lastSeen drives the 30s GC: a sender whose
-- lastSeen is older than 30s is dropped, both at receive time and
-- at render time, so a sender who left the raid freezes no row.
-- ============================================================

Private.readycheckCooldownBySender = {}

-- Live-remaining helper (Task 1 step (3)(i)). Used by every display,
-- sort, expiry, and /pull XX arithmetic site in the popup. Local
-- entries (under "player") have receivedAt == GetTime() at the
-- moment of write and use remaining directly -- but the popup calls
-- this unconditionally on every row (cheap, and a no-op when
-- receivedAt == now).
function Private:ComputeLiveRemaining(entry)
    if type(entry) ~= "table" then return 0 end
    local receivedAt = entry.receivedAt or GetTime()
    return math.max(0, (entry.remaining or 0) - (GetTime() - receivedAt))
end

-- 30s GC for the coalescing map. Called at receive time and at
-- render time (the popup's refresh fn walks the map and drops any
-- sender whose lastSeen is older than 30s).
local SENDER_GC_SECONDS = 30
function Private:GcStaleReadyCheckCooldownEntries()
    local map = Private.readycheckCooldownBySender
    if type(map) ~= "table" then return end
    local now = GetTime()
    for key, entry in pairs(map) do
        if type(entry) == "table" and entry.lastSeen and (now - entry.lastSeen) > SENDER_GC_SECONDS then
            map[key] = nil
        end
    end
end

-- ============================================================
-- Receiver handler (Task 1 step (3)(h))
--
-- Registers a "READYCHECK_CD" handler via Private:RegisterCommEvent.
-- Sender comes from the AceComm arg (Transmission.lua:80, "sender
-- comes from the wire" rule), NEVER from the payload -- a self-
-- asserted senderName field is a self-spoof vector. The receiver's
-- two trust gates (revised 2026-07-26 after user clarification that
-- Rank 3+ senders should still be visible to Rank 0-2 viewers):
--   (a) Viewer-side rank gate (ViewerHasCapability on LOCAL viewer)
--       -- a Rank 3+ viewer's own client must not store incoming
--       broadcast data, regardless of the sender's rank. This is the
--       "nur nicht empfangen" half of the user's clarification.
--   (b) Sender-side guild gate (inline GetGuildInfo comparison) -- the
--       sender must be in OUR guild on OUR realm. This preserves the
--       non-WT-member / cross-realm-spoof protection that the original
--       SenderHasCapability provided, but WITHOUT the rank filter on
--       the sender. A Rank 3+ WT-Blackhand sender is accepted; a
--       non-WT-Blackhand sender (or a same-named-elsewhere sender) is
--       rejected on the same guild/realm grounds.
-- Self-messages are already dropped centrally by Transmission.lua:79.
--
-- Clock-drift assumption (documented inline, RESEARCH Pitfall 4):
-- broadcast flight time ~0-1s is well within a "live countdown"
-- tolerance -- receivedAt is GetTime() at the moment the receiver
-- finishes parsing the entry, NOT GetTime() at sender-broadcast time.
-- ============================================================

Private:RegisterCommEvent("READYCHECK_CD", function(sender, payload)
    -- T-01: payload must be an array
    if type(payload) ~= "table" then return end

    -- T-r3x-01 (revised): VIEWER-side rank gate. Rank 3+ viewers must
    -- not store incoming data (per user clarification 2026-07-26).
    -- The capability key is "readycheck.cooldownwindow" with maxRank=2,
    -- so this is identical to the existing UI gating in the popup
    -- builder and the SettingsScreen wrap.
    if not Private:ViewerHasCapability("readycheck.cooldownwindow") then return end

    -- T-r3x-01 (cont.): SENDER-side guild gate. Inline because the
    -- rank check in SenderHasCapability is no longer the right filter
    -- (we want any-rank-WT to be accepted). Two-line lookup against
    -- GetGuildInfo("player") is enough; no cross-realm or same-name
    -- ambiguity survives this comparison.
    local myGuild, _, _, myRealm = GetGuildInfo("player")
    if not myGuild then return end
    local senderGuild, _, _, senderRealm = GetGuildInfo(sender)
    if senderGuild ~= myGuild or senderRealm ~= myRealm then return end

    -- T-r3x-02: per-entry numeric-shape validation. Mirrors the
    -- SummonComm.lua:345-347 hardening precedent. Negative remaings
    -- are clamped to 0 (a sender-stamped -5 is treated as 0, not as a
    -- "skip this entry" signal -- the popup's live formula re-clamps
    -- anyway, this is just the wire-time guard).
    --
    -- BUG-FIX 2026-07-26 (countdown not ticking): each per-spell entry
    -- MUST be stamped with its own `receivedAt` here. The popup's
    -- LiveRemaining reads the per-entry `receivedAt` -- if it's nil it
    -- falls back to `GetTime()`, making `(now - receivedAt) == 0` and
    -- the countdown never decrements. Sender-level `receivedAt` (set a
    -- few lines below) is not consulted by LiveRemaining -- the per-
    -- spell stamp is the only thing that drives the tick.
    local entries = {}
    local now = GetTime()
    for _, e in ipairs(payload) do
        if type(e) == "table"
            and type(e.spellID) == "number"
            and type(e.name) == "string"
            and type(e.remaining) == "number" then
            table.insert(entries, {
                spellID = e.spellID,
                name = e.name,
                remaining = math.max(0, e.remaining),
                specID = (type(e.specID) == "number") and e.specID or nil,
                receivedAt = now,
            })
        end
    end

    -- T-r3x-02 (cont.): class lookup uses CanonicalizeLiveName so a
    -- malformed sender name cannot crash SnapshotRaidStatus.
    local key = Private:CanonicalizeLiveName(sender)
    local class
    local raidStatus = Private:SnapshotRaidStatus()
    if raidStatus and raidStatus[key] then class = raidStatus[key].class end

    -- Coalesce. receivedAt at the SENDER level drives the 30s GC
    -- (GatherStaleReadyCheckCooldownEntries reads entry.lastSeen which is
    -- set just below -- the per-spell receivedAt stamps above drive the
    -- countdown ticks; the two clocks happen to coincide at parse time
    -- but they are NOT the same thing semantically).
    Private.readycheckCooldownBySender = Private.readycheckCooldownBySender or {}
    Private.readycheckCooldownBySender[key] = {
        senderName = sender,
        senderClass = class,
        entries = entries,
        lastSeen = now,
        receivedAt = now,
    }

    -- GC stale entries on receive: a sender who left the raid no longer
    -- broadcasts, so their lastSeen drifts past 30s and gets dropped at
    -- the next receive -- keeps a frozen row from sticking on screen.
    Private:GcStaleReadyCheckCooldownEntries()
end)

-- ============================================================
-- Orchestrator + popup open (Task 1 steps (3)(e) and (3)(k))
--
-- The orchestrator is defined here (B2 fix, not in the popup file)
-- so Core.lua / ReadyCheck.lua have a single, capability-checks-aware
-- entry point. The popup chrome is lazy-built on first open via
-- namespace:CreateReadyCheckCooldownPopup, which is defined in
-- UI/Widgets/PopupWindows/ReadyCheckCooldownPopup.lua (Task 2) and
-- becomes available once that file has loaded (UI/UI.xml loads after
-- the toc Lua files, so by the first READY_CHECK the popup builder
-- is registered).
-- ============================================================

function Private:BroadcastAndShowReadyCheckCooldownWindow()
    -- Gate 1 (broadcast) runs first -- independent of the local
    -- auto_popup toggle AND independent of the local viewer's rank
    -- (any rank broadcasts). A rank-3+ sender with the toggle ON
    -- or OFF still broadcasts whenever the READY-05 gate passes.
    if Private:ShouldBroadcastReadyCheckCooldown() then
        Private:BroadcastOwnReadyCheckCooldowns()
    end

    -- Gate 2 (popup-open) ADDS the local viewer's rank 0-2 AND the
    -- local auto_popup toggle. Only open the popup when the viewer
    -- is rank 0-2 in WT-Blackhand, has the toggle on, and the
    -- READY-05 gate passes.
    if Private:ShouldShowReadyCheckCooldownPopup() then
        Private:OpenReadyCheckCooldownPopup()
    end
end

-- Defined here (B2 fix) so the orchestrator has a single, capability-
-- checked entry point. Body is intentionally minimal: capability-gate,
-- lazy popup build, state refresh, restart the 1 Hz driver, Show.
--
-- BUG-FIX 2026-07-26: the original code called `:Pop()` here, which is
-- the CreatePopupWindow API. CreateChromeWindow does NOT expose Pop --
-- it positions the window via RestorePosition / SetPoint("CENTER")
-- inside the primitive and returns it shown-by-default. Calling Pop
-- on the chrome window threw "attempt to call a nil value" on every
-- OpenReadyCheckCooldownPopup call (which is on every Ready Check +
-- the cooldowntest popup path). Replaced with :Show() -- the chrome
-- window is already positioned and only needs to be un-hidden.
function Private:OpenReadyCheckCooldownPopup()
    if not Private:ViewerHasCapability("readycheck.cooldownwindow") then return end

    -- Lazy build on first call. The popup file (UI/UI.xml) loads
    -- AFTER this file's TOC slot, so Private.CreateReadyCheckCooldownPopup
    -- may be nil at the very first READY_CHECK of a session -- the
    -- capability check above + this nil-guard together make that
    -- path a no-op without error. The next READY_CHECK re-evaluates.
    if not Private.readycheckCooldownPopup then
        if not Private.CreateReadyCheckCooldownPopup then return end
        Private.readycheckCooldownPopup = Private:CreateReadyCheckCooldownPopup()
        -- CreateChromeWindow returns the popup SHOWN (no Hide in the
        -- primitive); explicitly hide it here so the popup stays
        -- invisible until the first :Show() in this function fires.
        -- The orchestrator's :Show() at the end of this function is
        -- the only path that brings it on screen.
        if Private.readycheckCooldownPopup.Hide then
            Private.readycheckCooldownPopup:Hide()
        end
    end

    -- Refresh state on every open (W8 fix: must restart the 1 Hz
    -- driver unconditionally, not only at first build, so a popup
    -- that was hidden then re-shown after the previous StartGlow's
    -- owner reference has gone stale still ticks). Re-fetch the
    -- local player's cooldowns so a freshly-used cooldown shows up
    -- at 0 immediately rather than at a stale broadcast value.
    Private:CollectAndStoreLocalPlayerCooldowns()

    -- The popup exposes an optional Refresh() that re-renders rows
    -- from the current map. Call it if exposed so the very first
    -- paint after open shows the post-Open state (not the prior
    -- frame's body).
    if Private.readycheckCooldownPopup.Refresh then
        Private.readycheckCooldownPopup:Refresh()
    end

    -- Restart the 1 Hz countdown driver on EVERY open (W8 fix: must
    -- restart unconditionally, not only at first build, because the
    -- popup may have been hidden then re-shown and the previous
    -- StartGlow's owner reference can be stale -- calling StartGlow
    -- again with the same owner+id is idempotent per GlowDriver.lua:75
    -- and updates the updateFn in place).
    if Private.readycheckCooldownPopup.CountdownTick then
        WTUI:StartGlow(Private.readycheckCooldownPopup, "countdown", Private.readycheckCooldownPopup.CountdownTick)
    end

    Private.readycheckCooldownPopup:Show()
end

-- ============================================================
-- /wt cooldowntest dev command (2026-07-26)
--
-- Single-client solo-test harness for the cooldown window. Bypasses
-- the wire (Transmission.lua:85 self-skip makes single-client
-- comm loopback impossible by construction) by either injecting
-- fixture entries directly into the coalescing map OR by calling
-- the broadcast entry point so a second WoW client on the same
-- machine receives them on WT_MSG.
--
-- Modes:
--   send                       Broadcast current local cooldowns
--                              (same path RunCooldownCheck uses; the
--                              second client will see them via WT_MSG
--                              if it's in the same raid).
--   inject                     Inject a fixture set of 4 entries
--                              (3 remote-style + 1 local "player")
--                              with varying remaining times so the
--                              popup rendering, sorting, and fade
--                              behavior can be exercised without a
--                              second client.
--   popup                      Open the popup directly (uses
--                              whatever is in the coalescing map).
--   clear                      Wipe Private.readycheckCooldownBySender.
--   (no mode)                  Print current state + mode help.
--
-- Safe ungated for the same reasons as summonstest/inspecttest:
-- ReadyCheckCooldownComm.lua ships in every build, the test does
-- not cross any trust boundary (inject writes only to a local
-- Private table; send reuses the existing capability-gated broadcast
-- entry point; popup/clear are pure local UI). Wrapping it in a
-- dev-build sentinel would make it a silent no-op in exactly the
-- packaged build whose behaviour it exists to prove.
-- ============================================================

function Private:CooldownTest(mode)
    mode = tostring(mode or ""):lower()

    if mode == "" or mode == "help" then
        local n = 0
        if type(Private.readycheckCooldownBySender) == "table" then
            for _ in pairs(Private.readycheckCooldownBySender) do n = n + 1 end
        end
        print("|cff99ff00Wild Things:|r cooldowntest: " .. n .. " senders in coalescing map")
        print("|cff99ff00Wild Things:|r Modes: send | inject | popup | clear")
        return
    end

    if mode == "send" then
        Private:BroadcastOwnReadyCheckCooldowns()
        print("|cff99ff00Wild Things:|r cooldowntest: broadcast fired (RAID channel). Second client must be in the same raid to receive.")
        return
    end

    if mode == "popup" then
        Private:OpenReadyCheckCooldownPopup()
        print("|cff99ff00Wild Things:|r cooldowntest: popup opened (uses whatever is in the coalescing map)")
        return
    end

    if mode == "clear" then
        Private.readycheckCooldownBySender = {}
        print("|cff99ff00Wild Things:|r cooldowntest: coalescing map cleared")
        return
    end

    if mode == "inject" then
        local now = GetTime()
        local fakeClass = select(2, UnitClass("player")) or "WARRIOR"
        -- 4 entries: 3 remote senders + the local "player" key. Mix of
        -- classes so the class-color rendering is exercised; specIDs
        -- are real specialization IDs so the spec icon renders a real
        -- atlas. Remaining times chosen to exercise the mm:ss formatter,
        -- the sort order, and the fade-out boundary (60s is still
        -- ticking; 5s is short; 0s is past the fade window -- row should
        -- be hidden after FADE_DELAY).
        Private.readycheckCooldownBySender = Private.readycheckCooldownBySender or {}
        Private.readycheckCooldownBySender["testpaladin-blackhand"] = {
            senderName = "Testpaladin",
            senderClass = "PALADIN",
            entries = {
                { spellID = 31884, name = "Avenging Wrath", remaining = 60, specID = 65 }, -- 65 = Retribution Paladin
            },
            lastSeen = now,
            receivedAt = now,
        }
        Private.readycheckCooldownBySender["testmage-blackhand"] = {
            senderName = "Testmage",
            senderClass = "MAGE",
            entries = {
                { spellID = 12051, name = "Evocation", remaining = 30, specID = 62 }, -- 62 = Arcane Mage
                { spellID = 190319, name = "Combustion", remaining = 5, specID = 62 },
            },
            lastSeen = now,
            receivedAt = now,
        }
        Private.readycheckCooldownBySender["testhunter-blackhand"] = {
            senderName = "Testhunter",
            senderClass = "HUNTER",
            entries = {
                { spellID = 19574, name = "Bestial Wrath", remaining = 0, specID = 253 }, -- 253 = Beast Mastery Hunter
            },
            lastSeen = now,
            receivedAt = now - 60,
        }
        Private.readycheckCooldownBySender["player"] = {
            senderName = UnitName("player") or "Player",
            senderClass = fakeClass,
            entries = {
                { spellID = 31884, name = "Avenging Wrath", remaining = 45, specID = 102 }, -- 102 = Balance Druid (current spec for this char)
            },
            lastSeen = now,
            receivedAt = now,
        }
        print("|cff99ff00Wild Things:|r cooldowntest: injected 4 senders (3 remote + local 'player') -- run /wt cooldowntest popup to view")
        return
    end

    print("|cff99ff00Wild Things:|r cooldowntest: unknown mode '" .. mode .. "' (try 'send' | 'inject' | 'popup' | 'clear')")
end
