-- test_zone_scanner_catalog.lua
-- Phase 3e: zone.vendorsInZone reads VendorAugment + HousingCatalogObserver,
-- drops the liveDecorIDs / hasLive / isReleased released-gate block entirely.
--
-- Covers:
--   1. zone.vendorsInZone returns empty when observer is cold (IsReady=false)
--   2. zone.vendorsInZone returns empty for mapID=0 (no zone)
--   3. Observer ready + vendor in zone: items built from catalog rows + collected flag
--   4. Faction filter: Alliance-only vendor excluded for Horde player
--   5. Vendor with no catalog presence produces zero items -> excluded from output
--   6. Shopping-list hash still works (npcID:itemID and wishlist w:itemID forms)
--   7. zone.vendorsInZone reads declaration includes session.catalog.sweepGeneration
--   8. zone.vendorsInZone reads declaration does NOT include account.collection.liveDecorIDs

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_Selectors_Status.lua")

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

-- ItemNameResolver not needed by zone selectors, but Decor selectors pull it in.
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.byVendor  = obs.byVendor  or {}
obs._ready    = false

function obs:IsReady()   return self._ready end
-- Canonical IsOwned helper stub (mirrors production formula).
function obs:IsOwned(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:GetRow(itemID)
    if not self:IsReady() then return nil end
    return self.byItemID[itemID]
end
function obs:GetItemsByVendor(vendorName)
    if not self:IsReady() then return nil end
    return self.byVendor[vendorName]
end

-- Pre-populate required globals so StaticData:Get() doesn't error.
_G.HDGR_ItemAugment   = _G.HDGR_ItemAugment   or {}
_G.HDGR_VendorAugment = _G.HDGR_VendorAugment or {}
_G.HDGR_VendorByMapID = _G.HDGR_VendorByMapID or {}

-- Stub StaticData.VendorAugment so we control the per-npcID data in each test.
HDG.StaticData = HDG.StaticData or {}
HDG.StaticData.VendorAugment = HDG.StaticData.VendorAugment or {}
local _vendorAugData = {}     -- test-local; reset per-section
local _mapData       = {}     -- mapID -> list of npcIDs

function HDG.StaticData.VendorAugment:Get(npcID)
    return _vendorAugData[npcID]
end
function HDG.StaticData.VendorAugment:GetVendorsByMap(mapID)
    return _mapData[mapID]
end

-- Decor selectors needed by zone.filteredVendors chain.
dofile("../UI/HDGR_Selectors_Decor.lua")
dofile("../UI/HDGR_Selectors_Zone.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(label, 2) end
end

-- Helper: reset observer + data tables.
local function resetObs()
    obs._ready   = false
    obs.byItemID = {}
    obs.byVendor = {}
    _vendorAugData = {}
    _mapData       = {}
end

-- Fetch zone.vendorsInZone directly from the selector registry.
local function runVendorsInZone(state)
    local sel = HDG.Selectors:GetRegistry()["zone.vendorsInZone"]
    assertTrue(sel ~= nil, "zone.vendorsInZone selector registered")
    return sel.fn(state, {})
end

HDG_DB = {}
HDG.Store:LoadFromSavedVariables()
local state = HDG.Store:GetState()

-- Set up a base state for all tests.
state.session.identity.factionGroup = "N"   -- Neutral player sees all
state.account.vendorShoppingLists   = {}
state.account.activeShoppingListId  = nil

-- ============================================================================
-- 1. Observer cold -> empty output
-- ============================================================================

resetObs()
state.session.zone.currentMapID = 500

-- Even with a vendor in the map index, cold observer returns nothing.
_mapData[500]           = { 9001 }
_vendorAugData[9001]    = { name = "Test Vendor", zone = "Testland", mapID = 500, x = 50, y = 50, faction = "N" }
obs.byVendor["Test Vendor"] = { items = { 1001, 1002 } }

local out = runVendorsInZone(state)
assertEq(#out, 0, "cold observer: no vendor rows emitted")

-- ============================================================================
-- 2. mapID == 0 -> empty output (even when observer ready)
-- ============================================================================

obs._ready = true
state.session.zone.currentMapID = 0
out = runVendorsInZone(state)
assertEq(#out, 0, "mapID=0: no vendor rows emitted")

-- ============================================================================
-- 3. Observer ready: items built from catalog rows, collected from row.collected
-- ============================================================================

resetObs()
obs._ready = true
state.session.zone.currentMapID = 500
state.session.identity.factionGroup = "N"

_mapData[500] = { 9001 }
_vendorAugData[9001] = { name = "Quartermaster Zeb", zone = "Testland", mapID = 500, x = 30, y = 40, faction = "N" }

obs.byVendor["Quartermaster Zeb"] = { items = { 1001, 1002, 1003 } }
obs.byItemID = {
    [1001] = { itemID = 1001, quantity = 1, collected = true,  vendors = { { name = "Quartermaster Zeb" } } },
    [1002] = { itemID = 1002, collected = false, vendors = { { name = "Quartermaster Zeb" } } },
    [1003] = { itemID = 1003, collected = false, vendors = { { name = "Quartermaster Zeb" } } },
}

-- Ownership now comes from row.collected, not decor.isCollected.
-- We do NOT need account.collection.ownedDecorIDs / catalogByItem for zone scanner.
out = runVendorsInZone(state)
assertEq(#out, 1, "ready observer: one vendor row emitted")
local v = out[1]
assertEq(v.npcID,  9001,               "vendor npcID correct")
assertEq(v.name,   "Quartermaster Zeb","vendor name correct")
assertEq(v.zone,   "Testland",         "vendor zone correct")
assertEq(v.mapID,  500,                "vendor mapID correct")
assertEq(v.x,      30,                 "vendor x correct")
assertEq(v.y,      40,                 "vendor y correct")
assertEq(#v.items, 3, "vendor has 3 items")
assertEq(v.collectedCount,   1, "collectedCount=1 (item 1001)")
assertEq(v.uncollectedCount, 2, "uncollectedCount=2 (1002+1003)")

-- Verify item shape.
local found1001 = false
for _, it in ipairs(v.items) do
    if it.itemID == 1001 then
        assertTrue(it.collected,       "item 1001 collected=true")
        assertFalse(it.onShoppingList, "item 1001 not on shopping list")
        found1001 = true
    end
end
assertTrue(found1001, "item 1001 present in vendor items")

-- ============================================================================
-- 4. Faction filter: Alliance-only vendor excluded for Horde player
-- ============================================================================

resetObs()
obs._ready = true
state.session.zone.currentMapID = 600
state.session.identity.factionGroup = "H"

_mapData[600] = { 9002, 9003 }
_vendorAugData[9002] = { name = "Alliance Vendor", zone = "Ally Land", mapID = 600, x = 0, y = 0, faction = "A" }
_vendorAugData[9003] = { name = "Neutral Vendor",  zone = "Ally Land", mapID = 600, x = 0, y = 0, faction = "N" }

obs.byVendor["Alliance Vendor"] = { items = { 2001 } }
obs.byVendor["Neutral Vendor"]  = { items = { 2002 } }
obs.byItemID = {
    [2001] = { itemID = 2001, collected = false, vendors = {} },
    [2002] = { itemID = 2002, collected = false, vendors = {} },
}

out = runVendorsInZone(state)
assertEq(#out, 1, "faction filter: only Neutral vendor emitted for Horde player")
assertEq(out[1].name, "Neutral Vendor", "faction filter: Neutral Vendor passes")

-- ============================================================================
-- 5. Vendor with no catalog presence -> excluded (zero items -> no row)
-- ============================================================================

resetObs()
obs._ready = true
state.session.zone.currentMapID = 700
state.session.identity.factionGroup = "N"

_mapData[700] = { 9004, 9005 }
_vendorAugData[9004] = { name = "Known Vendor",   zone = "Test Zone", mapID = 700, x = 0, y = 0, faction = "N" }
_vendorAugData[9005] = { name = "Unknown Vendor",  zone = "Test Zone", mapID = 700, x = 0, y = 0, faction = "N" }

-- Only 9004 has a catalog entry; 9005 returns nil from GetItemsByVendor.
obs.byVendor["Known Vendor"] = { items = { 3001 } }
-- "Unknown Vendor" not in obs.byVendor -> GetItemsByVendor returns nil
obs.byItemID = {
    [3001] = { itemID = 3001, collected = false, vendors = {} },
}

out = runVendorsInZone(state)
assertEq(#out, 1, "no-catalog vendor excluded: only 1 row emitted")
assertEq(out[1].name, "Known Vendor", "known vendor present")

-- ============================================================================
-- 6. Shopping-list hash: npcID:itemID + wishlist w:itemID forms
-- ============================================================================

resetObs()
obs._ready = true
state.session.zone.currentMapID = 800
state.session.identity.factionGroup = "N"

_mapData[800] = { 9006 }
_vendorAugData[9006] = { name = "Shop Vendor", zone = "Shop Zone", mapID = 800, x = 0, y = 0, faction = "N" }
obs.byVendor["Shop Vendor"] = { items = { 4001, 4002, 4003 } }
obs.byItemID = {
    [4001] = { itemID = 4001, collected = false, vendors = {} },
    [4002] = { itemID = 4002, collected = false, vendors = {} },
    [4003] = { itemID = 4003, collected = false, vendors = {} },
}

-- List with npcID:itemID entry for 4001 and wishlist (no npcID) for 4003.
state.account.vendorShoppingLists = {
    ["list-1"] = {
        items = {
            { itemID = 4001, npcID = 9006 },   -- npcID:itemID form
            { itemID = 4003 },                  -- wishlist form (no npcID)
        }
    }
}
state.account.activeShoppingListId = "list-1"

out = runVendorsInZone(state)
assertEq(#out, 1, "shopping list test: 1 vendor row")
local slItems = {}
for _, it in ipairs(out[1].items) do slItems[it.itemID] = it end

assertTrue(slItems[4001] and slItems[4001].onShoppingList, "4001 on shopping list (npcID form)")
assertFalse(slItems[4002] and slItems[4002].onShoppingList, "4002 NOT on shopping list")
assertTrue(slItems[4003] and slItems[4003].onShoppingList, "4003 on shopping list (wishlist form)")
assertEq(out[1].shoppingListCount, 2, "shoppingListCount=2")

-- Reset shopping list state.
state.account.vendorShoppingLists  = {}
state.account.activeShoppingListId = nil

-- ============================================================================
-- 7. zone.vendorsInZone reads includes session.catalog.sweepGeneration
-- ============================================================================

local sel = HDG.Selectors:GetRegistry()["zone.vendorsInZone"]
assertTrue(sel ~= nil, "zone.vendorsInZone is registered")

local hasSweepGen = false
for _, r in ipairs(sel.reads or {}) do
    if r == "session.catalog.sweepGeneration" then hasSweepGen = true end
end
assertTrue(hasSweepGen, "reads includes session.catalog.sweepGeneration")

-- ============================================================================
-- 8. zone.vendorsInZone reads does NOT include account.collection.liveDecorIDs
-- ============================================================================

local hasLiveDecorIDs = false
for _, r in ipairs(sel.reads or {}) do
    if r == "account.collection.liveDecorIDs" then hasLiveDecorIDs = true end
end
assertFalse(hasLiveDecorIDs, "reads does NOT include account.collection.liveDecorIDs (released-gate deleted)")

-- calls should not contain "decor.isCollected" either (ownership now from row.collected)
local hasIsCollected = false
for _, c in ipairs(sel.calls or {}) do
    if c == "decor.isCollected" then hasIsCollected = true end
end
assertFalse(hasIsCollected, "calls does NOT contain decor.isCollected")

print("test_zone_scanner_catalog.lua: ALL PASS")
