-- test_module_lifecycle.lua
-- Priority 5: Module lifecycle ordering.
--
-- Invariants tested:
--   a. Topo-sort produces dependency-before-dependant order.
--   b. Phase1 (onInitialize) runs in topo order.
--   c. Phase2 (onEnable) runs in topo order.
--   d. Shutdown runs in REVERSE topo order.
--   e. A module error in onEnable (pcall isolation) does NOT prevent
--      later modules from running.
--   f. Cycle detection errors loudly instead of infinite loop / silently
--      computing a wrong order.
--   g. Declaring a dependency on an unregistered module errors loudly.
--   h. Declaring after Boot() errors loudly.

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_StoreFurnishings.lua")
dofile("../Core/HDGR_Store.lua")
-- Selectors: Store unconditionally calls HDG.Selectors:InvalidateMemos on every
-- _RawDispatch (no-guards policy). Test must load Selectors so the registry exists.
dofile("../UI/HDGR_Selectors.lua")
dofile("../Core/HDGR_Modules.lua")

local function assertTrue(v, label) if not v then error(label, 2) end end
local function assertEq(a, b, label)
    if a ~= b then error(label .. ": expected " .. tostring(b) .. " got " .. tostring(a), 2) end
end
local function assertErrorContains(fn, sub, label)
    local ok, err = pcall(fn)
    if ok then error(label .. ": expected error but call succeeded", 2) end
    if not tostring(err):find(sub, 1, true) then
        error(label .. ": error missing expected substring.\n"
            .. "  Expected: " .. sub .. "\n"
            .. "  Got: " .. tostring(err), 2)
    end
end

-- ===== a + b + c. Topo order + Phase1 + Phase2 ==============================
-- Graph: B has no deps. A depends on B. C depends on A.
-- Required order: B, A, C.

HDG.Modules:_Reset()

local initOrder, enableOrder = {}, {}

HDG.Modules:Declare({
    name = "C",
    dependencies = { "A" },
    onInitialize = function(self) initOrder[#initOrder + 1] = "C" end,
    onEnable     = function(self) enableOrder[#enableOrder + 1] = "C" end,
})
HDG.Modules:Declare({
    name = "A",
    dependencies = { "B" },
    onInitialize = function(self) initOrder[#initOrder + 1] = "A" end,
    onEnable     = function(self) enableOrder[#enableOrder + 1] = "A" end,
})
HDG.Modules:Declare({
    name = "B",
    dependencies = {},
    onInitialize = function(self) initOrder[#initOrder + 1] = "B" end,
    onEnable     = function(self) enableOrder[#enableOrder + 1] = "B" end,
})

-- ADR-020: explicit phase calls instead of Modules:Boot() so the test's
-- narrow scope (registry sequencing only -- no engine wiring) is honest.
HDG.Modules:Topo()
HDG.Modules:Phase1()
HDG.Modules:Phase2()

-- Topo order.
local order = HDG.Modules:GetOrder()
assertTrue(type(order) == "table" and #order == 3, "topo: order list has 3 entries")

-- B must come before A; A must come before C.
local pos = {}
for i, n in ipairs(order) do pos[n] = i end
assertTrue(pos["B"] < pos["A"], "topo: B before A")
assertTrue(pos["A"] < pos["C"], "topo: A before C")

-- onInitialize ran in topo order.
assertEq(initOrder[1], "B", "phase1: B initialised first")
assertEq(initOrder[2], "A", "phase1: A initialised second")
assertEq(initOrder[3], "C", "phase1: C initialised third")

-- onEnable ran in topo order.
assertEq(enableOrder[1], "B", "phase2: B enabled first")
assertEq(enableOrder[2], "A", "phase2: A enabled second")
assertEq(enableOrder[3], "C", "phase2: C enabled third")

-- ===== d. Shutdown reverse order ============================================
HDG.Modules:_Reset()

local shutdownOrder = {}

HDG.Modules:Declare({
    name = "X",
    dependencies = {},
    onShutdown = function(self) shutdownOrder[#shutdownOrder + 1] = "X" end,
})
HDG.Modules:Declare({
    name = "Y",
    dependencies = { "X" },
    onShutdown = function(self) shutdownOrder[#shutdownOrder + 1] = "Y" end,
})
HDG.Modules:Declare({
    name = "Z",
    dependencies = { "Y" },
    onShutdown = function(self) shutdownOrder[#shutdownOrder + 1] = "Z" end,
})

-- ADR-020: explicit phase calls (see (a) above).
HDG.Modules:Topo()
HDG.Modules:Phase1()
HDG.Modules:Phase2()
HDG.Modules:Shutdown()

-- Topo order for X,Y,Z is [X, Y, Z]. Shutdown is REVERSE: Z, Y, X.
assertEq(shutdownOrder[1], "Z", "shutdown: Z (deepest dependant) first")
assertEq(shutdownOrder[2], "Y", "shutdown: Y second")
assertEq(shutdownOrder[3], "X", "shutdown: X (root) last")

-- ===== e. Phase2 fails loud (ADR-042; isolation class removed 2026-06-12) ===
-- Pre-registry doctrine asserted the INVERSE (pcall isolation: siblings keep
-- enabling past a throwing onEnable). Adjudicated wrong: a half-enabled addon
-- that quietly logs is worse than a loud halt. Phase2 now mirrors Phase1 --
-- log with module-name context, then re-raise.
HDG.Modules:_Reset()

local afterBoomRan = false

HDG.Modules:Declare({
    name = "First",
    dependencies = {},
    onEnable = function(_self) error("deliberate boom in onEnable") end,
})
HDG.Modules:Declare({
    name = "Second",
    dependencies = { "First" },
    onEnable = function(_self) afterBoomRan = true end,
})

HDG.Modules:Topo()
HDG.Modules:Phase1()
local bootOk, bootErr = pcall(function() HDG.Modules:Phase2() end)
assertTrue(not bootOk, "phase2: onEnable errors must propagate (fail loud)")
assertTrue(tostring(bootErr):find("First", 1, true) ~= nil,
    "phase2: propagated error names the failing module")
assertTrue(not afterBoomRan, "phase2: enabling halts at the throwing module")

-- ===== f. Cycle detection ====================================================
HDG.Modules:_Reset()

HDG.Modules:Declare({ name = "P", dependencies = { "Q" } })
HDG.Modules:Declare({ name = "Q", dependencies = { "P" } })

assertErrorContains(
    function() HDG.Modules:Topo() end,
    "cycle",
    "cycle detection: Topo() must error with 'cycle' in the message"
)

-- ===== g. Unregistered dependency ============================================
HDG.Modules:_Reset()

HDG.Modules:Declare({ name = "Lone", dependencies = { "Ghost" } })

assertErrorContains(
    function() HDG.Modules:Topo() end,
    "Ghost",
    "unregistered dep: Topo() must name the missing module"
)

-- ===== h. Declare after the registry is sealed errors =========================
-- Both the legacy Boot() path (_booted) and the production Topo()/Phase path
-- (_topoSorted) seal the registry; a late Declare must error either way.
HDG.Modules:_Reset()

HDG.Modules:Declare({ name = "Solo", dependencies = {} })
HDG.Modules:Boot()

assertErrorContains(
    function()
        HDG.Modules:Declare({ name = "Late", dependencies = {} })
    end,
    "topology was sealed",
    "post-boot declare: must error with 'topology was sealed'"
)

-- ===== h2. Declare after the PRODUCTION seal (Topo() without Boot) errors ======
-- The production boot path is Topo()+Phase1()+Phase2(), which never sets _booted;
-- the guard must still fire off _topoSorted. (Regression for the A5.1 finding.)
HDG.Modules:_Reset()
HDG.Modules:Declare({ name = "Solo", dependencies = {} })
HDG.Modules:Topo()
assertErrorContains(
    function()
        HDG.Modules:Declare({ name = "LateProd", dependencies = {} })
    end,
    "topology was sealed",
    "production-path declare after Topo(): must error"
)

print("test_module_lifecycle.lua passed")
