-- test_controller_wire_smoke.lua
-- Priority 7: Controller Wire smoke per controller.
--
-- Invariants tested:
--   For each Controller:Wire(rootFrame) call:
--   a. Wire completes without error (the common typo class is looking up
--      a widget ID that doesn't exist -- production guards with `if w then`
--      but we still want the happy-path to not crash).
--   b. OnClick handlers are actually attached to the correct widget IDs
--      (not silently skipped because the ID was a typo that returned nil).
--   c. Simulating OnClick fires a Dispatch call with a non-nil action type
--      from HDG.Constants.ACTIONS. This is the "controller dispatches
--      correct action" assertion -- the most dangerous gap in the test suite
--      before this file.
--
-- Strategy:
--   Build a minimal rootFrame with a spy widget table. Each widget ID that
--   the controller expects is pre-seeded with a spy frame that:
--     - Implements SetScript("OnClick", fn) to capture the handler.
--     - Implements other frame methods expected by the controller.
--   After Wire(), iterate the captured handlers and simulate clicks.
--   Assert that each click triggers a Store:Dispatch with a valid action type.
--
-- Scope: DecorController and ChromeController. These cover the two most
-- representative patterns (dedicated-widget ID wiring, and enum-driven
-- loop wiring). AcquisitionController, RecipesController, etc. follow
-- the same pattern and are tracked as TODOs at the bottom.

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")
dofile("../Core/HDGR_BlizzardEvents.lua")
dofile("../Core/HDGR_Environment.lua")
dofile("../Core/HDGR_Modules.lua")
dofile("../UI/HDGR_Theme.lua")
dofile("../Core/HDGR_Init.lua")
dofile("../Core/HDGR_TooltipRecipes.lua")
dofile("../Core/HDGR_TooltipEngine.lua")
dofile("../UI/HDGR_Selectors.lua")
dofile("../UI/HDGR_Selectors_Status.lua")
dofile("../UI/HDGR_Selectors_Decor.lua")
dofile("../UI/HDGR_Selectors_Acquisition.lua")
dofile("../UI/HDGR_Selectors_Recipes.lua")
dofile("../UI/HDGR_LayoutConfig.lua")
dofile("../UI/LayoutConfig/HDGR_LayoutConfig_Decor.lua")
dofile("../UI/LayoutConfig/HDGR_LayoutConfig_Acquisition.lua")
dofile("../UI/LayoutConfig/HDGR_LayoutConfig_Recipes.lua")
dofile("../UI/LayoutConfig/HDGR_LayoutConfig_Alts.lua")
dofile("../UI/LayoutConfig/HDGR_LayoutConfig_Mogul.lua")
dofile("../UI/LayoutConfig/HDGR_LayoutConfig_Styles.lua")
dofile("../UI/LayoutConfig/HDGR_LayoutConfig_Trainers.lua")
dofile("../UI/LayoutConfig/HDGR_LayoutConfig_HouseTab.lua")
dofile("../UI/LayoutConfig/HDGR_LayoutConfig_Debug.lua")
dofile("../UI/LayoutConfig/HDGR_LayoutConfig_Config.lua")
dofile("../UI/HDGR_Controllers.lua")
dofile("../UI/HDGR_Controller_Helpers_UI.lua")
dofile("../UI/HDGR_Controller_Helpers_Mechanics.lua")

-- Stub data DBs so selector fns don't crash.
_G.HDGR_AllDecorDB = _G.HDGR_AllDecorDB or {}
_G.HDGR_VendorDB   = _G.HDGR_VendorDB   or {}
_G.HDGR_DecorDB    = _G.HDGR_DecorDB    or {}

-- Stub HousingCatalogObserver: the decor source-filter selectors
-- (tagsForFilter/matchesTag 'sources' + 'all' Rep/Quest/Ach branches) read
-- byDecorID/byItemID. The real observer module isn't loaded in this
-- wiring-only test, so provide the load-order-guaranteed singleton here
-- (matches the stub pattern in test_companion_smoke / test_acquire_vendor_view).
HDG.HousingCatalogObserver = HDG.HousingCatalogObserver or {}
HDG.HousingCatalogObserver.byDecorID = HDG.HousingCatalogObserver.byDecorID or {}
HDG.HousingCatalogObserver.byItemID  = HDG.HousingCatalogObserver.byItemID  or {}
function HDG.HousingCatalogObserver:IsReady() return false end
function HDG.HousingCatalogObserver:IterateRows(_fn) end
function HDG.HousingCatalogObserver:GetRow(_id) return nil end

-- Stub HDG.Rows (used by Controller_Decor at file level to register the
-- decorRow factory). The test only cares about Wire() behavior; the row
-- factory itself is not exercised here.
HDG.Rows = HDG.Rows or { byName = {} }
function HDG.Rows:Register(name, def) self.byName[name] = def end

-- Stub HDG.UI (used by the destroy dialog builder inside Controller_Decor).
-- The Wire() function itself doesn't call HDG.UI directly, but the row
-- factory closure (captured at file level) does. We stub the methods
-- referenced so dofile of Controller_Decor doesn't crash.
HDG.UI = HDG.UI or {}
function HDG.UI:Button(_parent, _label, _role) return {} end
function HDG.UI.applyFontRole(_fs, _role) end
function HDG.UI:RowFirstPaint(_row, _key, _fn) end
function HDG.UI:CopyDialog(_opts) return {} end

-- Stub HDG.Theme:Register (used by destroy dialog construction).
if HDG.Theme and not HDG.Theme.Register then
    function HDG.Theme:Register(_widget, _key) end
end

-- Wire the Controllers module.
-- We load Controller_Chrome, Controller_Nav, and Controller_Decor only.
-- Others are TODO.
dofile("../UI/HDGR_Controller_Chrome.lua")
-- Minimal HDG.TreeList stub: Controller_Nav registers its navNode/navHeader
-- cell kinds into TreeList at load (the real module isn't needed for this
-- controller-wire smoke -- we test the dispatch logic via _dispatchClick).
HDG.TreeList = HDG.TreeList or {}
HDG.TreeList._cellKinds = HDG.TreeList._cellKinds or {}
function HDG.TreeList:RegisterCellKind(name, def) self._cellKinds[name] = def end
function HDG.TreeList:GetCellKind(name) return self._cellKinds[name] end
dofile("../UI/HDGR_Controller_Nav.lua")
dofile("../UI/HDGR_Controller_Decor.lua")

-- Boot.
HDG_DB = {}
HDG.Store:LoadFromSavedVariables()
if HDG.Theme and HDG.Theme.Initialize then HDG.Theme:Initialize() end  -- boundary
pcall(function() HDG:OnInitialize() end)

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

-- ===== Dispatch spy ==========================================================
-- Replace Store:Dispatch with a spy for the duration of the wire tests.
local dispatchLog = {}
local function makeDispatchSpy()
    dispatchLog = {}
    local orig = HDG.Store.Dispatch
    HDG.Store.Dispatch = function(self, action)
        if type(action) == "table" and action.type then
            dispatchLog[#dispatchLog + 1] = action
        end
        -- Still call the real dispatch so state stays valid.
        return orig(self, action)
    end
    return function()
        HDG.Store.Dispatch = orig
    end
end

-- ===== Spy widget builder ====================================================
-- A spy widget that captures SetScript("OnClick", ...) handlers.
-- Also implements EnableKeyboard, SetPropagateKeyboardInput so the
-- keyboard-navigation wiring in DecorController:Wire doesn't crash.
local function makeSpyWidget(id)
    local w = {
        _id        = id,
        _scripts   = {},
        _pushCount = 0,
    }
    function w:SetScript(event, fn)
        self._scripts[event] = fn
    end
    function w:GetScript(event)
        return self._scripts[event]
    end
    function w:EnableKeyboard(_on) end
    function w:SetPropagateKeyboardInput(_v) end
    function w:GetText() return "" end
    function w:SetText(_t) end
    function w:Enable() end
    function w:Disable() end
    function w:Show() end
    function w:Hide() end
    function w:IsShown() return true end
    return w
end

-- Build a rootFrame with spy widgets for all known widget IDs used by
-- the controllers being tested.
local function buildRootFrame()
    local rf = { widgets = {} }
    local function seed(id)
        rf.widgets[id] = makeSpyWidget(id)
    end

    -- DecorController widget IDs.
    seed("decorPanel.search")
    seed("decorPanel.list")
    seed("decorPanel.onlyUncollectedToggle")
    seed("decorPanel.onlyStoredToggle")

    -- Top filter chips (generated from HDG.Constants.TOP_FILTERS).
    for _, entry in ipairs(HDG.Constants.TOP_FILTERS or {}) do
        seed("decorPanel.topFilter_" .. entry.value)
    end

    -- Tag slot chips.
    for slot = 1, (HDG.Constants.TAG_SLOT_COUNT or 12) do
        seed("decorPanel.tagSlot_" .. slot)
    end

    -- Note edit box (DecorController wires this).
    seed("decorDetailPanel.noteBox")

    -- Destroy button.
    seed("decorDetailPanel.destroyButton")
    seed("decorDetailPanel.destroyStored")

    -- Chrome controller widget IDs (uses "chromePanel.tab_<view>" prefix).
    for _, tab in ipairs(HDG.Constants.TABS or {}) do
        seed("chromePanel.tab_" .. tab.view)
    end
    seed("chromePanel.close")

    -- Nav is now a treeList (HDG-ADR-025 step 4) -- no per-entry nav widgets to
    -- seed. Clicks live in the navNode/navHeader cell initializers; the nav test
    -- below exercises NavController._dispatchClick directly.

    return rf
end

-- ===== All-ACTIONS validation set ============================================
local knownActions = {}
for _, v in pairs(HDG.Constants.ACTIONS) do knownActions[v] = true end

-- ===== Test: DecorController:Wire ============================================

local restoreDispatch = makeDispatchSpy()

local rf = buildRootFrame()
local wireOk, wireErr = pcall(function()
    HDG.DecorController:Wire(rf)
end)
if not wireOk then
    restoreDispatch()
    error("DecorController:Wire crashed: " .. tostring(wireErr), 2)
end

-- Simulate OnClick on every wired widget and verify dispatches.
local clickedWidgets, missingHandlers = 0, {}
for id, widget in pairs(rf.widgets) do
    local handler = widget._scripts and widget._scripts["OnClick"]
    if handler then
        clickedWidgets = clickedWidgets + 1
        dispatchLog = {}
        local ok, err = pcall(handler, widget)
        -- Some handlers are legitimately no-ops (e.g. tag slot click when
        -- no tags are loaded in the test environment). We accept that
        -- provided the handler doesn't ERROR.
        if not ok then
            error(("DecorController: OnClick on widget %q errored: %s"):format(
                id, tostring(err)), 2)
        end
        -- If dispatch was called, verify the action type is known.
        for _, action in ipairs(dispatchLog) do
            if not knownActions[action.type] then
                error(("DecorController: OnClick on widget %q dispatched UNKNOWN action %q"):format(
                    id, tostring(action.type)), 2)
            end
        end
    else
        -- Track which widgets had no OnClick (informational -- not all widgets need one).
        missingHandlers[#missingHandlers + 1] = id
    end
end

assertTrue(clickedWidgets >= 3,
    "DecorController:Wire should attach OnClick to at least 3 widgets")

restoreDispatch()

-- ===== Test: ChromeController:Wire ===========================================
-- Tab-click wiring moved to NavController. Chrome now only owns close + back.
-- Seed those widgets and verify Wire() completes without error.

rf = buildRootFrame()
rf.widgets["chromePanel.backBtn"] = makeSpyWidget("chromePanel.backBtn")

restoreDispatch = makeDispatchSpy()

local chromOk, chromErr = pcall(function()
    if HDG.ChromeController and HDG.ChromeController.Wire then  -- boundary
        HDG.ChromeController:Wire(rf)
    end
end)
if not chromOk then
    restoreDispatch()
    error("ChromeController:Wire crashed: " .. tostring(chromErr), 2)
end

-- Close button must dispatch a known action.
local tabsWithHandlers = 0  -- kept for format string compat; always 0 now
local closeWidget = rf.widgets["chromePanel.close"]
assertTrue(closeWidget ~= nil, "ChromeController: chromePanel.close widget must be seeded")
local closeHandler = closeWidget._scripts and closeWidget._scripts["OnClick"]
assertTrue(closeHandler ~= nil, "ChromeController: chromePanel.close must have an OnClick handler")
dispatchLog = {}
local closeOk, closeErr = pcall(closeHandler, closeWidget)
assertTrue(closeOk, "ChromeController: close OnClick errored: " .. tostring(closeErr))
assertTrue(#dispatchLog >= 1, "ChromeController: close button must dispatch at least one action")
for _, action in ipairs(dispatchLog) do
    if not knownActions[action.type] then
        error(("ChromeController: close dispatched UNKNOWN action %q"):format(
            tostring(action.type)), 2)
    end
end

restoreDispatch()

-- ===== Test: Nav tree cell kinds + click dispatch ===========================
-- The nav is a treeList (HDG-ADR-025 step 4): cell kinds + clicks live in the
-- cell initializers, not a per-widget Wire loop. Verify (a) the cell kinds are
-- registered, (b) Wire is a harmless no-op, (c) every NAV_TREE node's click
-- payload dispatches a known action via NavController._dispatchClick.

assertTrue(HDG.NavController, "NavController present")
-- One cell kind handles both rows + the header (branch on data.isHeader) --
-- a single reused FontString avoids cross-kind recycle double-text.
assertTrue(HDG.TreeList and HDG.TreeList:GetCellKind("navNode"),
    "navNode cell kind registered")

local navOk, navErr = pcall(function()
    if HDG.NavController.Wire then HDG.NavController:Wire(buildRootFrame()) end
end)
assertTrue(navOk, "NavController:Wire no-op does not crash: " .. tostring(navErr))

-- Build the click payloads the nav.tree selector emits (mirrors its logic) and
-- assert each dispatches >= 1 known action through _dispatchClick.
local function navClickFor(node, child)
    if child then
        if child.activePath then
            return child.action
                and { kind = "action", view = node.view, action = child.action, payload = child.payload }
                or  { kind = "transient", view = node.view, transient = child.transient }
        end
        return { kind = "view", view = child.view }
    end
    if node.kind == "launcher" then return { kind = "launcher", action = node.action } end
    return { kind = "view", view = node.view }
end

restoreDispatch = makeDispatchSpy()
local navClicks = 0
local function exerciseClick(click)
    dispatchLog = {}
    local ok, err = pcall(HDG.NavController._dispatchClick, click)
    if not ok then error("nav _dispatchClick errored: " .. tostring(err), 2) end
    assertTrue(#dispatchLog >= 1, "nav click ("..click.kind..") dispatches >= 1 action")
    for _, action in ipairs(dispatchLog) do
        if not knownActions[action.type] then
            error(("nav click dispatched UNKNOWN action %q"):format(tostring(action.type)), 2)
        end
    end
    navClicks = navClicks + 1
end
for _, node in ipairs(HDG.Constants.NAV_TREE or {}) do
    if node.kind == "home" or node.kind == "config" or node.kind == "launcher" then
        exerciseClick(navClickFor(node))
    elseif node.kind == "parent" then
        exerciseClick(navClickFor(node))
        for _, child in ipairs(node.children or {}) do exerciseClick(navClickFor(node, child)) end
    end
end
restoreDispatch()

local navWidgetsWithHandlers = navClicks  -- for the summary line below

-- ===== TODOs ==================================================================
-- The following controllers follow the same wire pattern and should get their
-- own test cases once their widget IDs are enumerated:
--   TODO: AcquisitionController:Wire -- preset chip strip, search box, vendor-row click
--   TODO: RecipesController:Wire     -- scope chips, search box, section expand
--   TODO: ConfigController:Wire      -- scheme dropdown, toggle chips
--   TODO: MogulController:Wire       -- mode/view/optimizeBy chips
--   TODO: StylesController:Wire      -- view chip, curator toggles

-- ===== UI.SendReagentsToAuctionator: lumber exclusion + name resolution =======
-- Lumber is BoP (farmed, never on the AH) -> must be dropped from the buy-list.
-- Auctionator searches by NAME, so a bare "item:<id>" placeholder is a bug; names
-- resolve from the live cache, then the curated ReagentsDB (what the panel shows).
do
    HDG.ItemNameResolver = { ResolveName = function(_, id)
        if id == 100 then return "Crystal Vial" end   -- only 100 is "live-cached"
        return nil
    end }
    HDG.StaticData = HDG.StaticData or {}
    HDG.StaticData.Reagents = { Get = function(_, id)
        if id == 200 then return { "Gathering", "Mana Thistle" } end  -- row[2] = name
        return nil
    end }
    _G.C_Item = _G.C_Item or {}
    _G.C_Item.GetItemInfo = function() return nil end   -- force cold cache -> ReagentsDB path
    local captured
    _G.Auctionator = { API = { v1 = {
        ConvertToSearchString = function(_, opts) return opts.searchString end,  -- echo the name
        CreateShoppingList    = function(_, _, terms) captured = terms end,
    } } }
    local lumberID = HDG.Constants.LUMBER_DATA[2].id   -- Olemba Lumber (242691), BoP
    local n, present = HDG.UI.SendReagentsToAuctionator("Test", {
        { id = lumberID, qty = 30 },   -- dropped (BoP, farmed)
        { id = 100,      qty = 3  },   -- live-cached name
        { id = 200,      qty = 14 },   -- ReagentsDB name (cold live cache)
    })
    assertTrue(present, "AH export: Auctionator present")
    assertEq(n, 2, "AH export: BoP lumber dropped (3 in -> 2 sent)")
    assertEq(#captured, 2, "AH export: 2 terms created")
    for _, t in ipairs(captured) do
        assertTrue(not tostring(t):match("^item:"),
            "AH export: no 'item:<id>' placeholder term -> " .. tostring(t))
    end
    assertTrue(captured[1] == "Crystal Vial" or captured[2] == "Crystal Vial",
        "AH export: live-cached name used")
    assertTrue(captured[1] == "Mana Thistle" or captured[2] == "Mana Thistle",
        "AH export: ReagentsDB name used when live cache is cold")
end

print(string.format(
    "test_controller_wire_smoke.lua passed (DecorController: %d wired clicks; "
    .. "ChromeController: %d tabs wired; NavController: %d nav widgets wired)",
    clickedWidgets, tabsWithHandlers, navWidgetsWithHandlers))
