-- Trainers smoke (Phase 13).
--
-- Verifies the spec-clean composition:
--   trainers.searchQuery / .expandedProfessions / .midnightExpanded /
--   .selectedNpcID -- chrome bindings reading session state
--   trainers.allProfessions -- ordered prof name list from StaticData
--   trainers.profSectionsOrdered -- known-by-current-char-first ordering
--   trainers.charsNeedingTraining -- per-(prof,exp) "below cap" annotation
--   trainers.trainerCountForProfession -- per-prof aggregate
--   trainers.sectionRows -- flat row feed with ed.kind dispatching
--   trainers.midnightRecipeRows -- separate Midnight Recipe Sources section
--   trainers.allRows -- composite feed binding the scrollbox
--
-- Reducers exercised via dispatch: TRAINERS_TOGGLE_PROFESSION,
-- TRAINERS_TOGGLE_MIDNIGHT_SECTION, TRAINERS_SELECT_TRAINER, plus the
-- generic UI_SET_TRANSIENT for searchQuery.

dofile("wow_mock.lua")
dofile("../Core/HDGR_SchemeConstants.lua")
dofile("../Core/HDGR_Constants.lua")
dofile("../Core/HDGR_Format.lua")
dofile("../Core/HDGR_Paths.lua")
dofile("../Core/HDGR_Log.lua")
dofile("../Core/HDGR_Expansion.lua")  -- HDG.Expansion.NormalizeAlias used by trainers/alts selectors
dofile("../Core/HDGR_Modules.lua")    -- HDG.Modules:Declare needed by HDGR_SessionIdentity below
dofile("../Core/HDGR_StoreFurnishings.lua")
dofile("../Core/HDGR_Store.lua")
dofile("../UI/HDGR_Selectors.lua")
dofile("../UI/HDGR_Selectors_Status.lua")
dofile("../UI/HDGR_Selectors_Decor.lua")
dofile("../UI/HDGR_Selectors_Acquisition.lua")
dofile("../UI/HDGR_Selectors_Recipes.lua")
dofile("../UI/HDGR_Selectors_Alts.lua")
dofile("../UI/HDGR_Selectors_Trainers.lua")
dofile("../data/HDGR_DecorDB.lua")
HDGR_AllDecorDB = {}  -- stub: deprecated DB archived; test does not assert on its content
dofile("../data/HDGR_TrainersDB.lua")
dofile("../data/HDGR_VendorAugment.lua")  -- trainers.midnightRecipeRows resolves vendor recipes via it
dofile("../Modules/HDGR_StaticData.lua")
dofile("../Modules/HDGR_SessionIdentity.lua")  -- HDG.SessionIdentity.GetCharKey accessor

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

HDG_DB = {}
HDG.Store:LoadFromSavedVariables()
local state = HDG.Store:GetState()
local A     = HDG.Constants.ACTIONS

-- ============================================================================
-- 1. Data file shape + StaticData accessors
-- ============================================================================

assertTrue(type(_G.HDGR_TrainersDB) == "table", "HDGR_TrainersDB loaded")
assertTrue(type(_G.HDGR_TrainersDB.Alchemy) == "table", "Alchemy profession present")
assertTrue(type(_G.HDGR_TrainersDB.Alchemy.Classic) == "table", "Alchemy Classic expansion present")
assertTrue(type(_G.HDGR_TrainersDB.Alchemy.Classic.Alliance) == "table", "Alchemy Classic Alliance trainer present")
assertEq(_G.HDGR_TrainersDB.Alchemy.Classic.Alliance.npcID, 1215,
    "Alchemy Classic Alliance npcID matches shipped data")

-- StaticData facade
assertTrue(HDG.StaticData.Trainers ~= nil, "StaticData.Trainers domain registered")
assertTrue(HDG.StaticData.Trainers:GetAll() == _G.HDGR_TrainersDB, "GetAll returns DB table")
assertTrue(HDG.StaticData.Trainers:GetByProfession("Alchemy") ~= nil,
    "GetByProfession('Alchemy') returns profession table")
assertTrue(HDG.StaticData.Trainers:GetByProfessionAndExpansion("Alchemy", "Classic") ~= nil,
    "GetByProfessionAndExpansion('Alchemy', 'Classic') returns expansion table")
assertEq(HDG.StaticData.Trainers:GetByProfession("DoesNotExist"), nil,
    "GetByProfession unknown returns nil")

-- ============================================================================
-- 2. Default state shape
-- ============================================================================

assertTrue(type(state.session.ui.trainers) == "table", "session.ui.trainers present")
assertEq(state.session.ui.trainers.searchQuery, "", "default searchQuery empty")
assertEq(next(state.session.ui.trainers.expandedProfessions), nil,
    "default expandedProfessions empty (all collapsed)")
assertEq(state.session.ui.trainers.midnightExpanded, false, "default midnightExpanded false")
assertEq(state.session.ui.trainers.selectedNpcID, nil, "default selectedNpcID nil")

-- ============================================================================
-- 3. Bare-path selectors
-- ============================================================================

assertEq(HDG.Selectors:Call("trainers.searchQuery", state, {}), "",
    "trainers.searchQuery returns empty at default")
assertEq(HDG.Selectors:Call("trainers.midnightExpanded", state, {}), false,
    "trainers.midnightExpanded returns false at default")
assertEq(HDG.Selectors:Call("trainers.selectedNpcID", state, {}), nil,
    "trainers.selectedNpcID returns nil at default")

-- ============================================================================
-- 4. allProfessions ordering (canonical PROFESSION_DATA order)
-- ============================================================================

local allProfs = HDG.Selectors:Call("trainers.allProfessions", state, {})
assertTrue(type(allProfs) == "table", "allProfessions returns table")
assertTrue(#allProfs >= 9, "allProfessions has at least 9 entries (got " .. #allProfs .. ")")
-- Alchemy must appear in the list
local foundAlchemy = false
for _, p in ipairs(allProfs) do if p == "Alchemy" then foundAlchemy = true end end
assertTrue(foundAlchemy, "Alchemy present in allProfessions")

-- ============================================================================
-- 5. trainerCountsByProf (precomputed table; per-prof lookup via index)
-- ============================================================================

local counts = HDG.Selectors:Call("trainers.trainerCountsByProf", state, {})
assertTrue(type(counts) == "table", "trainerCountsByProf returns table")
assertTrue(counts.Alchemy > 5,
    "Alchemy has >5 trainers across all expansions (got " .. tostring(counts.Alchemy) .. ")")
assertEq(counts.DoesNotExist, nil, "Unknown profession not in counts table")

-- ============================================================================
-- 6. Default sectionRows: only profession headers, all collapsed
-- ============================================================================

local rows = HDG.Selectors:Call("trainers.sectionRows", state, {})
assertTrue(type(rows) == "table", "sectionRows returns table")
assertTrue(#rows >= 9, "sectionRows >= 9 (one profHeader per profession) -- got " .. #rows)
for _, row in ipairs(rows) do
    assertEq(row.kind, "profHeader",
        "all default-state rows are profHeaders (collapsed) -- got " .. tostring(row.kind))
end

-- ============================================================================
-- 7. Expand a profession -> expSection + trainerRow children appear
-- ============================================================================

HDG.Store:Dispatch({
    type    = A.TRAINERS_TOGGLE_PROFESSION,
    payload = { profession = "Alchemy" },
})
state = HDG.Store:GetState()
assertEq(state.session.ui.trainers.expandedProfessions.Alchemy, true,
    "Alchemy now in expandedProfessions")

rows = HDG.Selectors:Call("trainers.sectionRows", state, {})
local saw = { profHeader = 0, expSection = 0, trainerRow = 0 }
local alchemyHeaderExpanded = false
for _, row in ipairs(rows) do
    saw[row.kind] = (saw[row.kind] or 0) + 1
    if row.kind == "profHeader" and row.profName == "Alchemy" then
        alchemyHeaderExpanded = row.expanded
    end
end
assertTrue(alchemyHeaderExpanded == true, "Alchemy profHeader.expanded == true")
assertTrue(saw.expSection > 0, "Expanded Alchemy emits expSection rows (got " .. saw.expSection .. ")")
assertTrue(saw.trainerRow > 0, "Expanded Alchemy emits trainerRow rows (got " .. saw.trainerRow .. ")")

-- Collapse again -> back to header-only
HDG.Store:Dispatch({
    type    = A.TRAINERS_TOGGLE_PROFESSION,
    payload = { profession = "Alchemy" },
})
state = HDG.Store:GetState()
assertEq(state.session.ui.trainers.expandedProfessions.Alchemy, nil,
    "Toggle again removes Alchemy from expandedProfessions (collapsed)")

-- ============================================================================
-- 8. Search filter: with Alchemy expanded, search for "Mallory" (Classic A trainer)
-- ============================================================================

HDG.Store:Dispatch({
    type    = A.TRAINERS_TOGGLE_PROFESSION,
    payload = { profession = "Alchemy" },
})
state = HDG.Store:GetState()

-- Apply search via UI_SET_TRANSIENT (mirrors WireSearchBox path)
HDG.Store:Dispatch({
    type    = A.UI_SET_TRANSIENT,
    payload = { view = "trainers", key = "searchQuery", value = "Mallory" },
})
state = HDG.Store:GetState()
assertEq(state.session.ui.trainers.searchQuery, "Mallory",
    "Search query written to state via UI_SET_TRANSIENT")

rows = HDG.Selectors:Call("trainers.sectionRows", state, {})
local malloryFound = false
for _, row in ipairs(rows) do
    if row.kind == "trainerRow" and row.dbName == "Alchemist Mallory" then
        malloryFound = true
    end
end
assertTrue(malloryFound, "Mallory trainer row appears in filtered output")

-- Reset search
HDG.Store:Dispatch({
    type    = A.UI_SET_TRANSIENT,
    payload = { view = "trainers", key = "searchQuery", value = "" },
})

-- ============================================================================
-- 9. Midnight Recipe Sources section toggle
-- ============================================================================

local mnRows = HDG.Selectors:Call("trainers.midnightRecipeRows", state, {})
assertTrue(type(mnRows) == "table", "midnightRecipeRows returns table")
assertTrue(#mnRows >= 1, "midnightRecipeRows always emits at least the midnightHeader")
assertEq(mnRows[1].kind, "midnightHeader", "first row is midnightHeader")
assertEq(mnRows[1].expanded, false, "midnightHeader default expanded=false")
assertEq(#mnRows, 1, "collapsed midnight section emits only the header (got " .. #mnRows .. ")")

HDG.Store:Dispatch({
    type    = A.TRAINERS_TOGGLE_MIDNIGHT_SECTION,
    payload = {},
})
state = HDG.Store:GetState()
assertEq(state.session.ui.trainers.midnightExpanded, true,
    "TRAINERS_TOGGLE_MIDNIGHT_SECTION flips midnightExpanded to true")
mnRows = HDG.Selectors:Call("trainers.midnightRecipeRows", state, {})
assertEq(mnRows[1].expanded, true, "midnightHeader now expanded=true")
-- Expanded structure: header, then per profession a profHeader + columnHeader
-- followed by its recipe rows. Verify the kinds + the resolved row fields.
local kinds = {}
for i = 2, #mnRows do kinds[mnRows[i].kind] = (kinds[mnRows[i].kind] or 0) + 1 end
assertTrue((kinds.midnightProfHeader   or 0) >= 1, "at least one profession sub-header")
assertTrue((kinds.midnightColumnHeader or 0) >= 1, "at least one column header")
assertTrue((kinds.midnightRow          or 0) >= 1, "at least one recipe row")
assertEq(kinds.midnightProfHeader, kinds.midnightColumnHeader,
    "exactly one column header per profession sub-header")
for i = 2, #mnRows do
    local r = mnRows[i]
    if r.kind == "midnightProfHeader" then
        assertTrue(type(r.profName) == "string" and r.profName ~= "",
            "profession sub-header carries profName")
        assertEq(mnRows[i + 1] and mnRows[i + 1].kind, "midnightColumnHeader",
            "profession sub-header is immediately followed by its column header")
    elseif r.kind == "midnightRow" then
        assertTrue(type(r.itemID) == "number", "midnightRow has numeric itemID")
        assertTrue(type(r.recipeName) == "string", "midnightRow has recipeName")
        assertTrue(type(r.sourceType) == "string", "midnightRow has sourceType")
        assertTrue(r.npcOrigin ~= nil and r.zone ~= nil and r.costLine ~= nil,
            "midnightRow carries resolved npcOrigin / zone / costLine columns")
    end
end

-- ============================================================================
-- 10. allRows composes sectionRows + midnightRecipeRows
-- ============================================================================

local allRows = HDG.Selectors:Call("trainers.allRows", state, {})
local sectionLen   = #HDG.Selectors:Call("trainers.sectionRows", state, {})
local midnightLen  = #HDG.Selectors:Call("trainers.midnightRecipeRows", state, {})
assertEq(#allRows, sectionLen + midnightLen,
    "allRows = sectionRows + midnightRecipeRows length")

-- ============================================================================
-- 11. SELECT_TRAINER reducer
-- ============================================================================

HDG.Store:Dispatch({
    type    = A.TRAINERS_SELECT_TRAINER,
    payload = { npcID = 1215 },
})
state = HDG.Store:GetState()
assertEq(state.session.ui.trainers.selectedNpcID, 1215,
    "selectedNpcID written by TRAINERS_SELECT_TRAINER")

print("test_trainers_smoke.lua: ALL PASS")
