if not _G.unpack then _G.unpack = table.unpack end

-- Slash command stubs. Init.lua writes to these globals at file level.
_G.SlashCmdList = _G.SlashCmdList or {}
-- GetLocale returns the client locale string.
_G.GetLocale = _G.GetLocale or function() return "enUS" end
-- UIParent stub (needed by CreateFrame("Frame", ..., UIParent)).
_G.UIParent = _G.UIParent or {}

function _G.wipe(t)
    for k in pairs(t) do
        t[k] = nil
    end
    return t
end

function _G.time()
    return _G.__VFNTestTime or 1000
end

function _G.GetTime()
    return _G.__VFNTestTime or 1000.0
end

function _G.print()
end

-- C_Timer mock. Default: synchronous (callbacks run immediately) so that
-- tests can assert post-callback state without explicit drain calls.
-- Tests that need to observe deferred behavior (debounce coalescing, etc.)
-- call MockTimerDeferred(true), fire events, then MockTimerDrain() to flush.
_G._mockTimerDeferredMode = false
_G._mockTimerQueue = {}

_G.MockTimerDeferred = function(on) _G._mockTimerDeferredMode = on end

_G.MockTimerDrain = function()
    local queue = _G._mockTimerQueue
    _G._mockTimerQueue = {}
    for _, cb in ipairs(queue) do cb() end
end

_G.C_Timer = {
    NewTimer = function(_, callback)
        if _G._mockTimerDeferredMode then
            table.insert(_G._mockTimerQueue, callback)
        else
            callback()
        end
        return { Cancel = function() end }
    end,
    After = function(_, callback)
        if _G._mockTimerDeferredMode then
            table.insert(_G._mockTimerQueue, callback)
        else
            callback()
        end
    end,
    NewTicker = function(_, callback)
        if _G._mockTimerDeferredMode then
            table.insert(_G._mockTimerQueue, callback)
        else
            callback()
        end
        return { Cancel = function() end }
    end,
}

_G.CreateFrame = function()
    -- Per-frame mutable state for shown-tracking (test_window_smoke verifies
    -- Show/Hide reconciliation; previously a flat IsShown stub was always-true).
    local self = { _shown = true }
    self.RegisterEvent       = function() end
    self.UnregisterEvent     = function() end
    self.UnregisterAllEvents = function() end
    self.SetScript           = function() end
    -- WowStyle1DropdownTemplate / WowStyle1FilterDropdownTemplate methods.
    self.SetupMenu              = function() end
    self.GenerateMenu           = function() end
    self.SetDefaultText         = function() end
    self.SetSelectionTranslator = function() end
    self.SetText                = function() end
    self.SetHeight              = function() end
    -- Floating-window infra methods (HDG.Window:_CreateOne). Default Frame
    -- API; no-ops in tests. Show/Hide flip the _shown flag so IsShown is
    -- introspectable for reconciler smoke tests.
    self.SetSize          = function() end
    self.SetPoint         = function() end
    self.ClearAllPoints   = function() end
    self.EnableMouse      = function() end
    self.SetMovable       = function() end
    self.RegisterForDrag  = function() end
    self.SetFrameStrata     = function() end   -- MEDIUM strata (window-composition layering)
    self.SetToplevel        = function() end   -- raise-on-click focus
    self.SetClampedToScreen = function() end   -- keep draggable but on-screen
    self.StartMoving      = function() end
    self.StopMovingOrSizing = function() end
    self.Show             = function(f) f._shown = true end
    self.Hide             = function(f) f._shown = false end
    self.IsShown          = function(f) return f._shown end
    self.SetShown         = function(f, shown) f._shown = shown and true or false end
    self.SetAllPoints     = function() end
    self.SetFrameLevel    = function() end
    self.GetFrameLevel    = function() return 0 end
    self.GetLeft          = function() return 0 end
    self.GetTop           = function() return 100 end
    return self
end

-- MenuConstants stub for tests that load HDGR_Components without WoW's
-- Blizzard_Menu loaded (the dropdown widget uses MenuConstants only at click
-- time, but the global must exist for static analysis paths).
_G.MenuConstants = _G.MenuConstants or {
    VerticalLinearDirection = 1,
    VerticalGridDirection   = 2,
    HorizontalGridDirection = 3,
}

-- hooksecurefunc mock: stores the hook in a registry so tests can fire it.
_G._mockHooks = _G._mockHooks or {}
_G.hooksecurefunc = function(funcName, callback)
    _G._mockHooks[funcName] = _G._mockHooks[funcName] or {}
    table.insert(_G._mockHooks[funcName], callback)
end
_G.MockFireHook = function(funcName, ...)
    for _, cb in ipairs(_G._mockHooks[funcName] or {}) do cb(...) end
end

-- ===== Strict-mock helpers ==================================================
--
-- Tests that mount real UI build their own newFrame factory (test_mainframe_*).
-- This shared allowlist lets those factories wrap their frame tables in a
-- metatable that errors loudly on PascalCase method calls outside the WoW
-- frame surface -- catches typos AND silent "method not in mock so the
-- production if-guard skipped the call" cases.
--
-- Usage from a test mock:
--   setmetatable(frame, VFNTestStrictMeta(frameType))
--
-- The allowlist is a no-op default for every WoW Frame/FontString/EditBox/
-- Texture/Button method commonly used in addons. Methods NOT here error.
-- Lowercase fields (frame.panels, frame.widgets, etc.) fall through to nil.
_G.VFNTestFrameMethods = {
    -- Layout
    SetPoint = true, ClearAllPoints = true, SetAllPoints = true,
    SetSize = true, SetWidth = true, SetHeight = true,
    GetSize = function() return 100, 100 end,
    GetWidth = function() return 100 end, GetHeight = function() return 100 end,
    GetLeft = function() return 0 end,    GetTop = function() return 100 end,
    GetRight = function() return 100 end, GetBottom = function() return 0 end,
    GetCenter = function() return 50, 50 end,
    SetScale = true, GetScale = function() return 1 end,
    -- Visibility / order
    Show = true, Hide = true, IsShown = function() return true end,
    SetAlpha = true, GetAlpha = function() return 1 end,
    SetFrameLevel = true, GetFrameLevel = function() return 0 end,
    SetFrameStrata = true, GetFrameStrata = function() return "MEDIUM" end,
    Raise = true, Lower = true,
    -- Parenting
    SetParent = true, GetParent = function() return nil end,
    -- Mouse + drag
    EnableMouse = true, EnableMouseWheel = true,
    IsMouseOver = function() return false end,
    SetMovable = true, IsMovable = function() return false end,
    SetResizable = true, IsResizable = function() return false end,
    StartMoving = true, StartSizing = true, StopMovingOrSizing = true,
    RegisterForClicks = true, RegisterForDrag = true,
    -- Events + scripts
    RegisterEvent = true, UnregisterEvent = true, UnregisterAllEvents = true,
    SetScript = true, HookScript = true, GetScript = function() return nil end,
    -- VFN-factory-added methods. These get attached directly to frames by
    -- VFN.UI:Chip / :StatCard / :ScrollBox / :Button factories. In real
    -- production a `chip:SetVariant("ready")` call works because the factory
    -- put SetVariant on the chip frame. The mock allows them as no-ops so
    -- tests can call them on stand-in frames. (We've documented the case
    -- where production checks `if btn.SetVariant then` on a kind="button"
    -- widget that won't have SetVariant in real WoW -- the if-guard catches
    -- that correctly via the false-sentinel mechanism.)
    SetVariant = true, SetChipText = true,
    SetValue   = true, SetLabel    = true,
    SetActive  = true, SetHint     = true,
    SetItems   = true, GetItems    = function() return {} end,
    RefreshIntrinsicWidth = true,
    -- Children: return a strict mock so callers can chain method calls.
    -- The returned object shares the same metatable behaviour (allowlist or
    -- error), so a texture/fontstring/line that's then used with an unknown
    -- method still surfaces loudly.
    CreateFontString = function() return setmetatable({}, _G.VFNTestStrictMeta("FontString")) end,
    CreateTexture    = function() return setmetatable({}, _G.VFNTestStrictMeta("Texture")) end,
    CreateLine       = function() return setmetatable({}, _G.VFNTestStrictMeta("Line")) end,
    CreateMaskTexture= function() return setmetatable({}, _G.VFNTestStrictMeta("MaskTexture")) end,
    -- Backdrop
    SetBackdrop = true, SetBackdropColor = true, SetBackdropBorderColor = true,
    GetBackdrop = function() return nil end,
    -- Texture
    SetTexture = true, GetTexture = function() return nil end,
    SetColorTexture = true, SetAtlas = true, GetAtlas = function() return nil end,
    SetVertexColor = true, GetVertexColor = function() return 1,1,1,1 end,
    SetRotation = true, SetTexCoord = true, SetBlendMode = true, SetDesaturated = true,
    SetDrawLayer = true,
    -- FontString
    SetText = true, GetText = function() return "" end,
    SetTextColor = true, GetTextColor = function() return 1,1,1,1 end,
    SetJustifyH = true, SetJustifyV = true,
    SetWordWrap = true, SetNonSpaceWrap = true,
    SetFontObject = true, GetFontObject = function() return nil end,
    SetFont = true, GetFont = function() return nil, 12, "" end,
    SetShadowColor = true, SetShadowOffset = true,
    GetStringWidth = function() return 50 end, GetStringHeight = function() return 12 end,
    GetTextWidth = function() return 50 end,
    -- EditBox
    SetAutoFocus = true, SetMultiLine = true, IsMultiLine = function() return false end,
    SetMaxLetters = true, GetMaxLetters = function() return 0 end,
    SetTextInsets = true, GetTextInsets = function() return 0,0,0,0 end,
    HasFocus = function() return false end,
    SetFocus = true, ClearFocus = true,
    HighlightText = true,
    SetEnabled = true, IsEnabled = function() return true end,
    Enable = true, Disable = true,
    -- Button textures (frame-method form)
    GetNormalTexture = function() return nil end,
    GetPushedTexture = function() return nil end,
    GetHighlightTexture = function() return nil end,
    GetDisabledTexture = function() return nil end,
    SetNormalAtlas = true, SetPushedAtlas = true,
    SetHighlightAtlas = true, SetDisabledAtlas = true,
    SetNormalTexture = true, SetPushedTexture = true,
    SetHighlightTexture = true, SetDisabledTexture = true,
    SetButtonState = true, GetButtonState = function() return "NORMAL" end,
    -- StatusBar
    SetMinMaxValues = true, SetValue = true, GetValue = function() return 0 end,
    SetStatusBarTexture = true, SetStatusBarColor = true,
    -- ScrollFrame / WowScrollBox
    SetVerticalScroll = true, GetVerticalScroll = function() return 0 end,
    SetHorizontalScroll = true, GetHorizontalScroll = function() return 0 end,
    GetVerticalScrollRange = function() return 0 end,
    UpdateScrollChildRect = true, SetScrollChild = true,
    GetScrollChild = function() return nil end,
    Update = true, FullUpdate = true, RefreshAllData = true,
    SetDataProvider = true, GetDataProvider = function() return nil end,
    SetElementInitializer = true, SetElementExtent = true,
    SetElementExtentCalculator = true, SetElementResetter = true,
    Init = true, Acquire = true, Release = true, ReleaseAll = true,
    OnLoad = true, OnShow = true, OnHide = true,
    -- Misc framework
    SetClampedToScreen = true, SetClampRectInsets = true,
    SetUserPlaced = true, IsUserPlaced = function() return false end,
    SetToplevel = true, IsToplevel = function() return false end,
    SetIgnoreParentScale = true, SetIgnoreParentAlpha = true,
    SetHitRectInsets = true,
    -- DropdownButton (WowStyle1DropdownTemplate / WowStyle1FilterDropdownTemplate).
    -- The new kind="dropdown" widget calls these at build time + dispatch push;
    -- the strict-mock harness needs them allow-listed as no-ops.
    SetupMenu              = true,
    GenerateMenu           = true,
    SetDefaultText         = true,
    SetSelectionTranslator = true,
    SetIsDefaultCallback   = true,
    SetDefaultCallback     = true,
    -- Sentinel = false: this key is intentionally ABSENT (returns nil from
    -- __index). Used for optional methods the production code guards with
    -- `if widget.X then` -- when nil, the if-check is correctly false.
    -- (Lua can't distinguish `key = nil` from "key absent" in a table, so we
    -- explicitly set `false`.)
    ApplyLayout = false, OnLayoutChanged = false,
    -- Template-attached children commonly poked via `if frame.X then` -- the
    -- production code wants the absence path when those templates weren't
    -- applied. The test mock could synthesize them per-template at
    -- CreateFrame time later; for now, treat them as absent by default.
    CharCount = false, EditBox = false,
}

-- The strict metatable distinguishes three cases for PascalCase access:
--   1. Allowlisted method (entry == true OR a function)   -> return that
--   2. Unknown but plausibly a template-attached child    -> return a
--      strict-mock child (so `frame.CharCount:Hide()` works for templated
--      children like InputScrollFrameTemplate's CharCount/EditBox)
--   3. Used as a method call (`frame:Foo()`)               -> __call errors
--
-- Lowercase access (data fields) falls through to nil. PascalCase that's
-- actually a typo'd method call ends up at case 2->3 and errors loudly
-- the moment production tries to invoke it.
function _G.VFNTestStrictMeta(frameType)
    local function strictIndex(_, k)
        if type(k) ~= "string" or not k:match("^[A-Z]") then return nil end
        local entry = _G.VFNTestFrameMethods[k]
        if entry == false then return nil end  -- intentionally absent
        if entry == true then return function() end end
        if type(entry) == "function" then return entry end
        -- Unknown PascalCase. Return a strict-mock child so child-frame access
        -- works AND typos still error when actually called.
        return setmetatable({}, {
            __index = strictIndex,
            __call  = function()
                error(string.format(
                    "strict mock: frame:%s() not in allowlist (frameType=%s)",
                    tostring(k), tostring(frameType)), 2)
            end,
        })
    end
    return { __index = strictIndex }
end

-- ===== BootHDGREngine (ADR-020) =============================================
-- Test helper that mirrors `Core/HDGR_Init.lua`'s real four-phase interleaved
-- bootstrap sequence -- the production path. Tests that need the full engine
-- wired should call this instead of `HDG.Modules:Boot()` (which is the
-- legacy convenience that runs only Topo+Phase1+Phase2 inline, skipping
-- BlizzardEvents wiring + Environment build + Middleware install).
--
-- TOLERANT to partial test environments: each engine step is guarded with
-- a presence check so a test that only loaded `Modules + Store` (for
-- registry sequencing assertions) can still call this helper without
-- erroring on missing BlizzardEvents/Environment/Middleware/WidgetTypes.
-- Tests that want to ASSERT a specific engine wired correctly should load
-- that engine before calling -- the helper does not enforce loading order.
--
-- Sequence (matches Core/HDGR_Init.lua:540-596 verbatim):
--   1. BlizzardEvents:OnInitialize
--   2. Modules:Topo
--   3. BlizzardEvents:Boot(order/defs)
--   4. Environment:DeclareAll
--   5. Modules:Phase1
--   6. Environment:Build      -> env
--   7. Middleware.Apply(Store, StandardChain { actionMeta, env })
--   8. Modules:Phase2
--   9. WidgetTypes:Flush
--  10. FlowRunner:Boot (optional)
--
-- Returns the built env table (or nil if Environment wasn't loaded).
function _G.BootHDGREngine()
    assert(_G.HDG and _G.HDG.Modules, "BootHDGREngine: HDG.Modules must be loaded")

    if HDG.BlizzardEvents and HDG.BlizzardEvents.OnInitialize then  -- boundary
        HDG.BlizzardEvents:OnInitialize()
    end

    HDG.Modules:Topo()

    if HDG.BlizzardEvents and HDG.BlizzardEvents.Boot then  -- boundary
        local order = HDG.Modules:GetOrder()
        local defs  = {}
        for _, name in ipairs(order) do defs[name] = HDG.Modules:Get(name) end
        HDG.BlizzardEvents:Boot({ order = order, defs = defs })
    end

    if HDG.Environment and HDG.Environment.DeclareAll then  -- boundary
        HDG.Environment:DeclareAll()
    end

    HDG.Modules:Phase1()

    local env
    if HDG.Environment and HDG.Environment.Build then  -- boundary
        env = HDG.Environment:Build()
    end

    if HDG.Middleware and HDG.Middleware.Apply and HDG.Middleware.StandardChain then  -- boundary
        HDG.Middleware.Apply(HDG.Store, HDG.Middleware.StandardChain({
            actionMeta = HDG.Store and HDG.Store._actionMeta,  -- boundary
            env        = env,
        }))
    end

    HDG.Modules:Phase2()

    if HDG.WidgetTypes and HDG.WidgetTypes.Flush then  -- boundary
        HDG.WidgetTypes:Flush()
    end

    if HDG.FlowRunner and HDG.FlowRunner.Boot then  -- boundary
        HDG.FlowRunner:Boot()
    end

    return env
end

-- ============================================================================
-- C_StringUtil.StripHyperlinks shim (headless parity with in-game).
-- Real signature: StripHyperlinks(text, maintainColor, maintainBrackets,
--   stripNewlines, maintainAtlases, maintainTextures). The observer's
-- _ParseSourceText strips color/hyperlink/texture escapes from each sourceText
-- line before prefix-matching; without this the parse hard-errors headless.
-- Strip order matches Blizzard: color -> textures -> atlases -> |H..|h wrapper
-- (then optional [brackets]) -> trailing |h, leaving "Vendor: X" / "Cost: 15".
-- ============================================================================
_G.C_StringUtil = _G.C_StringUtil or {}
if not _G.C_StringUtil.StripHyperlinks then
    function _G.C_StringUtil.StripHyperlinks(text, maintainColor, maintainBrackets, stripNewlines, maintainAtlases, maintainTextures)
        if type(text) ~= "string" then return text end
        local s = text
        if not maintainColor    then s = s:gsub("|c%x%x%x%x%x%x%x%x", ""):gsub("|r", "") end
        if not maintainTextures then s = s:gsub("|T.-|t", "") end
        if not maintainAtlases  then s = s:gsub("|A.-|a", "") end
        if stripNewlines        then s = s:gsub("|n", "") end
        s = s:gsub("|H.-|h", "")
        if not maintainBrackets then s = s:gsub("%[.-%]", "") end
        s = s:gsub("|h", "")
        return s
    end
end

-- `bit` library shim (WoW ships it; standalone Lua 5.4 doesn't expose the table).
-- Minimal surface used by HDGR_StyleSerializer. Bitwise operators live inside
-- load()'d strings so this file still parses under luac 5.1 (Layer 0).
_G.bit = _G.bit or (function()
    local ok, m = pcall(function()
        return {
            band   = load("return function(a,b) return a & b end")(),
            bor    = load("return function(a,b) return a | b end")(),
            bxor   = load("return function(a,b) return a ~ b end")(),
            lshift = load("return function(a,b) return a << b end")(),
            rshift = load("return function(a,b) return a >> b end")(),
            bnot   = load("return function(a)   return ~a    end")(),
        }
    end)
    return ok and m or nil
end)()

-- C_Reputation / C_MajorFactions boundary stubs. The catalog observer reads
-- these during REP-gate baking (_repProgressForFaction). WoW always provides
-- C_Reputation; standalone Lua doesn't. Defaults model a fresh character with
-- no rep data (IsMajorFaction false, GetFactionDataByID nil -> no rep progress).
_G.C_Reputation = _G.C_Reputation or {
    IsMajorFaction     = function() return false end,
    GetFactionDataByID = function() return nil end,
}
_G.C_MajorFactions = _G.C_MajorFactions or {
    GetMajorFactionData = function() return nil end,
    HasMaximumRenown    = function() return false end,
}
