-- test_window_smoke.lua
-- ============================================================================
-- Phase 2 floating-frame infra smoke: HDG.Window:Register -> CreateAll ->
-- reconciler. Verifies:
--   1. Register loud-errors on missing required spec fields
--   2. CreateAll constructs one Frame per registered spec
--   3. Reconciler honors visibilityBinding (state true -> Show; false -> Hide)
--   4. Reconciler honors positionBinding (state { x, y } -> SetPoint)
--
-- Uses a dummy "smokeWindow" registered with stub selectors; doesn't
-- depend on any feature's window. Future lumber-tracker phases get their
-- own test_lumber_smoke.lua.

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_Memo.lua")
dofile("../Core/HDGR_TableUtils.lua")
dofile("../Core/HDGR_Modules.lua")
dofile("../Core/HDGR_StoreFurnishings.lua")
dofile("../Core/HDGR_Store.lua")
dofile("../Core/HDGR_WidgetTypes.lua")
dofile("../UI/HDGR_LayoutConfig.lua")
dofile("../UI/HDGR_Theme.lua")
dofile("../UI/HDGR_Layout.lua")
dofile("../UI/HDGR_Window.lua")
dofile("../UI/HDGR_Selectors.lua")

-- Cookbook 10 stubs: Window:_CreateOne strictly calls Environment/Controllers/
-- BindingEngine after the cascade cleanup. None of those engines are loaded
-- by this smoke harness (real flow wires them at OnEnable). The synthetic
-- testSmoke view has no panels so Layout:BuildAll iterates nothing and the
-- stub WireAll / Build calls are no-ops.
HDG.Environment = HDG.Environment or { _current = {} }
HDG.Controllers = HDG.Controllers or { WireAll = function() end }
HDG.BindingEngine = HDG.BindingEngine or {
    Build = function() end,
    Apply = function() 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

-- ===== 1. Synthetic satellite declared in LC.windows (no more :Register) ===
-- Dummy selectors the reconciler reads; we mutate state + reconcile to drive
-- Show/Hide + position.
HDG.Selectors:Register("test.window.smokeVisible", {
    reads = { "session.test.smokeVisible" },
    fn = function(state) return state.session.test and state.session.test.smokeVisible end,
})
HDG.Selectors:Register("test.window.smokePosition", {
    reads = { "session.test.smokePosition" },
    fn = function(state) return state.session.test and state.session.test.smokePosition end,
})

-- Synthetic fill view with no panels so BuildAll's viewFilter builds nothing
-- -- isolates this test to the Window infra (frame creation + reconciliation).
HDG.LayoutConfig.window.views.testSmoke = {
    explicit       = true,
    standalone     = true,
    width          = 200,
    height         = 200,
    columns        = { 200 },
    rows           = { 200 },
    cells          = { body = { col = 1, row = 1, colSpan = 1, rowSpan = 1 } },
}

-- Declare the satellite in LC.windows (HDG-ADR-025 step 5 -- replaces
-- HDG.Window:Register). A windows entry is a satellite iff it has a `shown`
-- field; `main` (no shown) is owned by the MainFrame pipeline -> CreateAll skips it.
HDG.LayoutConfig.windows.smokeWindow = {
    slots    = { fill = "testSmoke" },
    shown    = "test.window.smokeVisible",
    position = { binding = "test.window.smokePosition", default = { x = 50, y = -75 }, movable = true },
}

-- Seed the test state slot the selector reads. Direct state mutation is
-- the test-shim equivalent of a reducer dispatch; production goes through
-- the closed-action taxonomy.
HDG_DB = {}
HDG.Store:LoadFromSavedVariables()
local state = HDG.Store:GetState()
state.session.test = { smokeVisible = false, smokePosition = nil }

HDG.Window:CreateAll()
local frame = HDG.Window:_GetFrame("smokeWindow")
assertTrue(frame ~= nil, "frame created for the satellite windows entry")
assertEq(frame:IsShown(), false, "frame starts hidden (smokeVisible = false)")
-- `main` has no `shown` field -> it's owned by the MainFrame pipeline, so the
-- satellite reconciler must NOT create a frame for it.
assertTrue(HDG.Window:_GetFrame("main") == nil,
    "CreateAll skips `main` (no shown -> not a satellite)")

-- ===== 3. Visibility reconciliation ========================================
-- Flip the state slot, dispatch a no-op action to wake the subscriber,
-- assert the frame Show'd.

state.session.test.smokeVisible = true
-- Force reconcile (subscriber would fire on any dispatch; bypass with direct
-- call since wiring a fake action just to wake the subscriber is overkill
-- for this smoke test).
HDG.Window:_ReconcileOne("smokeWindow")
assertEq(frame:IsShown(), true, "frame shown after smokeVisible = true")

state.session.test.smokeVisible = false
HDG.Window:_ReconcileOne("smokeWindow")
assertEq(frame:IsShown(), false, "frame hidden after smokeVisible = false")

-- ===== 4. Position reconciliation ==========================================
-- positionBinding returning { x, y } drives SetPoint TOPLEFT.

state.session.test.smokeVisible = true
state.session.test.smokePosition = { x = 123, y = -456 }
HDG.Window:_ReconcileOne("smokeWindow")
-- The mock CreateFrame in wow_mock.lua tracks SetPoint args; we don't have
-- a strict assert API for it, so just verify the call didn't error and the
-- frame is still shown. Position binding integration with the real frame
-- coords is verified in-game (Phase 7+).
assertEq(frame:IsShown(), true, "frame stays shown across position reconcile")

-- ===== 4b. Combat auto-hide (HIDE_IN_COMBAT) ===============================
-- While in combat a satellite is suppressed regardless of its `shown` selector,
-- then restored when combat ends. The smoke harness doesn't load HDG.Config, so
-- stub it; the gate only consults Config when inLockdown is true (short-circuit).
HDG.Config = HDG.Config or { Get = function(_, key) return key == "HIDE_IN_COMBAT" end }
state.session.test.smokeVisible = true
state.session.combat.inLockdown = true
HDG.Window:_ReconcileOne("smokeWindow")
assertEq(frame:IsShown(), false, "satellite hidden while in combat (HIDE_IN_COMBAT on)")
state.session.combat.inLockdown = false
HDG.Window:_ReconcileOne("smokeWindow")
assertEq(frame:IsShown(), true, "satellite restored after combat ends")

-- Toggle OFF -> combat must NOT suppress.
HDG.Config.Get = function() return false end
state.session.combat.inLockdown = true
HDG.Window:_ReconcileOne("smokeWindow")
assertEq(frame:IsShown(), true, "satellite NOT hidden in combat when HIDE_IN_COMBAT is off")
HDG.Config.Get = function(_, key) return key == "HIDE_IN_COMBAT" end   -- restore
state.session.combat.inLockdown = false

-- ===== 5. Lazy satellite + parent resolver + EnsureCreated =================
-- A `lazy` satellite (parent frame is LoadOnDemand) is SKIPPED by CreateAll and
-- created on demand by EnsureCreated, parented via its `parent` resolver. Models
-- the HouseEditorCompanion (parent = HouseEditorFrame, created on editor-open).
HDG.Selectors:Register("test.window.lazyVisible", {
    reads = { "session.test.lazyVisible" },
    fn = function(state) return state.session.test and state.session.test.lazyVisible end,
})
HDG.LayoutConfig.window.views.testLazy = {
    explicit = true, standalone = true,
    width = 200, height = 200, columns = { 200 }, rows = { 200 },
    cells = { body = { col = 1, row = 1, colSpan = 1, rowSpan = 1 } },
}
local parentResolved = false
local stubParent = _G.CreateFrame("Frame")
HDG.LayoutConfig.windows.lazyWindow = {
    lazy   = true,
    parent = function() parentResolved = true; return stubParent end,
    slots  = { fill = "testLazy" },
    shown  = "test.window.lazyVisible",
    position = { default = { x = 10, y = -10 }, movable = true },
}
state.session.test.lazyVisible = false

-- CreateAll must NOT create a lazy satellite.
HDG.Window:CreateAll()
assertTrue(HDG.Window:_GetFrame("lazyWindow") == nil,
    "CreateAll skips a lazy satellite")
assertTrue(parentResolved == false, "parent resolver not invoked before EnsureCreated")

-- EnsureCreated builds it, invoking the parent resolver, and reconciles.
local lazyFrame = HDG.Window:EnsureCreated("lazyWindow")
assertTrue(lazyFrame ~= nil, "EnsureCreated builds the lazy satellite frame")
assertTrue(parentResolved == true, "EnsureCreated invoked the parent resolver")
assertEq(lazyFrame:IsShown(), false, "lazy frame starts hidden (lazyVisible = false)")

-- Idempotent: a second EnsureCreated returns the same frame, no rebuild.
assertEq(HDG.Window:EnsureCreated("lazyWindow"), lazyFrame,
    "EnsureCreated is idempotent (returns the existing frame)")

-- Now driven by the same reconciler as eager satellites.
state.session.test.lazyVisible = true
HDG.Window:_ReconcileOne("lazyWindow")
assertEq(lazyFrame:IsShown(), true, "lazy frame shown after lazyVisible = true")

print("test_window_smoke.lua passed")
