-- Layout config structural smoke: runs Layout:Validate on the
-- shipped LayoutConfig and asserts zero errors. Catches regressions where
-- a new panel/section/widget breaks the spec contract (missing cell map
-- key, dangling parent, etc.).

dofile("wow_mock.lua")
dofile("../Core/HDGR_SchemeConstants.lua")
dofile("../Core/HDGR_Constants.lua")
dofile("../Core/HDGR_Format.lua")
dofile("../Core/HDGR_Paths.lua")
dofile("../Core/HDGR_Log.lua")
dofile("../Core/HDGR_Locale.lua")
dofile("../Core/HDGR_TableUtils.lua")
dofile("../Core/HDGR_Memo.lua")
dofile("../Core/HDGR_BlizzardEvents.lua")
dofile("../Core/HDGR_Environment.lua")
dofile("../Core/HDGR_WidgetTypes.lua")
dofile("../Core/HDGR_TooltipEngine.lua")
dofile("../Core/HDGR_TooltipRecipes.lua")
dofile("../Core/HDGR_Modules.lua")
dofile("../Core/HDGR_Middleware.lua")
dofile("../Core/HDGR_StoreFurnishings.lua")
dofile("../Core/HDGR_Store.lua")
dofile("../UI/HDGR_Theme.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_Warehouse.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/LayoutConfig/HDGR_LayoutConfig_Data.lua")
dofile("../UI/LayoutConfig/HDGR_LayoutConfig_Projects.lua")
-- Window infra + standalone views. Window must load before any LayoutConfig
-- file that calls HDG.Window:Register (lumber, shopping, zone). Selectors
-- load before their LayoutConfig so any selector referenced at file-load
-- (dynamicRows, radarShouldRender) exists. shopping.windowVisible is
-- registered in HDGR_Selectors_Shopping.lua; zone.popupShown is in
-- HDGR_Selectors_Zone.lua. Both are called only at Compute time so they
-- don't strictly need to exist at LayoutConfig file-load, but loading them
-- here keeps the test harness representative of the production boot order.
dofile("../UI/HDGR_Window.lua")
dofile("../UI/HDGR_Selectors.lua")
dofile("../UI/HDGR_Selectors_Lumber.lua")
dofile("../UI/LayoutConfig/HDGR_LayoutConfig_Lumber.lua")
dofile("../UI/HDGR_Selectors_Shopping.lua")
dofile("../UI/LayoutConfig/HDGR_LayoutConfig_Shopping.lua")
-- Companion: standalone satellite view (Selectors_Companion loaded below at
-- file-set time; only referenced as strings here). rowKind/cellKind are
-- resolved at build, not Validate (Rows.Get stubbed truthy below).
dofile("../UI/LayoutConfig/HDGR_LayoutConfig_Companion.lua")
dofile("../UI/HDGR_Selectors_Zone.lua")
dofile("../UI/LayoutConfig/HDGR_LayoutConfig_Zone.lua")
-- Nav LayoutConfig: standalone sidebar view; references HDG.Constants.NAV_TREE
-- (loaded above via HDGR_Constants.lua). Selectors are evaluated at Compute
-- time, not file-load time, so they don't need to exist here.
dofile("../UI/LayoutConfig/HDGR_LayoutConfig_Nav.lua")
dofile("../UI/HDGR_Components.lua")
dofile("../UI/HDGR_Layout.lua")
-- Widget kinds registered OUTSIDE HDGR_Components -- needed so Layout:Validate
-- sees every kind the real config uses (else it false-flags "kind not
-- registered"): HDGR_Panels = the `panel` kind, HDGR_AcqMapWidget = `vendorMap`.
-- HDGR_RowRegistry defines HDG.Rows (consulted by the scrollbox rowKind check).
dofile("../UI/HDGR_Panels.lua")
dofile("../UI/HDGR_AcqMapWidget.lua")
dofile("../UI/HDGR_Controller_ProjectsCanvas.lua")   -- registers the `projectsCanvas` kind
dofile("../UI/HDGR_ProjectsHelpFlow.lua")             -- registers the `projectsHelpFlow` kind
dofile("../UI/HDGR_RowRegistry.lua")
HDG.Controllers = HDG.Controllers or { Register = function() end }  -- stub for Controllers:Register at module load
dofile("../UI/HDGR_Controller_Layouts.lua")          -- registers the `layoutPreview` kind + row factories

-- Tab selectors: load the full set so Layout:Validate's binding/visible ->
-- selector check resolves every reference in the merged config (else it
-- false-flags real selectors as "not registered"). These register selector
-- defs (closures) at file-load and are only evaluated at Compute time, so
-- their order relative to the LayoutConfig files above doesn't matter.
-- Lumber/Shopping/Zone are already loaded above (standalone-window views).
dofile("../UI/HDGR_Selectors_Status.lua")
dofile("../UI/HDGR_Selectors_Decor.lua")
dofile("../UI/HDGR_Selectors_Catalog.lua")
dofile("../UI/HDGR_Selectors_Acquisition.lua")
dofile("../UI/HDGR_Selectors_Recipes.lua")
dofile("../UI/HDGR_Selectors_Config.lua")
dofile("../UI/HDGR_Selectors_Alts.lua")
dofile("../UI/HDGR_Selectors_Mogul.lua")
dofile("../UI/HDGR_Selectors_Styles.lua")
dofile("../UI/HDGR_Selectors_Trainers.lua")
dofile("../UI/HDGR_Selectors_House.lua")
HDG.AchievementObserver = HDG.AchievementObserver or {
    IsEarned    = function() return false end,
    GetCriteria = function() return nil end,
}
dofile("../UI/HDGR_Selectors_Data.lua")
dofile("../UI/HDGR_Selectors_Companion.lua")
dofile("../UI/HDGR_Selectors_Projects.lua")

-- HDG.WidgetTypes is a deferred-queue registry: Register() enqueues at
-- file-load; Flush() processes the queue + normalizes defs. Production
-- triggers Flush via HDG:OnInitialize. The smoke test needs to call it
-- explicitly so the closed-schema check (which queries TryGet) sees the
-- registered kinds; otherwise the check silently no-ops.
HDG.WidgetTypes:Flush()

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

-- ===== Real config validates clean (closed schema + structure) =====
-- Mirrors the in-game boot validator headlessly so closed-schema violations
-- (e.g. a panel/widget spec field missing from PANEL_SPEC_FIELDS -- the
-- navPanel.skin regression on 2026-05-30 that passed CI but failed at login)
-- now fail in CI, not in-game. rowKind registration lives in the per-tab
-- controllers (covered by test_controller_wire_smoke), so stub Rows:Get truthy.
HDG.Rows.Get = function() return true end
do
    local realErrs = HDG.Layout:Validate(HDG.LayoutConfig)
    assertEq(#realErrs, 0,
        "real LayoutConfig validates clean (errors: " .. table.concat(realErrs, " | ") .. ")")
end

-- ===== LayoutConfig has the expected top-level shape =====
assertTrue(HDG.LayoutConfig, "LayoutConfig loaded")
assertTrue(HDG.LayoutConfig.window, "window block present")
assertTrue(HDG.LayoutConfig.window.views, "views table present")
assertTrue(HDG.LayoutConfig.panels, "panels table present")
assertTrue(HDG.LayoutConfig.sections, "sections table present")
assertTrue(HDG.LayoutConfig.widgets, "widgets table present")

-- ===== Three views (decor, acquisition, debug) =====
assertTrue(HDG.LayoutConfig.window.views.decor, "decor view present")
assertTrue(HDG.LayoutConfig.window.views.acquisition, "acquisition view present")
assertTrue(HDG.LayoutConfig.window.views.debug, "debug view present (Phase 8.4)")
assertTrue(HDG.LayoutConfig.window.views.warehouse, "warehouse view present (extracted 2026-05-30)")

-- ===== Chrome + status are slot-views, not per-view cells (HDG-ADR-025) =====
-- Window composition: the `main` window's top/bottom slots provide the chrome
-- strip + status rail. Content views are pure content -- they no longer carry
-- a chrome or status cell (a new view needs zero chrome wiring).
assertTrue(HDG.LayoutConfig.window.views.chrome, "chrome slot-view present")
assertTrue(HDG.LayoutConfig.window.views.status, "status slot-view present")
assertTrue(HDG.LayoutConfig.window.views.chrome.standalone, "chrome view is standalone")
assertTrue(HDG.LayoutConfig.window.views.status.standalone, "status view is standalone")
local mainSlots = HDG.LayoutConfig.windows.main.slots
assertEq(mainSlots.top, "chrome", "main window top slot = chrome")
assertEq(mainSlots.bottom, "status", "main window bottom slot = status")
assertEq(mainSlots.fill, "@view", "main window fill slot = @view")
for viewName, view in pairs(HDG.LayoutConfig.window.views) do
    if not view.standalone then
        assertTrue(view.cells.chrome == nil,
            ("content view %q has no chrome cell (window slot provides it)"):format(viewName))
        assertTrue(view.cells.status == nil,
            ("content view %q has no status cell (window slot provides it)"):format(viewName))
    end
end

-- ===== Surfaces are wired =====
assertTrue(HDG.LayoutConfig.panels.statusRailPanel, "statusRailPanel present")
assertTrue(HDG.LayoutConfig.panels.debugPanel, "debugPanel present")
assertTrue(HDG.LayoutConfig.widgets["statusRailPanel.rail"],
    "statusRail widget present")
assertTrue(HDG.LayoutConfig.widgets["debugPanel.body"],
    "debug scrollingTextBox widget present")

-- ===== Status rail binding is correct =====
assertEq(HDG.LayoutConfig.widgets["statusRailPanel.rail"].kind, "statusRail",
    "status rail kind")
assertEq(HDG.LayoutConfig.widgets["statusRailPanel.rail"].binding.entry,
    "status.current", "status rail binding entry")

-- ===== Debug body binding is correct =====
assertEq(HDG.LayoutConfig.widgets["debugPanel.body"].kind, "scrollingTextBox",
    "debug body kind")
assertEq(HDG.LayoutConfig.widgets["debugPanel.body"].binding.text,
    "log.formattedText", "debug body binding text")

-- ===== Chrome + status slot-view heights are reasonable =====
assertEq(HDG.LayoutConfig.window.views.chrome.height, HDG.LayoutConfig.CHROME_HEIGHT,
    "chrome slot-view height = CHROME_HEIGHT")
assertEq(HDG.LayoutConfig.window.views.status.height, HDG.LayoutConfig.STATUS_RAIL_HEIGHT,
    "status slot-view height = STATUS_RAIL_HEIGHT")

-- ===== chromePanel + statusRailPanel target their slot-views =====
-- Window composition: each chrome panel lives in exactly one slot-view (built
-- into the main frame via BuildMainWindow's viewFilter pass), NOT in every
-- content view's cell map.
local railPanel = HDG.LayoutConfig.panels.statusRailPanel
assertEq(railPanel.cell.status, "body", "statusRailPanel placed in the status view's body cell")
assertEq(#railPanel.visibleInViews, 1, "statusRailPanel visible in exactly one view")
assertEq(railPanel.visibleInViews[1], "status", "statusRailPanel visible in the status slot-view only")
local chromePanel = HDG.LayoutConfig.panels.chromePanel
assertEq(chromePanel.cell.chrome, "body", "chromePanel placed in the chrome view's body cell")
assertEq(#chromePanel.visibleInViews, 1, "chromePanel visible in exactly one view")
assertEq(chromePanel.visibleInViews[1], "chrome", "chromePanel visible in the chrome slot-view only")

-- ===== Debug tab gates correctly =====
local debugPanel = HDG.LayoutConfig.panels.debugPanel
assertEq(#debugPanel.visibleInViews, 1, "debugPanel visible in exactly one view")
assertEq(debugPanel.visibleInViews[1], "debug", "debugPanel visible in debug view only")

-- ===== Sub-table mis-keying audit fires on bad configs =====
-- Verifies that Layout:Validate catches the HouseTab/Trainers bug class:
-- widget specs silently dropped by being placed in the wrong sub-table.
do
    local function errSet(errs)
        local s = {}
        for _, e in ipairs(errs) do s[e] = true end
        return s
    end
    local function hasMatch(errs, pat)
        for _, e in ipairs(errs) do
            if e:find(pat, 1, true) then return true end
        end
        return false
    end

    -- Widget with kind="scrollbox" placed in config.panels: must flag.
    local badCfg1 = {
        window = HDG.LayoutConfig.window,
        panels = {
            myPanel = { kind = "panel", cell = { decor = "body" }, visibleInViews = { "decor" } },
            oops    = { kind = "scrollbox", ["in"] = "myPanel" },  -- widget, not panel
        },
        sections = {},
        widgets  = {},
    }
    local errs1 = HDG.Layout:Validate(badCfg1)
    assertTrue(hasMatch(errs1, "kind = \"scrollbox\"") or hasMatch(errs1, "has `in`"),
        "mis-keyed widget (kind=scrollbox) in panels flagged")

    -- Widget with `in` placed in config.panels: must flag on the `in` field.
    local badCfg2 = {
        window = HDG.LayoutConfig.window,
        panels = {
            myPanel = { kind = "panel", cell = { decor = "body" }, visibleInViews = { "decor" } },
            orphan  = { ["in"] = "myPanel" },  -- has `in`, belongs in sections/widgets
        },
        sections = {},
        widgets  = {},
    }
    local errs2 = HDG.Layout:Validate(badCfg2)
    assertTrue(hasMatch(errs2, "has `in`"),
        "panel with `in` field flagged")

    -- Widget spec in config.sections: must flag on kind.
    local badCfg3 = {
        window = HDG.LayoutConfig.window,
        panels = {
            myPanel = { kind = "panel", cell = { decor = "body" }, visibleInViews = { "decor" } },
        },
        sections = {
            oops = { kind = "label", ["in"] = "myPanel" },  -- widget, not section
        },
        widgets  = {},
    }
    local errs3 = HDG.Layout:Validate(badCfg3)
    assertTrue(hasMatch(errs3, "sections don't have a kind"),
        "mis-keyed widget (kind=label) in sections flagged")

    -- Panel spec (with cell) in config.widgets: must flag on cell.
    local badCfg4 = {
        window = HDG.LayoutConfig.window,
        panels = {
            myPanel = { kind = "panel", cell = { decor = "body" }, visibleInViews = { "decor" } },
        },
        sections = {},
        widgets  = {
            oops = { cell = { decor = "body" } },  -- panel, not widget
        },
    }
    local errs4 = HDG.Layout:Validate(badCfg4)
    assertTrue(hasMatch(errs4, "cell is panel placement"),
        "mis-keyed panel (has cell) in widgets flagged")
end

-- ===== Closed-schema check catches unknown widget fields (typos) =====
-- Validator must reject any field on a widget spec that's not in the universal
-- set OR the kind's specFields. Catches `placholder` typos AND retracted
-- patterns like `formatFn` (ADR-024 corrected).
do
    local function hasMatch(errs, pat)
        for _, e in ipairs(errs) do if e:find(pat, 1, true) then return true end end
        return false
    end
    -- Find any registered panel in the shipped config to attach test widgets to.
    local hostPanelID
    for id in pairs(HDG.LayoutConfig.panels) do hostPanelID = id; break end
    assertTrue(hostPanelID ~= nil, "shipped config has at least one panel for the harness")

    local badCfg = {
        window = HDG.LayoutConfig.window,
        panels = { [hostPanelID] = HDG.LayoutConfig.panels[hostPanelID] },
        sections = {},
        widgets = {
            badWidget = {
                kind = "label", ["in"] = hostPanelID, font = "body",
                placholder = "typo",                       -- not a label field
            },
        },
    }
    local errs = HDG.Layout:Validate(badCfg)
    assertTrue(hasMatch(errs, "placholder"),
        "closed schema must reject typo field on widget spec")

    -- Retracted formatFn pattern (ADR-024 corrected): must be flagged.
    local badCfg2 = {
        window = HDG.LayoutConfig.window,
        panels = { [hostPanelID] = HDG.LayoutConfig.panels[hostPanelID] },
        sections = {},
        widgets = {
            countLabel = {
                kind = "label", ["in"] = hostPanelID, font = "small",
                formatFn = function(d) return tostring(d) end,
            },
        },
    }
    local errs2 = HDG.Layout:Validate(badCfg2)
    assertTrue(hasMatch(errs2, "formatFn"),
        "closed schema must reject the retracted formatFn field (ADR-024 corrected)")
end

-- ===== Nav view: tree-list (HDG-ADR-025 step 4) =====
-- The nav is a single `treeList` widget bound to nav.tree -- pure data; the
-- per-entry navItem widget loop + NAV_CONTENT_HEIGHT layout-math were retired.
assertTrue(HDG.LayoutConfig.window.views.nav,
    "nav view present (left slot)")
assertTrue(HDG.LayoutConfig.window.views.nav.standalone,
    "nav view is standalone (skipped by main BuildAll; built via viewFilter)")
assertTrue(HDG.LayoutConfig.panels.navPanel, "navPanel present")
local navTree = HDG.LayoutConfig.widgets["navPanel.tree"]
assertTrue(navTree, "navPanel.tree widget present")
assertEq(navTree.kind, "treeList", "nav widget is a treeList")
assertEq(navTree.binding, "nav.tree", "nav treeList binds to nav.tree")
-- The retired function-file artefacts must be gone.
assertTrue(HDG.LayoutConfig.widgets["navPanel.item_decor"] == nil,
    "retired per-entry navItem widgets removed (replaced by treeList)")
assertTrue(HDG.LayoutConfig.NAV_CONTENT_HEIGHT == nil,
    "retired NAV_CONTENT_HEIGHT layout-math removed")
assertTrue(HDG.LayoutConfig.widgets["chromePanel.tab_decor"] == nil,
    "chromePanel.tab_decor removed (old tab strip)")
-- main window declares the nav as its left slot.
assertEq(HDG.LayoutConfig.windows.main.slots.left, "nav",
    "main window left slot = nav")

-- ===== Every registered WidgetType declares specFields =====
do
    local missing = {}
    for kind, def in pairs(HDG.WidgetTypes._registry or {}) do
        if def.specFields == nil then
            missing[#missing + 1] = kind
        end
    end
    if #missing > 0 then
        table.sort(missing)
        error("WidgetTypes missing specFields declaration: " .. table.concat(missing, ", ")
            .. "\nEvery kind must declare its closed-schema contract.", 2)
    end
end

print("test_layoutconfig_smoke.lua passed")
