-- test_lumber_observer.lua
-- ============================================================================
-- Phase 5 LumberObserver smoke: drives the observer through a synthetic
-- bag-delta sequence and verifies the dispatched action stream:
--   1. First scan = warm-up only; no HARVESTED dispatched even if items present
--   2. Subsequent bag-delta gain = SESSION_START + WINDOW_TOGGLE + HARVESTED
--   3. Same-session harvest within IDLE_SECONDS = just HARVESTED (no re-start)
--   4. Stale harvest > IDLE_SECONDS = fresh SESSION_START
--   5. Different lumber ID mid-session = fresh SESSION_START for the new ID
--   6. Negative delta (consumed) = no dispatch, cache refreshed
--   7. autoShowOnHarvest = false suppresses the WINDOW_TOGGLE

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")
dofile("../UI/HDGR_Selectors.lua")
dofile("../Modules/HDGR_BagObserver.lua")
dofile("../Modules/HDGR_LumberObserver.lua")

local A = HDG.Constants.ACTIONS
local L = HDG.LumberObserver
local B = HDG.BagObserver

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

-- ===== Capture dispatches ==================================================
-- Subscriber records every dispatched action so we can assert on the
-- exact sequence. Order matters (SESSION_START must precede HARVESTED).
local dispatched = {}
HDG.Store:Subscribe(function(actionType, _invalidation)
    dispatched[#dispatched + 1] = actionType
end)

-- ===== Setup ===============================================================
HDG_DB = {}
HDG.Store:LoadFromSavedVariables()
HDG.Store:GetState().session.identity.charKey = "TestChar-Frostmourne"

-- Stub BagObserver bag-count getters to return controlled values. Harvest
-- detection diffs GetBagCount (bags only); stub both so fakeTotals drives either.
local fakeTotals = {}
B.GetBagCount = function(self, itemID) return fakeTotals[itemID] or 0 end
B.GetTotal    = function(self, itemID) return fakeTotals[itemID] or 0 end

-- Stub time + coords. Tests inject the time so we can simulate idle windows
-- without sleeping. Coords are constant; the observer just passes them through.
local mockTime = 1000
local function setTime(t) mockTime = t end
_G.GetTime = function() return mockTime end

local IRONWOOD = 245586
local OLEMBA   = 242691

-- ===== 1. First scan = warm-up (no HARVESTED) =============================
fakeTotals[IRONWOOD] = 10  -- player already has 10 ironwood
fakeTotals[OLEMBA]   = 0
dispatched = {}
L:Scan()
assertEq(#dispatched, 0, "first scan dispatches nothing (warm-up)")
assertEq(L._lastTotals[IRONWOOD], 10, "warm-up records ironwood total")

-- ===== 2. Bag-delta gain = SESSION_START + WINDOW_TOGGLE + HARVESTED ======
fakeTotals[IRONWOOD] = 12  -- +2 ironwood harvested
setTime(1010)
dispatched = {}
L:Scan()
assertTrue(#dispatched >= 3, "scan dispatches SESSION_START + WINDOW_TOGGLE + HARVESTED")
assertEq(dispatched[1], A.LUMBER_SESSION_START, "SESSION_START dispatched first")
assertEq(dispatched[2], A.LUMBER_WINDOW_TOGGLE, "WINDOW_TOGGLE dispatched second")
assertEq(dispatched[3], A.LUMBER_HARVESTED,     "HARVESTED dispatched third")

-- Verify session record + window state
local state = HDG.Store:GetState()
assertEq(state.session.lumber.activeFarmingID, IRONWOOD, "activeFarmingID set")
assertEq(state.account.lumber.config.windowVisible, true, "window auto-shown")
local sess = state.account.lumber.sessions["TestChar-Frostmourne"][IRONWOOD]
assertEq(sess.startCount, 10, "startCount = bag total BEFORE harvest")
assertEq(sess.startedAt,  1010, "startedAt = harvest timestamp")

-- ===== 3. Same-session harvest within IDLE_SECONDS = just HARVESTED ========
fakeTotals[IRONWOOD] = 15  -- +3 more
setTime(1060)  -- 50s later, well within the idle window
dispatched = {}
L:Scan()
-- Should dispatch HARVESTED only (no SESSION_START, no WINDOW_TOGGLE)
local hadStart, hadHarvested = false, false
for _, t in ipairs(dispatched) do
    if t == A.LUMBER_SESSION_START then hadStart = true end
    if t == A.LUMBER_HARVESTED then hadHarvested = true end
end
assertTrue(hadHarvested, "HARVESTED dispatched on same-session harvest")
assertEq(hadStart, false, "no SESSION_START on same-session harvest")

-- ===== 4. Stale harvest (> IDLE_SECONDS) = fresh SESSION_START =============
fakeTotals[IRONWOOD] = 16
setTime(1060 + L.IDLE_SECONDS + 30)  -- 30s past the IDLE_SECONDS threshold (robust to the constant)
dispatched = {}
L:Scan()
hadStart = false
for _, t in ipairs(dispatched) do
    if t == A.LUMBER_SESSION_START then hadStart = true end
end
assertEq(hadStart, true, "stale-harvest dispatches fresh SESSION_START")

-- ===== 5. Switching lumber type mid-session = fresh SESSION_START =========
-- Reset to a clean active-IRONWOOD session
HDG.Store:Dispatch({ type = A.LUMBER_SESSION_END, payload = { timestamp = 2000 } })
fakeTotals[IRONWOOD] = 16   -- unchanged; just clear deltas
L._lastTotals[IRONWOOD] = 16
setTime(2100)

fakeTotals[OLEMBA] = 5      -- harvested 5 olemba
dispatched = {}
L:Scan()
hadStart = false
for _, t in ipairs(dispatched) do
    if t == A.LUMBER_SESSION_START then hadStart = true end
end
assertEq(hadStart, true, "different lumberID dispatches fresh SESSION_START")
assertEq(HDG.Store:GetState().session.lumber.activeFarmingID, OLEMBA,
         "activeFarmingID switched to OLEMBA")

-- ===== 6. Negative delta (consumed) = no dispatch =========================
fakeTotals[OLEMBA] = 2   -- crafted with 3 olemba
dispatched = {}
L:Scan()
for _, t in ipairs(dispatched) do
    assertTrue(t ~= A.LUMBER_HARVESTED, "no HARVESTED on negative delta")
end
assertEq(L._lastTotals[OLEMBA], 2, "cache refreshed even on consumed")

-- ===== 7. autoShowOnHarvest = false suppresses WINDOW_TOGGLE ==============
HDG.Store:GetState().account.lumber.config.autoShowOnHarvest = false
HDG.Store:GetState().account.lumber.config.windowVisible = false  -- start hidden
HDG.Store:Dispatch({ type = A.LUMBER_SESSION_END, payload = { timestamp = 3000 } })
fakeTotals[OLEMBA] = 2
L._lastTotals[OLEMBA] = 2
setTime(3100 + 5 * 60 + 30)  -- past IDLE so a new session fires

fakeTotals[OLEMBA] = 3
dispatched = {}
L:Scan()
local hadWindowToggle = false
for _, t in ipairs(dispatched) do
    if t == A.LUMBER_WINDOW_TOGGLE then hadWindowToggle = true end
end
assertEq(hadWindowToggle, false, "WINDOW_TOGGLE suppressed when autoShowOnHarvest=false")

-- ===== 8. Warband/bank swing must NOT fire a harvest (bag-only detection) ==
-- Regression: lumber syncing into the warband bank on login/reload jumps the
-- all-storage total but NOT the bag count. The observer must diff bags only,
-- or it logs the whole bank/warband stock as phantom 0s harvests.
HDG.Store:Dispatch({ type = A.LUMBER_SESSION_END, payload = { timestamp = 4000 } })
fakeTotals[IRONWOOD]    = 20   -- bags hold 20, unchanged
L._lastTotals[IRONWOOD] = 20   -- baseline already at the bag count
-- Warband bank finishes loading: all-storage total leaps, bags are untouched.
B.GetTotal = function(_self, itemID) return (itemID == IRONWOOD) and 999 or 0 end
dispatched = {}
L:Scan()
for _, t in ipairs(dispatched) do
    assertTrue(t ~= A.LUMBER_HARVESTED,
        "warband/bank swing does not fire HARVESTED when bags are unchanged")
end
assertEq(L._lastTotals[IRONWOOD], 20, "baseline tracks the bag count, not the all-storage total")
B.GetTotal = function(_self, itemID) return fakeTotals[itemID] or 0 end  -- restore

print("test_lumber_observer.lua passed")
