-- Smoke test: Phase 2 Store schema + reducer cases.
-- Verifies the engine loads, dispatches mutate state correctly, and the
-- generic action types behave as designed.

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

-- LoadFromSavedVariables produces a clean default state.
HDG_DB = {}
HDG.Store:LoadFromSavedVariables()
HDG.Config:Initialize()
local state = HDG.Store:GetState()
assertEq(state.account.schemaVersion, HDG.Constants.SCHEMA_VERSION, "schemaVersion default")
assertEq(state.account.config.debug, false, "debug default")
assertEq(state.account.config.showMinimapButton, true, "minimapButton default")
assertEq(state.account.config.scheme, "ColorblindSafe", "scheme default")
assertEq(state.account.ui.mainWindowShown, false, "mainWindowShown default")
assertEq(state.session.combat.inLockdown, false, "combat default")
-- Interface section defaults (Pass B 2026-05-26)
assertEq(state.account.config.tooltipDecorTag,       true,   "tooltipDecorTag default")
assertEq(state.account.config.showCompartment,       true,   "showCompartment default")
assertEq(state.account.config.showProfessionButtons, true,   "showProfessionButtons default")
assertEq(state.account.config.waypointProvider,      "auto", "waypointProvider default")

-- MAIN_WINDOW_TOGGLE flips mainWindowShown.
HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.MAIN_WINDOW_TOGGLE })
assertEq(state.account.ui.mainWindowShown, true, "toggle -> shown")
HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.MAIN_WINDOW_TOGGLE })
assertEq(state.account.ui.mainWindowShown, false, "toggle again -> hidden")

-- CONFIG_SET writes config keys.
HDG.Store:Dispatch({
    type = HDG.Constants.ACTIONS.CONFIG_SET,
    payload = { key = "debug", value = true },
})
assertEq(state.account.config.debug, true, "config debug flipped")

-- UI_SET_PERSISTENT writes account.ui.
HDG.Store:Dispatch({
    type = HDG.Constants.ACTIONS.UI_SET_PERSISTENT,
    payload = { key = "activeTab", value = "decor" },
})
assertEq(state.account.ui.activeTab, "decor", "activeTab persisted")

-- UI_SET_TRANSIENT writes session.ui (top-level form).
HDG.Store:Dispatch({
    type = HDG.Constants.ACTIONS.UI_SET_TRANSIENT,
    payload = { key = "searchQuery", value = "rug" },
})
assertEq(state.session.ui.searchQuery, "rug", "transient top-level")

-- UI_SET_TRANSIENT with view writes session.ui[view][key].
HDG.Store:Dispatch({
    type = HDG.Constants.ACTIONS.UI_SET_TRANSIENT,
    payload = { view = "decor", key = "sort", value = "ilvl" },
})
assertEq(state.session.ui.decor.sort, "ilvl", "transient per-view")

-- COMBAT_ENTER / COMBAT_EXIT flips session.combat.inLockdown.
HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.COMBAT_ENTER })
assertEq(state.session.combat.inLockdown, true, "combat enter")
HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.COMBAT_EXIT })
assertEq(state.session.combat.inLockdown, false, "combat exit")

-- SESSION_END forces the window closed (so next login is clean).
HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.MAIN_WINDOW_TOGGLE })  -- open it first
assertEq(state.account.ui.mainWindowShown, true, "pre-SESSION_END window open")
HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.SESSION_END })
assertEq(state.account.ui.mainWindowShown, false, "SESSION_END closes window")

-- Subscribers receive action type (deferred via C_Timer.After(0); mock fires sync).
local seen = {}
HDG.Store:Subscribe(function(actionType) seen[#seen + 1] = actionType end)
HDG.Store:Dispatch({
    type = HDG.Constants.ACTIONS.CONFIG_SET,
    payload = { key = "debug", value = false },
})
assertTrue(seen[#seen] == HDG.Constants.ACTIONS.CONFIG_SET, "subscriber receives action")

-- Flush copies account into HDG_DB.account; session is NOT persisted.
HDG.Store:Flush()
assertEq(HDG_DB.account.config.debug, false, "flush persists config")
assertEq(HDG_DB.account.ui.activeTab, "decor", "flush persists ui")
assertEq(HDG_DB.session, nil, "flush does not persist session")

-- HARD_RESET wipes both HDG_DB and state.
HDG.Store:Dispatch({ type = HDG.Constants.ACTIONS.HARD_RESET })
assertEq(next(HDG_DB), nil, "HDG_DB wiped by hardreset")
state = HDG.Store:GetState()
assertEq(state.account.config.debug, false, "state reset to defaults")
assertEq(state.account.config.scheme, "ColorblindSafe", "scheme reset to default")

-- Account.craft shape exists after fresh load + survives reload.
assertTrue(state.account.craft ~= nil, "craft slot present")
assertTrue(type(state.account.craft.queue) == "table", "craft.queue is a table")
assertTrue(type(state.account.craft.history) == "table", "craft.history is a table")
assertTrue(type(state.account.craft.history.entries) == "table", "craft.history.entries is a table")
assertEq(state.account.craft.history.nextID, 0, "craft.history.nextID seeded")

-- EnsureCraft restores a craft slot wiped from SavedVariables (forward-compat
-- migration: existing players reload after upgrade without a craft slot).
HDG_DB = { account = { schemaVersion = 1, config = {} } }
HDG.Store:LoadFromSavedVariables()
HDG.Config:Initialize()
state = HDG.Store:GetState()
assertTrue(state.account.craft ~= nil, "craft slot ensured on legacy SV")
assertTrue(type(state.account.craft.queue) == "table", "craft.queue ensured")
assertEq(state.account.craft.history.nextID, 0, "craft.history.nextID ensured")

-- Constants: CRAFT_HISTORY_CAP exposed for the history reducer (Phase 11.1).
assertEq(HDG.Constants.CRAFT_HISTORY_CAP, 500, "CRAFT_HISTORY_CAP exposed")

-- ===== Queue + history reducer cases ============================
HDG_DB = {}
HDG.Store:LoadFromSavedVariables()
HDG.Config:Initialize()
state = HDG.Store:GetState()
local A = HDG.Constants.ACTIONS

-- CRAFT_QUEUE_ADD appends a new row per distinct recipeID, but coalesces
-- repeated adds of the same recipeID into the existing row's qty.
HDG.Store:Dispatch({ type = A.CRAFT_QUEUE_ADD,
    payload = { recipeID = 100, itemID = 200, qty = 5 } })
HDG.Store:Dispatch({ type = A.CRAFT_QUEUE_ADD,
    payload = { recipeID = 101, itemID = 201, qty = 1, sessionKey = "abc" } })
assertEq(#state.account.craft.queue, 2, "queue length after 2 distinct adds")
assertEq(state.account.craft.queue[1].recipeID, 100, "queue[1].recipeID")
assertEq(state.account.craft.queue[1].remaining, 5, "queue[1].remaining seeded from qty")
assertEq(state.account.craft.queue[2].sessionKey, "abc", "sessionKey persisted")

-- Adding recipeID=100 again coalesces; row count stays at 2, remaining grows.
HDG.Store:Dispatch({ type = A.CRAFT_QUEUE_ADD,
    payload = { recipeID = 100, itemID = 200, qty = 3 } })
assertEq(#state.account.craft.queue, 2, "duplicate recipeID does NOT append new row")
assertEq(state.account.craft.queue[1].remaining, 8, "queue[1].remaining coalesced: 5 + 3 = 8")
assertEq(state.account.craft.queue[1].requested, 8, "queue[1].requested coalesced: 5 + 3 = 8")

-- CRAFT_QUEUE_DECREMENT matches by (position, recipeID); reduces remaining;
-- removes the row when remaining hits 0. Starts from remaining=8 (after the
-- coalesce step above).
HDG.Store:Dispatch({ type = A.CRAFT_QUEUE_DECREMENT,
    payload = { position = 1, recipeID = 100, qty = 2 } })
assertEq(state.account.craft.queue[1].remaining, 6, "decrement reduces remaining 8 -> 6")

-- Wrong recipeID at the position -> no-op (reviewer C2/C4 guard).
HDG.Store:Dispatch({ type = A.CRAFT_QUEUE_DECREMENT,
    payload = { position = 1, recipeID = 999, qty = 1 } })
assertEq(state.account.craft.queue[1].remaining, 6, "wrong recipeID -> no-op")

-- Decrement to zero removes the row.
HDG.Store:Dispatch({ type = A.CRAFT_QUEUE_DECREMENT,
    payload = { position = 1, recipeID = 100, qty = 6 } })
assertEq(#state.account.craft.queue, 1, "row removed when remaining <= 0")
assertEq(state.account.craft.queue[1].recipeID, 101, "row at pos 1 is now the old pos 2")

-- CRAFT_QUEUE_REMOVE pops by position.
HDG.Store:Dispatch({ type = A.CRAFT_QUEUE_REMOVE, payload = { position = 1 } })
assertEq(#state.account.craft.queue, 0, "queue empty after remove")

-- CRAFT_QUEUE_CLEAR wipes the whole queue.
HDG.Store:Dispatch({ type = A.CRAFT_QUEUE_ADD,
    payload = { recipeID = 1, itemID = 1, qty = 1 } })
HDG.Store:Dispatch({ type = A.CRAFT_QUEUE_ADD,
    payload = { recipeID = 2, itemID = 2, qty = 1 } })
assertEq(#state.account.craft.queue, 2, "queue refilled pre-clear")
HDG.Store:Dispatch({ type = A.CRAFT_QUEUE_CLEAR })
assertEq(#state.account.craft.queue, 0, "CRAFT_QUEUE_CLEAR empties queue")

-- CRAFT_HISTORY_PUSH appends only when payload.completed is true.
HDG.Store:Dispatch({ type = A.CRAFT_HISTORY_PUSH,
    payload = { type = "decor", recipeID = 1, itemID = 1, qty = 1, completed = false } })
assertEq(#state.account.craft.history.entries, 0, "history: completed=false -> phantom blocked")
HDG.Store:Dispatch({ type = A.CRAFT_HISTORY_PUSH,
    payload = { type = "decor", recipeID = 1, itemID = 1, qty = 1, completed = true } })
assertEq(#state.account.craft.history.entries, 1, "history: completed=true -> appended")
assertEq(state.account.craft.history.entries[1].id, 1, "history: nextID applied")
assertEq(state.account.craft.history.nextID, 1, "history: nextID bumped")

-- History trims to CRAFT_HISTORY_CAP. Push N=CAP+5 and verify oldest evicted.
local cap = HDG.Constants.CRAFT_HISTORY_CAP
for i = 1, cap + 4 do   -- 4 more on top of the 1 already there
    HDG.Store:Dispatch({ type = A.CRAFT_HISTORY_PUSH,
        payload = { type = "decor", recipeID = i, itemID = i, qty = 1, completed = true } })
end
assertEq(#state.account.craft.history.entries, cap, "history trims to CRAFT_HISTORY_CAP")

-- ===== Recipes session UI reducer cases =========================
HDG.Store:Dispatch({ type = A.RECIPES_SET_SEARCH, payload = { query = "phial" } })
assertEq(state.session.ui.recipes.searchQuery, "phial", "recipes searchQuery set")

HDG.Store:Dispatch({ type = A.RECIPES_SET_SECTION_EXPAND,
    payload = { key = "alchemy:tww", expanded = true } })
assertTrue(state.session.ui.recipes.expandedSections["alchemy:tww"],
           "section expanded")
HDG.Store:Dispatch({ type = A.RECIPES_SET_SECTION_EXPAND,
    payload = { key = "alchemy:tww", expanded = false } })
assertEq(state.session.ui.recipes.expandedSections["alchemy:tww"], nil,
         "section collapsed -> nil (cleaned)")

HDG.Store:Dispatch({ type = A.RECIPES_SET_MATERIALS_DEPTH, payload = { value = "raw" } })
assertEq(state.session.ui.recipes.materialsDepth, "raw", "materialsDepth set")

assertEq(state.session.ui.recipes.filters.knownOnly, false, "filter default")
HDG.Store:Dispatch({ type = A.RECIPES_TOGGLE_FILTER, payload = { filter = "knownOnly" } })
assertEq(state.session.ui.recipes.filters.knownOnly, true, "filter toggled on")
HDG.Store:Dispatch({ type = A.RECIPES_TOGGLE_FILTER, payload = { filter = "knownOnly" } })
assertEq(state.session.ui.recipes.filters.knownOnly, false, "filter toggled off")

HDG.Store:Dispatch({ type = A.RECIPES_SELECT_RECIPE, payload = { recipeID = 42 } })
assertEq(state.session.ui.recipes.selectedRecipeID, 42, "selectedRecipeID set")

-- RECIPES_FILTER_RESET wipes filters + searchQuery atomically (ADR-018).
-- selectedRecipeID is NOT a filter and stays untouched.
HDG.Store:Dispatch({ type = A.RECIPES_TOGGLE_FILTER, payload = { filter = "knownOnly" } })
HDG.Store:Dispatch({ type = A.RECIPES_SET_SEARCH, payload = { query = "x" } })
assertEq(state.session.ui.recipes.filters.knownOnly, true, "pre-reset: knownOnly on")
HDG.Store:Dispatch({ type = A.UI_FILTER_RESET, payload = { tab = "recipes" } })
assertEq(state.session.ui.recipes.searchQuery, "", "reset: searchQuery cleared")
assertEq(state.session.ui.recipes.filters.knownOnly, false, "reset: knownOnly cleared")
assertEq(state.session.ui.recipes.filters.expansionFilter, "all",
         "reset: expansionFilter restored to 'all'")
assertEq(state.session.ui.recipes.selectedRecipeID, 42,
         "reset preserves selectedRecipeID (not a filter)")

-- ===== Cross-feature observer dispatches ========================
local bagTick0 = state.session.resolvers.bag.tick
HDG.Store:Dispatch({ type = A.BAG_INVENTORY_UPDATED, payload = { tick = bagTick0 + 1 } })
assertEq(state.session.resolvers.bag.tick, bagTick0 + 1, "BAG_INVENTORY_UPDATED bumps tick")

-- ITEM_INFO_RESOLVED is a notification-only action (noop=true) with a bulk
-- payload (one dispatch per drain, not per item). Verify it doesn't error
-- and that subscribers see it.
local seenItem = nil
HDG.Store:Subscribe(function(actionType)
    if actionType == A.ITEM_INFO_RESOLVED then seenItem = true end
end)
HDG.Store:Dispatch({ type = A.ITEM_INFO_RESOLVED,
    payload = { itemIDs = { 555, 556, 557 }, count = 3 } })
assertTrue(seenItem, "ITEM_INFO_RESOLVED reaches subscribers")

print("test_store_smoke.lua passed")
