-- HousingDecorGuide/tests/test_style_engine_audit.lua
---@diagnostic disable: undefined-global
-- ^ Standalone Lua test file — `package` / `require` / `dofile` / `loadfile`
-- are valid here even though the project's WoW-context .luarc.json disables them.
package.path = "./?.lua;../modules/?.lua;" .. package.path

local t = require("test_helpers")

-- ============================================================================
-- Mock the HDG environment that StyleEngine touches at load time + runtime.
-- ============================================================================

-- privateDB is passed as the second vararg (...) in real WoW via TOC loader.
-- dofile() passes no extra args, so we use loadfile() and call with explicit args.
local privateDB = {}

local function setupEnv()
    -- Core globals StyleEngine references at module load / runtime
    _G.HDG = _G.HDG or {}
    _G.HousingDecorGuide = _G.HDG   -- alias used internally
    _G.HDG.EventBus = { Trigger = function() end, Register = function() end }
    _G.HDG_Debug = nil

    _G.HDG_AllDecorDB = {
        -- entry[1] is the decorID used as the compatCache key.
        -- We set decorID == itemID here so the test fixture stays simple;
        -- production data has decorID != itemID and BuildCache uses
        -- itemToDecor[itemID] = entry[1] to bridge them.
        [1001] = { [1] = 1001, name = "Item A" },
        [1002] = { [1] = 1002, name = "Item B" },
        [1003] = { [1] = 1003, name = "Item C" },
    }
    _G.HDG_FacetDB  = {}   -- empty: items get empty facets via step 3
    _G.HDG_FacetVocab = {
        category = {}, color = {}, palette = {}, material = {}, culture = {},
        mood = {}, room = {}, condition = {}, formality = {}, motif = {},
        light = {}, seasonal = {}, weight = {}, symmetry = {}, placement = {},
        noise = {}, size = {}, inout = {}, era = {}, subject = {},
        structural_role = {},
    }

    -- Minimal Store stub
    _G.HDG.Store = {
        state = { customStyles = {}, customStyleOrder = {} },
        GetState  = function(self) return self.state end,
        QueueSave = function() end,
    }

    -- WoW globals StyleEngine may call
    _G.wipe    = function(tbl) for k in pairs(tbl) do tbl[k] = nil end end
    _G.C_Timer = { After = function() end }
    -- GetBuildInfo is called at module top-level to set _clientInterfaceVersion
    -- select(4, GetBuildInfo()) must return a number (interface version)
    _G.GetBuildInfo = function() return "12.0.5", "12005", "May 20 2026", 120005 end
    _G.time         = os.time  -- used in IsItemUnreleased heuristics
end

local function defineCuratedStyle(key, displayName, itemIDs)
    local curatedItems = {}
    for _, id in ipairs(itemIDs) do curatedItems[id] = true end
    _G.HDG.Store.state.customStyles[key] = {
        tier        = "curated",
        displayName = displayName,
        curatedItems = curatedItems,
    }
    table.insert(_G.HDG.Store.state.customStyleOrder, key)
end

-- ============================================================================
-- Load StyleEngine
-- StyleEngine body: local addonName, privateDB = ...
-- It then does: privateDB.HDG_StyleEngine = StyleEngine
-- We must pass privateDB as second arg via loadfile()(addonName, privateDB).
-- ============================================================================
setupEnv()
defineCuratedStyle("cottage", "Cottage", { 1001, 1002 })
defineCuratedStyle("spooky",  "Spooky",  { 1002, 1003 })

local chunk, err = loadfile("../modules/HDG_StyleEngine.lua")
assert(chunk, "Failed to load HDG_StyleEngine.lua: " .. tostring(err))
chunk("HousingDecorGuide", privateDB)

-- StyleEngine attaches to privateDB.HDG_StyleEngine (not _G.HDG.StyleEngine).
local SE = privateDB.HDG_StyleEngine
t.assert_truthy(SE, "StyleEngine should attach to privateDB as HDG_StyleEngine")

-- ============================================================================
-- Tests
-- ============================================================================

t.describe("compatCache multi-membership accumulation", function()
    t.it("item in two curated styles has both styleIds as keys", function()
        SE:InvalidateCache()
        SE:BuildCache()

        local membership = SE:GetUserStyleMembership(1002)

        -- Build a set so we can check membership regardless of order
        local set = {}
        for _, sid in ipairs(membership) do set[sid] = true end

        t.assert_truthy(set.cottage, "1002 should be in cottage")
        t.assert_truthy(set.spooky,  "1002 should be in spooky")
        t.assert_eq(#membership, 2, "1002 should be in exactly 2 styles")
    end)
end)

t.describe("IsUnassigned", function()
    t.it("returns true for items in zero user styles", function()
        SE:InvalidateCache()
        SE:BuildCache()
        -- Add a 4th item that is in NO style.
        _G.HDG_AllDecorDB[1004] = { [1] = 1004, name = "Item D" }
        SE:InvalidateCache()
        SE:BuildCache()
        t.assert_truthy(SE:IsUnassigned(1004), "1004 should be unassigned")
    end)
    t.it("returns false for items in one or more styles", function()
        t.assert_falsy(SE:IsUnassigned(1001), "1001 in cottage -> not unassigned")
        t.assert_falsy(SE:IsUnassigned(1002), "1002 in cottage+spooky -> not unassigned")
    end)
    t.it("returns true after the only owning style is deleted", function()
        _G.HDG.Store.state.customStyles.cottage = nil
        -- 1001 was only in cottage; should now be unassigned even though
        -- compatCache still has stale 'cottage' key (Store is the truth).
        t.assert_truthy(SE:IsUnassigned(1001), "1001 should be unassigned after cottage delete")
        -- restore for downstream tests
        defineCuratedStyle("cottage", "Cottage", { 1001, 1002 })
        SE:InvalidateCache(); SE:BuildCache()
    end)
end)

t.describe("IterateUnassigned", function()
    t.it("yields only unassigned decorIDs", function()
        -- Stub HDG.Data.GetAllCatalog for the test
        _G.HDG.Data = {
            GetAllCatalog = function() return { [1001]=true, [1002]=true, [1003]=true, [1004]=true } end,
        }
        SE:InvalidateCache(); SE:BuildCache()
        local seen = {}
        for decorID in SE:IterateUnassigned() do seen[decorID] = true end
        t.assert_falsy(seen[1001], "1001 (in cottage) should not appear")
        t.assert_falsy(seen[1002], "1002 (in 2 styles) should not appear")
        t.assert_falsy(seen[1003], "1003 (in spooky) should not appear")
        t.assert_truthy(seen[1004], "1004 (in zero) should appear")
    end)
    t.it("respects optional predicate", function()
        local seen = {}
        for decorID in SE:IterateUnassigned(function(id) return id > 1004 end) do
            seen[decorID] = true
        end
        t.assert_falsy(seen[1004], "predicate filtered 1004 out")
    end)
end)

t.describe("AddItemToStyle", function()
    t.it("returns true when adding a new membership", function()
        SE:InvalidateCache(); SE:BuildCache()
        t.assert_truthy(SE:AddItemToStyle("spooky", 1001), "first add returns true")
        local m = SE:GetUserStyleMembership(1001)
        local set = {}; for _, s in ipairs(m) do set[s] = true end
        t.assert_truthy(set.cottage, "still in cottage")
        t.assert_truthy(set.spooky,  "now in spooky")
    end)
    t.it("is idempotent: re-adding existing membership returns false", function()
        t.assert_falsy(SE:AddItemToStyle("cottage", 1001), "re-add returns false")
    end)
    t.it("rejects unknown styleId", function()
        t.assert_falsy(SE:AddItemToStyle("nonexistent", 1001))
    end)
    t.it("rejects non-curated styles", function()
        _G.HDG.Store.state.customStyles.smart1 = { tier = "custom", displayName = "Smart One" }
        t.assert_falsy(SE:AddItemToStyle("smart1", 1001))
        _G.HDG.Store.state.customStyles.smart1 = nil
    end)
    t.it("fires HDG_STYLE_MEMBERSHIP_CHANGED with op='add'", function()
        local captured
        _G.HDG.EventBus.Trigger = function(_, ev, payload)
            if ev == "HDG_STYLE_MEMBERSHIP_CHANGED" then captured = payload end
        end
        -- Reset 1001 to be in cottage only, so add to spooky succeeds
        SE:RemoveItemFromStyle("spooky", 1001)
        captured = nil
        SE:AddItemToStyle("spooky", 1001)
        t.assert_truthy(captured, "event fired")
        t.assert_eq(captured.styleId, "spooky")
        t.assert_eq(captured.itemID, 1001)
        t.assert_eq(captured.op, "add")
    end)
end)

t.describe("RemoveItemFromStyle", function()
    t.it("returns true when removing existing membership", function()
        -- Reset to original fixture: 1001 in cottage only (prior AddItemToStyle tests mutated spooky)
        _G.HDG.Store.state.customStyles.spooky.curatedItems[1001] = nil
        SE:InvalidateCache(); SE:BuildCache()
        t.assert_truthy(SE:RemoveItemFromStyle("cottage", 1001))
        t.assert_truthy(SE:IsUnassigned(1001), "1001 should be unassigned after removal from only style")
    end)
    t.it("returns false when removing non-member", function()
        t.assert_falsy(SE:RemoveItemFromStyle("cottage", 1001))
    end)
    t.it("multi-membership: removing one does not affect the other", function()
        SE:AddItemToStyle("cottage", 1001)
        SE:AddItemToStyle("spooky",  1001)
        SE:RemoveItemFromStyle("cottage", 1001)
        local m = SE:GetUserStyleMembership(1001)
        local set = {}; for _, s in ipairs(m) do set[s] = true end
        t.assert_falsy(set.cottage, "removed from cottage")
        t.assert_truthy(set.spooky, "still in spooky")
    end)
    t.it("fires HDG_STYLE_MEMBERSHIP_CHANGED with op='remove'", function()
        local captured
        _G.HDG.EventBus.Trigger = function(_, ev, payload)
            if ev == "HDG_STYLE_MEMBERSHIP_CHANGED" then captured = payload end
        end
        SE:RemoveItemFromStyle("spooky", 1001)
        t.assert_truthy(captured, "event fired")
        t.assert_eq(captured.op, "remove")
    end)
end)

-- Task 7 regression: prove TIER_FIRST sentinel (value 1.0) flows through
-- GetCompatibility's `score >= TIER_FIRST` branch and returns "first-order".
-- If someone later changes TIER_FIRST to `true`, this test breaks immediately.
--
-- State note: after RemoveItemFromStyle tests, 1001 is not in cottage
-- (curatedItems[1001] = nil), so AddItemToStyle("cottage", 1001) will
-- genuinely re-add it (returns true) before BuildCache writes TIER_FIRST.
t.describe("GetCompatibility regression after curated compatCache write", function()
    t.it("returns 'first-order' for curated membership (TIER_FIRST sentinel)", function()
        SE:InvalidateCache(); SE:BuildCache()
        SE:AddItemToStyle("cottage", 1001)
        local result = SE:GetCompatibility("cottage", 1001)
        t.assert_eq(result, "first-order", "curated membership = first-order match")
    end)
end)

t.run_all()
