-- Reactivity smoke: state-path tracking + selector reads/calls
-- closure + BindingEngine selective Apply + memoization invalidation.

dofile("wow_mock.lua")
dofile("../Core/HDGR_SchemeConstants.lua")
dofile("../Core/HDGR_Constants.lua")
dofile("../Core/HDGR_Actions.lua")
dofile("../Core/HDGR_Resolvers.lua")
dofile("../Core/HDGR_Format.lua")
dofile("../Core/HDGR_Paths.lua")
dofile("../Core/HDGR_Log.lua")
dofile("../Core/HDGR_Memo.lua")
dofile("../Core/HDGR_StoreFurnishings.lua")
dofile("../Core/HDGR_Store.lua")
-- Config layer: reducer routes CONFIG_SET via HDG.Config:_GetSourceForScope.
dofile("../Core/HDGR_ConfigSchema.lua")
dofile("../Core/HDGR_Config.lua")
dofile("../UI/HDGR_Selectors.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")

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 assertFalse(v, label) if v then error(label .. " (expected false, got truthy)", 2) end end

-- Each test starts with a fresh selector registry
HDG.Selectors:_Reset()

-- ============================================================================
-- Reads-closure: legacy form + new form + transitive composition
-- ============================================================================

-- Legacy registration -> reads defaults to "*"
HDG.Selectors:Register("legacy.fn", function(state, ctx) return state.foo end)
assertEq(HDG.Selectors:GetReads("legacy.fn"), "*",
    "legacy registration defaults to wildcard reads")

-- New form with explicit reads
HDG.Selectors:Register("alpha", {
    reads = {"account.config.theme"},
    fn    = function(state, ctx) return "alpha" end,
})
local reads = HDG.Selectors:GetReads("alpha")
assertEq(#reads, 1, "alpha has 1 reads entry")
assertEq(reads[1], "account.config.theme", "alpha reads path")

-- Composition via calls: beta calls alpha, transitively pulls in its reads
HDG.Selectors:Register("beta", {
    reads = {"session.ui.search"},
    calls = {"alpha"},
    fn    = function(state, ctx) return "beta" end,
})
local betaReads = HDG.Selectors:GetReads("beta")
assertEq(#betaReads, 2, "beta closure has 2 entries")
local sawTheme, sawSearch = false, false
for _, p in ipairs(betaReads) do
    if p == "account.config.theme" then sawTheme = true end
    if p == "session.ui.search" then sawSearch = true end
end
assertTrue(sawTheme, "beta closure includes alpha's reads")
assertTrue(sawSearch, "beta closure includes own reads")

-- Cycle detection: gamma <-> delta should collapse to "*"
HDG.Selectors:Register("gamma", {
    reads = {"a"},
    calls = {"delta"},
    fn    = function() return 1 end,
})
HDG.Selectors:Register("delta", {
    reads = {"b"},
    calls = {"gamma"},
    fn    = function() return 2 end,
})
assertEq(HDG.Selectors:GetReads("gamma"), "*", "cycle collapses to wildcard")

-- Wildcard short-circuits
HDG.Selectors:Register("eps", {
    reads = "*",
    calls = {"alpha"},  -- shouldn't matter; wildcard wins
    fn    = function() return 0 end,
})
assertEq(HDG.Selectors:GetReads("eps"), "*", "wildcard reads short-circuits closure")

-- ============================================================================
-- Memoization: opt-in cache; invalidates on path match
-- ============================================================================

HDG.Selectors:_Reset()
local callCount = 0
HDG.Selectors:Register("cached", {
    reads    = {"account.collection.decorCatalog"},
    memoized = true,
    fn       = function(state, ctx)
        callCount = callCount + 1
        return "result_" .. callCount
    end,
})

HDG_DB = {}
HDG.Store:LoadFromSavedVariables()
HDG.Config:Initialize()
local state = HDG.Store:GetState()

-- First call -> fn runs, result cached
assertEq(HDG.Selectors:Call("cached", state, {}), "result_1", "first call runs fn")
assertEq(callCount, 1, "fn called once")
-- Second call -> cache hit, fn NOT called
assertEq(HDG.Selectors:Call("cached", state, {}), "result_1", "cache hit returns same value")
assertEq(callCount, 1, "fn not called on cache hit")

-- Invalidate with unrelated path -> cache survives
HDG.Selectors:InvalidateMemos({"session.ui.decor.searchQuery"})
HDG.Selectors:Call("cached", state, {})
assertEq(callCount, 1, "unrelated invalidation does not clear cache")

-- Invalidate with related path -> cache clears
HDG.Selectors:InvalidateMemos({"account.collection.decorCatalog"})
HDG.Selectors:Call("cached", state, {})
assertEq(callCount, 2, "related path invalidation clears cache")

-- Parent-path invalidation also matches (account.collection is parent of decorCatalog)
HDG.Selectors:Call("cached", state, {})   -- cache hit
assertEq(callCount, 2, "still cached")
HDG.Selectors:InvalidateMemos({"account.collection"})
HDG.Selectors:Call("cached", state, {})
assertEq(callCount, 3, "parent path invalidation clears cached children")

-- Wildcard invalidation clears everything
HDG.Selectors:Call("cached", state, {})   -- cache hit
assertEq(callCount, 3, "cache hit")
HDG.Selectors:InvalidateMemos("*")
HDG.Selectors:Call("cached", state, {})
assertEq(callCount, 4, "wildcard invalidation clears all memos")

-- ============================================================================
-- End-to-end: action meta invalidates flows through dispatch
-- ============================================================================

dofile("../Core/HDGR_Middleware.lua")

HDG.Selectors:_Reset()
HDG_DB = {}
HDG.Store:LoadFromSavedVariables()
HDG.Config:Initialize()
state = HDG.Store:GetState()

-- Capture the invalidation set passed to subscribers
local capturedInvalidations = {}
HDG.Store:Subscribe(function(actionType, invalidation)
    capturedInvalidations[#capturedInvalidations + 1] = {
        type = actionType, inv = invalidation,
    }
end)

-- Wire action meta (matches Init.lua's BuildActionMeta shape)
local A = HDG.Constants.ACTIONS
local P = HDG.Paths
HDG.Store._actionMeta = {
    [A.CONFIG_SET] = { persists = true, combatUnsafe = false, noop = false,
        invalidates = function(action)
            return { P.Join("account.config", action.payload and action.payload.key) }
        end },
    [A.MAIN_WINDOW_TOGGLE] = { persists = true, combatUnsafe = false, noop = false,
        invalidates = {"account.ui.mainWindowShown"} },
    [A.UI_SET_TRANSIENT] = { persists = false, combatUnsafe = false, noop = false,
        invalidates = function(action)
            local p = action.payload or {}
            return { P.Join("session.ui", p.view, p.key) }
        end },
    [A.LOG_PUSH] = { persists = false, combatUnsafe = false, noop = false,
        invalidates = {"session.log.entries"} },
    [A.LOG_CLEAR] = { persists = false, combatUnsafe = false, noop = false,
        invalidates = {"session.log.entries"} },
    [A.LOG_SET_FILTER] = { persists = false, combatUnsafe = false, noop = false,
        invalidates = {"session.log.tabFilter"} },
    [A.LOG_TRACE_TOGGLE] = { persists = false, combatUnsafe = false, noop = false,
        invalidates = {"session.log.activeTraces"} },
}

HDG.Store:Dispatch({ type = A.CONFIG_SET, payload = { key = "debug", value = true } })
local last = capturedInvalidations[#capturedInvalidations]
assertEq(last.type, A.CONFIG_SET, "subscriber received action type")
assertTrue(type(last.inv) == "table", "subscriber received invalidation table")
assertEq(last.inv[1], "account.config.debug", "dynamic invalidation built from payload")

HDG.Store:Dispatch({ type = A.MAIN_WINDOW_TOGGLE })
last = capturedInvalidations[#capturedInvalidations]
assertEq(last.inv[1], "account.ui.mainWindowShown", "static invalidation passes through")

HDG.Store:Dispatch({ type = A.UI_SET_TRANSIENT,
    payload = { view = "decor", key = "searchQuery", value = "test" } })
last = capturedInvalidations[#capturedInvalidations]
assertEq(last.inv[1], "session.ui.decor.searchQuery",
    "view-scoped payload-dependent invalidation")

-- ============================================================================
-- Memo invalidation triggered through dispatch (full integration)
-- ============================================================================

callCount = 0
HDG.Selectors:Register("acq_filtered", {
    reads    = {"session.ui.acquisition.preset"},
    memoized = true,
    fn       = function(state, ctx)
        callCount = callCount + 1
        return callCount
    end,
})

HDG.Selectors:Call("acq_filtered", state, {})   -- run 1
HDG.Selectors:Call("acq_filtered", state, {})   -- cache hit
assertEq(callCount, 1, "memo primed on first call")

-- Dispatch an action whose invalidation list matches the memo's reads.
-- ACQ_SET_PRESET invalidates session.ui.acquisition.preset (see
-- Init.lua action meta); the memo should re-run on the next call.
HDG.Store:Dispatch({ type = A.ACQ_SET_PRESET, payload = { value = "achievement" } })
HDG.Selectors:Call("acq_filtered", state, {})
assertEq(callCount, 2, "memo invalidated by dispatch with matching path")

-- Dispatch an unrelated action -- memo survives
HDG.Selectors:Call("decor_filtered", state, {})   -- cache hit
assertEq(callCount, 2, "verified cache hit")
HDG.Store:Dispatch({ type = A.MAIN_WINDOW_TOGGLE })
HDG.Selectors:Call("decor_filtered", state, {})
assertEq(callCount, 2, "unrelated dispatch leaves memo intact")

-- ============================================================================
-- Inputs-based memo (Phase 10 unification): Selectors:Register({ inputs })
-- ============================================================================
-- The `inputs` form wraps the selector body in an HDG.Memo so it caches by
-- input-reference equality instead of by single-cell single-result. Pure
-- function calls with the SAME input references short-circuit to the
-- cached compute result; new references run compute again.

local computeCount = 0
local sentinelInput = { tag = "stable" }
HDG.Selectors:Register("phase10_inputs_memo", {
    -- inputs is a list of (state, ctx) -> stable-reference values.
    -- Returning the same table across calls means "no change" and the
    -- compute below is skipped on subsequent calls.
    inputs = { function() return sentinelInput end },
    fn     = function(input)
        computeCount = computeCount + 1
        return computeCount
    end,
})

HDG.Selectors:Call("phase10_inputs_memo", state, {})   -- run 1
HDG.Selectors:Call("phase10_inputs_memo", state, {})   -- cache hit (same input)
HDG.Selectors:Call("phase10_inputs_memo", state, {})   -- cache hit
assertEq(computeCount, 1, "inputs-memo: compute runs once when inputs unchanged")

-- Swap the input reference. Compute MUST re-run.
sentinelInput = { tag = "different" }
HDG.Selectors:Call("phase10_inputs_memo", state, {})
assertEq(computeCount, 2, "inputs-memo: input-reference change re-runs compute")

print("test_reactivity_smoke.lua passed")
