-- test_selector_reads.lua
-- ============================================================================
-- Selector reads-closure enforcement. Every selector declares `reads` (state
-- paths) + `calls` (other selectors). The BindingEngine uses the transitive
-- closure to decide which dispatches invalidate a bound widget -- if a
-- selector READS a path it didn't DECLARE, the BindingEngine won't see the
-- invalidation, and the widget paints stale data.
--
-- This test runs every selector against a proxied state that records each
-- access, then asserts every recorded path is covered by the declared
-- reads-closure (via prefix match, so declared "account.foo" covers
-- recorded "account.foo.bar" from ipairs / nested reads).
--
-- LESSON (HDGR ADR-009): "the selector said it reads X; the BindingEngine
-- believed it." Under-declared reads = silent staleness; this test surfaces
-- the gap before any user does.

---@diagnostic disable: undefined-global
dofile("wow_mock.lua")
dofile("../Core/VDS_Constants.lua")
dofile("../Core/VDS_SchemeConstants.lua")
dofile("../Core/VDS_Paths.lua")
dofile("../Core/VDS_Log.lua")
dofile("../Core/VDS_Store.lua")
dofile("../Core/VDS_ActionMeta.lua")
VDS.Store:ValidateActionMetaExhaustiveness()
VDS_DB = {}
VDS.Store:LoadFromSavedVariables()

-- Pure-data modules selectors depend on (no events / Blizzard API).
dofile("../Modules/VDS_StaticData.lua")

-- Harmony engine (pure; no Blizzard deps): needed by VDS_Selectors_Harmony.
dofile("../Core/VDS_ColorMath.lua")
dofile("../Core/VDS_HarmonyWheel.lua")
dofile("../Core/VDS_Harmony.lua")

-- Selectors registry (loads file)
dofile("../UI/VDS_Selectors.lua")
-- All per-domain selector files (added in Phase 4)

local function tryDofile(path)
    -- Optional selector file; not all may exist yet during incremental build.
    local f = io.open(path, "r")
    if f then f:close(); dofile(path); return true end
    return false
end

tryDofile("../UI/VDS_Selectors_Progress.lua")
tryDofile("../UI/VDS_Selectors_Browse.lua")
tryDofile("../UI/VDS_Selectors_Detail.lua")
tryDofile("../UI/VDS_Selectors_Canvas.lua")
tryDofile("../UI/VDS_Selectors_Live.lua")
tryDofile("../UI/VDS_Selectors_View.lua")
tryDofile("../UI/VDS_Selectors_Warehouse.lua")
tryDofile("../UI/VDS_Selectors_Harmony.lua")

-- ============================================================================
-- Read-recording proxy. __index records the path; __pairs records iteration
-- (declared reads must cover the iterated table) then delegates to next()
-- on the real table.
-- ============================================================================

local function makeProxy(real, recorded, path)
    if type(real) ~= "table" then return real end
    return setmetatable({}, {
        __index = function(_, k)
            local newPath = (path == "") and tostring(k) or (path .. "." .. tostring(k))
            recorded[newPath] = true
            local v = real[k]
            if type(v) == "table" then
                return makeProxy(v, recorded, newPath)
            end
            return v
        end,
        __pairs = function()
            -- Iterating the table at `path` means the selector cares about
            -- the whole table's contents -- it must declare `reads = {path}`.
            recorded[path] = true
            return next, real, nil
        end,
        __len = function() return #real end,
        -- Writes via the proxy are a bug -- selectors are pure.
        __newindex = function(_, k)
            error("selector wrote to state via proxy at " .. path .. "." .. tostring(k), 2)
        end,
    })
end

-- ============================================================================
-- Coverage check. A recorded path R is covered by a declared path D if
-- either is an ancestor of the other (or they're equal). This lets a
-- selector declare reads at the TABLE level and still pass when ipairs
-- records "table.1", "table.2", etc.
-- ============================================================================

local function pathCovers(recorded, declared)
    if recorded == declared then return true end
    -- declared is an ancestor of recorded
    if recorded:sub(1, #declared + 1) == declared .. "." then return true end
    -- recorded is an ancestor of declared (intermediate-node read)
    if declared:sub(1, #recorded + 1) == recorded .. "." then return true end
    return false
end

local function uncoveredReads(recorded, declared)
    local out = {}
    for r in pairs(recorded) do
        local covered = false
        for d in pairs(declared) do
            if pathCovers(r, d) then covered = true; break end
        end
        if not covered then out[#out + 1] = r end
    end
    return out
end

-- ============================================================================
-- Run every selector through the proxy
-- ============================================================================

local selectorNames = {}
for name in pairs(VDS.Selectors._registry) do selectorNames[#selectorNames + 1] = name end
table.sort(selectorNames)

print(string.format("Selector reads check: %d selectors", #selectorNames))

local failures = {}
local skipped = 0

for _, name in ipairs(selectorNames) do
    local spec = VDS.Selectors:Get(name)
    local declared = VDS.Selectors:GetReads(name)
    -- Wildcard "*" in declared reads = "I read everything" (rare; opts out
    -- of the check). Skip with a counter so the test surfaces drift toward
    -- wildcards.
    if declared["*"] then
        skipped = skipped + 1
    else
        local recorded = {}
        local proxiedState = makeProxy(VDS.Store:GetState(), recorded, "")
        local ok, err = pcall(spec.fn, proxiedState, {})
        if not ok then
            failures[#failures + 1] = string.format(
                "  selector %s errored under proxy: %s", name, tostring(err))
        else
            local missing = uncoveredReads(recorded, declared)
            if #missing > 0 then
                table.sort(missing)
                failures[#failures + 1] = string.format(
                    "  selector %s: %d undeclared read(s): %s",
                    name, #missing, table.concat(missing, ", "))
            end
        end
    end
end

if skipped > 0 then
    print(string.format("  [WARN] %d selector(s) skipped (wildcard reads)", skipped))
end

if #failures > 0 then
    print("FAILURES:")
    for _, f in ipairs(failures) do print(f) end
    error(string.format("test_selector_reads: %d selectors have undeclared reads", #failures), 0)
end

print("test_selector_reads.lua passed")
