-- HouseTab foundation smoke (Phase 18 -- Phase A.1 reset).
--
-- Per-widget builders + selectors were stripped (per
-- docs/HDGR_HOUSETAB_WIDGET_SPEC.md), so this file covers ONLY the
-- foundation surface:
--   state shape (session.house + session.ui.houseTab + account.ui.houseTab)
--   8 actions: HOUSE_SNAPSHOT_UPDATED + 7 HOUSETAB_*
--     (toggle/order/setOrders/width/resize/reset/picker/designMode)
--   foundation selectors:
--     house.snapshot / .snapshotTick / .titleTier
--     houseTab.pickerOpen / .designMode + button-label selectors
--     house.viewColumns (dynamic-columns selector that widens the
--       window when the picker opens)
--   composite selectors:
--     house.widgetRows (bin-packed dashboard rows; no per-widget data)
--     house.pickerRows (every widget; prev/next neighbour IDs for arrows)

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_Store.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_Selectors_House.lua")
dofile("../data/HDGR_DecorDB.lua")
HDGR_AllDecorDB = {}  -- stub: deprecated DB archived; test does not assert on its content
dofile("../data/HDGR_HouseTab_TitleTiers.lua")
dofile("../data/HDGR_HouseTab_WidgetDefaults.lua")
dofile("../Modules/HDGR_StaticData.lua")
-- Stub HDG.Modules for the aggregator's file-load-time :Declare call.
-- BuildSnapshot is the only thing this test exercises; the lifecycle
-- registration is a no-op here.
HDG.Modules = HDG.Modules or { Declare = function(_self, _def) end }

-- Phase 4.4: BuildSnapshot reads HDG.HousingCatalogObserver.byDecorID directly
-- (state.account.collection.decorCatalog deleted). Minimal stub.
HDG.HousingCatalogObserver = HDG.HousingCatalogObserver or {}
-- Canonical IsOwned helper stub (mirrors production formula
-- quantity + remainingRedeemable + numPlaced > 0).
HDG.HousingCatalogObserver.IsOwned = HDG.HousingCatalogObserver.IsOwned
    or function(self, rowOrID)
        local row
        if type(rowOrID) == "table" then row = rowOrID
        elseif type(rowOrID) == "number" then
            row = HDG.HousingCatalogObserver.byItemID and HDG.HousingCatalogObserver.byItemID[rowOrID]
        end
        if not row then return false end
        return ((row.quantity or 0)
              + (row.remainingRedeemable or 0)
              + (row.numPlaced or 0)) > 0
    end

local _obs = HDG.HousingCatalogObserver
_obs.byDecorID = _obs.byDecorID or {}
_obs.byItemID  = _obs.byItemID  or {}
_obs.byVendor  = _obs.byVendor  or {}  -- HouseAggregator:_topVendorsSnapshot iterates this (prod inits {})
function _obs:IsReady() return next(self.byDecorID) ~= nil end
function _obs:GetRow(itemID) return self.byItemID[itemID] end
function _obs:GetExpansionForItem(_itemID) return nil end

dofile("../Modules/HDGR_HouseAggregator.lua")

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

HDG_DB = {}
HDG.Store:LoadFromSavedVariables()
local state = HDG.Store:GetState()
local A     = HDG.Constants.ACTIONS

-- ============================================================================
-- 1. Default state shape
-- ============================================================================

assertTrue(type(state.session.house) == "table", "session.house present")
assertTrue(type(state.session.house.snapshot) == "table", "snapshot is a table (empty default)")
assertEq(state.session.house.snapshotTick, 0, "snapshotTick starts at 0")

assertTrue(type(state.session.ui.houseTab) == "table", "session.ui.houseTab present")
assertEq(state.session.ui.houseTab.pickerOpen, false, "pickerOpen false at default")
assertEq(state.session.ui.houseTab.designMode, false, "designMode false at default")

assertTrue(type(state.account.ui.houseTab) == "table", "account.ui.houseTab present")
assertTrue(type(state.account.ui.houseTab.enabled)         == "table", "enabled slot present")
assertTrue(type(state.account.ui.houseTab.order)           == "table", "order slot present")
assertTrue(type(state.account.ui.houseTab.width)           == "table", "width slot present")
assertTrue(type(state.account.ui.houseTab.layoutOverrides) == "table", "layoutOverrides slot present")

-- ============================================================================
-- 2. TitleTiers data file + StaticData domain
-- ============================================================================

assertTrue(type(_G.HDGR_HouseTab_TitleTiers) == "table", "TitleTiers data file loaded")
local tiers = HDG.StaticData.TitleTiers:GetAll()
assertEq(#tiers, 9, "9 title tiers shipped")
assertEq(tiers[1].name, "Aspiring Decorator", "first tier name")
assertEq(tiers[1].threshold, 0, "first tier threshold")
assertEq(tiers[9].name, "Legendary Decorator", "last tier name")
assertEq(tiers[9].threshold, 2000, "last tier threshold")
assertEq(tiers[6].vamoose, true, "Decor Maven (tier 6) is vamoose-flagged")

-- ============================================================================
-- 3. Bare-path selectors
-- ============================================================================

local snap = HDG.Selectors:Call("house.snapshot", state, {})
assertTrue(type(snap) == "table", "house.snapshot returns table")
assertEq(HDG.Selectors:Call("house.snapshotTick", state, {}), 0, "snapshotTick selector")
assertEq(HDG.Selectors:Call("houseTab.pickerOpen", state, {}), false, "pickerOpen false default")
assertEq(HDG.Selectors:Call("houseTab.designMode", state, {}), false, "designMode false default")

-- ============================================================================
-- 4. HOUSE_SNAPSHOT_UPDATED replaces snapshot + bumps tick
-- ============================================================================

local fakeSnap = {
    collectedAll = 750,
    totalAll     = 1500,
    title        = "Decor Maven",
    titleTier    = {
        current = { threshold = 750, name = "Decor Maven", vamoose = true },
        prev    = { threshold = 500, name = "Fully Furnished" },
        next    = { threshold = 1000, name = "Decor Master", vamoose = true },
    },
}
HDG.Store:Dispatch({ type = A.HOUSE_SNAPSHOT_UPDATED, payload = { snapshot = fakeSnap } })
state = HDG.Store:GetState()
assertEq(state.session.house.snapshot.collectedAll, 750, "snapshot.collectedAll persisted")
assertEq(state.session.house.snapshotTick, 1, "snapshotTick bumped to 1")

-- ============================================================================
-- 5. house.titleTier composes prev/current/next + within-tier band progress
-- ============================================================================

local tt = HDG.Selectors:Call("house.titleTier", state, {})
assertEq(tt.current.name, "Decor Maven", "current tier name")
assertEq(tt.prev.name, "Fully Furnished", "prev tier name")
assertEq(tt.next.name, "Decor Master", "next tier name")
-- (750 - 500) / (1000 - 500) = 0.5
assertEq(tt.withinTierProgress, 0.5, "within-tier band progress 50%")

-- ============================================================================
-- 6. Picker / design-mode toggles + button-label selectors
-- ============================================================================

assertEq(HDG.Selectors:Call("house.pickerButtonLabel", state, {}), "Customise",
    "pickerButtonLabel default")
assertEq(HDG.Selectors:Call("house.designButtonLabel", state, {}), "Design Mode",
    "designButtonLabel default")

HDG.Store:Dispatch({ type = A.HOUSETAB_TOGGLE_PICKER,      payload = {} })
HDG.Store:Dispatch({ type = A.HOUSETAB_TOGGLE_DESIGN_MODE, payload = {} })
state = HDG.Store:GetState()
assertEq(state.session.ui.houseTab.pickerOpen, true, "pickerOpen flipped to true")
assertEq(state.session.ui.houseTab.designMode, true, "designMode flipped to true")
assertEq(HDG.Selectors:Call("house.pickerButtonLabel", state, {}), "Close Picker",
    "pickerButtonLabel after toggle")
assertEq(HDG.Selectors:Call("house.designButtonLabel", state, {}), "Exit Design",
    "designButtonLabel after toggle")

-- house.viewColumns: picker open -> two columns; closed -> one column.
local cols = HDG.Selectors:Call("house.viewColumns", state, {})
assertEq(#cols, 2, "viewColumns returns 2 entries when picker open")
assertEq(cols[1], 840, "viewColumns col 1 = 840 (matches HDG totalWidth)")
HDG.Store:Dispatch({ type = A.HOUSETAB_TOGGLE_PICKER, payload = {} })
state = HDG.Store:GetState()
cols = HDG.Selectors:Call("house.viewColumns", state, {})
assertEq(#cols, 1, "viewColumns returns 1 entry when picker closed")

-- Reset chrome state.
HDG.Store:Dispatch({ type = A.HOUSETAB_TOGGLE_DESIGN_MODE, payload = {} })
state = HDG.Store:GetState()
assertEq(state.session.ui.houseTab.designMode, false, "designMode toggled back off")

-- ============================================================================
-- 7. HOUSETAB_TOGGLE_WIDGET round-trip
-- ============================================================================

HDG.Store:Dispatch({ type = A.HOUSETAB_TOGGLE_WIDGET, payload = { widgetID = "sourceDonut" } })
state = HDG.Store:GetState()
assertEq(state.account.ui.houseTab.enabled.sourceDonut, true, "TOGGLE_WIDGET turns on")
HDG.Store:Dispatch({ type = A.HOUSETAB_TOGGLE_WIDGET, payload = { widgetID = "sourceDonut" } })
state = HDG.Store:GetState()
assertEq(state.account.ui.houseTab.enabled.sourceDonut, nil, "TOGGLE_WIDGET (twice) clears back to nil")

-- ============================================================================
-- 8. HOUSETAB_SET_ORDER + SET_WIDTH + RESIZE
-- ============================================================================

HDG.Store:Dispatch({ type = A.HOUSETAB_SET_ORDER,    payload = { widgetID = "featured", order = 5 } })
HDG.Store:Dispatch({ type = A.HOUSETAB_SET_WIDTH,    payload = { widgetID = "featured", width = "full" } })
HDG.Store:Dispatch({ type = A.HOUSETAB_RESIZE_WIDGET, payload = { widgetID = "featured", height = 220 } })
state = HDG.Store:GetState()
assertEq(state.account.ui.houseTab.order.featured, 5, "SET_ORDER persisted")
assertEq(state.account.ui.houseTab.width.featured, "full", "SET_WIDTH persisted")
assertEq(state.account.ui.houseTab.layoutOverrides.featured.height, 220, "RESIZE persisted")

-- ============================================================================
-- 9. HOUSETAB_RESET_LAYOUT clears all overrides
-- ============================================================================

HDG.Store:Dispatch({ type = A.HOUSETAB_RESET_LAYOUT, payload = {} })
state = HDG.Store:GetState()
assertEq(next(state.account.ui.houseTab.enabled),         nil, "RESET clears enabled")
assertEq(next(state.account.ui.houseTab.order),           nil, "RESET clears order")
assertEq(next(state.account.ui.houseTab.width),           nil, "RESET clears width")
assertEq(next(state.account.ui.houseTab.layoutOverrides), nil, "RESET clears layoutOverrides")

-- ============================================================================
-- 10. Tier resolution edge cases
-- ============================================================================

-- collectedAll = 0 -> current = Aspiring Decorator, prev = nil, next = Score a Decor
HDG.Store:Dispatch({ type = A.HOUSE_SNAPSHOT_UPDATED, payload = {
    snapshot = { collectedAll = 0, totalAll = 1500, title = tiers[1].name, titleTier = {
        current = tiers[1], prev = nil, next = tiers[2],
    }},
} })
state = HDG.Store:GetState()
local tt0 = HDG.Selectors:Call("house.titleTier", state, {})
assertEq(tt0.current.name, "Aspiring Decorator", "tier 0: current")
assertEq(tt0.prev, nil, "tier 0: no prev")
assertEq(tt0.next.name, "Score a Decor", "tier 0: next")
-- (0 - 0) / (1 - 0) = 0
assertEq(tt0.withinTierProgress, 0, "tier 0: 0% progress")

-- collectedAll = 2500 (over max tier) -> current = Legendary, prev = Grand Curator, next = nil
HDG.Store:Dispatch({ type = A.HOUSE_SNAPSHOT_UPDATED, payload = {
    snapshot = { collectedAll = 2500, totalAll = 3000, title = tiers[9].name, titleTier = {
        current = tiers[9], prev = tiers[8], next = nil,
    }},
} })
state = HDG.Store:GetState()
local ttMax = HDG.Selectors:Call("house.titleTier", state, {})
assertEq(ttMax.current.name, "Legendary Decorator", "max tier: current")
assertEq(ttMax.next, nil, "max tier: no next")
assertEq(ttMax.withinTierProgress, 1, "max tier: capped at 1 (no next threshold)")

-- ============================================================================
-- 11. WIDGET_DEFAULTS data file + StaticData domain
-- ============================================================================

assertTrue(type(_G.HDGR_HouseTab_WidgetDefaults) == "table", "WidgetDefaults data file loaded")
local defaults = HDG.StaticData.WidgetDefaults:GetAll()
assertTrue(#defaults >= 20, "at least 20 widget defaults shipped (got " .. #defaults .. ")")
assertEq(defaults[1].id, "decoratorProfile", "first widget is decoratorProfile")
assertEq(defaults[1].order, 1, "decoratorProfile order=1")

-- ============================================================================
-- 12. house.widgetRows (bin-packed dashboard rows; no per-widget data in
-- Phase A.1 -- each widget brings its own selector when ported).
-- ============================================================================

local function flattenCells(rows)
    local out = {}
    for _, r in ipairs(rows) do
        for _, c in ipairs(r.cells) do out[#out + 1] = c end
    end
    return out
end

local rows = HDG.Selectors:Call("house.widgetRows", state, {})
local cells = flattenCells(rows)
assertTrue(#cells >= 18, "widgetRows produces enabled cells (got " .. #cells .. ")")
assertEq(cells[1].id, "decoratorProfile", "widgetRows sorted by order; first cell is decoratorProfile")

-- Every row's units sum to <= 3 (packing invariant).
for i, r in ipairs(rows) do
    local sum = 0
    for _, c in ipairs(r.cells) do sum = sum + c.units end
    assertTrue(sum <= 3, "row " .. i .. " units sum <= 3 (got " .. sum .. ")")
end

-- Cells carry foundation fields (id, title, width, units, height). Per-
-- widget data envelopes (cell.data) are stamped by house.widgetRows when
-- a per-widget selector is plugged into its `calls` list. Phase A through
-- favorites/themedSets/etc all have per-widget selectors now; only the
-- initiative-event widgets (ritualSites/abyssAnglers/decorDuels/nextRewards)
-- and a few others (records/craftableNow/goblinTopLumber) lack selectors.
local first = cells[1]
assertTrue(first.title  ~= nil, "cell has title")
assertTrue(first.units  ~= nil, "cell has units")
assertTrue(first.height ~= nil, "cell has height")
-- favorites HAS a per-widget selector now (returns { items = {...} }).
local byID0 = {}
for _, c in ipairs(cells) do byID0[c.id] = c end
assertTrue(byID0.favorites and byID0.favorites.data ~= nil,
    "favorites cell carries data envelope")
assertTrue(type(byID0.favorites.data.items) == "table",
    "favorites data envelope has items[] field")
-- All Phase A through D widgets now carry data envelopes. Spot-check
-- a couple of them so a future regression that drops a selector wiring
-- still gets caught.
assertTrue(byID0.ritualSites and byID0.ritualSites.data ~= nil,
    "ritualSites cell carries data envelope")
assertTrue(type(byID0.ritualSites.data.items) == "table",
    "ritualSites data envelope has items[] field")
assertTrue(byID0.craftableNow and byID0.craftableNow.data ~= nil,
    "craftableNow cell carries data envelope")
-- records is enabled=false in defaults so it doesn't appear in
-- widgetRows. Tested separately via the toggle path below.

-- Toggle a default-enabled widget OFF -> it disappears from cells.
HDG.Store:Dispatch({ type = A.HOUSETAB_TOGGLE_WIDGET, payload = { widgetID = "decoratorProfile" } })
state = HDG.Store:GetState()
cells = flattenCells(HDG.Selectors:Call("house.widgetRows", state, {}))
local hasProfile = false
for _, c in ipairs(cells) do if c.id == "decoratorProfile" then hasProfile = true end end
assertEq(hasProfile, false, "TOGGLE flips default-enabled widget off; widgetRows omits it")

-- Toggle a default-disabled widget ON.
HDG.Store:Dispatch({ type = A.HOUSETAB_TOGGLE_WIDGET, payload = { widgetID = "records" } })
state = HDG.Store:GetState()
cells = flattenCells(HDG.Selectors:Call("house.widgetRows", state, {}))
local hasRecords = false
for _, c in ipairs(cells) do if c.id == "records" then hasRecords = true end end
assertEq(hasRecords, true, "TOGGLE flips default-disabled widget on; widgetRows includes it")

-- Order override moves a widget into the first cell.
HDG.Store:Dispatch({ type = A.HOUSETAB_SET_ORDER, payload = { widgetID = "records", order = 0 } })
state = HDG.Store:GetState()
cells = flattenCells(HDG.Selectors:Call("house.widgetRows", state, {}))
assertEq(cells[1].id, "records", "order=0 override puts records first")

-- ============================================================================
-- 13. house.pickerRows: all widgets (enabled or not), sorted by order,
-- with prev/next neighbour IDs for the up/down reorder arrows.
-- ============================================================================

HDG.Store:Dispatch({ type = A.HOUSETAB_RESET_LAYOUT, payload = {} })
state = HDG.Store:GetState()

local picks = HDG.Selectors:Call("house.pickerRows", state, {})
assertTrue(#picks >= 23, "pickerRows includes ALL widgets (got " .. #picks .. ")")
assertEq(picks[1].prevID, nil, "first picker row has no prevID")
assertEq(picks[#picks].nextID, nil, "last picker row has no nextID")
assertEq(picks[2].prevID, picks[1].id, "row 2 prevID points back to row 1")
assertEq(picks[1].nextID, picks[2].id, "row 1 nextID points to row 2")

local enabledSeen, disabledSeen = false, false
for _, p in ipairs(picks) do
    if p.enabled then enabledSeen = true else disabledSeen = true end
end
assertEq(enabledSeen, true,  "pickerRows includes enabled widgets")
assertEq(disabledSeen, true, "pickerRows includes disabled widgets")

-- ============================================================================
-- 13.5. HousingObserver state slots + house.decoratorProfileData composite.
-- HOUSE_LIST_UPDATED creates identity-only entries; HOUSE_LEVEL_UPDATED
-- fills level/favor; the composite stamps both into cell.data with
-- snapshot.title/titleTier/collectedAll/totalAll.
-- ============================================================================

HDG.Store:Dispatch({ type = A.HOUSETAB_RESET_LAYOUT, payload = {} })

-- Initial state: no owned houses, composite returns nil house fields.
state = HDG.Store:GetState()
local dp = HDG.Selectors:Call("house.decoratorProfileData", state, {})
assertEq(dp.houseName, nil, "decoratorProfileData: no house yet, houseName nil")
assertEq(dp.houseLevel, nil, "decoratorProfileData: no house yet, houseLevel nil")

-- Observer simulation: list-updated then level-updated for one house.
HDG.Store:Dispatch({ type = A.HOUSE_LIST_UPDATED, payload = {
    houses = {
        { houseGUID = "fake-guid-1", neighborhoodName = "Hawt Beef City", faction = "Alliance" },
    },
} })
state = HDG.Store:GetState()
assertEq(state.session.house.ownedHouses["fake-guid-1"].name, "Hawt Beef City",
    "HOUSE_LIST_UPDATED stamps identity fields")
assertEq(state.session.house.ownedHouses["fake-guid-1"].faction, "Alliance",
    "HOUSE_LIST_UPDATED stamps faction")

HDG.Store:Dispatch({ type = A.HOUSE_LEVEL_UPDATED, payload = {
    houseGUID  = "fake-guid-1",
    level      = 9,
    favor      = 15798,
    maxLevel   = 50,
    thresholds = { [1] = 0, [9] = 12900, [10] = 17000 },
} })
state = HDG.Store:GetState()
local h = state.session.house.ownedHouses["fake-guid-1"]
assertEq(h.level, 9,     "HOUSE_LEVEL_UPDATED stamps level")
assertEq(h.favor, 15798, "HOUSE_LEVEL_UPDATED stamps favor")
assertEq(h.name, "Hawt Beef City", "HOUSE_LEVEL_UPDATED preserves identity from list event")

-- Re-fire of list event preserves the captured level/favor.
HDG.Store:Dispatch({ type = A.HOUSE_LIST_UPDATED, payload = {
    houses = {
        { houseGUID = "fake-guid-1", neighborhoodName = "Hawt Beef City", faction = "Alliance" },
    },
} })
state = HDG.Store:GetState()
assertEq(state.session.house.ownedHouses["fake-guid-1"].level, 9,
    "HOUSE_LIST_UPDATED re-fire preserves previously-captured level")

-- ACTIVE_NEIGHBORHOOD_UPDATED reducer stores nullable guid.
HDG.Store:Dispatch({ type = A.ACTIVE_NEIGHBORHOOD_UPDATED, payload = { neighborhoodGUID = "neigh-guid-A" } })
state = HDG.Store:GetState()
assertEq(state.session.house.activeNeighborhoodGUID, "neigh-guid-A",
    "ACTIVE_NEIGHBORHOOD_UPDATED stamps guid")
HDG.Store:Dispatch({ type = A.ACTIVE_NEIGHBORHOOD_UPDATED, payload = { neighborhoodGUID = nil } })
state = HDG.Store:GetState()
assertEq(state.session.house.activeNeighborhoodGUID, nil,
    "ACTIVE_NEIGHBORHOOD_UPDATED nil payload clears")

-- Selector picks the matching house via neighborhoodGUID, not just first.
HDG.Store:Dispatch({ type = A.HOUSE_LIST_UPDATED, payload = {
    houses = {
        { houseGUID = "fake-guid-1", neighborhoodGUID = "neigh-A", neighborhoodName = "Alpha Manor",  faction = "Alliance" },
        { houseGUID = "fake-guid-2", neighborhoodGUID = "neigh-B", neighborhoodName = "Beta Cottage", faction = "Horde" },
    },
} })
HDG.Store:Dispatch({ type = A.ACTIVE_NEIGHBORHOOD_UPDATED, payload = { neighborhoodGUID = "neigh-B" } })
state = HDG.Store:GetState()
dp = HDG.Selectors:Call("house.decoratorProfileData", state, {})
assertEq(dp.houseName, "Beta Cottage", "decoratorProfileData: matches active neighborhoodGUID")
assertEq(dp.houseFaction, "Horde", "decoratorProfileData: faction from matched house")

-- Composite composes snapshot fields with the currently-active house.
HDG.Store:Dispatch({ type = A.HOUSE_SNAPSHOT_UPDATED, payload = {
    snapshot = {
        title = "Grand Curator",
        titleTier = { current = tiers[7], prev = tiers[6], next = tiers[8] },
        collectedAll = 1563,
        totalAll = 1660,
    },
} })
state = HDG.Store:GetState()
dp = HDG.Selectors:Call("house.decoratorProfileData", state, {})
assertEq(dp.title, "Grand Curator", "decoratorProfileData: title from snapshot")
assertEq(dp.collectedAll, 1563, "decoratorProfileData: collectedAll from snapshot")
assertEq(dp.totalAll, 1660, "decoratorProfileData: totalAll from snapshot")
-- Active is neigh-B (Beta Cottage / Horde) from the section above.
assertEq(dp.houseName, "Beta Cottage", "decoratorProfileData: houseName tracks active neighborhood")
assertEq(dp.houseFaction, "Horde",     "decoratorProfileData: faction from active house")

-- Composite must land into cell.data via house.widgetRows.
rows = HDG.Selectors:Call("house.widgetRows", state, {})
cells = flattenCells(rows)
for _, c in ipairs(cells) do
    if c.id == "decoratorProfile" then
        assertTrue(c.data ~= nil, "widgetRows stamps decoratorProfile.data")
        assertEq(c.data.houseName, "Beta Cottage", "decoratorProfile cell.data carries active house name")
        break
    end
end

-- ============================================================================
-- 13.7. HouseAggregator BuildSnapshot: trophy curation (isUniqueTrophy +
-- Preyseeker name-match heuristic). Validates the data shape that feeds
-- DecoratorProfile's trophy shelf.
-- ============================================================================

-- Phase 4.4: stub obs.byDecorID (state.account.collection.decorCatalog deleted).
-- 2026-05-26: ownership flows through R:IsOwned reading row counters.
-- Set quantity=1 on owned rows to fake ownership through the canonical formula.
_obs.byDecorID = {
    [1] = { decorID = 1, itemID = 100, name = "Ancient Banner",          isUniqueTrophy = true,  iconTexture = "icon1", quantity = 1 },  -- owned
    [2] = { decorID = 2, itemID = 101, name = "Preyseeker Wolf Bust",    isUniqueTrophy = false, iconTexture = "icon2", quantity = 1 },  -- owned
    [3] = { decorID = 3, itemID = 102, name = "Preyseeker Bear Effigy",  isUniqueTrophy = false, iconTexture = "icon3" },                -- not owned
    [4] = { decorID = 4, itemID = 103, name = "Ordinary Lamp",            isUniqueTrophy = false, iconTexture = "icon4", quantity = 1 }, -- owned
}
state.account.collection.ownedDecorIDs = { [1] = true, [2] = true, [3] = false, [4] = true }
-- (3 trophies total: 1 unique + 2 prey. 2 owned: 1 unique + 1 prey.)

local snap = HDG.HouseAggregator:BuildSnapshot(state)
assertEq(snap.trophiesTotal, 3,
    "trophiesTotal counts isUniqueTrophy + Preyseeker name-matches")
assertEq(snap.trophiesCollected, 2,
    "trophiesCollected counts owned trophies only")
assertEq(#snap.trophies, 2, "snap.trophies array length matches collected")
assertEq(snap.trophies[1].name, "Ancient Banner",
    "uniques sort first within their bucket (alphabetical)")
assertEq(snap.trophies[2].name, "Preyseeker Wolf Bust",
    "prey appears after uniques")
assertEq(snap.trophies[1].iconID, "icon1", "iconID carried from catalog row")

-- Composite stamps trophies into cell.data for the renderer.
HDG.Store:Dispatch({ type = A.HOUSE_SNAPSHOT_UPDATED, payload = { snapshot = snap } })
state = HDG.Store:GetState()
dp = HDG.Selectors:Call("house.decoratorProfileData", state, {})
assertEq(#dp.trophies, 2, "decoratorProfileData.trophies")
assertEq(dp.trophiesCollected, 2, "decoratorProfileData.trophiesCollected")
assertEq(dp.trophiesTotal, 3, "decoratorProfileData.trophiesTotal")

-- Reset catalog so downstream tests aren't affected.
state.account.collection.decorCatalog = {}
state.account.collection.ownedDecorIDs = {}

-- ============================================================================
-- 14. HOUSETAB_SET_ORDERS (bulk replace, used by picker drag-reorder).
-- Reducer replaces the whole order map atomically; selectors re-rank.
-- ============================================================================

HDG.Store:Dispatch({ type = A.HOUSETAB_RESET_LAYOUT, payload = {} })
HDG.Store:Dispatch({ type = A.HOUSETAB_SET_ORDERS, payload = {
    orders = {
        sourceDonut      = 1,
        decoratorProfile = 2,
        styleAffinity    = 3,
    },
} })
state = HDG.Store:GetState()
picks = HDG.Selectors:Call("house.pickerRows", state, {})
local firstThree = { picks[1].id, picks[2].id, picks[3].id }
assertEq(firstThree[1], "sourceDonut",      "SET_ORDERS first slot")
assertEq(firstThree[2], "decoratorProfile", "SET_ORDERS second slot")
assertEq(firstThree[3], "styleAffinity",    "SET_ORDERS third slot")

print("test_house_smoke.lua: ALL PASS")
