-- Smoke test: Phase 11.0 observer module skeletons.
-- Verifies HDG.BagObserver scans a stubbed bag and HDG.ItemNameResolver
-- batches ItemEventListener callback fires over a debounce window.

dofile("wow_mock.lua")
dofile("../Core/HDGR_SchemeConstants.lua")
dofile("../Core/HDGR_Constants.lua")
dofile("../Core/HDGR_Actions.lua")
dofile("../Core/HDGR_Resolvers.lua")
dofile("../Core/HDGR_Format.lua")
dofile("../Core/HDGR_Paths.lua")
dofile("../Core/HDGR_Log.lua")
dofile("../Core/HDGR_StoreFurnishings.lua")
dofile("../Core/HDGR_Store.lua")
-- Selectors: Store unconditionally calls HDG.Selectors:InvalidateMemos on every
-- _RawDispatch (no-guards policy). Test must load Selectors so the registry exists.
dofile("../UI/HDGR_Selectors.lua")
HDG_DB = {}
HDG.Store:LoadFromSavedVariables()

-- Stub HDG.Modules so the module-registration block in each observer
-- doesn't fail under test (real Modules engine is exercised in-game).
HDG.Modules = HDG.Modules or {}
HDG.Modules._decls = {}
function HDG.Modules:Declare(def)
    self._decls[def.name] = def
end

-- Stub C_Item.GetItemCount for the pull-on-demand BagObserver. Mock
-- returns total accessible (bag + bank + warband) per the all-flags
-- semantic the real Blizzard API uses. Items absent from the mock map
-- return 0 (matching API behavior).
local _itemCounts = { [100] = 8, [101] = 1 }   -- 999 omitted -> returns 0
_G.C_Item = _G.C_Item or {}
_G.C_Item.GetItemCount = function(itemID, includeBank, includeUses, includeReagentBank, includeAccountBank)
    return _itemCounts[itemID] or 0
end

-- Mock Blizzard's ItemEventListener (AsyncCallbackSystemMixin instance).
-- Records callback registrations so we can verify per-id dedup, and lets us
-- fire callbacks on demand to simulate the item data landing.
local _ielCallbacks = {}     -- [itemID] = { fn1, fn2, ... }
_G.ItemEventListener = {
    AddCallback = function(_, itemID, fn)
        _ielCallbacks[itemID] = _ielCallbacks[itemID] or {}
        table.insert(_ielCallbacks[itemID], fn)
    end,
}
local function fireItemEventListener(itemID)
    local list = _ielCallbacks[itemID]
    if not list then return end
    for _, fn in ipairs(list) do fn() end
    _ielCallbacks[itemID] = nil
end

-- Stub HDG.StaticData.Reagents for ResolveName's first-pass probe, and
-- Professions for the BagObserver counts-proxy quality-variant lookup (these
-- mock items aren't tiered -> no siblings -> proxy == plain GetTotal).
HDG.StaticData = HDG.StaticData or {}
HDG.StaticData.Reagents = HDG.StaticData.Reagents or { GetAll = function() return {} end }
HDG.StaticData.Professions = HDG.StaticData.Professions or { GetQualityVariants = function() return nil end }

dofile("../Modules/HDGR_BagObserver.lua")
dofile("../Modules/HDGR_ItemNameResolver.lua")

local function assertEq(actual, expected, label)
    if actual ~= expected then
        error(label .. ": expected " .. tostring(expected) .. ", got " .. tostring(actual), 2)
    end
end
local function assertTrue(v, label) if not v then error(label, 2) end end

-- BagObserver: pull-on-demand. GetTotal queries Blizzard's API directly;
-- the legacy GetCounts() proxy answers via __index for backward compat.
local B = HDG.BagObserver
assertTrue(B ~= nil, "BagObserver namespace exists")
assertEq(B:GetTick(), 0, "tick starts at 0")
assertEq(B:GetTotal(100), 8, "GetTotal queries via C_Item.GetItemCount")
assertEq(B:GetTotal(101), 1, "GetTotal returns mocked count")
assertEq(B:GetTotal(999), 0, "absent itemID returns 0 (not nil)")
B:Scan()
local counts = B:GetCounts()
assertEq(counts[100], 8, "counts proxy delegates to GetTotal (5+3 mocked total)")
assertEq(counts[101], 1, "counts proxy itemID 101")
assertEq(counts[999], 0, "counts proxy absent itemID returns 0")
assertEq(B:GetTick(), 1, "tick incremented after Scan")

-- Module declaration registered.
assertTrue(HDG.Modules._decls.BagObserver ~= nil, "BagObserver Module:Declare ran")
assertTrue(HDG.Modules._decls.BagObserver.blizzardEvents.BAG_UPDATE ~= nil,
           "BAG_UPDATE subscribed by BagObserver")
assertEq(HDG.Modules._decls.BagObserver.blizzardEvents.BAG_UPDATE.debounce, 0.2,
         "BAG_UPDATE debounce = 0.2 (DEBOUNCE_REQUIRED)")

-- ItemNameResolver: batches itemIDs across multiple GET_ITEM_INFO_RECEIVED fires,
-- flushes when timer fires (mocked C_Timer fires synchronously).
local R = HDG.ItemNameResolver
assertTrue(R ~= nil, "ItemNameResolver namespace exists")
assertEq(R:GetTick(), 0, "resolver tick starts at 0")

-- Defer timer execution so we can verify batching before drain.
_G.MockTimerDeferred(true)
R:OnItemInfoReceived(200, true)
R:OnItemInfoReceived(201, true)
R:OnItemInfoReceived(200, true)            -- duplicate itemID -> deduped in batch
R:OnItemInfoReceived(202, false)           -- success=false -> dropped
local pending = R:GetPending()
assertTrue(pending[200], "200 pending")
assertTrue(pending[201], "201 pending")
assertTrue(pending[202] == nil, "202 (success=false) dropped before pending")
assertEq(R:GetTick(), 0, "tick unchanged before drain")

_G.MockTimerDrain()
assertEq(R:GetTick(), 1, "tick bumped after drain")
assertEq(next(R:GetPending()), nil, "pending cleared after drain")
_G.MockTimerDeferred(false)

-- Module declaration registered (no blizzardEvents -- canonical path uses
-- ItemEventListener per-id callbacks registered from cache-miss in
-- ResolveName/ResolveIcon, not middleware subscription).
assertTrue(HDG.Modules._decls.ItemNameResolver ~= nil,
           "ItemNameResolver Module:Declare ran")
assertTrue(HDG.Modules._decls.ItemNameResolver.blizzardEvents == nil,
           "ItemNameResolver does NOT subscribe via blizzardEvents (canonical: ItemEventListener)")

-- ResolveName cache-miss path registers exactly one ItemEventListener callback
-- per itemID, deduped across repeated calls (the typical selector re-fire case).
_G.GetItemInfo = function() return nil end  -- force miss
local name, resolved = R:ResolveName(300)
assertEq(name, "item 300", "miss path returns placeholder")
assertTrue(resolved == false, "miss path returns resolved=false")
assertTrue(R._callbacks[300] == true, "miss path registered _callbacks tracker")
assertEq(#_ielCallbacks[300], 1, "ItemEventListener:AddCallback called once for 300")
R:ResolveName(300)   -- second call while callback in flight
assertEq(#_ielCallbacks[300], 1, "duplicate ResolveName does not register a second callback")

-- Firing the callback clears the tracker and feeds OnItemInfoReceived; the
-- batch + drain path remains the same as the direct-call assertions above.
_G.MockTimerDeferred(true)
fireItemEventListener(300)
assertTrue(R._callbacks[300] == nil, "_callbacks tracker cleared by callback fire")
assertTrue(R:GetPending()[300] == true, "callback fed OnItemInfoReceived -> _pending")
_G.MockTimerDrain()
assertEq(R:GetTick(), 2, "tick bumped by post-callback drain (was 1 from direct test above)")
_G.MockTimerDeferred(false)

print("test_observers_smoke.lua passed")
