-- Smoke test: Phase 11.2 PowerCrafter DAG resolver.
-- Verifies CalculateRawMaterials, AggregateByRecipe, GetSubsidiaryRecipes
-- against synthetic ProfessionsDB doubles.

dofile("wow_mock.lua")
dofile("../Core/HDGR_SchemeConstants.lua")
dofile("../Core/HDGR_Constants.lua")
dofile("../Core/HDGR_Format.lua")

-- Stub HDG.Modules so the registration block doesn't fail.
HDG.Modules = HDG.Modules or {}
HDG.Modules._decls = {}
function HDG.Modules:Declare(def) self._decls[def.name] = def end

-- ===== Synthetic ProfessionsDB =============================================
-- Topology:
--   Recipe 1001 (Side Table): 4x wood (200) + 2x cushion (1002)
--   Recipe 1002 (Cushion):    3x cloth (300) + 1x stuffing (400)
--   Recipe 1003 (Coffee Table): 6x wood (200) + 2x cushion (1002) + 1x marble (500)
--
-- Reagents (leaf items, no producing recipe):
--   200 = Wood, 300 = Cloth, 400 = Stuffing, 500 = Marble
_G.HDGR_ProfessionsDB = {
    [1001] = { recipeID = 1001, itemID = 1001, name = "Side Table",
        slots = {
            { type = "basic",     qty = 4, itemID = 200 },  -- Wood
            { type = "basic",     qty = 2, itemID = 1002 }, -- Cushion (craftable!)
            { type = "modifying", qty = 1, itemID = 999 },  -- optional, ignored
        } },
    [1002] = { recipeID = 1002, itemID = 1002, name = "Cushion",
        slots = {
            { type = "basic", qty = 3, itemID = 300 },      -- Cloth
            { type = "basic", qty = 1, itemID = 400 },      -- Stuffing
        } },
    [1003] = { recipeID = 1003, itemID = 1003, name = "Coffee Table",
        slots = {
            { type = "basic", qty = 6, itemID = 200 },      -- Wood
            { type = "basic", qty = 2, itemID = 1002 },     -- Cushion
            { type = "basic", qty = 1, itemID = 500 },      -- Marble
        } },
    -- Rank variant of Cushion (higher recipeID; primary should still be 1002).
    [1004] = { recipeID = 1004, itemID = 1002, name = "Cushion (rank 2)",
        slots = {
            { type = "basic", qty = 2, itemID = 300 },
            { type = "basic", qty = 1, itemID = 400 },
        } },
}

-- Reagent source categories. The raw walker gates expansion on "Crafted"
-- (HDG parity: expand any crafted reagent that HAS a recipe, regardless of
-- whether the player knows it). Leaves carry a gathering/vendor source.
_G.HDGR_ReagentsDB = {
    [200] = { "Gathering", "Wood" },
    [300] = { "Gathering", "Cloth" },
    [400] = { "Vendor",    "Stuffing" },
    [500] = { "Gathering", "Marble" },
    [1002] = { "Crafted",  "Cushion" },
    [2001] = { "Crafted",  "Cycle A" },
    [2002] = { "Crafted",  "Cycle B" },
}
for i = 0, 10 do _G.HDGR_ReagentsDB[3000 + i] = { "Crafted", "Chain " .. i } end

dofile("../Modules/HDGR_StaticData.lua")
dofile("../Modules/HDGR_PowerCrafter.lua")
dofile("../Modules/HDGR_BagObserver.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 PC = HDG.PowerCrafter
assertTrue(PC ~= nil, "PowerCrafter namespace exists")

-- ===== Direct mode (AggregateByRecipe) =====================================
-- Queue: 2x Side Table, 1x Coffee Table. Direct materials = exactly the
-- declared slots * remaining; no DAG expansion.
local queue = {
    { recipeID = 1001, itemID = 1001, remaining = 2 },
    { recipeID = 1003, itemID = 1003, remaining = 1 },
}
local byRecipe = PC:AggregateByRecipe(queue)
assertEq(byRecipe[1].recipeID, 1001, "byRecipe[1] is Side Table")
assertEq(byRecipe[1].materials[200], 8,  "Side Table: 4 wood x 2 = 8")
assertEq(byRecipe[1].materials[1002], 4, "Side Table: 2 cushion x 2 = 4")
-- Modifying slots are excluded from the basic-required materials list.
assertEq(byRecipe[1].materials[999], nil, "modifying slot excluded")
assertEq(byRecipe[2].materials[200], 6,  "Coffee Table: 6 wood x 1 = 6")
assertEq(byRecipe[2].materials[1002], 2, "Coffee Table: 2 cushion x 1 = 2")
assertEq(byRecipe[2].materials[500], 1,  "Coffee Table: 1 marble x 1 = 1")

-- ===== Raw mode: crafted reagents expand regardless of known (HDG parity) ===
-- Cushion (1002) is a "Crafted" reagent WITH a recipe -> it expands to its base
-- mats whether or not the player knows the recipe. The knownRecipeItemIDs arg
-- no longer gates expansion (it only fed the retired known-only mode).
-- Side Table needs 4 cushion -> 12 cloth + 4 stuffing
-- Coffee Table needs 2 cushion -> 6 cloth + 2 stuffing  -> cloth 18, stuffing 6.
local raw = PC:CalculateRawMaterials(queue)
assertEq(raw[200], 14, "raw: wood = 8 + 6 = 14")
assertEq(raw[1002], nil, "raw: cushion expanded (not a leaf) even when unknown")
assertEq(raw[300], 18, "raw: cloth = (4+2)*3 = 18 (cushion expanded)")
assertEq(raw[400], 6,  "raw: stuffing = (4+2)*1 = 6")
assertEq(raw[500], 1,  "raw: marble = 1")

-- ===== Cycle defense =======================================================
-- Inject a synthetic cycle: recipe 2001 produces itemID 2001 and uses
-- itemID 2002 as a reagent; recipe 2002 uses itemID 2001 as a reagent.
-- Without cycle defense, expansion would recurse forever.
_G.HDGR_ProfessionsDB[2001] = { recipeID = 2001, itemID = 2001,
    slots = { { type = "basic", qty = 1, itemID = 2002 } } }
_G.HDGR_ProfessionsDB[2002] = { recipeID = 2002, itemID = 2002,
    slots = { { type = "basic", qty = 1, itemID = 2001 } } }
PC:_ResetIndex()

local cycleQueue = { { recipeID = 2001, itemID = 2001, remaining = 1 } }
local cycleRaw = PC:CalculateRawMaterials(cycleQueue)
-- Recipe 2001 (queued) -> reagent 2002 (crafted, expand into recipe 2002)
-- -> reagent 2001 (crafted, expand into recipe 2001) -> reagent 2002 already
-- visited -> cycle break, treat as leaf. Final: out[2002] = 1 (not infinite).
assertEq(cycleRaw[2002], 1, "cycle break: leaf qty = 1")

-- ===== Depth cap ===========================================================
-- Chain of intermediates: 3000 -> 3001 -> 3002 -> ... -> 3010 (each needs
-- the next). PC.MAX_DEPTH = 6 -> deeper-than-cap items appear as leaves
-- at the cap.
for i = 0, 9 do
    _G.HDGR_ProfessionsDB[3000 + i] = {
        recipeID = 3000 + i, itemID = 3000 + i,
        slots = { { type = "basic", qty = 1, itemID = 3000 + i + 1 } },
    }
end
PC:_ResetIndex()
local deepQueue = { { recipeID = 3000, itemID = 3000, remaining = 1 } }
local deepRaw = PC:CalculateRawMaterials(deepQueue)
-- After MAX_DEPTH=6 expansions, the chain truncates and some node appears
-- as a leaf with qty=1. We assert SOME leaf in 3000..3010 exists with qty>0
-- and that we didn't infinite-recurse.
local foundLeaf = false
for itemID, qty in pairs(deepRaw) do
    if itemID >= 3000 and itemID <= 3010 and qty == 1 then foundLeaf = true end
end
assertTrue(foundLeaf, "depth cap: deep chain produced a leaf without runaway")

-- ===== GetSubsidiaryRecipes ================================================
PC:_ResetIndex()
-- Reset to clean DB (drop cycle + chain pollution) so the assertion is
-- against the original topology.
_G.HDGR_ProfessionsDB = {
    [1001] = { recipeID = 1001, itemID = 1001, slots = {
        { type = "basic", qty = 4, itemID = 200 },
        { type = "basic", qty = 2, itemID = 1002 },
    } },
    [1002] = { recipeID = 1002, itemID = 1002, slots = {
        { type = "basic", qty = 3, itemID = 300 },
        { type = "basic", qty = 1, itemID = 400 },
    } },
}
PC:_ResetIndex()
-- Cushion (1002) is known -> subsidiary for Side Table (1001).
local subs = PC:GetSubsidiaryRecipes(1001, { [1002] = true })
assertEq(subs[1002], 1002, "subsidiary: cushion recipe surfaced")
assertEq(subs[200], nil, "subsidiary: wood is a leaf, not a subsidiary")
-- Cushion unknown -> no subsidiaries (player can't queue it).
local subsNone = PC:GetSubsidiaryRecipes(1001, {})
assertEq(next(subsNone), nil, "subsidiary: cushion unknown -> no subs")

-- ===== Primary recipe = lowest recipeID for an itemID ======================
-- Rank variant 1004 produces same itemID as 1002; primary should be 1002.
_G.HDGR_ProfessionsDB[1004] = { recipeID = 1004, itemID = 1002, slots = {
    { type = "basic", qty = 2, itemID = 300 },   -- different qty
    { type = "basic", qty = 1, itemID = 400 },
} }
PC:_ResetIndex()
local primary = PC:_GetPrimaryRecipeForItem(1002)
assertEq(primary.recipeID, 1002, "primary picks lowest recipeID for stable expansion")

-- ===== Quality-variant material counting (Midnight 2-tier parity) ==========
-- Tiered crafting reagents are separate itemIDs per quality; the recipe slot
-- lists siblings in `variants`. "have" must sum bag stock across the whole
-- group (any quality satisfies the slot). HDG parity: GetItemCountWithVariants.
PC:_ResetIndex()
_G.HDGR_ProfessionsDB = {
    [5001] = { recipeID = 5001, itemID = 5001, slots = {
        { type = "basic", qty = 1, itemID = 600, variants = { 601 } },  -- 2-tier reagent
        { type = "basic", qty = 1, itemID = 999, variants = {} },       -- non-tiered
    } },
}
local Prof = HDG.StaticData.Professions
local qv600 = Prof:GetQualityVariants(600)
assertTrue(qv600 and qv600[1] == 601 and #qv600 == 1, "GetQualityVariants(600) -> {601}")
local qv601 = Prof:GetQualityVariants(601)
assertTrue(qv601 and qv601[1] == 600 and #qv601 == 1, "GetQualityVariants(601) -> {600} (every id maps to siblings)")
assertEq(Prof:GetQualityVariants(999), nil, "non-tiered reagent has no variants")

-- Counts proxy sums across the quality group. Stub GetItemCount: own 0 of
-- the q1 itemID (600) but 3 of the q2 sibling (601).
_G.C_Item = { GetItemCount = function(id) return ({ [600] = 0, [601] = 3, [999] = 2 })[id] or 0 end }
local counts = HDG.BagObserver:GetCounts()
assertEq(counts[600], 3, "counts[600] sums sibling 601 (0 + 3) -- the Radiant Shard bug")
assertEq(counts[601], 3, "counts[601] sums sibling 600 (3 + 0) -- symmetric")
assertEq(counts[999], 2, "counts[non-tiered] = plain count (no variants)")

print("test_powercrafter_smoke.lua passed")
