-- test_lumber_actions.lua
-- ============================================================================
-- Phase 4 lumber action smoke: dispatches each of the 8 lumber actions and
-- verifies the reducer mutates the expected state slots + nothing else.
-- Pairs with test_lumber_state.lua (which covers the factory shape) and
-- the existing test_action_meta_exhaustiveness.lua (which checks every
-- action in HDG.Constants.ACTIONS has a BuildActionMeta row + invalidates).

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_TableUtils.lua")
dofile("../Core/HDGR_Modules.lua")
dofile("../Core/HDGR_Middleware.lua")
dofile("../Core/HDGR_StoreFurnishings.lua")
dofile("../Core/HDGR_Store.lua")
-- Selectors registry needed because Store:_RawDispatch calls
-- HDG.Selectors:InvalidateMemos after every reducer pass.
dofile("../UI/HDGR_Selectors.lua")

local A = HDG.Constants.ACTIONS

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

HDG_DB = {}
HDG.Store:LoadFromSavedVariables()

-- Seed identity so the reducer can scope per-char session writes.
HDG.Store:GetState().session.identity.charKey = "Vamoose-Frostmourne"

-- ===== LUMBER_HARVESTED appends a blip + bumps tick =======================
local s = HDG.Store:GetState().session.lumber
local tickBefore = s.tick
HDG.Store:Dispatch({
    type = A.LUMBER_HARVESTED,
    payload = { lumberID = 245586, qty = 1, x = 0.5, y = 0.5, mapID = 2552, timestamp = 1000 },
})
assertEq(#s.blips, 1, "first harvest -> one blip")
assertEq(s.blips[1].lumberID, 245586, "blip lumberID matches payload")
assertEq(s.blips[1].mapID, 2552, "blip mapID matches payload")
assertEq(s.tick, tickBefore + 1, "tick bumped by harvest")

HDG.Store:Dispatch({
    type = A.LUMBER_HARVESTED,
    payload = { lumberID = 245586, qty = 1, x = 0.6, y = 0.4, mapID = 2552, timestamp = 1010 },
})
assertEq(#s.blips, 2, "second harvest -> two blips")
assertEq(s.tick, tickBefore + 2, "tick bumped on each harvest")

-- ===== LUMBER_SESSION_START sets activeFarmingID + per-char record =======
HDG.Store:Dispatch({
    type = A.LUMBER_SESSION_START,
    payload = { lumberID = 245586, timestamp = 1000, startCount = 5 },
})
assertEq(s.activeFarmingID, 245586, "activeFarmingID set on SESSION_START")
local charSessions = HDG.Store:GetState().account.lumber.sessions["Vamoose-Frostmourne"]
assertTrue(charSessions and charSessions[245586], "per-char session record created")
assertEq(charSessions[245586].startedAt,  1000, "startedAt recorded")
assertEq(charSessions[245586].startCount, 5,    "startCount recorded")
assertEq(charSessions[245586].finalizedAt, nil, "finalizedAt nil until SESSION_END")

-- ===== Subsequent HARVESTED on the active ID updates lastHarvestAt =======
HDG.Store:Dispatch({
    type = A.LUMBER_HARVESTED,
    payload = { lumberID = 245586, qty = 1, x = 0.7, y = 0.3, mapID = 2552, timestamp = 1020 },
})
assertEq(charSessions[245586].lastHarvestAt, 1020, "lastHarvestAt updated by HARVESTED")

-- ===== LUMBER_SESSION_END clears activeFarmingID + stamps finalizedAt =====
HDG.Store:Dispatch({
    type = A.LUMBER_SESSION_END,
    payload = { timestamp = 1500 },
})
assertEq(s.activeFarmingID, nil, "activeFarmingID nil after SESSION_END")
assertEq(charSessions[245586].finalizedAt, 1500, "finalizedAt stamped")

-- ===== LUMBER_BLIP_GC drops blips older than 3600s (1 hr TTL) ==============
-- Bumped 2026-05-24 from 600s. Donor retains blips through the full color
-- cycle up to DECAY.OLD (1 hr); HDG matches via HDG.LumberObserver
-- .RESPAWN_SECONDS = 3600. Existing blips ts 1000/1010/1020; now = 5000 ->
-- ages 4000/3990/3980, all > 3600 -> all dropped.
HDG.Store:Dispatch({
    type = A.LUMBER_BLIP_GC,
    payload = { now = 5000 },
})
assertEq(#s.blips, 0, "all expired blips dropped")

-- Refresh: harvest a new blip + GC at a time inside the window.
HDG.Store:Dispatch({
    type = A.LUMBER_HARVESTED,
    payload = { lumberID = 245586, qty = 1, x = 0.5, y = 0.5, mapID = 2552, timestamp = 6000 },
})
HDG.Store:Dispatch({
    type = A.LUMBER_BLIP_GC,
    payload = { now = 7800 },  -- 1800s after harvest, < 3600s window
})
assertEq(#s.blips, 1, "fresh blip kept (within respawn window)")

-- ===== LUMBER_WINDOW_TOGGLE handles explicit + flip semantics =============
local c = HDG.Store:GetState().account.lumber.config
assertEq(c.windowVisible, false, "windowVisible starts false")
HDG.Store:Dispatch({ type = A.LUMBER_WINDOW_TOGGLE, payload = { visible = true } })
assertEq(c.windowVisible, true, "explicit { visible = true } sets visible")
HDG.Store:Dispatch({ type = A.LUMBER_WINDOW_TOGGLE })  -- flip
assertEq(c.windowVisible, false, "no-payload toggle flips off")
HDG.Store:Dispatch({ type = A.LUMBER_WINDOW_TOGGLE })  -- flip back
assertEq(c.windowVisible, true,  "no-payload toggle flips back on")

-- ===== LUMBER_WINDOW_POSITION_SET writes the position record ==============
HDG.Store:Dispatch({
    type = A.LUMBER_WINDOW_POSITION_SET,
    payload = { x = 250, y = -400 },
})
assertEq(c.position.x, 250,  "position.x updated by drag")
assertEq(c.position.y, -400, "position.y updated by drag")

-- ===== LUMBER_RADAR_COLLAPSE_TOGGLE flips the flag ========================
assertEq(c.radarCollapsed, false, "radarCollapsed starts false")
HDG.Store:Dispatch({ type = A.LUMBER_RADAR_COLLAPSE_TOGGLE })
assertEq(c.radarCollapsed, true,  "first toggle collapses")
HDG.Store:Dispatch({ type = A.LUMBER_RADAR_COLLAPSE_TOGGLE })
assertEq(c.radarCollapsed, false, "second toggle expands")

-- ===== LUMBER_RADAR_SCALE_SET writes the scale within range ===============
assertEq(c.radarScale, 1.0, "radarScale starts 1.0")
HDG.Store:Dispatch({
    type = A.LUMBER_RADAR_SCALE_SET,
    payload = { scale = 1.5 },
})
assertEq(c.radarScale, 1.5, "valid scale written")

-- Reject non-positive scale (defensive boundary on user input -- a slider
-- shouldn't be able to send 0 or negative, but the reducer rejects to keep
-- divide-by-zero out of the radar math).
HDG.Store:Dispatch({
    type = A.LUMBER_RADAR_SCALE_SET,
    payload = { scale = 0 },
})
assertEq(c.radarScale, 1.5, "scale unchanged on invalid (0) input")
HDG.Store:Dispatch({
    type = A.LUMBER_RADAR_SCALE_SET,
    payload = { scale = -1 },
})
assertEq(c.radarScale, 1.5, "scale unchanged on negative input")

print("test_lumber_actions.lua passed")
