-- test_acquire_costvariants.lua
-- Dual-cost path ("buyable for 30 Community Coupons OR 500g"): the observer
-- bakes distinct payment OPTIONS into row.costVariants; acq.selected.listRows
-- lists the item once PER option; the scrollbox row keys stay unique.
--
-- Regression: the shipped key-collision bug
--   "scrollbox acqVendorItemListRow: key collision 'vil_262453' at index 2
--    (also at index 1)"
-- -- two same-itemID rows hashed to one key. Locked by check #4.
--
-- End-to-end on the REAL observer (_bakeCost dedup + gold-string path) + the
-- REAL acq.selected.items / acq.selected.listRows selectors.
--
-- Verifies:
--   1. _bakeCost: 4 vendor blocks (2 zones x {30 coupons, 500g}) -> 2 distinct
--      costVariants; a single-price item -> 1 variant.
--   2. acq.selected.items stays ONE row per item (cardGrid + milestone count
--      must not double-count).
--   3. acq.selected.listRows expands the 2-option item into 2 rows with distinct
--      variantIndex (1,2) + matching costLineVariant; the single-option item
--      stays 1 row with no variant stamps.
--   4. Row keys (controller formula) are unique across the expanded list.
--   5. Purity: the memoized acq.selected.items source rows are NOT mutated.

local function out(...) io.stderr:write(table.concat({ ... }, " ") .. "\n") end

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")

-- Load the REAL catalog observer (stub the engine bits it touches at load).
HDG.Modules = { Declare = function() end }
HDG.ItemNameResolver = HDG.ItemNameResolver or {
    ResolveIcon = function() return nil end,
    ResolveName = function() return nil end,
}
dofile("../Modules/HDGR_HousingCatalogObserver.lua")
local obs = HDG.HousingCatalogObserver

-- StaticData stubs: no recipes for this vendor; ItemAugment empty; one Architect
-- at Razorwind Shores (npcID 253596).
HDG.StaticData = HDG.StaticData or {}
HDG.StaticData.Recipes = {
    Get = function() return nil end, GetAll = function() return {} end,
    RecipeVendorCounts = function() return {} end, GetBySourceNpcID = function() return nil end,
}
HDG.StaticData.ItemAugment = { Get = function() return nil end }
HDG.StaticData.VendorAugment = {
    Get = function(_, npcID)
        if npcID == 253596 then
            return { name = "The Last Architect", zone = "Razorwind Shores",
                     mapID = 2351, x = 53.7, y = 57.4, faction = "N", expansion = "Midnight" }
        end
        return nil
    end,
    ResolveName = function(_, name, zone)
        if name == "The Last Architect" and zone == "Razorwind Shores" then return 253596 end
        return nil
    end,
}

dofile("../UI/HDGR_Selectors_Acquisition.lua")
dofile("../UI/HDGR_Selectors_Decor.lua")
dofile("../UI/HDGR_Selectors_Recipes.lua")

local function assertEq(a, e, label)
    if a ~= e then error(label .. ": expected " .. tostring(e) .. ", got " .. tostring(a), 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

-- Row key formula MIRRORS HDGR_Controller_Acquisition.lua acqVendorItemListRow.key.
local function rowKey(ed)
    return "vil_" .. tostring(ed and ed.itemID or "?")
        .. (ed and ed.variantIndex and ("_" .. ed.variantIndex) or "")
end

HDG_DB = {}
HDG.Store:LoadFromSavedVariables()

-- Catalog rows. 262453 (Hearthlight Armillary) is buyable two ways -- the catalog
-- ships 4 blocks (2 zones x {30 coupons, 500g}). 1002 is single-price (100g).
obs.byItemID = {
    [262453] = {
        itemID = 262453, name = "Hearthlight Armillary", iconTexture = 100,
        quantity = 0, numPlaced = 0, isAllowedIndoors = true, isAllowedOutdoors = false,
        factionGate = nil,
        vendors = {
            { name = "The Last Architect", zone = "Razorwind Shores", cost = "30",
              costEntries = { { currencyID = 3363, amount = 30 } } },
            { name = "The Last Architect", zone = "Founder's Point",  cost = "30",
              costEntries = { { currencyID = 3363, amount = 30 } } },
            { name = "The Last Architect", zone = "Razorwind Shores", cost = "500" },
            { name = "The Last Architect", zone = "Founder's Point",  cost = "500" },
        },
    },
    [1002] = {
        itemID = 1002, name = "Plain Chair", iconTexture = 200,
        quantity = 0, numPlaced = 0, isAllowedIndoors = true, isAllowedOutdoors = true,
        factionGate = nil,
        vendors = { { name = "The Last Architect", zone = "Razorwind Shores", cost = "100" } },
    },
}
-- Bake costs (exercises the real _bakeCost: dedup across blocks + gold-string path).
for _, row in pairs(obs.byItemID) do obs:_bakeCost(row) end

obs.byVendor = {
    ["The Last Architect::Razorwind Shores"] = {
        name = "The Last Architect", zone = "Razorwind Shores",
        items = { 262453, 1002 }, faction = "", standing = "",
    },
}
obs.allVendorNames = { "The Last Architect" }

-- ===== 1. Observer baking: distinct payment options ========================
assertEq(#obs.byItemID[262453].costVariants, 2, "262453 -> 2 distinct cost variants")
assertEq(#obs.byItemID[1002].costVariants,   1, "1002 -> 1 cost variant")
assertTrue(obs.byItemID[262453].costVariants[1] ~= obs.byItemID[262453].costVariants[2],
    "262453 the two variants differ (coupons vs gold)")

-- Mark catalog ready + select the Razorwind Architect.
HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.CATALOG_LOAD_COMPLETED,
    payload = { loadedAt = 0, itemCount = 2, vendorCount = 1, generation = 1 } })
HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.UI_SET_TRANSIENT,
    payload = { view = "acquisition", key = "selectedVendorName", value = "The Last Architect" } })
HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.UI_SET_TRANSIENT,
    payload = { view = "acquisition", key = "selectedVendorZone", value = "Razorwind Shores" } })
HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.UI_SET_TRANSIENT,
    payload = { view = "acquisition", key = "selectedNpcID", value = 253596 } })
HDG.Selectors:InvalidateMemos("*")
local state = HDG.Store:GetState()

-- ===== 2. acq.selected.items: ONE row per item =============================
local items = HDG.Selectors:Call("acq.selected.items", state, {})
assertEq(#items, 2, "acq.selected.items: 2 rows (one per item, NOT expanded)")
local itemsById = {}
for _, it in ipairs(items) do itemsById[it.itemID] = it end
assertTrue(itemsById[262453] ~= nil, "items has 262453")
assertTrue(itemsById[1002] ~= nil,   "items has 1002")
assertEq(#itemsById[262453].costVariants, 2, "262453 envelope carries costVariants")

-- ===== 3. acq.selected.listRows: expand 2-option item, keep 1-option =======
local rows = HDG.Selectors:Call("acq.selected.listRows", state, {})
assertEq(#rows, 3, "listRows: 3 rows (262453 x2 options + 1002 x1)")

local variantRows, plainRow = {}, nil
for _, r in ipairs(rows) do
    if r.itemID == 262453 then variantRows[#variantRows + 1] = r
    elseif r.itemID == 1002 then plainRow = r end
end
assertEq(#variantRows, 2, "262453 listed twice")
-- Order preserved: variantIndex 1 then 2, each carrying its option's cost line.
assertEq(variantRows[1].variantIndex, 1, "first option variantIndex=1")
assertEq(variantRows[2].variantIndex, 2, "second option variantIndex=2")
assertEq(variantRows[1].costLineVariant, itemsById[262453].costVariants[1], "row1 cost = variant 1")
assertEq(variantRows[2].costLineVariant, itemsById[262453].costVariants[2], "row2 cost = variant 2")
-- Single-option item: not expanded, no variant stamps.
assertTrue(plainRow ~= nil, "1002 present")
assertNil(plainRow.variantIndex,    "1002 has no variantIndex (not expanded)")
assertNil(plainRow.costLineVariant, "1002 has no costLineVariant")

-- ===== 4. Row keys unique (the shipped collision regression) ===============
local seen = {}
for _, r in ipairs(rows) do
    local k = rowKey(r)
    assertTrue(not seen[k], "row key collision on " .. k)
    seen[k] = true
end
assertEq(rowKey(variantRows[1]), "vil_262453_1", "variant 1 key")
assertEq(rowKey(variantRows[2]), "vil_262453_2", "variant 2 key")
assertEq(rowKey(plainRow),       "vil_1002",     "single-option key unchanged")

-- ===== 5. Purity: source rows not mutated ==================================
assertNil(itemsById[262453].costLineVariant,
    "acq.selected.items source row NOT mutated with costLineVariant")
assertNil(itemsById[262453].variantIndex,
    "acq.selected.items source row NOT mutated with variantIndex")

out("test_acquire_costvariants: PASS (262453 -> 2 options, keys vil_262453_1 / _2)")
