-- test_decor_catalog_first.lua
-- Phase 3a: decor.allItems reads from HousingCatalogObserver, not AllDecorDB.
-- Verifies:
--   1. decor.allItems returns {} when observer is not ready (cold catalog)
--   2. After observer ready, rows come from byItemID (iconTexture = catalog preview)
--   3. iconTexture is NOT the GetItemIconByID fallback (wrong-icon bug fix)
--   4. Row shape is backward-compatible: itemID, name, exp, sourceType,
--      isOwned, numStored present with correct types
--   5. decor.allItemsByID O(1) index is populated from the migrated list

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("../UI/HDGR_Selectors.lua")
dofile("../UI/HDGR_Selectors_Status.lua")

-- Stub ItemNameResolver so Selectors_Decor can load without it.
HDG.ItemNameResolver = HDG.ItemNameResolver or {
    ResolveIcon = function() return nil end,
    ResolveName = function() return nil end,
}

-- Stub HousingCatalogObserver before loading Selectors_Decor so the
-- selector body can reference it at call time (not at load time).
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.byItemID   = obs.byItemID   or {}
obs.tagIDToGroup = obs.tagIDToGroup or {}

function obs:IsReady()
    local s = HDG.Store:GetState().session.catalog
    return s and s.status == "ready"
end

function obs:GetRow(itemID)
    if not self:IsReady() then return nil end
    return self.byItemID[itemID]
end

function obs:GetIcon(itemID)
    local row = self:GetRow(itemID)
    return row and row.iconTexture
end

function obs:GetExpansionForItem(itemID)
    local row = self:GetRow(itemID)
    if not (row and row.dataTagsByID) then return nil end
    for tagID, displayName in pairs(row.dataTagsByID) do
        if self.tagIDToGroup[tagID] == "Expansion" then
            return displayName
        end
    end
    return nil
end

function obs:IterateRows(fn)
    if not self:IsReady() then return end
    for itemID, row in pairs(self.byItemID) do fn(itemID, row) end
end

-- Stub StaticData so selector helper references don't blow up.
HDG.StaticData = HDG.StaticData or {}
HDG.StaticData.ItemAugment = HDG.StaticData.ItemAugment or {
    Get = function(_, itemID) return nil end,
}

dofile("../UI/HDGR_Selectors_Acquisition.lua")
dofile("../UI/HDGR_Selectors_Recipes.lua")
dofile("../UI/HDGR_Selectors_Decor.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()

-- ============================================================================
-- 1. Cold catalog: decor.allItems returns {} (not ready)
-- ============================================================================
local state = HDG.Store:GetState()
local cold = HDG.Selectors:Call("decor.allItems", state, {})
assertEq(type(cold), "table", "cold allItems is table")
assertEq(#cold, 0, "cold allItems is empty when observer not ready")

-- ============================================================================
-- 2. Inject observer rows and mark catalog ready
-- ============================================================================
-- Three test rows: two owned, one not; different expansion tags.
obs.tagIDToGroup = { [110] = "Expansion", [200] = "Sizes" }
obs.byItemID = {
    [12345] = {
        itemID                = 12345,
        decorID               = 500,
        name                  = "Azerothian Chair",
        iconTexture           = 999,   -- catalog rendered preview icon
        iconAtlas             = nil,
        canCustomize          = false,
        isOwned               = true,
        quantity              = 2,
        numPlaced             = 1,
        placementCost         = 5,
        isAllowedIndoors      = true,
        isAllowedOutdoors     = false,
        firstAcquisitionBonus = 50,
        dataTagsByID          = { [110] = "Midnight" },
        expansion             = "Midnight",  -- baked by BuildRow:_bakeTags; decor.allItems reads row.expansion
        vendors               = {},
        category              = nil,
        quest                 = nil,
        achievement           = nil,
        factionGate           = nil,
    },
    [67890] = {
        itemID                = 67890,
        decorID               = 501,
        name                  = "Alchemy Vial Rack",
        iconTexture           = 12345,  -- different icon (catalog preview)
        iconAtlas             = nil,
        canCustomize          = true,
        isOwned               = false,
        quantity              = 0,
        numPlaced             = 0,
        placementCost         = 0,
        isAllowedIndoors      = true,
        isAllowedOutdoors     = true,
        firstAcquisitionBonus = 0,
        dataTagsByID          = { [200] = "Small" },
        vendors               = {},
        category              = "Professions",
        quest                 = nil,
        achievement           = nil,
        factionGate           = nil,
    },
    [11111] = {
        itemID                = 11111,
        decorID               = 502,
        name                  = "Warcraft Tapestry",
        iconTexture           = 777,
        iconAtlas             = nil,
        canCustomize          = false,
        isOwned               = true,
        quantity              = 1,
        numPlaced             = 0,
        placementCost         = 10,
        isAllowedIndoors      = true,
        isAllowedOutdoors     = true,
        firstAcquisitionBonus = 0,
        dataTagsByID          = { [110] = "Midnight" },
        vendors               = { { name = "Test Vendor", zone = "", cost = "", faction = "", standing = "" } },
        category              = nil,
        quest                 = nil,
        achievement           = nil,
        factionGate           = nil,
    },
}

HDG.Store:Dispatch({
    type    = HDG.Constants.ACTIONS.CATALOG_LOAD_COMPLETED,
    payload = { loadedAt = 0, itemCount = 3, vendorCount = 0, generation = 1 },
})

-- ============================================================================
-- 3. decor.allItems returns rows from observer
-- ============================================================================
state = HDG.Store:GetState()
local all = HDG.Selectors:Call("decor.allItems", state, {})
assertEq(type(all), "table", "allItems is table after ready")
assertEq(#all, 3, "allItems count == byItemID row count")

-- Rows sorted alphabetically by name
assertEq(all[1].name, "Alchemy Vial Rack",  "sorted[1] = Alchemy Vial Rack")
assertEq(all[2].name, "Azerothian Chair",    "sorted[2] = Azerothian Chair")
assertEq(all[3].name, "Warcraft Tapestry",  "sorted[3] = Warcraft Tapestry")

-- ============================================================================
-- 4. Find the chair row and validate fields (especially iconTexture)
-- ============================================================================
local found
for _, r in ipairs(all) do
    if r.itemID == 12345 then found = r; break end
end
assertTrue(found, "row for itemID 12345 present")

-- THE critical assertion: iconTexture comes from the catalog rendered preview
-- (obs row.iconTexture = 999), NOT from GetItemIconByID (which returns the
-- generic inventory icon). This is the wrong-icon bug fix.
assertEq(found.iconTexture, 999, "iconTexture comes from observer (catalog preview), not GetItemIconByID")

-- Other fields backward-compatible
assertEq(found.itemID,               12345,       "itemID")
assertEq(found.decorID,              500,          "decorID")
assertEq(found.name,                 "Azerothian Chair", "name")
assertEq(type(found.exp),            "string",    "exp is string")
assertEq(found.exp,                  "Midnight",  "exp from GetExpansionForItem via tagIDToGroup")
-- sourceType dropped from the decor.allItems list row in the canonical-row-bake
-- refactor (commit ccbd7921): detail-surface field now (decor.selectedItem); list
-- filtering keys off row.sourceTags. No list consumer reads it.
assertEq(found.isOwned,              true,        "isOwned from observer row")
assertEq(found.numStored,            2,           "numStored = row.quantity")
assertEq(found.numPlaced,            1,           "numPlaced from observer")
assertEq(found.placementCost,        5,           "placementCost from observer")
assertEq(found.isAllowedIndoors,     true,        "isAllowedIndoors from observer")
assertEq(found.isAllowedOutdoors,    false,       "isAllowedOutdoors from observer")
assertEq(found.firstAcquisitionBonus, 50,         "firstAcquisitionBonus from observer")
assertEq(found.canCustomize,         false,       "canCustomize from observer")
-- subcats: dropped from the decor.allItems list row (Phase 4 / canonical-row-bake).
-- No consumer indexes it; categoryID is the canonical field. (Stale assertion removed.)

-- ============================================================================
-- 5. decor.allItemsByID O(1) index populated correctly
-- ============================================================================
local byID = HDG.Selectors:Call("decor.allItemsByID", state, {})
assertTrue(type(byID) == "table", "allItemsByID is table")
assertTrue(byID[12345] ~= nil,    "allItemsByID[12345] populated")
assertEq(byID[12345].iconTexture, 999, "allItemsByID icon matches allItems")
assertTrue(byID[67890] ~= nil,    "allItemsByID[67890] populated")
assertTrue(byID[11111] ~= nil,    "allItemsByID[11111] populated")

-- ============================================================================
-- 6. Expansion tag missing -> exp == "?"
-- ============================================================================
-- The Alchemy Vial Rack has only a Sizes tag (tagID 200, not "Expansion").
local rack
for _, r in ipairs(all) do
    if r.itemID == 67890 then rack = r; break end
end
assertTrue(rack, "rack row found")
assertEq(rack.exp, "?", "exp = ? when no Expansion tag in dataTagsByID")

-- ============================================================================
-- 7. ItemAugment sourceType/altSourceType flow through to rows
-- ============================================================================
-- Set up an ItemAugment entry for itemID 11111 (Warcraft Tapestry)
HDG.StaticData.ItemAugment = {
    Get = function(_, id)
        if id == 11111 then return { sourceType = 5, altSourceType = 1 } end
        return nil
    end,
}

-- Dispatch a new CATALOG_LOAD_COMPLETED to bump sweepGeneration so the
-- memoized selector re-runs with the updated ItemAugment. (The memo
-- invalidates on session.resolvers.catalog.tick, not on external table
-- mutation -- this matches production behavior where augment changes only
-- arrive with a catalog refresh.)
HDG.Store:Dispatch({
    type    = HDG.Constants.ACTIONS.CATALOG_LOAD_COMPLETED,
    payload = { loadedAt = 0, itemCount = 3, vendorCount = 0, generation = 2 },
})
state = HDG.Store:GetState()
all = HDG.Selectors:Call("decor.allItems", state, {})
local tapestry
for _, r in ipairs(all) do
    if r.itemID == 11111 then tapestry = r; break end
end
assertTrue(tapestry, "tapestry row found")
-- sourceType / altSourceType dropped from decor.allItems list rows in the
-- canonical-row-bake refactor (commit ccbd7921). ItemAugment source info is
-- consumed at the detail surface (decor.selectedItem) + via row.sourceTags for
-- filtering, not projected onto list rows. (Stale list-row assertions removed.)

print("test_decor_catalog_first: PASS")
