-- test_acquire_catalog_first.lua
-- Phase 3b: acq.selectedItem reads from HousingCatalogObserver + augments.
-- Verifies:
--   1. Returns nil when catalog is not ready (cold)
--   2. Returns nil for unknown itemID (not in byItemID)
--   3. iconTexture comes from observer row (not GetItemIconByID)
--   4. expansion comes from GetExpansionForItem
--   5. isCollected reflects observed row.collected
--   6. vendors[] joins catalog row.vendors[] with VendorAugment for coords
--   7. Rep gate: catalog Faction: line wins when present
--   8. Rep gate: ItemAugment fallback when catalog has no factionGate
--   9. sourceType/sourceName derived from catalog row for backward-compat with
--      downstream acq.selectedItem.sourceLine
--  10. reads list includes session.resolvers.catalog.tick (invalidates on sweep)

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 (required by Selectors_Decor load path).
HDG.ItemNameResolver = HDG.ItemNameResolver or {
    ResolveIcon = function() return nil end,
    ResolveName = function() return nil end,
}

-- Stub HousingCatalogObserver.
HDG.HousingCatalogObserver = HDG.HousingCatalogObserver or {}
local obs = HDG.HousingCatalogObserver
obs.byItemID     = obs.byItemID     or {}
obs.tagIDToGroup = obs.tagIDToGroup or {}
-- Canonical IsOwned helper stub (mirrors production formula).
obs.IsOwned = obs.IsOwned or function(_self, rowOrID)
    local row
    if type(rowOrID) == "table" then row = rowOrID
    elseif type(rowOrID) == "number" then row = obs.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

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 (facades needed by selector file load).
HDG.StaticData = HDG.StaticData or {}
HDG.StaticData.Recipes = HDG.StaticData.Recipes or {
    Get    = function() return nil end,
    GetAll = function() return {}  end,
}
-- VendorAugment and ItemAugment are overridden per test below.
HDG.StaticData.VendorAugment = {
    Get         = function() return nil end,
    ResolveName = function() return nil end,
}
HDG.StaticData.ItemAugment = {
    Get = function() return nil end,
}

dofile("../UI/HDGR_Selectors_Acquisition.lua")
dofile("../UI/HDGR_Selectors_Decor.lua")
dofile("../UI/HDGR_Selectors_Recipes.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
local function assertNil(v, label)
    if v ~= nil then error(label .. ": expected nil, got " .. tostring(v), 2) end
end

HDG_DB = {}
HDG.Store:LoadFromSavedVariables()

-- ============================================================================
-- Setup: observer rows
-- ============================================================================
obs.tagIDToGroup = { [110] = "Expansion", [200] = "Sizes" }
obs.byItemID = {
    -- Item with a vendor that has VendorAugment coords + catalog faction gate
    [12345] = {
        itemID           = 12345,
        decorID          = 500,
        name             = "Midnight Table",
        iconTexture      = 999,
        iconAtlas        = nil,
        canCustomize     = false,
        collected        = false,
        isOwned          = false,
        quantity         = 0,
        numPlaced        = 0,
        placementCost    = 5,
        isAllowedIndoors = true,
        isAllowedOutdoors = false,
        firstAcquisitionBonus = 0,
        dataTagsByID     = { [110] = "Midnight" },
        expansion        = "Midnight",  -- baked by BuildRow:_bakeTags; acq.selectedItem reads row.expansion
        category         = nil,
        quest            = nil,
        achievement      = nil,
        sourceText       = "Vendor: Test Vendor\nZone: Valdrakken\nFaction: K'aresh Trust - Honored",
        -- Rep gate is baked into row.sourceTags (REP entry) at BuildRow; the
        -- selector reads sourceTags, not row.factionGate.
        sourceTags       = { { kind = "REP", factionName = "K'aresh Trust", standing = "Honored" } },
        factionGate      = { factionName = "K'aresh Trust", standing = "Honored" },
        -- Vendor enrichment (npcID/mapID/coords/canWaypoint) is baked onto
        -- row.vendors[i] by the observer's _bakeVendors (resolves VendorAugment
        -- at BuildRow); the selector passes it through.
        vendors          = {
            { name = "Test Vendor", zone = "Valdrakken", cost = "100g",
              npcID = 999001, mapID = 2369, x = 45.6, y = 32.1,
              faction = "N", canWaypoint = true },
        },
    },
    -- Item with NO catalog faction gate but has ItemAugment rep gate
    [67890] = {
        itemID           = 67890,
        decorID          = 501,
        name             = "Azerothian Stool",
        iconTexture      = 777,
        iconAtlas        = nil,
        canCustomize     = false,
        collected        = true,
        isOwned          = true,
        quantity         = 1,
        numPlaced        = 0,
        placementCost    = 0,
        isAllowedIndoors = true,
        isAllowedOutdoors = true,
        firstAcquisitionBonus = 0,
        dataTagsByID     = { [200] = "Small" },
        category         = nil,
        quest            = nil,
        achievement      = nil,
        sourceText       = "Vendor: Augment Vendor\nZone: Stormwind",
        -- ItemAugment rep gate baked into sourceTags (no standing -> gate.standing nil).
        sourceTags       = { { kind = "REP", factionID = 42, minRep = 19, factionName = "Valdrakken Accord" } },
        factionGate      = nil,
        vendors          = {
            { name = "Augment Vendor", zone = "Stormwind", cost = "50g",
              npcID = 999002, mapID = 84, x = 60.0, y = 70.0,
              faction = "A", canWaypoint = true },
        },
    },
    -- Item with quest source (for sourceType/sourceName backward-compat test)
    [11111] = {
        itemID           = 11111,
        decorID          = 502,
        name             = "Quest Chair",
        iconTexture      = 555,
        iconAtlas        = nil,
        canCustomize     = false,
        collected        = false,
        isOwned          = false,
        quantity         = 0,
        numPlaced        = 0,
        placementCost    = 0,
        isAllowedIndoors = true,
        isAllowedOutdoors = false,
        firstAcquisitionBonus = 0,
        dataTagsByID     = { [110] = "Midnight" },
        expansion        = "Midnight",  -- baked by BuildRow:_bakeTags; acq.selectedItem reads row.expansion
        category         = nil,
        quest            = "The Final Stand",
        sourceType       = 2,                  -- baked at BuildRow (_bakeSourceTypes): QUEST
        sourceName       = "The Final Stand",  -- baked
        achievement      = nil,
        sourceText       = "Quest: The Final Stand",
        factionGate      = nil,
        vendors          = {},
    },
}

-- VendorAugment stubs: Test Vendor has coords; Augment Vendor has coords too.
HDG.StaticData.VendorAugment = {
    Get = function(_, npcID)
        if npcID == 999001 then
            return { name = "Test Vendor", zone = "Valdrakken", mapID = 2369, x = 45.6, y = 32.1, faction = "N" }
        end
        if npcID == 999002 then
            return { name = "Augment Vendor", zone = "Stormwind", mapID = 84, x = 60.0, y = 70.0, faction = "A" }
        end
        return nil
    end,
    ResolveName = function(_, name, zone)
        if name == "Test Vendor"    then return 999001 end
        if name == "Augment Vendor" then return 999002 end
        return nil
    end,
}

-- ItemAugment stub: item 67890 has a rep gate (ItemAugment fallback path).
HDG.StaticData.ItemAugment = {
    Get = function(_, itemID)
        if itemID == 67890 then
            return { factionID = 42, minRep = 19, factionName = "Valdrakken Accord" }
        end
        return nil
    end,
}

-- ============================================================================
-- 1. Cold catalog: acq.selectedItem returns nil (observer not ready)
-- ============================================================================
local state = HDG.Store:GetState()
HDG.Store:Dispatch({
    type    = HDG.Constants.ACTIONS.UI_SET_TRANSIENT,
    payload = { view = "acquisition", key = "selectedItemID", value = 12345 },
})
assertNil(HDG.Selectors:Call("acq.selectedItem", state, {}),
    "cold catalog: acq.selectedItem is nil before observer ready")

-- ============================================================================
-- 2. Mark catalog ready + re-run
-- ============================================================================
HDG.Store:Dispatch({
    type    = HDG.Constants.ACTIONS.CATALOG_LOAD_COMPLETED,
    payload = { loadedAt = 0, itemCount = 3, vendorCount = 1, generation = 1 },
})
state = HDG.Store:GetState()

-- Unknown itemID -> nil
HDG.Store:Dispatch({
    type    = HDG.Constants.ACTIONS.UI_SET_TRANSIENT,
    payload = { view = "acquisition", key = "selectedItemID", value = -99999 },
})
assertNil(HDG.Selectors:Call("acq.selectedItem", state, {}),
    "bogus itemID -> nil after ready")

-- ============================================================================
-- 3. Valid item: basic fields from observer row
-- ============================================================================
HDG.Store:Dispatch({
    type    = HDG.Constants.ACTIONS.UI_SET_TRANSIENT,
    payload = { view = "acquisition", key = "selectedItemID", value = 12345 },
})
local view = HDG.Selectors:Call("acq.selectedItem", state, {})
assertTrue(view ~= nil, "acq.selectedItem resolves for known itemID")
assertEq(view.itemID,      12345,          "itemID matches")
assertEq(view.name,        "Midnight Table", "name from observer row")
assertEq(view.iconTexture, 999,            "iconTexture from observer (not GetItemIconByID)")
assertEq(view.expansion,   "Midnight",     "expansion from GetExpansionForItem via tagIDToGroup")
assertEq(view.isCollected, false,          "isCollected from observer row.collected")
assertEq(view.placementCost, 5,            "placementCost from observer row")
assertEq(view.indoor,      true,           "indoor from observer row")
assertEq(view.outdoor,     false,          "outdoor from observer row")

-- ============================================================================
-- 4. Vendor list: coords from VendorAugment, gate from catalog factionGate
-- ============================================================================
assertTrue(type(view.vendors) == "table", "vendors is table")
assertEq(#view.vendors, 1, "one vendor for item 12345")
local vend = view.vendors[1]
assertEq(vend.name,       "Test Vendor",  "vendor name from catalog row")
assertEq(vend.zone,       "Valdrakken",   "vendor zone from catalog row")
assertEq(vend.npcID,      999001,         "npcID resolved via VendorAugment:ResolveName")
assertEq(vend.mapID,      2369,           "mapID from VendorAugment:Get")
assertEq(vend.x,          45.6,           "x from VendorAugment:Get")
assertEq(vend.y,          32.1,           "y from VendorAugment:Get")
assertEq(vend.faction,    "N",            "faction from VendorAugment:Get")
assertEq(vend.canWaypoint, true,          "canWaypoint true when augment found")
-- Gate from catalog factionGate (standing ~= "")
assertTrue(vend.gate ~= nil,              "gate set when catalog factionGate present")
assertEq(vend.gate.factionName, "K'aresh Trust", "gate.factionName from catalog factionGate")
assertEq(vend.gate.standing,    "Honored",        "gate.standing from catalog factionGate")

-- ============================================================================
-- 5. Item 67890: collected=true, no catalog gate, ItemAugment fallback
-- ============================================================================
HDG.Store:Dispatch({
    type    = HDG.Constants.ACTIONS.UI_SET_TRANSIENT,
    payload = { view = "acquisition", key = "selectedItemID", value = 67890 },
})
local view2 = HDG.Selectors:Call("acq.selectedItem", state, {})
assertTrue(view2 ~= nil, "acq.selectedItem resolves for item 67890")
assertEq(view2.isCollected, true,  "isCollected true from observer row.collected")
assertEq(view2.iconTexture, 777,   "iconTexture from observer row")
-- Expansion: tagID 200 is "Sizes" not "Expansion" -> returns nil from GetExpansionForItem
assertNil(view2.expansion,         "expansion nil when no Expansion tag in dataTagsByID")

local vend2 = view2.vendors[1]
assertTrue(vend2 ~= nil, "vendor row for 67890 present")
assertEq(vend2.npcID,  999002, "npcID resolved for Augment Vendor")
assertEq(vend2.mapID,  84,     "mapID from VendorAugment for Augment Vendor")
assertEq(vend2.faction, "A",   "faction A from VendorAugment")
assertEq(vend2.canWaypoint, true, "canWaypoint true when augment found")
-- No catalog factionGate (standing == "") -> ItemAugment fallback
assertNil(vend2.gate and vend2.gate.standing,
    "gate.standing absent: catalog vendor has no standing -> ItemAugment path")
assertTrue(vend2.gate ~= nil,          "gate from ItemAugment fallback")
assertEq(vend2.gate.factionID,   42,   "gate.factionID from ItemAugment")
assertEq(vend2.gate.minRep,      19,   "gate.minRep from ItemAugment")
assertEq(vend2.gate.factionName, "Valdrakken Accord", "gate.factionName from ItemAugment")

-- ============================================================================
-- 6. Vendor the observer couldn't resolve -> npcID nil, canWaypoint false, faction "N"
-- ============================================================================
-- _bakeVendors leaves npcID nil + canWaypoint false when VendorAugment can't
-- resolve the vendor (and normalizes faction to "N"); acq.selectedItem passes
-- those through. Re-bake 12345's vendor to that un-enriched shape and re-select.
obs.byItemID[12345].vendors = {
    { name = "Test Vendor", zone = "Valdrakken", cost = "100g",
      npcID = nil, faction = "N", canWaypoint = false },
}
-- Bump generation so the memoized selector re-runs (memoized on sweepGeneration).
HDG.Store:Dispatch({
    type    = HDG.Constants.ACTIONS.CATALOG_LOAD_COMPLETED,
    payload = { loadedAt = 0, itemCount = 3, vendorCount = 1, generation = 2 },
})
state = HDG.Store:GetState()
HDG.Store:Dispatch({
    type    = HDG.Constants.ACTIONS.UI_SET_TRANSIENT,
    payload = { view = "acquisition", key = "selectedItemID", value = 12345 },
})
local viewNoAug = HDG.Selectors:Call("acq.selectedItem", state, {})
assertTrue(viewNoAug ~= nil, "resolves even when vendor is unenriched")
local vendNoAug = viewNoAug.vendors[1]
assertEq(vendNoAug.npcID,       nil,  "npcID nil when observer couldn't resolve the vendor")
assertEq(vendNoAug.mapID,       nil,  "mapID nil when no augment")
assertEq(vendNoAug.faction,     "N",  "faction defaults to N when no augment")
assertEq(vendNoAug.canWaypoint, false, "canWaypoint false when no augment")
-- Gate: still from the row's REP sourceTag (rep gate persists across vendors).
assertTrue(vendNoAug.gate ~= nil, "gate from row.sourceTags REP even without vendor augment")

-- ============================================================================
-- 7. sourceType/sourceName backward-compat: quest source (item 11111)
-- ============================================================================
HDG.Store:Dispatch({
    type    = HDG.Constants.ACTIONS.CATALOG_LOAD_COMPLETED,
    payload = { loadedAt = 0, itemCount = 3, vendorCount = 0, generation = 3 },
})
state = HDG.Store:GetState()
HDG.Store:Dispatch({
    type    = HDG.Constants.ACTIONS.UI_SET_TRANSIENT,
    payload = { view = "acquisition", key = "selectedItemID", value = 11111 },
})
local viewQuest = HDG.Selectors:Call("acq.selectedItem", state, {})
assertTrue(viewQuest ~= nil, "quest-source item resolves")
-- sourceType 2 = Quest per HDG.Constants.SOURCE_TYPE_LABEL mapping
assertEq(viewQuest.sourceType, 2,                "sourceType 2 (Quest) for quest-source item")
assertEq(viewQuest.sourceName, "The Final Stand", "sourceName is quest name from row.quest")
assertEq(#viewQuest.vendors,   0,                "no vendors for quest-source item")

-- ============================================================================
-- 8. reads list must include session.resolvers.catalog.tick
-- ============================================================================
local meta = HDG.Selectors._registry and HDG.Selectors._registry["acq.selectedItem"]
if meta and meta.reads then
    local hasSweep = false
    for _, r in ipairs(meta.reads) do
        if r == "session.resolvers.catalog.tick" then hasSweep = true end
    end
    assertTrue(hasSweep, "reads list includes session.resolvers.catalog.tick")
end

print("test_acquire_catalog_first: PASS")
