-- test_invalidation_correctness.lua
-- ============================================================================
-- The HIGHEST-LEVERAGE TEST in the redux-style harness (HDGR calls it "R15").
--
-- If a selector's reads-closure is INCOMPLETE OR an action's `invalidates`
-- declares the WRONG paths, BindingEngine:Apply skips re-pushing widgets
-- that depend on the mutated slot. Result: stale data shown to the user
-- with NO error. The test suite goes green; production silently corrupts.
--
-- Mechanic: build a probe matrix (synthetic widgets with controlled
-- readsClosure values), resolve each action's invalidation, run the same
-- Paths.MatchesAny predicate the engine uses, assert hits + misses match
-- the expected sets.

---@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()
local actionMeta = VDS.Store._actionMeta
local A = VDS.Constants.ACTIONS

local function assertEq(a, b, label)
    if a ~= b then error(label .. ": expected " .. tostring(b) .. " got " .. tostring(a), 2) end
end

-- ===== Probe matrix: synthetic widgets covering each major state region =====
local function makeProbe(id, readsClosure)
    return { id = id, readsClosure = readsClosure, pushCount = 0 }
end

local function buildProbeMatrix()
    return {
        -- session.ui.* (UI state - most actions invalidate this region)
        ui_filterMode       = makeProbe("ui_filterMode",       { "session.ui.filterMode" }),
        ui_searchText       = makeProbe("ui_searchText",       { "session.ui.searchText" }),
        ui_activeTab        = makeProbe("ui_activeTab",        { "session.ui.activeTab" }),
        -- Selection moved back to FLAT slots after the byView regression
        -- caused a hover-storm; SET_SELECTED_* invalidates narrow paths now.
        ui_selectedDye      = makeProbe("ui_selectedDye",      { "session.ui.selectedDyeID" }),
        ui_windowShown      = makeProbe("ui_windowShown",      { "session.ui.windowShown" }),
        ui_configShown      = makeProbe("ui_configShown",      { "session.ui.configShown" }),
        -- session.ui.canvas.* (canvas tab)
        canvas_decorID      = makeProbe("canvas_decorID",      { "session.ui.canvas.decorID" }),
        canvas_dyes         = makeProbe("canvas_dyes",         { "session.ui.canvas.dyes" }),
        -- account.* (persisted)
        account_config      = makeProbe("account_config",      { "account.config" }),
        account_themes      = makeProbe("account_themes",      { "account.customThemes" }),
        account_loadouts    = makeProbe("account_loadouts",    { "account.canvas.loadouts" }),
        -- session.dyes.* (catalog from observer)
        dyes_catalog        = makeProbe("dyes_catalog",        { "session.dyes.catalog" }),
        dyes_ownership      = makeProbe("dyes_ownership",      { "session.dyes.ownership" }),
        -- session.bag (inventory tick)
        session_bag         = makeProbe("session_bag",         { "session.bag.tick" }),
        -- session.identity
        session_identity    = makeProbe("session_identity",    { "session.identity" }),
        -- Unrelated probe (should NOT fire for targeted actions)
        unrelated_session   = makeProbe("unrelated_session",   { "session.somethingElse" }),
        -- Star probe (fires for any invalidation)
        star_widget         = makeProbe("star_widget",         "*"),
    }
end

local function simulateApply(probes, invalidation)
    for _, probe in pairs(probes) do
        if VDS.Paths.MatchesAny(probe.readsClosure, invalidation) then
            probe.pushCount = probe.pushCount + 1
        end
    end
end

local function resolveInvalidation(meta, action)
    if meta == nil then return "*" end
    local inv = meta.invalidates
    if type(inv) == "function" then
        local ok, result = pcall(inv, action)
        return (ok and result) or "*"
    elseif inv ~= nil then
        return inv
    end
    return "*"
end

-- ===== Core invariants =====

-- 1. Empty invalidation: only star_widget fires.
do
    local probes = buildProbeMatrix()
    simulateApply(probes, {})
    local nonStarPushed = 0
    for id, probe in pairs(probes) do
        if id ~= "star_widget" and probe.pushCount > 0 then
            nonStarPushed = nonStarPushed + 1
        end
    end
    assertEq(nonStarPushed, 0, "Empty invalidation: only star_widget should fire")
end

-- 2. Wildcard "*" pushes every widget.
do
    local probes = buildProbeMatrix()
    simulateApply(probes, "*")
    local total, fired = 0, 0
    for _, probe in pairs(probes) do
        total = total + 1
        if probe.pushCount > 0 then fired = fired + 1 end
    end
    assertEq(fired, total, "Wildcard invalidation must fire every widget")
end

-- 3. Per-action: representative sample covering each major invalidation region.
local sampleActions = {
    {
        action = { type = A.MAIN_WINDOW_TOGGLE },
        hits   = { "ui_windowShown", "star_widget" },
        misses = { "ui_searchText", "account_config", "session_bag", "dyes_catalog",
                   "unrelated_session" },
    },
    {
        action = { type = A.SET_SEARCH_TEXT, payload = { text = "horde" } },
        hits   = { "ui_searchText", "star_widget" },
        misses = { "ui_windowShown", "account_config", "session_bag", "unrelated_session" },
    },
    {
        action = { type = A.SET_ACTIVE_TAB, payload = { tab = "color" } },
        hits   = { "ui_activeTab", "star_widget" },
        misses = { "ui_searchText", "ui_windowShown", "account_config", "unrelated_session" },
    },
    {
        action = { type = A.SET_SELECTED_DYE, payload = { id = 1 } },
        hits   = { "ui_selectedDye", "star_widget" },
        misses = { "ui_windowShown", "account_config", "session_bag", "unrelated_session" },
    },
    {
        action = { type = A.BAG_TICK },
        hits   = { "session_bag", "star_widget" },
        misses = { "ui_searchText", "account_config", "dyes_catalog", "unrelated_session" },
    },
    {
        action = { type = A.SESSION_IDENTITY_SET, payload = { name = "P", realm = "R", class = "DRUID" } },
        hits   = { "session_identity", "star_widget" },
        misses = { "ui_searchText", "account_config", "session_bag", "unrelated_session" },
    },
    {
        -- HARD_RESET invalidates "account" (parent path). Every account.* widget
        -- should fire; session.* widgets should NOT (only star + account-prefixed).
        action = { type = A.HARD_RESET },
        hits   = { "account_config", "account_themes", "account_loadouts",
                   "star_widget" },
        misses = { "ui_windowShown", "session_bag", "session_identity", "dyes_catalog",
                   "unrelated_session" },
    },
}

for _, tc in ipairs(sampleActions) do
    local meta = actionMeta[tc.action.type]
    local inv  = resolveInvalidation(meta, tc.action)
    local probes = buildProbeMatrix()
    simulateApply(probes, inv)

    for _, hitID in ipairs(tc.hits) do
        if probes[hitID].pushCount == 0 then
            error(("invalidation correctness: action=%s should push widget %q "
                .. "(reads=%s intersects invalidates=%s) but did NOT")
                :format(tc.action.type, hitID,
                    type(probes[hitID].readsClosure) == "table"
                        and table.concat(probes[hitID].readsClosure, ",")
                        or tostring(probes[hitID].readsClosure),
                    type(inv) == "table" and table.concat(inv, ",") or tostring(inv)), 2)
        end
    end

    for _, missID in ipairs(tc.misses) do
        if probes[missID].pushCount > 0 then
            error(("invalidation correctness: action=%s must NOT push widget %q "
                .. "(reads=%s does NOT intersect invalidates=%s) but did")
                :format(tc.action.type, missID,
                    type(probes[missID].readsClosure) == "table"
                        and table.concat(probes[missID].readsClosure, ",")
                        or tostring(probes[missID].readsClosure),
                    type(inv) == "table" and table.concat(inv, ",") or tostring(inv)), 2)
        end
    end
end

-- 4. Narrow path that no probe reads: zero non-star pushes.
do
    local probes = buildProbeMatrix()
    simulateApply(probes, { "account.nonExistent.deep.path" })
    local nonStarPushed = 0
    for id, probe in pairs(probes) do
        if id ~= "star_widget" and probe.pushCount > 0 then
            nonStarPushed = nonStarPushed + 1
        end
    end
    assertEq(nonStarPushed, 0, "Narrow path with no matching widget: zero pushes")
end

print("test_invalidation_correctness.lua passed")
