-- test_resolver_registry.lua
-- ============================================================================
-- Contract tests for HDG.Resolver (Core/HDGR_Resolvers.lua) -- the resolver
-- registry's Lua-side contracts, standalone (no Store): slot minting, bump
-- profiles (default +1 / bump=false signal-only / bump=fn generation stamp),
-- action registration flowing through Actions:Register (tick path auto-
-- invalidated, closed taxonomy enforced), duplicate/malformed registration
-- errors, RegisterStatic + RegisterFacadeReads degenerate forms, and the
-- ValidateSelectorReads boot cross-check. The facade-side enforcement
-- (selector calls a facade without its required reads) is the AST rule in
-- scripts/semantic_sweep.lua 4c, exercised by running the sweep itself.
--
-- NOTE: print() is a no-op in wow_mock.lua; use io.write() for output.

dofile("wow_mock.lua")
dofile("../Core/HDGR_SchemeConstants.lua")
dofile("../Core/HDGR_Constants.lua")
dofile("../Core/HDGR_Actions.lua")
dofile("../Core/HDGR_Resolvers.lua")

local A = HDG.Constants.ACTIONS
local R = HDG.Resolver

local failures = 0
local function check(cond, label)
    if cond then
        io.write(("  ok  %s\n"):format(label))
    else
        failures = failures + 1
        io.write(("FAIL  %s\n"):format(label))
    end
end
local function expectError(fn, pattern, label)
    local ok, err = pcall(fn)
    check(not ok and tostring(err):find(pattern, 1, true) ~= nil,
        ("%s (got: %s)"):format(label, tostring(err)))
end
local function freshState()
    return { session = { resolvers = R:MintSlots() } }
end

-- ===== 1. Register: minting + default bump profile ===========================
R:_Reset(); HDG.Actions:_Reset()
R:Register{ name = "bag", facade = "BagObserver",
    actions = { { name = "BAG_INVENTORY_UPDATED" } } }

local slots = R:MintSlots()
check(slots.bag and slots.bag.tick == 0, "1. Register mints session.resolvers.bag = { tick = 0 }")

local entry = HDG.Actions:Get(A.BAG_INVENTORY_UPDATED)
check(entry ~= nil, "1. bump action registered through Actions:Register")
check(entry.persists == false, "1. resolver signals default persists = false")
check(type(entry.invalidates) == "table" and entry.invalidates[1] == "session.resolvers.bag.tick",
    "1. tick path auto-invalidated")

local st = freshState()
entry.reduce(st, {})
check(st.session.resolvers.bag.tick == 1, "1. default profile bumps tick by 1")
entry.reduce(st, {})
check(st.session.resolvers.bag.tick == 2, "1. bump is monotonic")

-- ===== 2. bump = false (signal-only) + extra invalidates ======================
R:_Reset(); HDG.Actions:_Reset()
local sideEffect = nil
R:Register{ name = "catalog", facade = "HousingCatalogObserver",
    actions = {
        { name = "COLLECTION_CATALOG_ROW_REMOVED", bump = false,
          invalidates = { "account.collection.ownedDecorIDs" },
          reduce = function(state, payload) sideEffect = payload.decorID end },
    } }
entry = HDG.Actions:Get(A.COLLECTION_CATALOG_ROW_REMOVED)
check(entry.invalidates[1] == "session.resolvers.catalog.tick"
    and entry.invalidates[2] == "account.collection.ownedDecorIDs",
    "2. extra invalidates appended after the tick path")
st = freshState()
entry.reduce(st, { decorID = 42 })
check(st.session.resolvers.catalog.tick == 0, "2. bump=false leaves the tick value untouched (signal-only)")
check(sideEffect == 42, "2. custom reduce ran")

-- ===== 3. bump = fn (generation stamp) ========================================
R:_Reset(); HDG.Actions:_Reset()
R:Register{ name = "catalog", facade = "HousingCatalogObserver",
    actions = {
        { name = "CATALOG_LOAD_COMPLETED",
          bump = function(cur, payload) return payload.generation end },
    } }
st = freshState()
HDG.Actions:Get(A.CATALOG_LOAD_COMPLETED).reduce(st, { generation = 7 })
check(st.session.resolvers.catalog.tick == 7, "3. bump=fn stamps the payload generation")

-- ===== 4. persists override ===================================================
R:_Reset(); HDG.Actions:_Reset()
R:Register{ name = "prices", facade = { "PriceSource", "Goblin" },
    requires = { "account.prices" },
    actions = {
        { name = "PRICES_SET_TSM_MODE", persists = true,
          invalidates = { "account.config.tsmPriceMode" },
          reduce = function(state, payload) end },
    } }
check(HDG.Actions:Get(A.PRICES_SET_TSM_MODE).persists == true, "4. per-action persists override honored")

-- ===== 5. EnsureSlots top-up preserves live values ============================
R:_Reset(); HDG.Actions:_Reset()
R:Register{ name = "bag", facade = "BagObserver",
    actions = { { name = "BAG_INVENTORY_UPDATED" } } }
local live = { bag = { tick = 9 } }
R:RegisterStatic{ name = "staticData", facade = "StaticData" }
R:EnsureSlots(live)
check(live.bag.tick == 9, "5. EnsureSlots preserves an existing slot's value")
check(live.staticData and live.staticData.tick == 0, "5. EnsureSlots mints newly-registered slots")

-- ===== 6. RegisterStatic: marker only =========================================
check(R:Get("staticData").static == true, "6. RegisterStatic marks the entry static")
expectError(function() R:RegisterStatic{ name = "staticData", facade = "X" } end,
    "duplicate resolver", "6. duplicate RegisterStatic errors")

-- ===== 7. Malformed registrations fail loud ===================================
expectError(function() R:Register{ name = "bag", facade = "X", actions = { { name = "REP_PROGRESS_TICK" } } } end,
    "duplicate resolver", "7. duplicate resolver name errors")
expectError(function() R:Register{ name = "noFacade", actions = { { name = "REP_PROGRESS_TICK" } } } end,
    "facade required", "7. missing facade errors")
expectError(function() R:Register{ name = "noActions", facade = "X" } end,
    "actions list required", "7. missing actions errors")
expectError(function() R:Register{ name = "badAction", facade = "X", actions = { { name = "NOT_AN_ACTION" } } } end,
    "closed taxonomy", "7. unknown action name rejected by Actions:Register")
expectError(function() R:Register{ name = "dupAction", facade = "X", actions = { { name = "BAG_INVENTORY_UPDATED" } } } end,
    "duplicate registration", "7. action already registered rejected by Actions:Register")
expectError(function() R:RegisterFacadeReads{ facade = "ItemNameResolver" } end,
    "requires", "7. RegisterFacadeReads without requires errors")

-- ===== 8. ValidateSelectorReads boot cross-check ==============================
R:_Reset(); HDG.Actions:_Reset()
R:Register{ name = "bag", facade = "BagObserver",
    actions = { { name = "BAG_INVENTORY_UPDATED" } } }
local okRegistry = {
    ["sel.fine"]     = { reads = { "session.resolvers.bag.tick", "account.prices" } },
    ["sel.wildcard"] = { reads = "*" },
}
local ok = pcall(function() R:ValidateSelectorReads(okRegistry) end)
check(ok, "8. valid resolver reads pass the boot cross-check")
expectError(function()
    R:ValidateSelectorReads({ ["sel.typo"] = { reads = { "session.resolvers.bagg.tick" } } })
end, "no resolver", "8. typo'd resolver path is a boot error")

-- ===== Result =================================================================
if failures > 0 then
    io.write(("test_resolver_registry.lua FAILED (%d)\n"):format(failures))
    os.exit(1)
end
io.write("test_resolver_registry.lua passed\n")
