-- test_styles_catalog.lua
-- Phase 3g: Styles selectors walk HousingCatalogObserver instead of AllDecorDB.
--
-- Covers:
--   1. unassignedCount returns 0 when observer cold
--   2. unassignedCount reflects catalog item set when observer ready
--   3. curator.sourceItems "all" mode returns empty when observer cold
--   4. curator.sourceItems "all" mode returns rows from observer when ready
--   5. curator.sourceItems "unassigned" mode excludes items already in a style
--   6. styles.detail.collection reads declaration has session.catalog.sweepGeneration
--   7. styles.detail.collection reads declaration does NOT have session.staticData.tick
--   8. styles.curator.sourceItems reads declaration has session.catalog.sweepGeneration
--   9. styles.curator.sourceItems reads declaration does NOT have session.staticData.tick
--  10. styles.curator.unassignedCount reads declaration has session.catalog.sweepGeneration
--  11. styles.detail.items reads declaration has session.catalog.sweepGeneration
--  12. styles.import.previewRows reads declaration has session.catalog.sweepGeneration

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

-- ============================================================================
-- Stubs
-- ============================================================================

HDG.ItemNameResolver = HDG.ItemNameResolver or {
    ResolveIcon = function() return nil end,
    ResolveName = function() return nil end,
}

-- Stub HousingCatalogObserver.
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._ready   = false
obs.tagIDToGroup = obs.tagIDToGroup or {}

function obs:IsReady()         return self._ready end
function obs:GetRow(itemID)
    if not self:IsReady() then return nil end
    return self.byItemID[itemID]
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
function obs:GetExpansionForItem(itemID)
    if not self:IsReady() then return nil end
    local row = self.byItemID[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:GetItemCount()
    if not self:IsReady() then return 0 end
    local n = 0
    for _ in pairs(self.byItemID) do n = n + 1 end
    return n
end

-- Stub StaticData sub-domains.
HDG.StaticData = HDG.StaticData or {}

local _itemAugData = {}
HDG.StaticData.ItemAugment = HDG.StaticData.ItemAugment or {}
function HDG.StaticData.ItemAugment:Get(itemID) return _itemAugData[itemID] end

HDG.StaticData.Styles = HDG.StaticData.Styles or {}
function HDG.StaticData.Styles:GetDefinitions() return {} end

HDG.StaticData.Collections = HDG.StaticData.Collections or {}
function HDG.StaticData.Collections:GetDefinitions() return {} end

-- Stub FacetDB (used by smartset selectors; not under test here).
_G.HDGR_FacetDB    = _G.HDGR_FacetDB    or {}
_G.HDGR_FacetVocab = _G.HDGR_FacetVocab or {}
HDG.StaticData.Facets = HDG.StaticData.Facets or {}
function HDG.StaticData.Facets:GetAll()   return _G.HDGR_FacetDB end
function HDG.StaticData.Facets:GetVocab() return _G.HDGR_FacetVocab end

-- Stub StyleDefinitions / CollectionDefinitions globals.
_G.HDGR_StyleDefinitions      = {}
_G.HDGR_CollectionDefinitions = {}

dofile("../UI/HDGR_Selectors_Decor.lua")
dofile("../UI/HDGR_Selectors_Styles.lua")

-- ============================================================================
-- Helpers
-- ============================================================================

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
local function assertFalse(v, label)
    if v then error("expected false: " .. (label or ""), 2) end
end
local function assertNil(v, label)
    if v ~= nil then error(label .. ": expected nil, got " .. tostring(v), 2) end
end

local A = HDG.Constants.ACTIONS
local function dispatch(t, p) HDG.Store:Dispatch({ type = t, payload = p }) end
local function getState()     return HDG.Store:GetState() end

-- Each reset bumps sweepGeneration so the catalog-memoized selectors
-- (unassignedCount et al. read session.catalog.sweepGeneration) re-run instead
-- of returning a value cached from a prior section. Mirrors production, where
-- every catalog state change arrives via CATALOG_LOAD_COMPLETED (new generation).
local _resetGen = 0
local function resetObs()
    obs._ready   = false
    obs.byItemID = {}
    _itemAugData = {}
    _resetGen = _resetGen + 1
    dispatch(A.CATALOG_LOAD_COMPLETED,
        { generation = _resetGen, loadedAt = 0, itemCount = 0, vendorCount = 0 })
end

-- Owned by default (quantity = 1) so the curator's owned-gate keeps fixtures.
-- Pass quantity = 0 to model an UN-owned (not-yet-collected) decor item.
local function makeRow(itemID, opts)
    opts = opts or {}
    return {
        itemID       = itemID,
        name         = opts.name or ("Item " .. tostring(itemID)),
        iconTexture  = opts.iconTexture,
        iconAtlas    = opts.iconAtlas,
        canCustomize = opts.canCustomize or false,
        isUniqueTrophy = opts.isUniqueTrophy or false,
        categoryID   = opts.categoryID,
        subcategoryID= opts.subcategoryID,
        dataTagsByID = opts.dataTagsByID or {},
        quantity     = opts.quantity or 1,
    }
end

HDG.Store:LoadFromSavedVariables()

-- ============================================================================
-- 1. unassignedCount = 0 when observer cold
-- ============================================================================

resetObs()  -- _ready = false
local n = HDG.Selectors:Call("styles.curator.unassignedCount", getState(), {})
assertEq(n, 0, "1. cold observer -> unassignedCount = 0")

-- ============================================================================
-- 2. unassignedCount reflects catalog item count when observer ready
-- ============================================================================

resetObs()
obs.byItemID[201] = makeRow(201)
obs.byItemID[202] = makeRow(202)
obs.byItemID[203] = makeRow(203)
obs._ready = true

n = HDG.Selectors:Call("styles.curator.unassignedCount", getState(), {})
assertEq(n, 3, "2. ready observer: 3 items -> unassignedCount = 3")

-- ============================================================================
-- 3. curator.sourceItems "all" mode returns empty when observer cold
-- ============================================================================

resetObs()  -- _ready = false
-- sourceMode defaults to "unassigned" in state; set state directly via Store.
-- Use STYLES_CURATOR_SET_SOURCE (the real action name).
dispatch(A.STYLES_CURATOR_SET_SOURCE, { mode = "all" })
local rows = HDG.Selectors:Call("styles.curator.sourceItems", getState(), {})
assertEq(#rows, 0, "3. cold observer + mode=all -> sourceItems empty")

-- ============================================================================
-- 4. curator.sourceItems "all" mode returns rows from observer when ready
-- ============================================================================

resetObs()
obs.byItemID[301] = makeRow(301, { name = "Rug A" })
obs.byItemID[302] = makeRow(302, { name = "Rug B" })
obs._ready = true

dispatch(A.STYLES_CURATOR_SET_SOURCE, { mode = "all" })
rows = HDG.Selectors:Call("styles.curator.sourceItems", getState(), {})
assertEq(#rows, 2, "4. ready observer + mode=all -> 2 sourceItems rows")
assertTrue(rows[1].name == "Rug A" or rows[1].name == "Rug B",
    "4. rows carry observer name")

-- ============================================================================
-- 5. curator.sourceItems "unassigned" excludes items in a user style
-- ============================================================================

resetObs()
obs.byItemID[401] = makeRow(401, { name = "Chair" })
obs.byItemID[402] = makeRow(402, { name = "Table" })
obs.byItemID[403] = makeRow(403, { name = "Lamp" })
obs._ready = true

-- Create a user style and file item 401 into it.
dispatch(A.STYLES_CREATE_STYLE, { displayName = "My Style" })
-- The style ID is "style:my-style" (slug of displayName).
dispatch(A.COLLECTION_STYLE_ITEM_ADDED, { collectionID = "style:my-style", itemID = 401 })

dispatch(A.STYLES_CURATOR_SET_SOURCE, { mode = "unassigned" })
rows = HDG.Selectors:Call("styles.curator.sourceItems", getState(), {})
-- 402 + 403 are unassigned; 401 is in style "my-style"
assertEq(#rows, 2, "5. unassigned mode: 2 rows (402+403), not 401")
for _, row in ipairs(rows) do
    assertTrue(row.itemID == 402 or row.itemID == 403,
        "5. unassigned rows must not include 401")
end

-- ============================================================================
-- 5b. Curator is OWNED-only: un-owned (quantity 0) decor is excluded from both
--     unassignedCount and sourceItems, matching classic HDG's IsItemCollected
--     gate (a Style is a grouping of decor you actually own).
-- ============================================================================

resetObs()
obs.byItemID[501] = makeRow(501, { name = "Owned Rug" })                  -- owned
obs.byItemID[502] = makeRow(502, { name = "Unowned Rug", quantity = 0 })  -- not collected
obs._ready = true

n = HDG.Selectors:Call("styles.curator.unassignedCount", getState(), {})
assertEq(n, 1, "5b. unassignedCount counts only owned decor (501, not 502)")

dispatch(A.STYLES_CURATOR_SET_SOURCE, { mode = "all" })
rows = HDG.Selectors:Call("styles.curator.sourceItems", getState(), {})
assertEq(#rows, 1, "5b. sourceItems mode=all returns only owned decor")
assertEq(rows[1].itemID, 501, "5b. the owned item is the surviving row")

-- ============================================================================
-- 6-12. Reads declaration assertions
-- ============================================================================

local function readsHas(selectorName, path)
    local registry = HDG.Selectors:GetRegistry()
    local sel = registry and registry[selectorName]
    if not sel then
        error("selector not found in registry: " .. selectorName, 2)
    end
    local reads = sel.reads or {}
    for _, r in ipairs(reads) do
        if r == path then return true end
    end
    return false
end

-- Test 6: styles.detail.collection reads sweepGeneration
assertTrue(readsHas("styles.detail.collection", "session.catalog.sweepGeneration"),
    "6. styles.detail.collection reads session.catalog.sweepGeneration")

-- Test 7: styles.detail.collection does NOT read staticData.tick
assertFalse(readsHas("styles.detail.collection", "session.staticData.tick"),
    "7. styles.detail.collection does NOT read session.staticData.tick")

-- Test 8: styles.curator.sourceItems reads sweepGeneration
assertTrue(readsHas("styles.curator.sourceItems", "session.catalog.sweepGeneration"),
    "8. styles.curator.sourceItems reads session.catalog.sweepGeneration")

-- Test 9: styles.curator.sourceItems does NOT read staticData.tick
assertFalse(readsHas("styles.curator.sourceItems", "session.staticData.tick"),
    "9. styles.curator.sourceItems does NOT read session.staticData.tick")

-- Test 10: styles.curator.unassignedCount reads sweepGeneration
assertTrue(readsHas("styles.curator.unassignedCount", "session.catalog.sweepGeneration"),
    "10. styles.curator.unassignedCount reads session.catalog.sweepGeneration")

-- Test 11: styles.detail.items reads sweepGeneration
assertTrue(readsHas("styles.detail.items", "session.catalog.sweepGeneration"),
    "11. styles.detail.items reads session.catalog.sweepGeneration")

-- Test 12: styles.import.previewRows reads sweepGeneration
assertTrue(readsHas("styles.import.previewRows", "session.catalog.sweepGeneration"),
    "12. styles.import.previewRows reads session.catalog.sweepGeneration")

-- ============================================================================
-- 13. Detail surface resolves a vendor shopping list ("vsl:" id). Regression:
--     RecordFor only handled account.collections + concept/collection prefixes,
--     so clicking a shopping list showed the generic "Detail" title + an empty
--     item grid (detail.collection returned nil).
-- ============================================================================

resetObs()
do
    local st = getState()
    st.account.vendorShoppingLists["TestHaul"] = {
        name  = "Tavern Haul",
        items = { { itemID = 701 }, { itemID = 702 }, { itemID = 703 } },
        meta  = { source = "wowhead", url = "https://www.wowhead.com/housing/list/abc" },
    }
    dispatch(A.STYLES_SELECT_COLLECTION, { collectionID = "vsl:TestHaul" })
    dispatch(A.STYLES_SET_VIEW, { view = "detail" })

    local coll = HDG.Selectors:Call("styles.detail.collection", getState(), {})
    assertTrue(coll, "13a. detail.collection resolves a vsl: shopping list")
    assertEq(coll.displayName, "Tavern Haul", "13b. resolved displayName from the list")
    assertEq(#coll.items, 3, "13c. resolved 3 flattened item ids")
    assertEq(HDG.Selectors:Call("styles.headerTitle", getState(), {}), "Tavern Haul",
        "13d. detail title shows the list name, not generic 'Detail'")

    -- URL source + wowhead icon (browse row + detail copy box)
    assertEq(coll.url, "https://www.wowhead.com/housing/list/abc", "13e. record carries meta.url")
    assertTrue(coll.isWowhead, "13f. wowhead source flagged")
    assertEq(coll.icon, HDG.Constants.WOWHEAD_TEXTURE, "13g. browse/detail icon = wowhead logo")
    assertEq(HDG.Selectors:Call("styles.detail.sourceUrl", getState(), {}),
        "https://www.wowhead.com/housing/list/abc", "13h. detail.sourceUrl resolves")
    assertTrue(HDG.Selectors:Call("styles.detail.hasSourceUrl", getState(), {}),
        "13i. hasSourceUrl true")
    assertEq(HDG.Selectors:Call("styles.detail.sourceLabel", getState(), {}),
        "www.wowhead.com", "13j. sourceLabel = parsed host")
    assertEq(HDG.Selectors:Call("styles.detail.sourceIcon", getState(), {}),
        HDG.Constants.WOWHEAD_TEXTURE, "13k. sourceIcon = wowhead logo")
end

print("test_styles_catalog.lua: ALL PASS")
