-- test_acquire_vendor_view.lua
-- Phase 3c: acq.allVendors + acq.vendors (filtered) + acq.vendorRows
-- read from HousingCatalogObserver + VendorAugment, not VendorDB.
--
-- Verifies:
--   1. acq.allVendors returns empty when observer is not ready
--   2. acq.allVendors returns catalog vendor names after observer ready
--   3. VendorAugment coords + faction are joined onto each row
--   4. Vendor with no augment match has canWaypoint=false, faction="Neutral", npcID=nil
--   5. reps set is built from per-item factionGate in catalog rows
--   6. itemCount reflects catalog items list length
--   7. acq.vendors "missing" preset walks catalog items (not VendorDB)
--   8. acq.vendors "reputation" preset uses v.reps set
--   9. acq.vendorRows stamps allCollected from catalog items list
--  10. reads list includes 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_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,
}

-- ============================================================================
-- Observer stub
-- ============================================================================
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.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,  -- no recipe vendors in this fixture
    GetBySourceNpcID   = function() return nil end,
}
HDG.StaticData.ItemAugment = {
    Get = function() return nil end,
}

-- VendorAugment: "Midnight Vendor" resolves to npcID 101, "Horde Vendor" to 102.
-- "Unknown Vendor" has no augment (tests canWaypoint=false path).
HDG.StaticData.VendorAugment = {
    Get = function(_, npcID)
        if npcID == 101 then
            return { name = "Midnight Vendor", zone = "Dornogal", mapID = 2248,
                     x = 55.2, y = 40.1, faction = "N", expansion = "Midnight" }
        end
        if npcID == 102 then
            return { name = "Horde Vendor", zone = "Orgrimmar", mapID = 85,
                     x = 30.0, y = 60.0, faction = "H", expansion = "Classic" }
        end
        return nil
    end,
    ResolveName = function(_, name, zone)
        if name == "Midnight Vendor" then return 101 end
        if name == "Horde Vendor"    then return 102 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
local function assertFalse(v, label)
    if v then error(label .. ": expected false/nil, got " .. tostring(v), 2) end
end

HDG_DB = {}
HDG.Store:LoadFromSavedVariables()

-- ============================================================================
-- Catalog data
-- ============================================================================
obs.tagIDToGroup = { [10] = "Expansion" }

-- Item 1001: sold by "Midnight Vendor", has a rep gate (K'aresh Trust)
-- Item 1002: sold by "Midnight Vendor", no rep gate, uncollected
-- Item 1003: sold by "Horde Vendor", no rep gate, collected
-- Item 1004: sold by "Unknown Vendor", no rep gate
obs.byItemID = {
    [1001] = {
        itemID = 1001, name = "Rep-Gated Table", iconTexture = 100,
        collected = false, quantity = 0, numPlaced = 0,
        dataTagsByID = { [10] = "Midnight" },
        category = nil, quest = nil, achievement = nil,
        sourceText = "Vendor: Midnight Vendor\nZone: Dornogal\nFaction: K'aresh Trust - Honored",
        factionGate = { factionName = "K'aresh Trust", standing = "Honored" },
        -- Source classification baked into sourceTags at BuildRow; the rep preset
        -- (_presetMatchesFlag "REP") + source filters key off these, not factionGate.
        sourceTags = { { kind = "VENDOR" },
                       { kind = "REP", factionName = "K'aresh Trust", standing = "Honored" } },
        vendors = { { name = "Midnight Vendor", zone = "Dornogal", cost = "500g",
                      faction = "K'aresh Trust", standing = "Honored" } },
    },
    [1002] = {
        itemID = 1002, name = "Plain Chair", iconTexture = 200,
        collected = false, quantity = 0, numPlaced = 0,
        dataTagsByID = { [10] = "Midnight" },
        category = nil, quest = nil, achievement = nil,
        sourceText = "Vendor: Midnight Vendor\nZone: Dornogal",
        factionGate = nil,
        vendors = { { name = "Midnight Vendor", zone = "Dornogal", cost = "100g",
                      faction = "", standing = "" } },
    },
    [1003] = {
        itemID = 1003, name = "Horde Rug", iconTexture = 300,
        collected = true, quantity = 1, numPlaced = 0,
        dataTagsByID = {},
        category = nil, quest = nil, achievement = nil,
        sourceText = "Vendor: Horde Vendor\nZone: Orgrimmar",
        factionGate = nil,
        vendors = { { name = "Horde Vendor", zone = "Orgrimmar", cost = "200g",
                      faction = "", standing = "" } },
    },
    [1004] = {
        itemID = 1004, name = "Mystery Lamp", iconTexture = 400,
        collected = false, quantity = 0, numPlaced = 0,
        dataTagsByID = {},
        category = nil, quest = nil, achievement = nil,
        sourceText = "Vendor: Unknown Vendor",
        factionGate = nil,
        vendors = { { name = "Unknown Vendor", zone = "", cost = "50g",
                      faction = "", standing = "" } },
    },
}

-- byVendor built from above rows. The observer's _indexVendor stamps `name` on
-- each entry (acq.allVendors reads catalogVendor.name); the stub must too.
obs.byVendor = {
    ["Midnight Vendor"] = { name = "Midnight Vendor", items = { 1001, 1002 }, zone = "Dornogal",  faction = "", standing = "" },
    ["Horde Vendor"]    = { name = "Horde Vendor",    items = { 1003 },       zone = "Orgrimmar", faction = "", standing = "" },
    ["Unknown Vendor"]  = { name = "Unknown Vendor",  items = { 1004 },       zone = "",          faction = "", standing = "" },
}
-- allVendorNames is pre-sorted
obs.allVendorNames = { "Horde Vendor", "Midnight Vendor", "Unknown Vendor" }

-- Stub QuestNameResolver (needed by acq.milestoneText path)
HDG.QuestNameResolver = HDG.QuestNameResolver or {
    ResolveTitle = function(_, questID) return "Quest " .. tostring(questID) end,
}

-- ============================================================================
-- 1. Cold catalog: acq.allVendors returns empty
-- ============================================================================
local state = HDG.Store:GetState()
local vendors = HDG.Selectors:Call("acq.allVendors", state, {})
assertEq(type(vendors), "table", "cold: allVendors returns table")
assertEq(#vendors, 0, "cold: allVendors is empty while observer not ready")

-- ============================================================================
-- 2. Mark catalog ready; verify vendor count
-- ============================================================================
HDG.Store:Dispatch({
    type    = HDG.Constants.ACTIONS.CATALOG_LOAD_COMPLETED,
    payload = { loadedAt = 0, itemCount = 4, vendorCount = 3, generation = 1 },
})
state = HDG.Store:GetState()

vendors = HDG.Selectors:Call("acq.allVendors", state, {})
assertEq(#vendors, 3, "allVendors returns 3 vendors after ready")

-- ============================================================================
-- 3. Row shape: VendorAugment joined onto Midnight Vendor
-- ============================================================================
-- allVendorNames is alphabetical: Horde(0), Midnight(1), Unknown(2)
local mv
for _, v in ipairs(vendors) do
    if v.name == "Midnight Vendor" then mv = v break end
end
assertTrue(mv ~= nil, "Midnight Vendor row present")
assertEq(mv.zone,       "Dornogal",       "zone from VendorAugment")
assertEq(mv.mapID,      2248,             "mapID from VendorAugment")
assertEq(mv.x,          55.2,             "x from VendorAugment")
assertEq(mv.y,          40.1,             "y from VendorAugment")
assertEq(mv.faction,    "Neutral",        "faction label from VendorAugment factionRaw=N")
assertEq(mv.factionRaw, "N",              "factionRaw from VendorAugment")
assertEq(mv.expansion,  "Midnight",       "expansion from VendorAugment")
assertEq(mv.itemCount,  2,                "itemCount = 2 catalog items")
assertEq(mv.canWaypoint, true,            "canWaypoint true when augment found")
assertEq(mv.npcID,      101,              "npcID resolved via VendorAugment")
-- reps set: item 1001 has K'aresh Trust gate; item 1002 has no gate
assertTrue(mv.reps["K'aresh Trust"] == true, "reps includes K'aresh Trust from item 1001")
local repCount = 0
for _ in pairs(mv.reps) do repCount = repCount + 1 end
assertEq(repCount, 1, "exactly 1 rep faction in Midnight Vendor reps set")

-- ============================================================================
-- 4. Horde Vendor: faction=Horde (from augment)
-- ============================================================================
local hv
for _, v in ipairs(vendors) do
    if v.name == "Horde Vendor" then hv = v break end
end
assertTrue(hv ~= nil, "Horde Vendor row present")
assertEq(hv.faction,    "Horde",    "faction Horde from VendorAugment factionRaw=H")
assertEq(hv.factionRaw, "H",        "factionRaw H")
assertEq(hv.itemCount,  1,          "1 item for Horde Vendor")
assertEq(hv.canWaypoint, true,      "canWaypoint true for Horde Vendor (has augment)")
local hvReps = 0
for _ in pairs(hv.reps) do hvReps = hvReps + 1 end
assertEq(hvReps, 0, "Horde Vendor has no rep-gated items -> reps set empty")

-- ============================================================================
-- 5. Unknown Vendor: no augment -> canWaypoint=false, faction="Neutral"
-- ============================================================================
local uv
for _, v in ipairs(vendors) do
    if v.name == "Unknown Vendor" then uv = v break end
end
assertTrue(uv ~= nil, "Unknown Vendor row present")
assertEq(uv.canWaypoint, false,    "canWaypoint false when no VendorAugment")
assertEq(uv.faction,    "Neutral", "faction defaults to Neutral when no augment")
assertEq(uv.factionRaw, "N",       "factionRaw defaults to N")
assertNil(uv.npcID,                "npcID nil when ResolveName returns nil")
assertNil(uv.mapID,                "mapID nil when no augment")

-- ============================================================================
-- 6. reads list includes session.catalog.sweepGeneration
-- ============================================================================
local meta = HDG.Selectors._registry and HDG.Selectors._registry["acq.allVendors"]
if meta and meta.reads then
    local hasSweep = false
    for _, r in ipairs(meta.reads) do
        if r == "session.catalog.sweepGeneration" then hasSweep = true end
    end
    assertTrue(hasSweep, "acq.allVendors reads includes session.catalog.sweepGeneration")
end

-- ============================================================================
-- 7. acq.vendors "missing" preset: uses catalog items
-- Setup: mark item 1003 as collected (only Horde Rug is collected).
-- decor.isCollected uses catalogByItem[itemID] -> decorID -> ownedDecorIDs.
-- Missing preset should exclude Horde Vendor (all its items collected),
-- keep Midnight Vendor and Unknown Vendor.
-- ============================================================================
-- Phase 4.4: decor.isCollected resolves decorID via byItemID[itemID].decorID,
-- then checks ownedDecorIDs[decorID]. catalogByItem deleted.
-- Seed decorID on the existing byItemID row; mark it owned.
do
    local col = HDG.Store:GetState().account.collection
    col.ownedDecorIDs  = col.ownedDecorIDs  or {}
    -- Map itemID 1003 -> decorID 9000 via observer row; mark decorID 9000 as owned.
    obs.byItemID[1003].decorID = 9000
    col.ownedDecorIDs[9000] = true
end
-- Nudge the store so selectors that read ownedDecorIDs/catalogByItem invalidate.
HDG.Store:Dispatch({
    type    = HDG.Constants.ACTIONS.UI_SET_TRANSIENT,
    payload = { view = "acquisition", key = "_collectionBump", value = 1 },
})
state = HDG.Store:GetState()

-- Enable the orthogonal Missing toggle (collection axis -- replaces the old
-- "missing" preset value, 2026-05-31).
HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.ACQ_TOGGLE_MISSING })
state = HDG.Store:GetState()

local filtered = HDG.Selectors:Call("acq.vendors", state, {})
-- Midnight Vendor has items 1001 (uncollected) + 1002 (uncollected) -> passes
-- Horde Vendor has item 1003 (collected) -> fails missing toggle
-- Unknown Vendor has item 1004 (uncollected) -> passes
local names = {}
for _, v in ipairs(filtered) do names[v.name] = true end
assertTrue(names["Midnight Vendor"],  "Midnight Vendor passes missing toggle (has uncollected items)")
assertFalse(names["Horde Vendor"],    "Horde Vendor filtered out by missing toggle (all collected)")
assertTrue(names["Unknown Vendor"],   "Unknown Vendor passes missing toggle (has uncollected items)")
assertEq(#filtered, 2, "2 vendors remain after missing toggle filter")

-- Clear the Missing toggle so it doesn't leak into later sections. Unlike the
-- old single-select "missing" preset (overwritten when the next preset was
-- set), missingOnly is an orthogonal persistent toggle -- explicit off needed.
HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.ACQ_TOGGLE_MISSING })
state = HDG.Store:GetState()

-- ============================================================================
-- 8. acq.vendors "reputation" preset: uses v.reps set
-- ============================================================================
HDG.Store:Dispatch({
    type    = HDG.Constants.ACTIONS.UI_SET_TRANSIENT,
    payload = { view = "acquisition", key = "preset", value = "reputation" },
})
state = HDG.Store:GetState()

local repFiltered = HDG.Selectors:Call("acq.vendors", state, {})
-- Only Midnight Vendor has reps (K'aresh Trust from item 1001)
local repNames = {}
for _, v in ipairs(repFiltered) do repNames[v.name] = true end
assertTrue(repNames["Midnight Vendor"],   "Midnight Vendor passes rep preset (has rep-gated items)")
assertFalse(repNames["Horde Vendor"],     "Horde Vendor filtered out by rep preset (no reps)")
assertFalse(repNames["Unknown Vendor"],   "Unknown Vendor filtered out by rep preset (no reps)")
assertEq(#repFiltered, 1, "1 vendor passes reputation preset")

-- ============================================================================
-- 9. acq.vendorRows stamps allCollected from catalog
-- Setup: with only 1003 collected, Horde Vendor (sells only 1003) -> allCollected=true.
-- Midnight Vendor (sells 1001+1002, both uncollected) -> allCollected=false.
-- ============================================================================
HDG.Store:Dispatch({
    type    = HDG.Constants.ACTIONS.UI_SET_TRANSIENT,
    payload = { view = "acquisition", key = "preset", value = nil },
})
state = HDG.Store:GetState()

local rows = HDG.Selectors:Call("acq.vendorRows", state, {})
local rowByName = {}
for _, r in ipairs(rows) do rowByName[r.name] = r end

assertTrue(rowByName["Horde Vendor"] and rowByName["Horde Vendor"].allCollected == true,
    "Horde Vendor allCollected=true (item 1003 collected)")
assertTrue(rowByName["Midnight Vendor"] and rowByName["Midnight Vendor"].allCollected == false,
    "Midnight Vendor allCollected=false (items 1001+1002 uncollected)")
assertTrue(rowByName["Unknown Vendor"] and rowByName["Unknown Vendor"].allCollected == false,
    "Unknown Vendor allCollected=false (item 1004 uncollected)")

print("test_acquire_vendor_view: PASS")
