-- catalog_fixture.lua
-- ============================================================================
-- Real-data catalog fixture harness for HDG observer tests.
--
-- Loads the ProfTools vendor dump (dev_data/ProfTools_VendorDump.lua), exposes
-- the real C_HousingCatalog entry-info records it captured, and installs
-- C_HousingCatalog mocks so the observer's REAL sweep runs headless:
--
--   observer:ReconcileFull()
--     -> C_HousingCatalog.CreateCatalogSearcher()   (mock searcher)
--     -> searcher:RunSearch()                        (fires callback sync)
--     -> observer:_OnSearcherResults(searcher)
--          -> searcher:GetAllSearchItems()           (the real records)
--          -> C_HousingCatalog.GetCatalogEntryInfo(e) (identity: e is the info)
--          -> observer:BuildRow(info)                (real parse)
--          -> atomic-assign byItemID / byVendor / tagIDToGroup
--
-- This replaces the per-test hand-rolled observer stubs (fake GetExpansionForItem,
-- name-only byVendor keys, manually-set tagIDToGroup) that drift from the real
-- observer contract. Tests assert against the REAL observer fed REAL data.
--
-- Dump layout (verified 2026-05-29):
--   ProfTools_VendorDump = {
--     capturedAt = <ts>, counts = {...},
--     noVendor   = { { source=, sourceText=, infoDump={...} }, ... },
--     vendors    = { [name] = { items = { { sourceText=, infoDump={...} }, ... },
--                               zone=, faction=, standing= }, ... },
--   }
-- The rich catalog sourceText lives on infoDump.sourceText (full multi-vendor
-- text). ProfTools captured the same itemID under multiple vendors; we dedup
-- by itemID keeping the longest sourceText (= the canonical full text).
-- ============================================================================

local CatalogFixture = {}
_G.CatalogFixture = CatalogFixture

-- Resolve the dump path relative to the tests/ cwd (run_all.sh runs from there).
local DEFAULT_DUMP = "../dev_data/ProfTools_VendorDump.lua"

-- ---------------------------------------------------------------------------
-- 0. Stub the Modules engine. The observer self-registers via
--    HDG.Modules:Declare(def) at file end; the real engine also registers
--    def.logTags with HDG.Log (Core/HDGR_Modules.lua:175). Mirror that so the
--    Log's closed-tag validation accepts the observer's catalog_* tags.
--    MUST be called BEFORE dofile-ing the observer.
-- ---------------------------------------------------------------------------
function CatalogFixture.StubModulesEngine()
    HDG.Modules = HDG.Modules or {}
    HDG.Modules._decls = HDG.Modules._decls or {}
    function HDG.Modules:Declare(def)
        self._decls[def.name] = def
        if def and type(def.logTags) == "table" then
            HDG.Log:RegisterTags(def.logTags)
        end
    end
end

-- ---------------------------------------------------------------------------
-- 1. Load real entry-info records from the dump.
-- ---------------------------------------------------------------------------
function CatalogFixture.LoadRecords(dumpPath)
    dofile(dumpPath or DEFAULT_DUMP)
    local dump = _G.ProfTools_VendorDump
    assert(type(dump) == "table", "ProfTools_VendorDump not loaded")

    local best = {}  -- itemID -> infoDump (richest sourceText wins)
    local function consider(info)
        if type(info) ~= "table" or type(info.itemID) ~= "number" then return end
        local cur = best[info.itemID]
        if not cur or #(info.sourceText or "") > #(cur.sourceText or "") then
            best[info.itemID] = info
        end
    end

    for _, entry in ipairs(dump.noVendor or {}) do consider(entry.infoDump) end
    for _, vdata in pairs(dump.vendors or {}) do
        for _, it in ipairs(vdata.items or {}) do consider(it.infoDump) end
    end

    local records = {}
    for _, info in pairs(best) do records[#records + 1] = info end
    return records
end

-- ---------------------------------------------------------------------------
-- 2. Build GetAllFilterTagGroups from the records' dataTagsByID.
--    Real shape: { { groupName, groupID, tags = { {tagID, tagName}, ... } } }.
--    The observer maps tagIDToGroup[tagID] = groupName; GetExpansionForItem
--    keys off groupName == "Expansion", size labels off "Size". We bucket each
--    tagID by name. (Verified expansion tagIDs 99-110, size tagIDs 65-69.)
-- ---------------------------------------------------------------------------
local EXPANSION_NAMES = {
    ["Classic"] = true, ["The Burning Crusade"] = true, ["Burning Crusade"] = true,
    ["Wrath of the Lich King"] = true, ["Cataclysm"] = true, ["Mists of Pandaria"] = true,
    ["Warlords of Draenor"] = true, ["Legion"] = true, ["Battle for Azeroth"] = true,
    ["Shadowlands"] = true, ["Dragonflight"] = true, ["The War Within"] = true,
    ["War Within"] = true, ["Midnight"] = true, ["The Last Titan"] = true,
}
local SIZE_NAMES = {
    Tiny = true, Small = true, Medium = true, Large = true,
    Huge = true, Giant = true, Massive = true,
}

function CatalogFixture.BuildTagGroups(records)
    local byTag = {}  -- tagID -> { name=, group= }
    for _, info in ipairs(records) do
        if type(info.dataTagsByID) == "table" then
            for tagID, name in pairs(info.dataTagsByID) do
                if type(tagID) == "number" and not byTag[tagID] then
                    local group = "Style"
                    if EXPANSION_NAMES[name] then group = "Expansion"
                    elseif SIZE_NAMES[name] then group = "Size" end
                    byTag[tagID] = { name = name, group = group }
                end
            end
        end
    end

    local groupsByName, order = {}, {}
    local function group(name)
        if not groupsByName[name] then
            order[#order + 1] = name
            groupsByName[name] = { groupName = name, groupID = #order, tags = {} }
        end
        return groupsByName[name]
    end
    for tagID, info in pairs(byTag) do
        local g = group(info.group)
        g.tags[#g.tags + 1] = { tagID = tagID, tagName = info.name }
    end

    local out = {}
    for _, name in ipairs(order) do out[#out + 1] = groupsByName[name] end
    return out
end

-- ---------------------------------------------------------------------------
-- 3. Install C_HousingCatalog mocks (searcher + entry-info + tag groups).
-- ---------------------------------------------------------------------------
function CatalogFixture.Install(records)
    local tagGroups = CatalogFixture.BuildTagGroups(records)

    local searcher = {}
    function searcher:SetResultsUpdatedCallback(fn) self._cb = fn end
    function searcher:RunSearch() if self._cb then self._cb() end end
    -- Observer reads GetCatalogSearchResults (the FILTERED set); the fixture's
    -- ALL-category + all-tags config means that == the full record set.
    function searcher:GetCatalogSearchResults() return records end
    function searcher:GetAllSearchItems()       return records end  -- diagnostic/legacy
    -- Config setters the observer applies (Blizzard's exact OneTimeInit +
    -- Filters:ResetFiltersToDefault). No-ops here -- the fixture returns the
    -- full record set regardless of filter params.
    function searcher:SetAutoUpdateOnParamChanges() end
    function searcher:SetStoredOnly()               end
    function searcher:SetBaseVariantOnly()          end
    function searcher:SetEditorModeContext()        end
    function searcher:SetCustomizableOnly()         end
    function searcher:SetAllowedIndoors()           end
    function searcher:SetAllowedOutdoors()          end
    function searcher:SetCollected()                end
    function searcher:SetUncollected()              end
    function searcher:SetFirstAcquisitionBonusOnly()end
    function searcher:SetFilteredCategoryID()       end
    function searcher:SetFilteredSubcategoryID()    end
    function searcher:SetAllInFilterTagGroup()      end

    -- Globals the observer's strict config reads.
    _G.Enum = _G.Enum or {}
    _G.Enum.HouseEditorMode = _G.Enum.HouseEditorMode or { BasicDecor = 1 }
    _G.Constants = _G.Constants or {}
    _G.Constants.HousingCatalogConsts = _G.Constants.HousingCatalogConsts
        or { HOUSING_CATALOG_ALL_CATEGORY_ID = 18 }

    _G.C_HousingCatalog = _G.C_HousingCatalog or {}
    local C = _G.C_HousingCatalog
    C.CreateCatalogSearcher  = function() return searcher end
    C.GetCatalogEntryInfo    = function(entry) return entry end
    C.GetAllFilterTagGroups  = function() return tagGroups end
    C.GetCatalogCategoryInfo = C.GetCatalogCategoryInfo
        or function(catID) return { name = "Category_" .. tostring(catID) } end
    C.GetCatalogSubcategoryInfo = C.GetCatalogSubcategoryInfo
        or function(subID) return { name = "Subcategory_" .. tostring(subID) } end

    return searcher, tagGroups
end

-- ---------------------------------------------------------------------------
-- 4. Run the real sweep. wow_mock's C_Timer.NewTimer fires synchronously, so
--    the observer's 0.5s settle-timer atomic-assign commits inside RunSearch();
--    byItemID / byVendor / tagIDToGroup are populated on return.
-- ---------------------------------------------------------------------------
function CatalogFixture.RunSweep(observer)
    observer.tagIDToGroup = {}            -- clear _BuildTagIDIndex idempotency guard
    observer._sweepInProgress = false
    observer:ReconcileFull()
end

-- Convenience: load + install + sweep. Returns the records array.
function CatalogFixture.Bootstrap(observer, dumpPath)
    local records = CatalogFixture.LoadRecords(dumpPath)
    CatalogFixture.Install(records)
    CatalogFixture.RunSweep(observer)
    return records
end

return CatalogFixture
