-- test_observer_boundary.lua
-- ============================================================================
-- Observer boundary contract — for each Blizzard-event subscription option,
-- assert the BE engine honours it. Each contract has a known historical bug
-- shape; the corresponding case here is the regression test.
--
--   debounce          - coalesces N rapid fires into 1 dispatch
--   requiresMainWindow - skipped while window is hidden
--   filter            - skipped when predicate returns false
--   once              - unsubscribed after first fire
--   action mode       - turns event into Store dispatch with args as payload

---@diagnostic disable: undefined-global
dofile("wow_mock.lua")
dofile("../Core/VDS_Constants.lua")
dofile("../Core/VDS_SchemeConstants.lua")
dofile("../Core/VDS_Paths.lua")
dofile("../Core/VDS_Log.lua")
dofile("../Core/VDS_Store.lua")
dofile("../UI/VDS_BlizzardEvents.lua")

local A = VDS.Constants.ACTIONS
dofile("../Core/VDS_ActionMeta.lua")
VDS.Store:ValidateActionMetaExhaustiveness()
VDS_DB = {}
VDS.Store:LoadFromSavedVariables()

local function assertEq(actual, expected, label)
    if actual ~= expected then
        error(label .. ": expected " .. tostring(expected) .. ", got " .. tostring(actual), 2)
    end
end
local function resetSubs() VDS.BlizzardEvents._subscribers = {}; VDS.BlizzardEvents._debounceTimers = {} end

-- ============================================================================
-- Case 1: debounce coalesces 5 fires → 1 invocation
-- ============================================================================

resetSubs()
local fireCount = 0
local module = { OnBagBurst = function() fireCount = fireCount + 1 end }
VDS.BlizzardEvents:Subscribe("BAG_UPDATE", {
    mode = "handler",
    module = module,
    handlerName = "OnBagBurst",
    opts = { debounce = 0.2 },
    _event = "BAG_UPDATE",
})

MockTimerDeferred(true)
for _ = 1, 5 do VDS.BlizzardEvents:Dispatch("BAG_UPDATE") end
assertEq(fireCount, 0, "debounce: nothing fires before timer drains")
MockTimerDrain()
assertEq(fireCount, 1, "debounce: 5 dispatches coalesce into 1 handler call")
MockTimerDeferred(false)
print("  [OK] case 1: debounce coalesces N fires → 1")

-- ============================================================================
-- Case 2: requiresMainWindow gates while hidden
-- ============================================================================

resetSubs()
VDS.Store:GetState().session.ui.windowShown = false
local mwFireCount = 0
VDS.BlizzardEvents:Subscribe("PLAYER_TARGET_CHANGED", {
    mode = "handler",
    module = { OnTarget = function() mwFireCount = mwFireCount + 1 end },
    handlerName = "OnTarget",
    opts = { requiresMainWindow = true },
    _event = "PLAYER_TARGET_CHANGED",
})
VDS.BlizzardEvents:Dispatch("PLAYER_TARGET_CHANGED")
assertEq(mwFireCount, 0, "requiresMainWindow: skipped while window hidden")

VDS.Store:GetState().session.ui.windowShown = true
VDS.BlizzardEvents:Dispatch("PLAYER_TARGET_CHANGED")
assertEq(mwFireCount, 1, "requiresMainWindow: fires when window shown")
print("  [OK] case 2: requiresMainWindow gates by ui.windowShown")

-- ============================================================================
-- Case 3: filter predicate gates
-- ============================================================================

resetSubs()
local filterFireCount = 0
VDS.BlizzardEvents:Subscribe("CHAT_MSG_LOOT", {
    mode = "handler",
    module = { OnLoot = function() filterFireCount = filterFireCount + 1 end },
    handlerName = "OnLoot",
    opts = { filter = function(msg) return msg and msg:find("epic") end },
    _event = "CHAT_MSG_LOOT",
})
VDS.BlizzardEvents:Dispatch("CHAT_MSG_LOOT", "grey item")
assertEq(filterFireCount, 0, "filter: skipped when predicate false")
VDS.BlizzardEvents:Dispatch("CHAT_MSG_LOOT", "epic drop!")
assertEq(filterFireCount, 1, "filter: fires when predicate true")
print("  [OK] case 3: filter predicate gates dispatch")

-- ============================================================================
-- Case 4: action-mode dispatches Store action with args as payload
-- ============================================================================

resetSubs()
local lastAction = nil
local sub = VDS.Store:Subscribe(function(actionType) lastAction = actionType end)
VDS.BlizzardEvents:Subscribe("BAG_UPDATE_DELAYED", {
    mode = "action",
    opts = { action = A.BAG_TICK },
    _event = "BAG_UPDATE_DELAYED",
})
VDS.BlizzardEvents:Dispatch("BAG_UPDATE_DELAYED")
assertEq(lastAction, A.BAG_TICK, "action mode: dispatches the declared action")
VDS.Store:Unsubscribe(sub)
print("  [OK] case 4: action mode → Store dispatch")

-- ============================================================================
-- Case 5: once unsubscribes after first fire
-- ============================================================================

resetSubs()
local onceCount = 0
VDS.BlizzardEvents:Subscribe("PLAYER_LOGIN", {
    mode = "handler",
    module = { OnLogin = function() onceCount = onceCount + 1 end },
    handlerName = "OnLogin",
    opts = { once = true },
    _event = "PLAYER_LOGIN",
})
VDS.BlizzardEvents:Dispatch("PLAYER_LOGIN")
VDS.BlizzardEvents:Dispatch("PLAYER_LOGIN")
VDS.BlizzardEvents:Dispatch("PLAYER_LOGIN")
assertEq(onceCount, 1, "once: fires exactly once across 3 dispatches")
print("  [OK] case 5: once unsubscribes after first fire")

print("test_observer_boundary.lua passed")
