-- test_selector_reads.lua
-- Priority 2: Selector reads-declaration accuracy.
--
-- For each registered selector, instrument the state table with a __index
-- proxy that records every top-level path accessed during the selector call.
-- Compare the recorded path set against the selector's declared reads-closure
-- (GetReads). Under-declared reads are silent staleness bugs; this test
-- surfaces them.
--
-- Strategy:
--   1. Build a full default state table (same shape as production).
--   2. Wrap it with a proxy that records every first-level and second-level
--      key access (e.g. state.session -> "session"; state.session.ui -> "session.ui").
--   3. Run each selector via Selectors:Call(name, proxy, {}).
--   4. Compare recorded paths against GetReads().
--      - Under-declared (read not in declared) -> FAIL.
--      - Over-declared (declared but never read) -> WARN only (safe).
--      - Selector with reads="*" -> skip check (conservative, no false fails).
--
-- Caveats + TODOs:
--   - The proxy intercepts Lua table __index; it cannot intercept nil checks
--     (e.g. `if state.session.ui.decor then` followed by no further access).
--     So we can only verify accesses that ACTUALLY happen on the test state.
--   - Selectors that branch on data that doesn't exist in the default state
--     may have reads that only appear on specific data shapes. We note these
--     as "conditional reads not exercised in default state" rather than errors.
--   - Selectors referencing HDG module globals (ADR-003a carve-out) and
--     selectors whose fn errors (pcall returns nil) are skipped gracefully.
--   - The test FAILS on under-declaration (reads path accessed but not in
--     declared closure). This is the load-bearing assertion. If a selector
--     is found to be under-declared, the test output names it so Vamoose
--     can decide whether to fix the declared reads or add a comment explaining
--     the ADR-003a exception.
--
-- TODO (requires production refactor): selectors with deep path accesses
-- (state.session.ui.decor.filters.onlyUncollected) are recorded at the
-- segment level by the proxy; aliasing them to their dotted canonical
-- form needs a richer proxy that threads the key chain. Current proxy
-- captures segments as far as we can chain; deeper accesses fall through
-- to the real value. This is an acceptable limitation -- under-declarations
-- at depth are partially covered; the most common case (wrong top-level
-- bucket) is fully covered.
--
-- NOTE: No goto/continue (Lua 5.2+ only). Skip logic uses nested if-else.

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_Store.lua")
dofile("../Core/HDGR_BlizzardEvents.lua")
dofile("../Core/HDGR_Environment.lua")
dofile("../Core/HDGR_Modules.lua")
dofile("../UI/HDGR_Theme.lua")
dofile("../Core/HDGR_Init.lua")
dofile("../UI/HDGR_LayoutConfig.lua")
dofile("../UI/LayoutConfig/HDGR_LayoutConfig_Decor.lua")
dofile("../UI/LayoutConfig/HDGR_LayoutConfig_Acquisition.lua")
dofile("../UI/LayoutConfig/HDGR_LayoutConfig_Recipes.lua")
dofile("../UI/LayoutConfig/HDGR_LayoutConfig_Alts.lua")
dofile("../UI/LayoutConfig/HDGR_LayoutConfig_Mogul.lua")
dofile("../UI/LayoutConfig/HDGR_LayoutConfig_Styles.lua")
dofile("../UI/LayoutConfig/HDGR_LayoutConfig_Trainers.lua")
dofile("../UI/LayoutConfig/HDGR_LayoutConfig_HouseTab.lua")
dofile("../UI/LayoutConfig/HDGR_LayoutConfig_Debug.lua")
dofile("../UI/LayoutConfig/HDGR_LayoutConfig_Config.lua")
dofile("../UI/HDGR_Selectors.lua")
dofile("../UI/HDGR_CategoryNav.lua")
dofile("../UI/HDGR_Selectors_Status.lua")
dofile("../UI/HDGR_Selectors_Decor.lua")
dofile("../UI/HDGR_Selectors_Acquisition.lua")
dofile("../UI/HDGR_Selectors_Recipes.lua")
-- Projects selectors + their pure-module deps (IDs/AutoLayout/ShapeAtlas).
dofile("../Core/HDGR_ProjectsIDs.lua")
dofile("../Core/HDGR_ProjectsAutoLayout.lua")
dofile("../Core/HDGR_ProjectsShapeAtlas.lua")   -- roomDetail/breadcrumb/landing use GetLabel
dofile("../Core/HDGR_ProjectsFloorMap.lua")     -- canvasModel projection + collision
dofile("../UI/HDGR_Selectors_Projects.lua")
-- Stub data DBs at minimal size so selector fns don't crash on nil.
_G.HDGR_AllDecorDB  = _G.HDGR_AllDecorDB  or {}
_G.HDGR_VendorDB    = _G.HDGR_VendorDB    or {}
_G.HDGR_DecorDB     = _G.HDGR_DecorDB     or {}

-- Boot.
HDG_DB = {}
HDG.Store:LoadFromSavedVariables()
if HDG.Theme and HDG.Theme.Initialize then HDG.Theme:Initialize() end  -- boundary
pcall(function() HDG:OnInitialize() end)

-- ===== Proxy builder =========================================================
-- Builds a transparent recording proxy for a state table. Any key access on
-- the returned table is logged as "parentPath.key". If the value is itself a
-- table, recursively wraps it with a sub-proxy so deeper accesses are also
-- recorded.
--
-- `recorded` is a set: { ["session.ui"] = true, ["account.config"] = true, ... }
-- `parentPath` is the dotted prefix accumulated so far.

local function makeProxy(target, recorded, parentPath)
    if type(target) ~= "table" then return target end
    local proxy = {}
    setmetatable(proxy, {
        __index = function(_, key)
            if type(key) ~= "string" then return target[key] end
            local fullPath = parentPath == "" and key or (parentPath .. "." .. key)
            recorded[fullPath] = true
            local val = target[key]
            -- Recursively wrap table values so deeper accesses are recorded.
            if type(val) == "table" then
                return makeProxy(val, recorded, fullPath)
            end
            return val
        end,
        __newindex = function(_, key, val)
            target[key] = val
        end,
    })
    return proxy
end

-- ===== Run each selector =====================================================
local state = HDG.Store:GetState()
local ctx   = {}

-- Curated list of the most important selectors (those most likely to have
-- under-declared reads in practice). Full enumeration would require
-- exposing the _registry; this list covers the highest-risk selectors
-- without needing internal access.
local selectorNames = {
    -- Decor tab
    "decor.allItems",
    "decor.filterQuery",
    -- Acquisition tab
    "acq.allVendors",
    -- Chrome
    "chrome.activeTab",
    "chrome.isTabActive_decor",
    "chrome.isTabActive_acquisition",
    -- Projects (Phase 1)
    "projects.hasRooms",
    "projects.activeVersionID",
    "projects.versionTabs",
    "projects.activeVersionLabel",
    "projects.isWhatIfMode",
    "projects.isStockMode",
    "projects.architectColumns",
    "projects.placementCaps",
    "projects.roomBudget",
    "projects.roomCatalog",
    "projects.crateRows",
    "projects.floorTabs",
    -- Projects (Phase 2 render layer)
    "projects.canvasModel",
    "projects.sidePanelOpen",
    "projects.roomDetail",
    "projects.breadcrumb",
    "projects.ambiguousMatches",
    "projects.budgetProgress",
    "projects.budgetText",
    "projects.breadcrumbText",
    "projects.noRooms",
    -- Crate ledger (landing)
    "projects.orphanCount",
    "projects.hasOrphans",
    "projects.orphanAlertText",
    "projects.reclaimTargets",
    "projects.crateLedgerRows",
    "projects.roomDetailName",
    "projects.roomDetailMeta",
    -- Layouts tab
    "projects.layoutGroups",
    "projects.layoutPreviewModel",
    "projects.layoutDetail",
    "projects.layoutListRows",
    "projects.hasLayoutSelection",
    "projects.layoutDetailHeader",
    "projects.layoutDetailStats",
    "projects.houseMenuItems",
    "projects.activeHouseID",
}

local underDeclared  = {}   -- list of { name, path, declared }
local erroredSelectors = {} -- selectors that errored during the call
local skippedSelectors = {} -- selectors skipped because GetReads = "*"
local checkedSelectors = 0

for _, name in ipairs(selectorNames) do
    local declared = HDG.Selectors:GetReads(name)

    if declared == "*" then
        -- Skip: "*" opts out of path tracking. Not a failure.
        skippedSelectors[#skippedSelectors + 1] = name
    else
        -- Build a fresh recording proxy for each call so paths don't bleed.
        local recorded = {}
        local stateProxy = makeProxy(state, recorded, "")

        -- Call the selector wrapped in test-local pcall. Production no
        -- longer wraps Selectors:Call in pcall (per Reference/MIDNIGHT_
        -- SECRET_VALUES.md anti-pattern); the test still needs the wrap
        -- because this harness doesn't load every Module/StaticData that
        -- selectors might touch in the real boot path.
        local ok, result = pcall(HDG.Selectors.Call, HDG.Selectors, name, stateProxy, ctx)

        if not ok or result == nil then
            -- Selector errored or legitimately returned nil for this
            -- state. Cannot use recorded paths as definitive -- skip.
            erroredSelectors[#erroredSelectors + 1] = name
        else
            checkedSelectors = checkedSelectors + 1
            -- Check that every recorded path is covered by the declared
            -- reads-closure. Use Paths.MatchesAny: declared is a path list;
            -- we probe each recorded path individually.
            for path, _ in pairs(recorded) do
                local isCovered = HDG.Paths.MatchesAny(declared, { path })
                if not isCovered then
                    underDeclared[#underDeclared + 1] = {
                        name     = name,
                        path     = path,
                        declared = (type(declared) == "table")
                                    and table.concat(declared, ", ")
                                    or tostring(declared),
                    }
                end
            end
        end
    end
end

-- ===== Report =================================================================

if #skippedSelectors > 0 then
    print("  [INFO] Skipped (reads='*'): " .. table.concat(skippedSelectors, ", "))
end
if #erroredSelectors > 0 then
    print("  [INFO] Skipped (errored or returned nil in default state): "
        .. table.concat(erroredSelectors, ", "))
end

if #underDeclared > 0 then
    local lines = {}
    for _, ud in ipairs(underDeclared) do
        lines[#lines + 1] = string.format(
            "    selector %q reads path %q but it is not covered by declared reads [%s]",
            ud.name, ud.path, ud.declared)
    end
    error("SELECTOR READS UNDER-DECLARED -- silent staleness bugs (R15):\n"
        .. table.concat(lines, "\n")
        .. "\n\nFix: expand the selector's `reads` list in HDGR_Selectors_*.lua "
        .. "to include each listed path (or a parent path).", 2)
end

print(string.format(
    "test_selector_reads.lua passed (%d selectors checked, %d skipped-star, %d skipped-error)",
    checkedSelectors,
    #skippedSelectors,
    #erroredSelectors))
