-- test_layout_compute.lua
-- ============================================================================
-- Verifies Layout:Compute (unified engine API) resolves each view to a
-- sensible placements table:
--   - GetViewDimensions returns the expected window size per view
--   - every panel declared `cell` for the active view appears in placements
--   - panels NOT in the view's cell map are absent (hidden)
--   - column offsets correct in multi-col views (canvas, liveWithGallery)
--   - track collapse: subfilter / palette panels' `visible` selector
--     hides the panel when state says so
--
-- The new API is Compute(config, ctx) where ctx carries view + state.
-- Placements are keyed by panel/section/widget id; each value is a
-- rect { x, y, width, height }.

---@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("../Core/VDS_ActionMeta.lua")
VDS.Store:ValidateActionMetaExhaustiveness()
VDS_DB = {}
VDS.Store:LoadFromSavedVariables()

dofile("../Modules/VDS_StaticData.lua")
dofile("../UI/VDS_Selectors.lua")
dofile("../UI/VDS_Selectors_Progress.lua")
dofile("../UI/VDS_Selectors_View.lua")
dofile("../UI/VDS_Selectors_Browse.lua")
dofile("../UI/VDS_Selectors_Detail.lua")
dofile("../UI/VDS_Selectors_Canvas.lua")
dofile("../UI/VDS_Selectors_CanvasScope.lua")
dofile("../UI/VDS_Selectors_CanvasDecor.lua")
dofile("../UI/VDS_Selectors_Live.lua")
dofile("../UI/VDS_Selectors_Warehouse.lua")
dofile("../Core/VDS_ColorMath.lua")
dofile("../Core/VDS_HarmonyWheel.lua")
dofile("../Core/VDS_Harmony.lua")
dofile("../UI/VDS_Selectors_Harmony.lua")
dofile("../UI/VDS_LayoutConfig.lua")
-- Sub-LayoutConfig modules contribute the panel-specific sections + widgets.
-- Without these, sections like stock.herbs.stack would be MISSING from n.sections
-- and the over-spec check inside the herbs panel would never run -- masking
-- the kind of column-width overflow the runtime engine catches.
local function tryDofile(path)
    local f = io.open(path, "r")
    if f then f:close(); dofile(path); return true end
    return false
end
tryDofile("../UI/VDS_LayoutConfig_Default.lua")
tryDofile("../UI/VDS_LayoutConfig_Canvas.lua")
tryDofile("../UI/VDS_LayoutConfig_Live.lua")
tryDofile("../UI/VDS_LayoutConfig_Warehouse.lua")

dofile("../UI/VDS_Layout.lua")
dofile("../UI/VDS_LayoutNormalize.lua")

local A = VDS.Constants.ACTIONS
local LC = VDS.LayoutConfig

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

local function normalizeForView(viewName)
    return VDS.Layout:Normalize(LC, {
        view = viewName, state = VDS.Store:GetState(), intrinsics = {},
    })
end
-- Reset the over-spec dedup before EVERY Compute. Runtime never does this
-- (would feedback-loop through Log:Warn) but tests are single-shot, so each
-- view gets a clean slate and we can assert overflow-free post-Compute.
local function computeForView(viewName)
    VDS.Layout._overSpecWarned = {}
    VDS.Layout._overSpecWarnedCount = 0
    local n = normalizeForView(viewName)
    local placements = VDS.Layout:Compute(n)
    local overflows = {}
    for key in pairs(VDS.Layout._overSpecWarned) do
        overflows[#overflows + 1] = key
    end
    if #overflows > 0 then
        error(string.format("layout overflow in view %q: %s",
            viewName, table.concat(overflows, "; ")), 2)
    end
    return placements
end
local function viewDims(viewName)
    local n = normalizeForView(viewName)
    return n.window.frameW, n.window.frameH
end

print("Layout:Compute (4 views):")

-- ============================================================================
-- Default view: 1 col (420) + 2*4 padding = 428 wide
-- ============================================================================

local w, h = viewDims("default")
eq(w, 428, "default view width = 428 (420 + 8 padding)")
eq(h, 520, "default view height = 520 (fixed)")

local p = computeForView("default")
truthy(p.titlePanel,     "titlePanel placed in default")
truthy(p.searchPanel,    "searchPanel placed in default")
truthy(p.tabsPanel,      "tabsPanel placed in default")
truthy(p.subfilterPanel, "subfilterPanel placed in default (visible by default)")
truthy(p.browsePanel,    "browsePanel placed in default (body)")
truthy(p.detailPanel,    "detailPanel placed in default (footer)")
eq(p.palettePanel, nil,  "palettePanel hidden when palette.shown returns false")
eq(p.canvasPickerPanel,  nil, "canvasPickerPanel absent from default")
eq(p.canvasPreviewPanel, nil, "canvasPreviewPanel absent from default")
eq(p.canvasFooterPanel,  nil, "canvasFooterPanel absent from default")
eq(p.liveEditPanel,      nil, "liveEditPanel absent from default")
eq(p.galleryPanel,       nil, "galleryPanel absent from default")
print("  [OK] default: 6 panels placed, 6 hidden")

-- ============================================================================
-- palettePanel was retired (its job moved into the customPanel sidebar +
-- subfilter chip strip). The row from each view's schema is gone; no panel
-- should be placed under "palette" cell anymore.
-- ============================================================================

VDS.Store:Dispatch({ type = A.CUSTOM_THEME_CREATE, payload = { name = "T", dyes = {} } })
VDS.Store:Dispatch({ type = A.CUSTOM_THEME_EDIT_START, payload = { name = "T" } })
p = computeForView("default")
eq(p.palettePanel, nil, "palettePanel retired -- no longer placed in default view")
VDS.Store:Dispatch({ type = A.CUSTOM_THEME_EDIT_END })
print("  [OK] default+edit: palettePanel correctly absent (retired)")

-- ============================================================================
-- Canvas view: 3 cols (280, 420, 380) + 2 col gaps + 8 padding = 1092
-- ============================================================================

VDS.Store:Dispatch({ type = A.SET_FILTER_MODE, payload = { mode = "canvas" } })
w, _ = viewDims("canvas")
eq(w, 1092, "canvas view width = 1092 (3 cols + 2 gaps + 8 padding)")

p = computeForView("canvas")
truthy(p.titlePanel,        "titlePanel placed in canvas (col 2)")
truthy(p.searchPanel,       "searchPanel placed in canvas (col 2)")
truthy(p.tabsPanel,         "tabsPanel placed in canvas (col 2)")
-- subfilterPanel has no `canvas` entry in its cell map -- canvasScopePanel
-- (theme/custom-palette picker) claims the subfilter cell in canvas view.
eq(p.subfilterPanel, nil,   "subfilterPanel absent from canvas (canvasScopePanel claims the cell)")
truthy(p.canvasScopePanel,  "canvasScopePanel placed in canvas (subfilter cell)")
truthy(p.browsePanel,       "browsePanel placed in canvas (col 2)")
-- canvasFooterPanel removed in Phase 6: Save Loadout moved into the
-- preview action bar (right of Clear). No separate footer row.
eq(p.canvasFooterPanel, nil, "canvasFooterPanel removed in Phase 6")
truthy(p.canvasPickerPanel, "canvasPickerPanel placed in canvas (col 1)")
truthy(p.canvasPreviewPanel,"canvasPreviewPanel placed in canvas (col 3)")
eq(p.detailPanel,    nil, "detailPanel absent from canvas")
eq(p.palettePanel,   nil, "palettePanel absent from canvas")
eq(p.liveEditPanel,  nil, "liveEditPanel absent from canvas")
eq(p.galleryPanel,   nil, "galleryPanel absent from canvas")

-- Title in canvas sits in col 2: x = padding(4) + col1(280) + colGap(2) = 286
eq(p.titlePanel.x, 286, "canvas titlePanel x = 286 (col 2 offset)")
print("  [OK] canvas: 8 panels placed (canvasScope swaps in for subfilter), col 2 offset correct")

-- ============================================================================
-- Live view: 1 col (420) + 8 padding = 428
-- ============================================================================

VDS.Store:Dispatch({ type = A.SET_FILTER_MODE, payload = { mode = "category" } })
VDS.Store:Dispatch({
    type = A.LIVE_MODE_ENTER,
    payload = {
        decorID = 7777, decorName = "Test", dyeSlots = { { ID = 1, dyeColorID = 100 } },
        originalDyes = {},
    },
})
w, _ = viewDims("live")
eq(w, 428, "live view width = 428 (1 col)")

p = computeForView("live")
truthy(p.titlePanel,    "titlePanel in live")
truthy(p.browsePanel,   "browsePanel in live")
truthy(p.liveEditPanel, "liveEditPanel in live (replaces detailPanel)")
eq(p.detailPanel,   nil, "detailPanel absent from live")
eq(p.galleryPanel,  nil, "galleryPanel absent from live (no variants)")
print("  [OK] live: liveEditPanel claims footer; no gallery")

-- ============================================================================
-- Stock view: covers warehouse + herbs popout. This is where the buy-gap
-- footer rows live; without computing this view, the layout engine never
-- runs the over-spec check on them.
-- ============================================================================

VDS.Store:Dispatch({ type = A.LIVE_MODE_EXIT })
VDS.Store:Dispatch({ type = A.SET_FILTER_MODE, payload = { mode = "category" } })
VDS.Store:Dispatch({ type = A.SET_ACTIVE_TAB, payload = { tab = "stock" } })
-- stockHerbsPanel is gated on `stock.craftMode` (the dynamic herbs popout
-- only appears in Craft fill mode). Force craft mode so the footer rows --
-- including the dual buy-gap totals -- actually get Computed.
VDS.Store:Dispatch({ type = A.STOCK_SET_FILL_MODE, payload = { mode = "craft" } })

p = computeForView("stock")
truthy(p.stockBodyPanel,     "stockBodyPanel placed in stock view")
truthy(p.stockControlsPanel, "stockControlsPanel placed in stock view (footer + restock button)")
truthy(p.stockHerbsPanel,    "stockHerbsPanel placed in stock view (herbs popout, Craft mode)")
-- The over-spec assertion in computeForView is what catches footer-row width
-- overflows -- the bug shipped on 2026-05-26 where the dual footer rows added
-- up to 244px in a 224px container. computeForView errors before we get here
-- if any panel/section over-specs.
print("  [OK] stock: warehouse + controls + herbs popout placed; no over-spec")

-- Restore buy mode + default tab so downstream test sections start clean.
VDS.Store:Dispatch({ type = A.STOCK_SET_FILL_MODE, payload = { mode = "buy" } })

-- ============================================================================
-- liveWithGallery view: 2 cols (420 + 146) + 2 col gap + 8 padding = 576
-- ============================================================================

VDS.Store:Dispatch({
    type = A.CANVAS_SAVE_LOADOUT,
    payload = { decorID = 7777, dyes = { 1, 2, 3 }, name = "Saved" },
})
w, _ = viewDims("liveWithGallery")
eq(w, 576, "liveWithGallery width = 576 (2 cols + 1 gap + 8 padding)")

p = computeForView("liveWithGallery")
truthy(p.galleryPanel, "galleryPanel placed in liveWithGallery (col 2)")
-- gallery col 2: x = padding(4) + col1(420) + colGap(2) = 426
eq(p.galleryPanel.x, 426, "galleryPanel x = 426 (col 2 offset)")
print("  [OK] liveWithGallery: galleryPanel appears; col 2 offset correct")

-- ============================================================================
-- Idempotency
-- ============================================================================

VDS.Store:Dispatch({ type = A.LIVE_MODE_EXIT })
VDS.Store:Dispatch({ type = A.SET_FILTER_MODE, payload = { mode = "category" } })
local p1 = computeForView("default")
local p2 = computeForView("default")
for id in pairs(p1) do
    truthy(p2[id], "idempotent: panel " .. id .. " present in second compute")
end
print("  [OK] idempotent: two Compute calls produce equivalent placements")

print("test_layout_compute.lua passed")
