-- Log smoke: tag registry + Push API + reducer cases.
-- Verifies the engine-level tag taxonomy, duplicate detection, unknown-tag
-- error, Push routing, ring buffer cap, dispatch sub-cap, and trace toggling.

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")
-- Config layer: reducer routes CONFIG_SET via HDG.Config:_GetSourceForScope.
dofile("../Core/HDGR_ConfigSchema.lua")
dofile("../Core/HDGR_Config.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")

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
local function assertError(fn, needle, label)
    local ok, err = pcall(fn)
    if ok then error(label .. ": expected error containing " .. tostring(needle), 2) end
    if needle and not tostring(err):find(needle, 1, true) then
        error(label .. ": error " .. tostring(err) .. " did not contain " .. tostring(needle), 2)
    end
end

-- ===== Engine tags pre-registered at file load =====
assertTrue(HDG.Log:HasTag("dispatch"), "dispatch engine tag registered")
assertTrue(HDG.Log:HasTag("error"),    "error engine tag registered")
assertTrue(HDG.Log:HasTag("combat"),   "combat engine tag registered")
assertTrue(HDG.Log:HasTag("theme"),    "theme engine tag registered")
assertEq(HDG.Log:GetTag("error").user, true,  "error tag is user-visible")
assertEq(HDG.Log:GetTag("dispatch").user, false, "dispatch tag is dev-only")
assertEq(HDG.Log:GetTag("theme").duration, 3, "theme tag has 3s duration")

-- ===== Tag registration: validation =====
assertError(function() HDG.Log:RegisterTags({ ["bad"] = "not-a-table" }) end,
    "definition must be a table", "non-table def errors")
assertError(function() HDG.Log:RegisterTags({ [""] = { user = true } }) end,
    "non-empty string", "empty tag name errors")
assertError(function() HDG.Log:RegisterTags({ ["missing_user"] = { level = "info" } }) end,
    "missing required `user`", "missing user flag errors")
assertError(function() HDG.Log:RegisterTags({ ["dispatch"] = { user = false } }) end,
    "duplicate", "duplicate tag name errors")

-- Register a fresh module-style tag
HDG.Log:RegisterTags({
    test_module = { user = false, level = "debug" },
    test_user   = { user = true,  level = "success", duration = 5 },
})
assertTrue(HDG.Log:HasTag("test_module"), "test_module registered")
assertTrue(HDG.Log:HasTag("test_user"),   "test_user registered")

-- ===== Push: store state mutation =====
HDG_DB = {}
HDG.Store:LoadFromSavedVariables()
HDG.Config:Initialize()

local function entryCount()
    return #HDG.Store:GetState().session.log.entries
end
local function lastEntry()
    local es = HDG.Store:GetState().session.log.entries
    return es[#es]
end

assertEq(entryCount(), 0, "log starts empty")

HDG.Log:Info("theme", "Theme switched to Solarized")
assertEq(entryCount(), 1, "after Info push -> 1 entry")
local e1 = lastEntry()
assertEq(e1.tag,   "theme", "entry tag")
assertEq(e1.level, "info",  "entry level (explicit)")
assertEq(e1.text,  "Theme switched to Solarized", "entry text")
assertEq(e1.duration, 3,    "entry duration from tag def default")

HDG.Log:Push({ tag = "test_user", text = "explicit duration", duration = 99 })
local e2 = lastEntry()
assertEq(e2.duration, 99, "explicit duration overrides tag default")

HDG.Log:Success("test_user", "Tag with success level")
assertEq(lastEntry().level, "success", "Success() wrapper sets level")

-- ===== Push: unknown tag errors =====
assertError(function() HDG.Log:Info("not_registered", "...") end,
    "unknown tag", "unknown tag errors")

-- ===== Cap: ring buffer =====
HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.LOG_CLEAR })
assertEq(entryCount(), 0, "clear empties the log")

for i = 1, 250 do
    HDG.Log:Info("test_module", "entry " .. i)
end
assertEq(entryCount(), 200, "ring buffer caps at 200")
assertEq(lastEntry().text, "entry 250", "newest entry preserved")
-- Oldest 50 evicted; current first entry is entry 51
assertEq(HDG.Store:GetState().session.log.entries[1].text, "entry 51",
    "oldest 50 evicted")

-- ===== Cap: dispatch sub-cap =====
HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.LOG_CLEAR })
for i = 1, 30 do
    HDG.Log:Debug("dispatch", "dispatch " .. i)
end
for i = 1, 30 do
    HDG.Log:Info("theme", "theme " .. i)   -- different tag
end
for i = 1, 30 do
    HDG.Log:Debug("dispatch", "dispatch " .. (30 + i))
end

local dispatchCount = 0
for _, e in ipairs(HDG.Store:GetState().session.log.entries) do
    if e.tag == "dispatch" then dispatchCount = dispatchCount + 1 end
end
assertEq(dispatchCount, 50, "dispatch sub-cap holds at 50")

-- Theme entries should NOT have been evicted by dispatch overflow
local themeCount = 0
for _, e in ipairs(HDG.Store:GetState().session.log.entries) do
    if e.tag == "theme" then themeCount = themeCount + 1 end
end
assertEq(themeCount, 30, "theme entries protected from dispatch firehose")

-- ===== Clear: per-tag =====
HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.LOG_CLEAR,
    payload = { tag = "dispatch" } })
local dispatchAfter = 0
for _, e in ipairs(HDG.Store:GetState().session.log.entries) do
    if e.tag == "dispatch" then dispatchAfter = dispatchAfter + 1 end
end
assertEq(dispatchAfter, 0, "tag-scoped clear removes only that tag")
local themeAfter = 0
for _, e in ipairs(HDG.Store:GetState().session.log.entries) do
    if e.tag == "theme" then themeAfter = themeAfter + 1 end
end
assertEq(themeAfter, 30, "tag-scoped clear leaves other tags untouched")

-- ===== Filter state =====
HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.LOG_SET_FILTER,
    payload = { tag = "theme", level = "warn", autoScroll = false } })
local f = HDG.Store:GetState().session.log.tabFilter
assertEq(f.tag, "theme", "filter.tag set")
assertEq(f.level, "warn", "filter.level set")
assertEq(f.autoScroll, false, "filter.autoScroll set")

-- ===== Trace toggle =====
HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.LOG_TRACE_TOGGLE,
    payload = { tag = "selector", on = true } })
assertEq(HDG.Store:GetState().session.log.activeTraces.selector, true,
    "trace enabled")

HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.LOG_TRACE_TOGGLE,
    payload = { tag = "selector" } })  -- omitting `on` toggles
assertEq(HDG.Store:GetState().session.log.activeTraces.selector, nil,
    "trace toggled off when on=nil")

HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.LOG_TRACE_TOGGLE,
    payload = { tag = "binding", on = true } })
HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.LOG_TRACE_TOGGLE,
    payload = { tag = "cache", on = true } })
HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.LOG_TRACE_TOGGLE,
    payload = { tag = "*" } })  -- wipes all
assertEq(HDG.Store:GetState().session.log.activeTraces.binding, nil,
    "wildcard trace-off clears binding")
assertEq(HDG.Store:GetState().session.log.activeTraces.cache, nil,
    "wildcard trace-off clears cache")

-- ===== Reset test ensures engine tags re-register =====
HDG.Log:_Reset()
assertTrue(HDG.Log:HasTag("dispatch"), "engine tags re-registered after _Reset")
assertFalse(HDG.Log:HasTag("test_module"), "module tags wiped on _Reset")

-- ============================================================================
-- Step 3 integration: LoggerMiddleware refactor + trace consumer
-- ============================================================================

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

-- Build the middleware chain (production wires this in OnInitialize; tests
-- usually skip it. For this integration check we apply manually.).
local A = HDG.Constants.ACTIONS
local actionMeta = {
    [A.CONFIG_SET]                  = { persists = true,  combatUnsafe = false, noop = false },
    [A.HARD_RESET]                  = { persists = true,  combatUnsafe = false, noop = false },
    [A.UI_SET_PERSISTENT]           = { persists = true,  combatUnsafe = false, noop = false },
    [A.UI_SET_TRANSIENT]            = { persists = false, combatUnsafe = false, noop = false },
    [A.MAIN_WINDOW_TOGGLE]          = { persists = true,  combatUnsafe = false, noop = false },
    [A.SESSION_END]                 = { persists = true,  combatUnsafe = false, noop = false },
    [A.COMBAT_ENTER]                = { persists = false, combatUnsafe = false, noop = false },
    [A.COMBAT_EXIT]                 = { persists = false, combatUnsafe = false, noop = false },
    [A.COMBAT_QUEUE_ACTION]         = { persists = false, combatUnsafe = false, noop = false },
    [A.DECOR_CATALOG_READY]         = { persists = false, combatUnsafe = false, noop = true  },
    [A.LOG_PUSH]                    = { persists = false, combatUnsafe = false, noop = false },
    [A.LOG_CLEAR]                   = { persists = false, combatUnsafe = false, noop = false },
    [A.LOG_SET_FILTER]              = { persists = false, combatUnsafe = false, noop = false },
    [A.LOG_TRACE_TOGGLE]            = { persists = false, combatUnsafe = false, noop = false },
}
HDG.Middleware.Apply(HDG.Store, HDG.Middleware.StandardChain({
    actionMeta = actionMeta, env = {},
}))

-- Reset log + enable debug flag
HDG.Store:Dispatch({ type = A.LOG_CLEAR })
HDG.Store:Dispatch({ type = A.CONFIG_SET, payload = { key = "debug", value = true } })

-- Dispatch a non-log action; LoggerMiddleware should produce a LOG_PUSH
-- dispatch tag entry.
local dispatchBefore = 0
for _, e in ipairs(HDG.Store:GetState().session.log.entries) do
    if e.tag == "dispatch" then dispatchBefore = dispatchBefore + 1 end
end
HDG.Store:Dispatch({ type = A.MAIN_WINDOW_TOGGLE })
local dispatchAfter = 0
for _, e in ipairs(HDG.Store:GetState().session.log.entries) do
    if e.tag == "dispatch" then dispatchAfter = dispatchAfter + 1 end
end
assertTrue(dispatchAfter > dispatchBefore,
    "LoggerMiddleware produces dispatch entries when debug=true")

-- Verify the entry's text format: action name (HDGR_ prefix stripped, padded)
local lastDispatch
for _, e in ipairs(HDG.Store:GetState().session.log.entries) do
    if e.tag == "dispatch" then lastDispatch = e end
end
assertTrue(lastDispatch ~= nil, "found a dispatch entry")
assertTrue(lastDispatch.text:find("MAIN_WINDOW_TOGGLE", 1, true) ~= nil,
    "dispatch entry text contains action name")

-- LOG_PUSH itself should NOT generate a dispatch entry (self-logging guard)
local beforeSelf = 0
for _, e in ipairs(HDG.Store:GetState().session.log.entries) do
    if e.tag == "dispatch" then beforeSelf = beforeSelf + 1 end
end
HDG.Log:Info("theme", "test")  -- dispatches LOG_PUSH internally
local afterSelf = 0
for _, e in ipairs(HDG.Store:GetState().session.log.entries) do
    if e.tag == "dispatch" then afterSelf = afterSelf + 1 end
end
assertEq(afterSelf, beforeSelf,
    "LOG_PUSH dispatch is NOT logged as a dispatch entry (no recursion)")

-- ===== Trace consumer renders to chat (capture print output) =====
local captured = {}
local origPrint = _G.print
_G.print = function(s) captured[#captured + 1] = s end

-- Attach the trace consumer for this test (production runs it via OnInitialize)
HDG.Log:AttachTraceConsumer()

-- Push an error -- always traces regardless of debug flag
HDG.Log:Error("error", "test error message")
local foundError = false
for _, line in ipairs(captured) do
    if line:find("test error message", 1, true) then foundError = true; break end
end
assertTrue(foundError, "trace consumer rendered error entry to chat")

-- Push a dispatch entry -- traced because debug=true is still on
captured = {}
HDG.Store:Dispatch({ type = A.COMBAT_ENTER })
-- Drain the timer-deferred subscriber fan-out (wow_mock runs C_Timer.After
-- synchronously by default; the dispatch should have already rendered).
local foundDispatch = false
for _, line in ipairs(captured) do
    if line:find("COMBAT_ENTER", 1, true) then foundDispatch = true; break end
end
assertTrue(foundDispatch, "trace consumer rendered dispatch entry with debug flag on")

-- Disable debug; dispatch entries should not render
HDG.Store:Dispatch({ type = A.CONFIG_SET, payload = { key = "debug", value = false } })
captured = {}
HDG.Store:Dispatch({ type = A.MAIN_WINDOW_TOGGLE })
local nonDispatchPrint = false
for _, line in ipairs(captured) do
    if line:find("MAIN_WINDOW_TOGGLE", 1, true) then nonDispatchPrint = true; break end
end
assertFalse(nonDispatchPrint, "debug flag off suppresses dispatch chat output")

-- Restore print
_G.print = origPrint

print("test_log_smoke.lua passed")
