-- test_acquire_vendor_nil_npcid.lua
-- Regression: npcID-less vendors (catalog NPCs absent from VendorAugment, e.g.
-- "Dershway the Triggered" in Deeprun Tram, and synthetic "World Vendors"
-- groupings) must be SELECTABLE and RESOLVABLE by their (name, zone) composite.
--
-- The shipped v3.6.0 bug: clicking such a vendor stamped selectedNpcID = nil.
--   * acq.selectedVendor matched on npcID only -> returned nil -> right panel
--     showed the wrong (auto-selected) vendor.
--   * AcquisitionController:Refresh validated the selection on npcID only; a nil
--     npcID was treated as "no selection", so it re-auto-selected the top vendor
--     every pass. When that top vendor was itself npcID-less (filtered to one
--     result), the reconciliation never converged: Refresh -> dispatch -> Notify
--     -> Refresh, the pending-notification queue grew unbounded, and a single
--     flush eventually tripped the "script ran too long" watchdog (hard freeze).
--
-- Both fixes resolve the selection by (name, zone) when npcID is nil. This test
-- locks in the contract both depend on:
--   1. acq.selectedVendor resolves a npcID-less selection by (name, zone).      [Fix 2]
--   2. acq.selectedVendor still resolves an npcID-bearing selection.            [no regression]
--   3. acq.selectedVendor returns nil when nothing is selected.
--   4. CONVERGENCE PRECONDITION [Fix 1]: a npcID-less selection stamped from a
--      visible row matches a vendor in acq.vendors by (name, zone) -- i.e. the
--      controller's membership check recognises it as "present" and returns
--      early instead of re-auto-selecting. (The controller loop itself is
--      exercised in-game; this asserts the data contract it relies on.)

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

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

-- ============================================================================
-- Observer stub (minimal -- mirrors test_acquire_vendor_view.lua)
-- ============================================================================
HDG.HousingCatalogObserver = HDG.HousingCatalogObserver or {}
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.byVendor       = obs.byVendor       or {}
obs.allVendorNames = obs.allVendorNames 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:GetItemsByVendor(name) if not self:IsReady() then return nil end return self.byVendor[name] end
function obs:GetAllVendorNames() if not self:IsReady() then return {} end return self.allVendorNames 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

-- ============================================================================
-- StaticData stubs
-- ============================================================================
HDG.StaticData = HDG.StaticData or {}
HDG.StaticData.Recipes = HDG.StaticData.Recipes or {
    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 }

-- "Quackenbush" resolves to npcID 68363. "Dershway the Triggered" has NO augment
-- (the real-world bug) -> npcID nil. Both in Deeprun Tram.
HDG.StaticData.VendorAugment = {
    Get = function(_, npcID)
        if npcID == 68363 then
            return { name = "Quackenbush", zone = "Deeprun Tram", mapID = 500,
                     x = 53.9, y = 25.7, faction = "A", expansion = "Classic" }
        end
        return nil
    end,
    ResolveName = function(_, name, zone)
        if name == "Quackenbush" then return 68363 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(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()

-- ============================================================================
-- Catalog fixture: two Deeprun Tram vendors selling the same item.
--   Quackenbush          -> augment -> npcID 68363
--   Dershway the Triggered -> NO augment -> npcID nil   (the bug trigger)
-- ============================================================================
obs.byItemID = {
    [263026] = {
        itemID = 263026, name = "Brawler's Barricade", iconTexture = 100,
        collected = false, quantity = 0, numPlaced = 0, dataTagsByID = {},
        sourceText = "Vendor: Dershway the Triggered\nZone: Deeprun Tram",
        factionGate = nil,
        vendors = {
            { name = "Dershway the Triggered", zone = "Deeprun Tram", cost = "500g", faction = "", standing = "" },
            { name = "Quackenbush",            zone = "Deeprun Tram", cost = "500g", faction = "", standing = "" },
        },
    },
}
obs.byVendor = {
    ["Dershway the Triggered"] = { name = "Dershway the Triggered", items = { 263026 }, zone = "Deeprun Tram", faction = "", standing = "" },
    ["Quackenbush"]            = { name = "Quackenbush",            items = { 263026 }, zone = "Deeprun Tram", faction = "", standing = "" },
}
obs.allVendorNames = { "Dershway the Triggered", "Quackenbush" }

HDG.QuestNameResolver = HDG.QuestNameResolver or {
    ResolveTitle = function(_, questID) return "Quest " .. tostring(questID) end,
}

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

-- Locate the npcID-less vendor row exactly as the UI sees it.
local state = HDG.Store:GetState()
local allVendors = HDG.Selectors:Call("acq.allVendors", state, {})
local dershway
for _, v in ipairs(allVendors) do
    if v.name == "Dershway the Triggered" then dershway = v break end
end
assertTrue(dershway ~= nil, "Dershway row present in acq.allVendors")
assertNil(dershway.npcID, "Dershway has nil npcID (no VendorAugment entry)")

-- The row click stamps these three fields (see _wireAcqVendorRow).
local stampName = dershway.name
local stampZone = dershway.catalogZone or dershway.zone

-- ============================================================================
-- 1. Fix 2: selecting Dershway by (name, zone) resolves to his record.
--    Under the OLD code acq.selectedVendor matched npcID only -> returned nil.
-- ============================================================================
HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.UI_SET_TRANSIENT,
    payload = { view = "acquisition", key = "selectedNpcID", value = nil } })
HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.UI_SET_TRANSIENT,
    payload = { view = "acquisition", key = "selectedVendorName", value = stampName } })
HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.UI_SET_TRANSIENT,
    payload = { view = "acquisition", key = "selectedVendorZone", value = stampZone } })
state = HDG.Store:GetState()

local sel = HDG.Selectors:Call("acq.selectedVendor", state, {})
assertTrue(sel ~= nil, "acq.selectedVendor resolves npcID-less vendor by (name, zone)")
assertEq(sel.name, "Dershway the Triggered", "resolved vendor is Dershway, not the auto-selected top row")
assertNil(sel.npcID, "resolved Dershway record still has nil npcID")

-- ============================================================================
-- 4. Fix 1 convergence precondition: the stamped selection matches a vendor in
--    acq.vendors by (name, zone) -- so AcquisitionController:Refresh recognises
--    it as present and returns early instead of re-auto-selecting (the loop).
-- ============================================================================
local visible = HDG.Selectors:Call("acq.vendors", state, {})
local present = false
for _, v in ipairs(visible) do
    if (not v.npcID) and v.name == stampName and (v.catalogZone or v.zone) == stampZone then
        present = true break
    end
end
assertTrue(present, "npcID-less selection is found in acq.vendors by (name, zone) -> Refresh converges")

-- ============================================================================
-- 2. No regression: an npcID-bearing selection (Quackenbush) still resolves.
-- ============================================================================
HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.UI_SET_TRANSIENT,
    payload = { view = "acquisition", key = "selectedNpcID", value = 68363 } })
HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.UI_SET_TRANSIENT,
    payload = { view = "acquisition", key = "selectedVendorName", value = "Quackenbush" } })
HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.UI_SET_TRANSIENT,
    payload = { view = "acquisition", key = "selectedVendorZone", value = "Deeprun Tram" } })
state = HDG.Store:GetState()
local q = HDG.Selectors:Call("acq.selectedVendor", state, {})
assertTrue(q ~= nil, "acq.selectedVendor resolves npcID-bearing vendor")
assertEq(q.npcID, 68363, "Quackenbush resolved by npcID")

-- ============================================================================
-- 3. No selection -> nil.
-- ============================================================================
HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.UI_SET_TRANSIENT,
    payload = { view = "acquisition", key = "selectedNpcID", value = nil } })
HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.UI_SET_TRANSIENT,
    payload = { view = "acquisition", key = "selectedVendorName", value = nil } })
HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.UI_SET_TRANSIENT,
    payload = { view = "acquisition", key = "selectedVendorZone", value = nil } })
state = HDG.Store:GetState()
assertNil(HDG.Selectors:Call("acq.selectedVendor", state, {}),
    "acq.selectedVendor nil when neither npcID nor name set")

print("test_acquire_vendor_nil_npcid: PASS")
