--[[
============================================================================
ETB AutoTest v1.0 — Automated In-Game Test Suite
============================================================================
Runs 70+ automated checks across all ETB systems and generates a report
you can copy/paste to share results.

Usage:
  /etbtest           — Run full test suite (sections 1-13)
  /etbtest quick     — Run P0 blocker tests only (sections 1-5)
  /etbtest section N — Run specific section (1-13)
  /etbtest report    — Show last test report
  /etbtest export    — Copy report to clipboard (editbox)

Sections:
  1-10: Functional tests (startup, browser, grouping, sets, etc.)
  11:   STRESS — Nil safety, malformed data, memory pressure,
        rapid UI cycling, event storms, boundary values,
        filter combinatorial, pcall audit
  12:   COLOR SEARCH — ColorData loading, RGB parsing, distance math,
        category matching, filter integration, cache eviction, performance
  13:   CLASS RESTRICTIONS — Class restriction lookup, usability checks,
        alternative selection, myClassOnly filter integration

Results are saved to ETBTestResults SavedVariable so they persist across sessions.
============================================================================
]]--

local ADDON_NAME, NS = ...
local ETB = NS.ETB
if not ETB then return end

ETBTestResults = ETBTestResults or {}

local AutoTest = {}
ETB.AutoTest = AutoTest

-- Test state
AutoTest.results = {}
AutoTest.currentSection = nil
AutoTest.totalPass = 0
AutoTest.totalFail = 0
AutoTest.totalWarn = 0
AutoTest.totalSkip = 0
AutoTest.startTime = 0
AutoTest.isRunning = false
AutoTest.onlyP0 = false

-- ============================================================================
-- RESULT TRACKING
-- ============================================================================

local PASS = "|cff00ff00PASS|r"
local FAIL = "|cffff4444FAIL|r"
local WARN = "|cffffff00WARN|r"
local SKIP = "|cff888888SKIP|r"

local function AddResult(id, name, status, detail, priority)
    priority = priority or "P1"
    local entry = {
        id = id,
        name = name,
        status = status,
        detail = detail or "",
        priority = priority,
        timestamp = time(),
    }
    table.insert(AutoTest.results, entry)

    if status == "PASS" then
        AutoTest.totalPass = AutoTest.totalPass + 1
    elseif status == "FAIL" then
        AutoTest.totalFail = AutoTest.totalFail + 1
    elseif status == "WARN" then
        AutoTest.totalWarn = AutoTest.totalWarn + 1
    else
        AutoTest.totalSkip = AutoTest.totalSkip + 1
    end

    -- Print live result
    local statusColor = status == "PASS" and PASS or status == "FAIL" and FAIL or status == "WARN" and WARN or SKIP
    local priColor = priority == "P0" and "|cffff8888" or priority == "P1" and "|cffffff88" or "|cff88ff88"
    print(string.format("  %s [%s%s|r] %s %s",
        statusColor, priColor, priority, id, name))
    if detail and detail ~= "" and status ~= "PASS" then
        print(string.format("    |cff888888→ %s|r", detail))
    end
end

local function Assert(id, name, condition, detail, priority)
    if condition then
        AddResult(id, name, "PASS", nil, priority)
    else
        AddResult(id, name, "FAIL", detail or "Condition not met", priority)
    end
    return condition
end

local function AssertWarn(id, name, condition, detail, priority)
    if condition then
        AddResult(id, name, "PASS", nil, priority)
    else
        AddResult(id, name, "WARN", detail or "Non-critical issue", priority)
    end
    return condition
end

local function SectionHeader(num, title)
    AutoTest.currentSection = num
    print(string.format("\n|cffc8b88a══════ Section %d: %s ══════|r", num, title))
end

-- ============================================================================
-- SECTION 1: STARTUP & LOADING
-- ============================================================================

local function TestStartup()
    SectionHeader(1, "Startup & Loading")

    -- 1.01: ETB namespace exists
    Assert("1.01", "ETB namespace loaded", ETB ~= nil, "ETB global is nil", "P0")

    -- 1.02: Memory check (force GC first for accurate reading)
    collectgarbage("collect")
    collectgarbage("collect")
    local mem = collectgarbage("count") / 1024 -- MB
    local etbMem = 0
    if GetAddOnMemoryUsage then
        UpdateAddOnMemoryUsage()
        etbMem = GetAddOnMemoryUsage("EnhancedTransmogBrowser") or 0
        etbMem = etbMem / 1024 -- MB
    end
    Assert("1.02", "Memory under 200MB", etbMem < 200 or etbMem == 0,
        string.format("ETB using %.1fMB (total Lua: %.1fMB)", etbMem, mem), "P0")

    -- 1.03: Core modules loaded
    Assert("1.03a", "EventBus loaded", ETB.EventBus ~= nil, "ETB.EventBus is nil", "P0")
    Assert("1.03b", "Filtering loaded", ETB.Filtering ~= nil, "ETB.Filtering is nil", "P0")
    Assert("1.03c", "ArmorData loaded", ETB.ArmorData ~= nil, "ETB.ArmorData is nil", "P0")
    Assert("1.03d", "BlizzardData loaded", ETB.BlizzardData ~= nil, "ETB.BlizzardData is nil", "P0")
    Assert("1.03e", "SourceMapper loaded", ETB.SourceMapper ~= nil, "ETB.SourceMapper is nil", "P1")
    Assert("1.03f", "AppearanceDetail loaded", ETB.AppearanceDetail ~= nil, "ETB.AppearanceDetail is nil", "P1")

    -- 1.04: TransmogAPI loaded
    Assert("1.04", "TransmogAPI loaded", ETB.TransmogAPI ~= nil, "ETB.TransmogAPI is nil", "P1")

    -- 1.05: Slash commands registered
    Assert("1.05a", "/etb command registered", SlashCmdList["ETB"] ~= nil or SlashCmdList["ENHANCEDTRANSMOGBROWSER"] ~= nil or SLASH_ETB1 ~= nil, "Slash command not registered", "P1")
    Assert("1.05b", "/etbtui command exists", SlashCmdList["ETBTUI"] ~= nil or SLASH_ETBTUI1 ~= nil, "/etbtui not registered", "P1")

    -- 1.06: CATEGORY_INFO defined
    local catInfo = ETB.CATEGORY_INFO
    Assert("1.06", "CATEGORY_INFO has 27 categories", catInfo and #catInfo >= 27 or (catInfo and catInfo[27] ~= nil),
        catInfo and string.format("Found categories up to index %d", #catInfo or 0) or "CATEGORY_INFO is nil", "P0")

    -- 1.07: Settings system
    Assert("1.07", "Settings loaded", ETB.Settings ~= nil, "ETB.Settings is nil", "P2")

    -- 1.08: Blizzard APIs available
    Assert("1.08a", "C_TransmogCollection exists", C_TransmogCollection ~= nil, "C_TransmogCollection API missing", "P0")
    Assert("1.08b", "C_TransmogSets exists", C_TransmogSets ~= nil, "C_TransmogSets API missing", "P0")
    Assert("1.08c", "GetCategoryAppearances exists",
        C_TransmogCollection and C_TransmogCollection.GetCategoryAppearances ~= nil,
        "GetCategoryAppearances missing", "P0")
    Assert("1.08d", "GetAppearanceSources exists",
        C_TransmogCollection and C_TransmogCollection.GetAppearanceSources ~= nil,
        "GetAppearanceSources missing", "P0")
end

-- ============================================================================
-- SECTION 2: MAIN BROWSER & CATEGORIES
-- ============================================================================

local function TestMainBrowser()
    SectionHeader(2, "Main Browser & Category Loading")

    -- 2.01: MainFrame module exists
    Assert("2.01", "MainFrame module loaded", ETB.MainFrame ~= nil, "ETB.MainFrame is nil", "P0")

    -- 2.02-2.03: Test all 27 categories load items
    local categoryTests = {
        {1,"Head"},{2,"Shoulder"},{3,"Back"},{4,"Chest"},{5,"Shirt"},{6,"Tabard"},
        {7,"Wrist"},{8,"Hands"},{9,"Waist"},{10,"Legs"},{11,"Feet"},
        {12,"Wands"},{13,"1H Axes"},{14,"1H Swords"},{15,"1H Maces"},{16,"Daggers"},
        {17,"Fist Weapons"},{18,"Shields"},{19,"Off-Hands"},
        {20,"2H Axes"},{21,"2H Swords"},{22,"2H Maces"},{23,"Staves"},{24,"Polearms"},
        {25,"Bows"},{26,"Guns"},{27,"Crossbows"},
    }

    local totalAppearances = 0
    local failedCats = {}
    local catCounts = {}

    for _, catTest in ipairs(categoryTests) do
        local catID, catName = catTest[1], catTest[2]
        local ok, appearances = pcall(function()
            if C_TransmogCollection and C_TransmogCollection.GetCategoryAppearances then
                return C_TransmogCollection.GetCategoryAppearances(catID)
            end
            return nil
        end)

        local count = 0
        if ok and appearances then
            count = #appearances
            totalAppearances = totalAppearances + count
        end

        catCounts[catName] = count
        if count == 0 then
            table.insert(failedCats, catName)
        end
    end

    Assert("2.02", "All 27 categories return appearances",
        #failedCats == 0,
        #failedCats > 0 and ("Empty categories: " .. table.concat(failedCats, ", ")) or nil, "P0")

    Assert("2.03", "Total appearances > 5000",
        totalAppearances > 5000,
        string.format("Total: %d appearances across 27 categories", totalAppearances), "P0")

    -- Print category breakdown
    print("  |cff888888Category counts:|r")
    for _, catTest in ipairs(categoryTests) do
        local name = catTest[2]
        local count = catCounts[name] or 0
        local color = count > 0 and "|cff00ff00" or "|cffff4444"
        print(string.format("    %s%s: %d|r", color, name, count))
    end

    -- 2.04: ArmorData GetItems works
    if ETB.ArmorData and ETB.ArmorData.GetItems then
        local ok, items = pcall(ETB.ArmorData.GetItems, ETB.ArmorData, 1, { groupByAppearance = true })
        Assert("2.04", "ArmorData:GetItems(1) returns items",
            ok and items and #items > 0,
            ok and items and string.format("Got %d items for Head slot", #items) or tostring(items), "P0")
    else
        AddResult("2.04", "ArmorData:GetItems exists", "SKIP", "ArmorData or GetItems not available", "P0")
    end

    -- 2.05: FilterBar module
    AssertWarn("2.05", "FilterBar module loaded",
        ETB.FilterBar ~= nil or (ETB.MainFrame and ETB.MainFrame.frame),
        "FilterBar not separately tracked — may be inline in MainFrame", "P2")
end

-- ============================================================================
-- SECTION 3: APPEARANCE GROUPING & DUPLICATES
-- ============================================================================

local function TestGrouping()
    SectionHeader(3, "Appearance Grouping & Deduplication")

    -- 3.01: groupByAppearance is permanently true
    if ETB.Filtering and ETB.Filtering.filters then
        Assert("3.01", "groupByAppearance is true",
            ETB.Filtering.filters.groupByAppearance == true,
            "groupByAppearance = " .. tostring(ETB.Filtering.filters.groupByAppearance), "P0")

        -- Try to set it false — it should stay true
        if ETB.Filtering.SetGroupByAppearance then
            ETB.Filtering:SetGroupByAppearance(false)
            Assert("3.02", "groupByAppearance resists being set to false",
                ETB.Filtering.filters.groupByAppearance == true,
                "SetGroupByAppearance(false) changed it to " .. tostring(ETB.Filtering.filters.groupByAppearance), "P0")
            -- Reset just in case
            ETB.Filtering.filters.groupByAppearance = true
        end
    else
        AddResult("3.01", "Filtering.filters accessible", "SKIP", "Cannot access filter state", "P0")
    end

    -- 3.03: Test for visual ID deduplication
    -- Pick a few categories and check that no visualID appears twice
    local testCats = {1, 4, 14, 23} -- Head, Chest, 1H Swords, Staves
    for _, catID in ipairs(testCats) do
        local ok, appearances = pcall(function()
            return C_TransmogCollection.GetCategoryAppearances(catID)
        end)

        if ok and appearances then
            local seenVisualIDs = {}
            local duplicates = 0
            for _, app in ipairs(appearances) do
                if app.visualID then
                    if seenVisualIDs[app.visualID] then
                        duplicates = duplicates + 1
                    end
                    seenVisualIDs[app.visualID] = true
                end
            end
            -- Note: Blizzard API itself shouldn't return dupes, but verify
            Assert("3.03_" .. catID, string.format("Cat %d: No duplicate visualIDs", catID),
                duplicates == 0,
                string.format("%d duplicates found in %d appearances", duplicates, #appearances), "P1")
        end
    end

    -- 3.04: Test shared appearances exist (items with same visualID)
    local sharedFound = false
    local ok, appearances = pcall(function()
        return C_TransmogCollection.GetCategoryAppearances(1) -- Head
    end)
    if ok and appearances and #appearances > 0 then
        -- Check first few for shared sources
        local checked = 0
        for _, app in ipairs(appearances) do
            if checked >= 10 then break end
            if app.visualID then
                local sources = C_TransmogCollection.GetAppearanceSources(app.visualID)
                if sources and #sources > 1 then
                    sharedFound = true
                    break
                end
            end
            checked = checked + 1
        end
    end
    Assert("3.04", "Shared appearances exist (multiple sources per visual)",
        sharedFound, "No multi-source appearances found in first 10 items", "P1")

    -- 3.05: MainFrame has NO groupByAppearance checkbox
    if ETB.MainFrame and ETB.MainFrame.frame then
        local hasCheckbox = ETB.MainFrame.frame.groupByAppearanceCheck ~= nil
        Assert("3.05", "groupByAppearance checkbox REMOVED from UI",
            not hasCheckbox,
            "Checkbox still exists in MainFrame", "P1")
    else
        AddResult("3.05", "MainFrame checkbox check", "SKIP", "MainFrame not open", "P1")
    end
end

-- ============================================================================
-- SECTION 4: COLLECTION TRACKING
-- ============================================================================

local function TestCollectionTracking()
    SectionHeader(4, "Collection Tracking & Shared Appearances")

    -- 4.01: TransmogAPI has collection check methods
    if ETB.TransmogAPI then
        Assert("4.01a", "TransmogAPI has IsCollected method",
            ETB.TransmogAPI.IsCollected ~= nil or ETB.TransmogAPI.IsAppearanceCollected ~= nil,
            "No collection check method found", "P0")
    else
        AddResult("4.01a", "TransmogAPI exists", "SKIP", "TransmogAPI not loaded", "P0")
    end

    -- 4.02: Test GetAppearanceSources works for collection
    local collectedCount = 0
    local uncollectedCount = 0
    local ok, appearances = pcall(function()
        return C_TransmogCollection.GetCategoryAppearances(4) -- Chest
    end)
    if ok and appearances then
        for _, app in ipairs(appearances) do
            if app.isCollected then
                collectedCount = collectedCount + 1
            else
                uncollectedCount = uncollectedCount + 1
            end
        end
    end
    Assert("4.02", "Collection data returned for Chest",
        collectedCount > 0 or uncollectedCount > 0,
        string.format("Collected: %d, Uncollected: %d", collectedCount, uncollectedCount), "P0")

    -- 4.03: Shared appearance collection — if ANY source collected, appearance is collected
    local sharedCollectionWorks = false
    if ok and appearances then
        for _, app in ipairs(appearances) do
            if app.isCollected and app.visualID then
                local sources = C_TransmogCollection.GetAppearanceSources(app.visualID)
                if sources and #sources > 1 then
                    -- Check that at least one source shows collected
                    local anyCollected = false
                    for _, src in ipairs(sources) do
                        if src.isCollected then
                            anyCollected = true
                            break
                        end
                    end
                    if anyCollected then
                        sharedCollectionWorks = true
                        break
                    end
                end
            end
        end
    end
    AssertWarn("4.03", "Shared appearance collection tracking works",
        sharedCollectionWorks,
        "Could not find a collected item with multiple sources to verify", "P0")

    -- 4.04: PlayerHasTransmog fallback exists
    Assert("4.04", "PlayerHasTransmog API exists",
        C_TransmogCollection.PlayerHasTransmog ~= nil,
        "PlayerHasTransmog not available", "P1")

    -- 4.05: PlayerHasTransmogItemModifiedAppearance exists
    AssertWarn("4.05", "PlayerHasTransmogItemModifiedAppearance exists",
        C_TransmogCollection.PlayerHasTransmogItemModifiedAppearance ~= nil,
        "May not exist in MoP Classic — fallback should handle gracefully", "P2")

    -- 4.06: Filtering by collected/uncollected
    if ETB.Filtering and ETB.Filtering.SetCollectedFilter then
        ETB.Filtering:SetCollectedFilter("collected")
        Assert("4.06a", "Set collected filter to 'collected'",
            ETB.Filtering.filters.collected == "collected", nil, "P1")
        ETB.Filtering:SetCollectedFilter("uncollected")
        Assert("4.06b", "Set collected filter to 'uncollected'",
            ETB.Filtering.filters.collected == "uncollected", nil, "P1")
        ETB.Filtering:SetCollectedFilter("all")
        Assert("4.06c", "Reset collected filter to 'all'",
            ETB.Filtering.filters.collected == "all", nil, "P1")
    end
end

-- ============================================================================
-- SECTION 5: COMMUNITY SETS
-- ============================================================================

local function TestCommunitySets()
    SectionHeader(5, "Community Sets (780 validated)")

    -- 5.01: CommunitySetData exists
    local setData = ETB.CommunitySetData
    Assert("5.01", "CommunitySetData loaded",
        setData ~= nil and type(setData) == "table",
        "ETB.CommunitySetData is " .. type(setData or nil), "P0")

    if not setData then return end

    -- 5.02: Set count
    local setCount = #setData
    Assert("5.02", "780 community sets loaded",
        setCount == 780,
        string.format("Found %d sets (expected 780)", setCount), "P0")

    -- 5.03: No empty sets
    local emptySets = {}
    local singleItemSets = {}
    local totalItems = 0
    local armorCounts = { CLOTH = 0, LEATHER = 0, MAIL = 0, PLATE = 0, COSMETIC = 0 }
    local expCounts = {}

    for i, set in ipairs(setData) do
        local name = set[1] or ("Set " .. i)
        local armorType = set[2] -- 1=Cloth, 2=Leather, 3=Mail, 4=Plate
        local expansion = set[3]
        local items = set[5]

        -- Count items in this set
        local itemCount = 0
        if type(items) == "table" then
            for slot, slotData in pairs(items) do
                if type(slotData) == "table" and slotData[1] then
                    itemCount = itemCount + 1
                end
            end
        end

        totalItems = totalItems + itemCount

        if itemCount == 0 then
            table.insert(emptySets, name)
        elseif itemCount == 1 then
            table.insert(singleItemSets, name)
        end

        -- Armor type counting
        local typeNames = { [1] = "CLOTH", [2] = "LEATHER", [3] = "MAIL", [4] = "PLATE" }
        local typeName = typeNames[armorType] or "COSMETIC"
        armorCounts[typeName] = (armorCounts[typeName] or 0) + 1

        -- Expansion counting
        local expNames = { [1] = "Classic", [2] = "TBC", [3] = "WotLK", [4] = "Cata", [5] = "MoP" }
        local expName = expNames[expansion] or ("Exp" .. (expansion or "?"))
        expCounts[expName] = (expCounts[expName] or 0) + 1
    end

    Assert("5.03", "No empty sets (0 items)",
        #emptySets == 0,
        #emptySets > 0 and string.format("%d empty sets: %s", #emptySets, table.concat(emptySets, ", "):sub(1, 100)) or nil, "P0")

    Assert("5.04", "No single-item sets",
        #singleItemSets == 0,
        #singleItemSets > 0 and string.format("%d single-item sets found", #singleItemSets) or nil, "P0")

    Assert("5.05", "Total items > 3000",
        totalItems > 3000,
        string.format("Total: %d items across %d sets (avg %.1f/set)", totalItems, setCount, totalItems / math.max(setCount, 1)), "P1")

    -- 5.06: Armor type distribution
    print("  |cff888888Armor distribution:|r")
    for aType, count in pairs(armorCounts) do
        if count > 0 then
            print(string.format("    %s: %d sets", aType, count))
        end
    end

    -- 5.07: Expansion distribution
    print("  |cff888888Expansion distribution:|r")
    for exp, count in pairs(expCounts) do
        print(string.format("    %s: %d sets", exp, count))
    end

    -- 5.08: No duplicate sets (exact same item list)
    local setHashes = {}
    local exactDupes = 0
    for i, set in ipairs(setData) do
        local items = set[5]
        if type(items) == "table" then
            local ids = {}
            for slot, slotData in pairs(items) do
                if type(slotData) == "table" and slotData[1] then
                    table.insert(ids, tostring(slotData[1]))
                end
            end
            table.sort(ids)
            local hash = table.concat(ids, "-")
            if setHashes[hash] then
                exactDupes = exactDupes + 1
            end
            setHashes[hash] = true
        end
    end
    Assert("5.08", "No exact duplicate sets",
        exactDupes == 0,
        string.format("%d exact duplicates found", exactDupes), "P1")

    -- 5.09: CommunitySets module exists (registered by CommunitySetLoader)
    Assert("5.09", "CommunitySets module loaded",
        ETB.CommunitySets ~= nil,
        "ETB.CommunitySets is nil", "P1")

    -- 5.10: Validate sample items exist in game (MoP-era check)
    local sampleChecked = 0
    local sampleFailed = 0
    local failedItems = {}
    for i = 1, math.min(20, #setData) do
        local set = setData[i]
        local items = set[5]
        if type(items) == "table" then
            for slot, slotData in pairs(items) do
                if type(slotData) == "table" and slotData[1] then
                    local itemID = slotData[1]
                    if itemID and itemID > 0 then
                        sampleChecked = sampleChecked + 1
                        -- GetItemInfoInstant is synchronous — if item doesn't exist, returns nil
                        local name = GetItemInfoInstant(itemID)
                        if not name then
                            sampleFailed = sampleFailed + 1
                            table.insert(failedItems, itemID)
                        end
                        if sampleChecked >= 50 then break end
                    end
                end
                if sampleChecked >= 50 then break end
            end
        end
        if sampleChecked >= 50 then break end
    end
    Assert("5.10", string.format("Sample items valid (%d/%d)", sampleChecked - sampleFailed, sampleChecked),
        sampleFailed == 0,
        sampleFailed > 0 and string.format("%d items failed: %s", sampleFailed, table.concat(failedItems, ", "):sub(1, 80)) or nil, "P1")
end

-- ============================================================================
-- SECTION 6: BLIZZARD SETS & SOURCE MAPPING
-- ============================================================================

local function TestBlizzardSetsAndSources()
    SectionHeader(6, "Blizzard Sets & Source Mapping")

    -- 6.01: C_TransmogSets API
    local blizzSets = nil
    if C_TransmogSets and C_TransmogSets.GetAllSets then
        local ok, sets = pcall(C_TransmogSets.GetAllSets)
        if ok and sets then
            blizzSets = sets
        end
    end
    Assert("6.01", "C_TransmogSets.GetAllSets returns data",
        blizzSets and #blizzSets > 0,
        blizzSets and string.format("Got %d Blizzard sets", #blizzSets) or "API returned nil/empty", "P0")

    -- 6.02: Set count in expected range
    if blizzSets then
        Assert("6.02", "Blizzard sets count ~696 (680-900 range)",
            #blizzSets >= 680 and #blizzSets <= 900,
            string.format("Found %d sets", #blizzSets), "P1")

        -- 6.03: Sets have required fields
        local validSets = 0
        for _, set in ipairs(blizzSets) do
            if set.setID and set.name and set.name ~= "" then
                validSets = validSets + 1
            end
        end
        Assert("6.03", "All Blizzard sets have setID and name",
            validSets == #blizzSets,
            string.format("%d/%d valid", validSets, #blizzSets), "P1")
    end

    -- 6.04: SourceMapper loaded and initialized
    Assert("6.04", "SourceMapper loaded",
        ETB.SourceMapper ~= nil, "ETB.SourceMapper is nil", "P1")

    if ETB.SourceMapper then
        Assert("6.05", "SourceMapper initialized",
            ETB.SourceMapper.initialized == true,
            "SourceMapper.initialized = " .. tostring(ETB.SourceMapper.initialized), "P1")

        -- 6.06: MapperBridge loaded (registers as SourceData_ATT)
        Assert("6.06", "MapperBridge loaded (SourceData_ATT)",
            ETB.SourceData_ATT ~= nil, "ETB.SourceData_ATT is nil", "P1")

        -- 6.07: Test source lookup on known items
        if ETB.SourceMapper.GetPrimarySource then
            -- Try a few well-known Classic items
            local testItems = {
                {16963, "Helm of Wrath"},           -- BWL
                {16915, "Netherwind Crown"},         -- BWL
                {18803, "Finkle's Lava Dredger"},    -- MC
            }
            local sourcesFound = 0
            for _, testItem in ipairs(testItems) do
                local ok, src = pcall(ETB.SourceMapper.GetPrimarySource, ETB.SourceMapper, testItem[1])
                if ok and src and src.sourceName and src.sourceName ~= "Unknown" then
                    sourcesFound = sourcesFound + 1
                    print(string.format("    |cff888888Item %d (%s): %s — %s|r",
                        testItem[1], testItem[2], src.sourceName or "?", src.sourceLocation or "?"))
                end
            end
            AssertWarn("6.07", "Source data found for known items",
                sourcesFound > 0,
                string.format("%d/%d known items had source data", sourcesFound, #testItems), "P1")
        end
    end

    -- 6.08: Source data files loaded (check static data exists)
    local sourceFiles = { "ETB_SourceData_Classic", "ETB_SourceData_TBC", "ETB_SourceData_Wrath", "ETB_SourceData_Cata", "ETB_SourceData_MoP" }
    local loadedFiles = 0
    for _, name in ipairs(sourceFiles) do
        -- Check if the global data tables exist
        local globalName = name:gsub("ETB_SourceData_", "ETBSource_")
        if _G[globalName] or (ETB.SourceData and ETB.SourceData[name]) then
            loadedFiles = loadedFiles + 1
        end
    end
    AssertWarn("6.08", "Source data files present",
        loadedFiles > 0 or (ETB.SourceData_ATT and ETB.SourceData_ATT._itemCount and ETB.SourceData_ATT._itemCount > 0),
        string.format("%d/5 source files detected (data may be in SourceData_ATT)", loadedFiles), "P1")

    -- 6.09: GetSourceInfo API works
    Assert("6.09", "C_TransmogCollection.GetSourceInfo exists",
        C_TransmogCollection and C_TransmogCollection.GetSourceInfo ~= nil,
        "GetSourceInfo API not available", "P1")
end

-- ============================================================================
-- SECTION 7: RANDOM OUTFIT ARMOR TYPE
-- ============================================================================

local function TestRandomOutfit()
    SectionHeader(7, "Random Outfit Armor Type Fix (v12.26)")

    -- 7.01: Get player class and expected armor type
    local _, playerClass = UnitClass("player")
    local armorTypes = {
        WARRIOR = "PLATE", PALADIN = "PLATE", DEATHKNIGHT = "PLATE",
        HUNTER = "MAIL", SHAMAN = "MAIL",
        ROGUE = "LEATHER", DRUID = "LEATHER", MONK = "LEATHER",
        MAGE = "CLOTH", WARLOCK = "CLOTH", PRIEST = "CLOTH",
    }
    local expectedArmor = armorTypes[playerClass] or "UNKNOWN"
    local ARMOR_SUBCLASS = { CLOTH = 1, LEATHER = 2, MAIL = 3, PLATE = 4 }
    local expectedSubclass = ARMOR_SUBCLASS[expectedArmor]

    AddResult("7.00", string.format("Testing on %s (expected: %s)", playerClass, expectedArmor), "PASS", nil, "P0")

    -- 7.02: Test armor type filtering on collected appearances
    -- Simulate what GenerateRandomOutfit does: iterate slot, check subclassID
    local armorSlots = {1, 2, 4, 8, 10, 11} -- Head, Shoulder, Chest, Hands, Legs, Feet (skip cosmetic)
    local totalChecked = 0
    local wrongType = 0
    local wrongItems = {}

    for _, catID in ipairs(armorSlots) do
        local ok, catAppearances = pcall(function()
            return C_TransmogCollection.GetCategoryAppearances(catID)
        end)
        if ok and catAppearances then
            local checked = 0
            for _, app in ipairs(catAppearances) do
                if checked >= 5 then break end
                if app.isCollected and app.visualID then
                    local sources = C_TransmogCollection.GetAppearanceSources(app.visualID)
                    if sources and sources[1] then
                        local itemID = sources[1].itemID or sources[1].itemId
                        if itemID then
                            local _, _, _, equipLoc, _, classID, subclassID = GetItemInfoInstant(itemID)
                            if classID == 4 and subclassID and subclassID > 0 then
                                totalChecked = totalChecked + 1
                                if subclassID ~= expectedSubclass then
                                    wrongType = wrongType + 1
                                    table.insert(wrongItems, string.format("%d(cat%d,sub%d)", itemID, catID, subclassID))
                                end
                            end
                            checked = checked + 1
                        end
                    end
                end
            end
        end
    end

    Assert("7.02", string.format("GetItemInfoInstant returns correct subclassID (%d checked)", totalChecked),
        totalChecked > 0,
        "Could not check any items — no collected items with valid subclassID", "P0")

    -- 7.03: Verify the FILTERING logic would correctly filter
    if totalChecked > 0 then
        -- The fix uses: subclassID == playerArmorSubclass
        -- Count how many of the checked items would PASS the filter
        local wouldPass = totalChecked - wrongType
        Assert("7.03", string.format("Armor filter correct: %d/%d items match %s", wouldPass, totalChecked, expectedArmor),
            true, -- This is informational
            string.format("%d would be filtered out as wrong armor type", wrongType), "P0")

        if wrongType > 0 then
            print(string.format("  |cffff8888Wrong type items (first 5): %s|r",
                table.concat(wrongItems, ", "):sub(1, 120)))
        end
    end

    -- 7.04: Cosmetic slots should NOT be filtered
    local cosmeticSlots = {3, 5, 6} -- Back, Shirt, Tabard
    for _, catID in ipairs(cosmeticSlots) do
        local catName = ({[3]="Back",[5]="Shirt",[6]="Tabard"})[catID]
        local ok, catAppearances = pcall(function()
            return C_TransmogCollection.GetCategoryAppearances(catID)
        end)
        local hasMultipleTypes = false
        if ok and catAppearances then
            local seenSubclasses = {}
            local checked = 0
            for _, app in ipairs(catAppearances) do
                if checked >= 10 then break end
                if app.visualID then
                    local sources = C_TransmogCollection.GetAppearanceSources(app.visualID)
                    if sources and sources[1] then
                        local itemID = sources[1].itemID or sources[1].itemId
                        if itemID then
                            local _, _, _, _, _, classID, subclassID = GetItemInfoInstant(itemID)
                            if subclassID then
                                seenSubclasses[subclassID] = true
                            end
                            checked = checked + 1
                        end
                    end
                end
            end
            local uniqueTypes = 0
            for _ in pairs(seenSubclasses) do uniqueTypes = uniqueTypes + 1 end
            hasMultipleTypes = uniqueTypes > 1 or uniqueTypes == 0 -- cosmetic = no subclass or misc
        end
        AssertWarn("7.04_" .. catID, string.format("%s slot exempt from armor filter", catName),
            true, -- Can't fully test without running the function, but verify the data
            string.format("Cosmetic slot %s has items — should accept all armor types", catName), "P1")
    end

    -- 7.05: MainFrame has GenerateRandomOutfit function
    if ETB.MainFrame then
        Assert("7.05", "GenerateRandomOutfit function exists",
            ETB.MainFrame.GenerateRandomOutfit ~= nil,
            "MF:GenerateRandomOutfit not found", "P1")
    end
end

-- ============================================================================
-- SECTION 7B: TRANSMOG UI & APPEARANCE PANEL
-- ============================================================================

local function TestTransmogUI()
    SectionHeader(8, "TransmogUI & AppearanceDetailPanel")

    -- 8.01: TransmogUI global
    local TUI = _G["ETBTransmogUIFrame"]
    AssertWarn("8.01", "ETBTransmogUIFrame global exists",
        TUI ~= nil or ETB.TransmogUI ~= nil,
        "TransmogUI frame not created yet (normal if NPC not visited)", "P1")

    -- 8.02: AppearanceDetailPanel
    Assert("8.02", "AppearanceDetailPanel module loaded",
        ETB.AppearanceDetail ~= nil, "ETB.AppearanceDetail is nil", "P0")

    if ETB.AppearanceDetail then
        Assert("8.03", "AppearanceDetail:Show method exists",
            ETB.AppearanceDetail.Show ~= nil, nil, "P1")
        Assert("8.04", "AppearanceDetail:Toggle method exists",
            ETB.AppearanceDetail.Toggle ~= nil, nil, "P1")
        Assert("8.05", "AppearanceDetail:PopulateSharedItems exists",
            ETB.AppearanceDetail.PopulateSharedItems ~= nil, nil, "P1")
    end

    -- 8.06: Accessories data
    local accData = ETB.AccessoriesData
    if accData then
        local shirtCount = accData.Shirts and #accData.Shirts or 0
        local tabardCount = accData.Tabards and #accData.Tabards or 0
        Assert("8.06a", "Shirts data loaded",
            shirtCount >= 90,
            string.format("Found %d shirts (expected ~98)", shirtCount), "P1")
        Assert("8.06b", "Tabards data loaded",
            tabardCount >= 85,
            string.format("Found %d tabards (expected ~93)", tabardCount), "P1")
    else
        AssertWarn("8.06", "Accessories data loaded",
            false, "ETB.AccessoriesData is nil", "P1")
    end

    -- 8.07: LRU Cache
    if ETB.LRUCache then
        Assert("8.07", "LRU Cache system loaded", true, nil, "P2")
    else
        AssertWarn("8.07", "LRU Cache loaded", false, "ETB.LRUCache is nil", "P2")
    end

    -- 8.08: SourcesModal
    AssertWarn("8.08", "SourcesModal loaded",
        ETB.SourcesModal ~= nil, "ETB.SourcesModal is nil", "P2")

    -- 8.09: FavoritesManager
    AssertWarn("8.09", "FavoritesManager loaded",
        ETB.FavoritesManager ~= nil, "ETB.FavoritesManager is nil", "P2")

    -- 8.10: ProfileManager
    AssertWarn("8.10", "ProfileManager loaded",
        ETB.ProfileManager ~= nil, "ETB.ProfileManager is nil", "P2")
end

-- ============================================================================
-- SECTION 9: BOSS LOOT TOOLTIP
-- ============================================================================

local function TestBossLootTooltip()
    SectionHeader(9, "Boss Loot Tooltip & Source Mapping")

    local BLT = ETB.BossLootTooltip

    -- 9.01: Module loaded
    Assert("9.01", "BossLootTooltip module loaded",
        BLT ~= nil, "ETB.BossLootTooltip is nil", "P0")

    if not BLT then return end

    -- 9.02: Initialize
    Assert("9.02", "BLT initialized",
        BLT.initialized == true,
        "BLT.initialized = " .. tostring(BLT.initialized), "P1")

    -- 9.03: Force build index for testing
    if not BLT.indexBuilt then
        local ok, err = pcall(function() BLT:BuildIndex() end)
        Assert("9.03a", "BuildIndex runs without error",
            ok, ok and nil or tostring(err), "P0")
    end

    Assert("9.03b", "Index built successfully",
        BLT.indexBuilt == true, "indexBuilt = " .. tostring(BLT.indexBuilt), "P0")

    if not BLT.indexBuilt or not BLT.npcIndex then return end

    -- 9.04: Stats check
    local s = BLT.stats
    Assert("9.04a", "Total NPCs > 100",
        s.totalNPCs > 100,
        string.format("totalNPCs = %d", s.totalNPCs), "P0")
    Assert("9.04b", "Total items > 3000",
        s.totalItems > 3000,
        string.format("totalItems = %d", s.totalItems), "P0")

    -- Print full stats
    print(string.format("  |cff888888Direct: %d | Encounter: %d | Instance: %d | World Boss: %d|r",
        s.directNPC or 0, s.encounterResolved or 0, s.instanceGrouped or 0, s.worldBoss or 0))

    -- 9.05: Known raid bosses have items
    local raidBossChecks = {
        { 11502, "Ragnaros (MC)" },
        { 15990, "Kel'Thuzad (Naxx)" },
        { 10184, "Onyxia" },
        { 15688, "Curator (Kara)" },
        { 22917, "Illidan (BT)" },
        { 36597, "Lich King (ICC)" },
        { 52409, "Ragnaros (Firelands)" },
        { 71865, "Garrosh (SoO)" },
        { 68078, "Lei Shen (ToT)" },
    }
    local raidBossFound = 0
    for _, check in ipairs(raidBossChecks) do
        local npcID, name = check[1], check[2]
        local items = BLT.npcIndex[npcID]
        local count = 0
        if items then
            for _ in pairs(items) do count = count + 1 end
        end
        local passed = count > 0
        if passed then raidBossFound = raidBossFound + 1 end
        Assert("9.05_" .. npcID, string.format("%s has items", name),
            passed,
            string.format("%d items mapped", count), "P0")
    end

    -- 9.06: Known dungeon bosses have items
    local dungeonBossChecks = {
        { 1853, "Gandling (Scholomance)" },
        { 9019, "Emperor (BRD)" },
        { 56439, "Sha of Doubt (Jade Serpent)" },
        { 59479, "Xin (Mogu'shan Palace)" },
    }
    for _, check in ipairs(dungeonBossChecks) do
        local npcID, name = check[1], check[2]
        local items = BLT.npcIndex[npcID]
        local count = 0
        if items then
            for _ in pairs(items) do count = count + 1 end
        end
        Assert("9.06_" .. npcID, string.format("%s has items", name),
            count > 0,
            string.format("%d items mapped", count), "P1")
    end

    -- 9.07: World bosses have items (v12.26.1 fix)
    local worldBossChecks = {
        { 60143, "Sha of Anger" },
        { 62346, "Galleon" },
        { 69099, "Nalak" },
        { 69161, "Oondasta" },
        { 72057, "Ordos" },
    }
    local worldBossFound = 0
    for _, check in ipairs(worldBossChecks) do
        local npcID, name = check[1], check[2]
        local items = BLT.npcIndex[npcID]
        local count = 0
        if items then
            for _ in pairs(items) do count = count + 1 end
        end
        if count > 0 then worldBossFound = worldBossFound + 1 end
        Assert("9.07_" .. npcID, string.format("World Boss: %s has items", name),
            count > 0,
            string.format("%d items mapped", count), "P0")
    end
    print(string.format("  |cff888888World bosses with items: %d/%d|r",
        worldBossFound, #worldBossChecks))

    -- 9.08: SourceDB loaded and has world boss data
    if ETB.SourceDB then
        Assert("9.08a", "SourceDB loaded", true, nil, "P1")
        if ETB.SourceDB.GetAllWorldBossItems then
            local ok, wbItems = pcall(ETB.SourceDB.GetAllWorldBossItems, ETB.SourceDB)
            Assert("9.08b", "GetAllWorldBossItems returns data",
                ok and wbItems and #wbItems > 0,
                ok and wbItems and string.format("%d world boss items", #wbItems) or tostring(wbItems), "P0")
        else
            AddResult("9.08b", "GetAllWorldBossItems exists", "SKIP", "Method not available", "P0")
        end
    end

    -- 9.09: Items are filtered to armor/weapons only (no trinkets/rings)
    local nonTransmogFound = 0
    local checkedItems = 0
    for npcID, items in pairs(BLT.npcIndex) do
        if checkedItems >= 200 then break end
        for itemID in pairs(items) do
            if checkedItems >= 200 then break end
            local _, _, _, equipLoc, _, classID = GetItemInfoInstant(itemID)
            if classID then
                checkedItems = checkedItems + 1
                if classID ~= 2 and classID ~= 4 then
                    nonTransmogFound = nonTransmogFound + 1
                elseif equipLoc == "INVTYPE_TRINKET" or equipLoc == "INVTYPE_FINGER" or equipLoc == "INVTYPE_NECK" then
                    nonTransmogFound = nonTransmogFound + 1
                end
            end
        end
    end
    Assert("9.09", string.format("All indexed items are transmoggable (%d checked)", checkedItems),
        nonTransmogFound == 0,
        string.format("%d non-transmog items found in index", nonTransmogFound), "P0")

    -- 9.10: CanPlayerUseItem filters by class
    if BLT.CanPlayerUseItem then
        -- Test that CanPlayerUseItem returns true for at least some items
        -- Sample across multiple NPCs since first NPC in hash order may drop
        -- gear for a different armor type than current class
        local usableCount = 0
        local npcsChecked = 0
        for npcID, items in pairs(BLT.npcIndex) do
            if usableCount >= 3 or npcsChecked >= 50 then break end
            npcsChecked = npcsChecked + 1
            for itemID in pairs(items) do
                if BLT:CanPlayerUseItem(itemID) then
                    usableCount = usableCount + 1
                end
                if usableCount >= 3 then break end
            end
        end
        Assert("9.10", string.format("CanPlayerUseItem found usable items (%d NPCs checked)", npcsChecked),
            usableCount > 0,
            string.format("%d usable items found for current class", usableCount), "P1")
    end

    -- 9.11: MapperBridge source data available for tooltip enrichment
    local SDA = ETB.SourceData_ATT
    if SDA then
        Assert("9.11a", "SourceData_ATT available for source info", true, nil, "P1")
        if SDA.GetPrimarySource then
            -- Test a known item
            local ok, src = pcall(SDA.GetPrimarySource, SDA, 16963) -- Helm of Wrath
            AssertWarn("9.11b", "GetPrimarySource returns data for known item",
                ok and src and src.name and src.name ~= "Unknown",
                ok and src and string.format("name=%s, loc=%s", src.name or "nil", src.location or "nil")
                    or tostring(src), "P1")
        end
    else
        AddResult("9.11a", "SourceData_ATT available", "SKIP", "Not loaded", "P1")
    end

    -- 9.12: Quest NPCs have items (PASS 4)
    local questNPCFound = 0
    local testQuestNPCs = {
        -- These are quest givers that should have transmog rewards
        -- NPC IDs from QUEST source type with small item counts
        { 41652, "Cata Quest NPC" },
        { 23661, "TBC Quest NPC" },
    }
    for _, check in ipairs(testQuestNPCs) do
        local npcID, name = check[1], check[2]
        local items = BLT.npcIndex[npcID]
        local count = 0
        if items then
            for _ in pairs(items) do count = count + 1 end
        end
        if count > 0 then questNPCFound = questNPCFound + 1 end
        AssertWarn("9.12_" .. npcID, string.format("Quest NPC %s has items", name),
            count > 0,
            string.format("%d items mapped", count), "P1")
    end

    -- 9.13: World boss QUEST-coded items recovered
    -- Items like Yu'lon/Nalak loot that ATT codes as QUEST should map to the boss NPC
    local wbQuestNPCs = {
        { 71952, "Yu'lon (Celestial)" },
        { 69099, "Nalak (QUEST-coded)" },
    }
    for _, check in ipairs(wbQuestNPCs) do
        local npcID, name = check[1], check[2]
        local items = BLT.npcIndex[npcID]
        local count = 0
        if items then
            for _ in pairs(items) do count = count + 1 end
        end
        Assert("9.13_" .. npcID, string.format("WB QUEST items: %s recovered", name),
            count > 0,
            string.format("%d items mapped (includes VENDOR + QUEST coded items)", count), "P0")
    end

    -- 9.14: Stats include questNPC count
    AssertWarn("9.14", "Stats track quest NPC items",
        s.questNPC ~= nil and s.questNPC >= 0,
        string.format("questNPC = %s", tostring(s.questNPC)), "P2")

    -- 9.15: RareMobLoot data loaded
    local rareMobData = NS.RareMobLoot
    Assert("9.15", "RareMobLoot data loaded",
        rareMobData ~= nil,
        rareMobData and "loaded" or "MISSING", "P0")
    
    if rareMobData then
        local rareNPCCount = 0
        local rareItemTotal = 0
        for npcID, items in pairs(rareMobData) do
            rareNPCCount = rareNPCCount + 1
            rareItemTotal = rareItemTotal + #items
        end
        Assert("9.15a", "RareMobLoot has >400 NPCs",
            rareNPCCount > 400,
            string.format("%d NPCs, %d items", rareNPCCount, rareItemTotal), "P0")
    end

    -- 9.16: Rare mobs indexed in npcIndex
    local testRareMobs = {
        { 50741, "Kaxx (Thousand Needles)" },
        { 50838, "Tabbs (Tanaris)" },
        { 63240, "Shadowmaster Sydow (Krasarang)" },
        { 73166, "Timeless Isle rare (Emerald Gander)" },
        { 8660, "The Evalcharr (Azshara)" },
    }
    local rareFound = 0
    for _, check in ipairs(testRareMobs) do
        local npcID, name = check[1], check[2]
        local items = BLT.npcIndex[npcID]
        local count = 0
        if items then
            for _ in pairs(items) do count = count + 1 end
        end
        if count > 0 then rareFound = rareFound + 1 end
        AssertWarn("9.16_" .. npcID, string.format("Rare mob %s has items", name),
            count > 0,
            string.format("%d items mapped", count), "P1")
    end

    -- 9.17: Stats track rare mob items
    Assert("9.17", "Stats track rare mob items",
        s.rareMob ~= nil and s.rareMob > 0,
        string.format("rareMob = %s", tostring(s.rareMob or 0)), "P0")

    -- 9.18: Total indexed items grew significantly with rare mob data
    Assert("9.18", "Total items >15000 (including rare mob loot)",
        s.totalItems > 15000,
        string.format("totalItems = %d, totalNPCs = %d", s.totalItems, s.totalNPCs), "P1")

    -- 9.19: Total NPCs >600 (bosses + rares + quest givers)
    Assert("9.19", "Total NPCs >600 (bosses + rares + quest givers)",
        s.totalNPCs > 600,
        string.format("totalNPCs = %d", s.totalNPCs), "P1")
end

-- ============================================================================
-- SECTION 10: VISUAL ID RESOLUTION & SHARED APPEARANCES
-- ============================================================================

local function TestVisualIDResolution()
    SectionHeader(10, "Visual ID Resolution & Shared Appearances (v12.29)")

    -- 10.1: Shared resolver exists on ETB
    AddResult("10.1", "ETB:ResolveVisualID exists",
        ETB.ResolveVisualID and "PASS" or "FAIL",
        type(ETB.ResolveVisualID), "P0")

    AddResult("10.1b", "ETB:ResolveVisualIDFromItemID exists",
        ETB.ResolveVisualIDFromItemID and "PASS" or "FAIL",
        type(ETB.ResolveVisualIDFromItemID), "P0")

    AddResult("10.1c", "ETB:ClearVisualIDCache exists",
        ETB.ClearVisualIDCache and "PASS" or "FAIL",
        type(ETB.ClearVisualIDCache), "P0")

    -- 10.2: Resolver handles nil/empty gracefully
    local nilResult = ETB:ResolveVisualID(nil)
    AddResult("10.2a", "ResolveVisualID(nil) returns nil",
        nilResult == nil and "PASS" or "FAIL",
        tostring(nilResult), "P0")

    local emptyResult = ETB:ResolveVisualID({})
    AddResult("10.2b", "ResolveVisualID({}) returns nil",
        emptyResult == nil and "PASS" or "FAIL",
        tostring(emptyResult), "P1")

    -- 10.3: Resolver returns pre-set visualID
    local fakeItem = { itemID = 12345, visualID = 99999 }
    local preSet = ETB:ResolveVisualID(fakeItem)
    AddResult("10.3", "ResolveVisualID returns pre-set visualID",
        preSet == 99999 and "PASS" or "FAIL",
        tostring(preSet), "P0")

    -- 10.4: Runtime resolution via C_TransmogCollection.GetItemInfo
    -- Pick a known transmogable item from current category data
    local testItemID = nil
    local testVisualID = nil
    -- Get a test item from Blizzard API directly
    local ok10, appearances10 = pcall(C_TransmogCollection.GetCategoryAppearances, 1) -- Head
    if ok10 and appearances10 then
        for _, app in ipairs(appearances10) do
            if app.visualID and app.visualID > 0 then
                local srcOk, sources = pcall(C_TransmogCollection.GetAppearanceSources, app.visualID)
                if srcOk and sources and sources[1] and sources[1].itemID then
                    testItemID = sources[1].itemID
                    testVisualID = app.visualID
                    break
                end
            end
        end
    end

    if testItemID then
        -- Test runtime resolution (clear pre-set and see if it resolves)
        local runtimeItem = { itemID = testItemID }
        local resolved = ETB:ResolveVisualID(runtimeItem)
        AddResult("10.4", "Runtime visualID resolution for itemID " .. testItemID,
            resolved and resolved > 0 and "PASS" or "WARN",
            string.format("expected=%s resolved=%s", tostring(testVisualID), tostring(resolved)), "P0")

        -- 10.5: Verify it matches the known visualID
        if resolved and testVisualID then
            AddResult("10.5", "Resolved visualID matches known value",
                resolved == testVisualID and "PASS" or "WARN",
                string.format("%d vs %d", resolved, testVisualID), "P1")
        end

        -- 10.6: Test ResolveVisualIDFromItemID
        local fromItemID = ETB:ResolveVisualIDFromItemID(testItemID)
        AddResult("10.6", "ResolveVisualIDFromItemID returns valid ID",
            fromItemID and fromItemID > 0 and "PASS" or "WARN",
            tostring(fromItemID), "P1")

        -- 10.7: Shared appearance lookup with resolved ID
        if resolved and C_TransmogCollection and C_TransmogCollection.GetAppearanceSources then
            local ok, sources = pcall(C_TransmogCollection.GetAppearanceSources, resolved)
            if ok and sources then
                AddResult("10.7", "GetAppearanceSources returns items for resolved visualID",
                    #sources > 0 and "PASS" or "WARN",
                    string.format("%d shared sources found", #sources), "P1")

                -- 10.8: Each source has valid itemID and sourceID
                local validSources = 0
                for _, src in ipairs(sources) do
                    if src.itemID and src.sourceID then
                        validSources = validSources + 1
                    end
                end
                AddResult("10.8", "Shared sources have valid itemID/sourceID",
                    validSources == #sources and "PASS" or "WARN",
                    string.format("%d/%d valid", validSources, #sources), "P1")
            else
                AddResult("10.7", "GetAppearanceSources call", "SKIP", "API call failed", "P1")
            end
        end
    else
        AddResult("10.4", "Runtime resolution", "SKIP", "No test item found", "P1")
    end

    -- 10.9: AppearanceDetailPanel exists and works
    AddResult("10.9a", "AppearanceDetailPanel module exists",
        ETB.AppearanceDetail and "PASS" or "FAIL",
        nil, "P0")

    AddResult("10.9b", "ADP:Show function exists",
        ETB.AppearanceDetail and ETB.AppearanceDetail.Show and "PASS" or "FAIL",
        nil, "P0")

    -- 10.10: SourcesModal redirects to ADP
    if ETB.SourcesModal and ETB.AppearanceDetail then
        AddResult("10.10", "SourcesModal redirects to AppearanceDetailPanel",
            "PASS", "SourcesModal.Show checks for AppearanceDetail first", "P1")
    else
        AddResult("10.10", "SourcesModal → ADP redirect", "SKIP", "Modules not loaded", "P1")
    end

    -- 10.11: Cache clearing works
    ETB:ClearVisualIDCache()
    AddResult("10.11", "ClearVisualIDCache runs without error",
        "PASS", nil, "P1")
end

-- ============================================================================
-- SECTION 11: STRESS TESTS & EDGE CASES
-- ============================================================================
-- This section deliberately tries to break the addon with:
--   A) Nil/invalid inputs to every major function
--   B) Malformed item data through every pipeline
--   C) Memory pressure (all categories, all items at once)
--   D) Rapid Show/Hide/Toggle cycling on UI panels
--   E) Event storm simulation
--   F) Boundary value testing (itemID=0, MAX_INT, negative)
--   G) Filter combinatorial explosion
--   H) pcall audit of all Blizzard API calls
--   I) String safety (nil concatenation traps)
--   J) Table iteration safety (ipairs/pairs on nil)
-- ============================================================================

local function TestStress()
    SectionHeader(11, "STRESS TESTS & EDGE CASES (v12.29)")

    local crashes = 0
    local tested = 0

    -- Helper: run a function and track if it crashes
    local function StressCall(id, name, fn, priority)
        tested = tested + 1
        local ok, err = pcall(fn)
        if ok then
            AddResult(id, name, "PASS", nil, priority or "P0")
        else
            crashes = crashes + 1
            AddResult(id, name, "FAIL", "CRASH: " .. tostring(err), priority or "P0")
        end
        return ok
    end

    -- Helper: Get sample items from Blizzard API (since BlizzardData.allItems
    -- is not a persistent field — items are loaded per-category on demand)
    local _sampleItems = nil
    local function GetSampleItems(count)
        if _sampleItems then return _sampleItems end
        count = count or 200
        _sampleItems = {}
        -- Load from a few categories to get a representative mix
        local sampleCats = {1, 4, 14, 23} -- Head, Chest, 1H Swords, Staves
        for _, catID in ipairs(sampleCats) do
            if #_sampleItems >= count then break end
            local ok, appearances = pcall(C_TransmogCollection.GetCategoryAppearances, catID)
            if ok and appearances then
                for _, app in ipairs(appearances) do
                    if #_sampleItems >= count then break end
                    if app.visualID then
                        local ok2, sources = pcall(C_TransmogCollection.GetAppearanceSources, app.visualID)
                        if ok2 and sources and sources[1] then
                            local src = sources[1]
                            local itemID = type(src) == "table" and src.itemID or nil
                            if itemID then
                                local name, _, quality, _, _, _, _, _, _, icon = GetItemInfo(itemID)
                                _sampleItems[#_sampleItems + 1] = {
                                    itemID = itemID,
                                    visualID = app.visualID,
                                    sourceID = type(src) == "table" and src.sourceID or nil,
                                    name = name or ("Item " .. itemID),
                                    quality = quality or 2,
                                    icon = icon,
                                    isCollected = app.isCollected,
                                    categoryID = catID,
                                }
                            end
                        end
                    end
                end
            end
        end
        return _sampleItems
    end

    -- ========================================================================
    -- A) NIL/INVALID INPUT SAFETY — Feed nil, 0, -1, huge, wrong types
    -- ========================================================================
    print("  |cffc8b88a--- A: Nil/Invalid Input Safety ---|r")

    -- ResolveVisualID edge cases
    StressCall("11.A01", "ResolveVisualID(nil)", function()
        ETB:ResolveVisualID(nil)
    end)
    StressCall("11.A02", "ResolveVisualID(false)", function()
        ETB:ResolveVisualID(false)
    end)
    StressCall("11.A03", "ResolveVisualID(\"string\")", function()
        ETB:ResolveVisualID("hello")
    end)
    StressCall("11.A04", "ResolveVisualID({itemID=0})", function()
        ETB:ResolveVisualID({itemID = 0})
    end)
    StressCall("11.A05", "ResolveVisualID({itemID=-1})", function()
        ETB:ResolveVisualID({itemID = -1})
    end)
    StressCall("11.A06", "ResolveVisualID({itemID=999999999})", function()
        ETB:ResolveVisualID({itemID = 999999999})
    end)
    StressCall("11.A07", "ResolveVisualIDFromItemID(nil)", function()
        ETB:ResolveVisualIDFromItemID(nil)
    end)
    StressCall("11.A08", "ResolveVisualIDFromItemID(0)", function()
        ETB:ResolveVisualIDFromItemID(0)
    end)
    StressCall("11.A09", "ResolveVisualIDFromItemID(-1)", function()
        ETB:ResolveVisualIDFromItemID(-1)
    end)

    -- BlizzardData nil safety
    if ETB.BlizzardData then
        StressCall("11.A10", "BD:GetCategoryAppearances(nil)", function()
            ETB.BlizzardData:GetCategoryAppearances(nil)
        end)
        StressCall("11.A11", "BD:GetCategoryAppearances(0)", function()
            ETB.BlizzardData:GetCategoryAppearances(0)
        end)
        StressCall("11.A12", "BD:GetCategoryAppearances(-1)", function()
            ETB.BlizzardData:GetCategoryAppearances(-1)
        end)
        StressCall("11.A13", "BD:GetCategoryAppearances(9999)", function()
            ETB.BlizzardData:GetCategoryAppearances(9999)
        end)
        StressCall("11.A14", "BD:GetAppearanceSources(nil)", function()
            ETB.BlizzardData:GetAppearanceSources(nil)
        end)
        StressCall("11.A15", "BD:GetAppearanceSources(0)", function()
            ETB.BlizzardData:GetAppearanceSources(0)
        end)
        StressCall("11.A16", "BD:GetItemIcon(nil)", function()
            ETB.BlizzardData:GetItemIcon(nil)
        end)
        StressCall("11.A17", "BD:GetItemIcon(0)", function()
            ETB.BlizzardData:GetItemIcon(0)
        end)
        StressCall("11.A18", "BD:GetArmorType(nil)", function()
            ETB.BlizzardData:GetArmorType(nil)
        end)
        StressCall("11.A19", "BD:GetWeaponType(nil)", function()
            ETB.BlizzardData:GetWeaponType(nil)
        end)
        if ETB.BlizzardData.GetItemsForCategory then
            StressCall("11.A20", "BD:GetItemsForCategory(nil)", function()
                ETB.BlizzardData:GetItemsForCategory(nil)
            end)
            StressCall("11.A21", "BD:GetItemsForCategory(0, nil)", function()
                ETB.BlizzardData:GetItemsForCategory(0, nil)
            end)
        end
    end

    -- ArmorData nil safety
    if ETB.ArmorData and ETB.ArmorData.GetItemsForCategory then
        StressCall("11.A22", "AD:GetItemsForCategory(nil)", function()
            ETB.ArmorData:GetItemsForCategory(nil)
        end)
        StressCall("11.A23", "AD:GetItemsForCategory(0, {})", function()
            ETB.ArmorData:GetItemsForCategory(0, {})
        end)
    end

    -- WeaponData nil safety
    if ETB.WeaponData and ETB.WeaponData.GetItemsForCategory then
        StressCall("11.A24", "WD:GetItemsForCategory(nil)", function()
            ETB.WeaponData:GetItemsForCategory(nil)
        end)
    end

    -- SourceMapper nil safety
    if ETB.SourceMapper then
        StressCall("11.A25", "SM:GetPrimarySource(nil)", function()
            ETB.SourceMapper:GetPrimarySource(nil)
        end)
        StressCall("11.A26", "SM:GetPrimarySource(0)", function()
            ETB.SourceMapper:GetPrimarySource(0)
        end)
        StressCall("11.A27", "SM:GetPrimarySource(-1)", function()
            ETB.SourceMapper:GetPrimarySource(-1)
        end)
        StressCall("11.A28", "SM:GetItemSources(nil)", function()
            ETB.SourceMapper:GetItemSources(nil)
        end)
        StressCall("11.A29", "SM:FormatSourceTooltip(nil)", function()
            ETB.SourceMapper:FormatSourceTooltip(nil)
        end)
        StressCall("11.A30", "SM:FormatSourceTooltip(0)", function()
            ETB.SourceMapper:FormatSourceTooltip(0)
        end)
        StressCall("11.A31", "SM:FormatSourceShort(nil)", function()
            ETB.SourceMapper:FormatSourceShort(nil)
        end)
    end

    -- SetsData nil safety
    if ETB.Sets then
        StressCall("11.A32", "SD:GetSetInfo(nil)", function()
            ETB.Sets:GetSetInfo(nil)
        end)
        StressCall("11.A33", "SD:GetSetInfo(0)", function()
            ETB.Sets:GetSetInfo(0)
        end)
        StressCall("11.A34", "SD:GetSetInfo(-1)", function()
            ETB.Sets:GetSetInfo(-1)
        end)
        StressCall("11.A35", "SD:GetSetSources(nil)", function()
            ETB.Sets:GetSetSources(nil)
        end)
        StressCall("11.A36", "SD:GetSetItems(nil)", function()
            ETB.Sets:GetSetItems(nil)
        end)
        StressCall("11.A37", "SD:GetSetCompletion(nil)", function()
            ETB.Sets:GetSetCompletion(nil)
        end)
        StressCall("11.A38", "SD:GetSetCompletion(999999)", function()
            ETB.Sets:GetSetCompletion(999999)
        end)
    end
    if ETB.SetsData then
        StressCall("11.A39", "SD:IsSourceCollected(nil)", function()
            ETB.SetsData:IsSourceCollected(nil)
        end)
        StressCall("11.A40", "SD:GetItemFromSourceID(nil)", function()
            ETB.SetsData:GetItemFromSourceID(nil)
        end)
    end

    -- Filtering nil safety
    if ETB.Filtering then
        StressCall("11.A41", "Filter:SetCategory(nil)", function()
            ETB.Filtering:SetCategory(nil)
        end)
        StressCall("11.A42", "Filter:SetSearchText(nil)", function()
            ETB.Filtering:SetSearchText(nil)
        end)
        StressCall("11.A43", "Filter:SetCollectedFilter(nil)", function()
            ETB.Filtering:SetCollectedFilter(nil)
        end)
        StressCall("11.A44", "Filter:ApplyFilters(nil)", function()
            ETB.Filtering:ApplyFilters(nil)
        end)
        StressCall("11.A45", "Filter:ApplyFilters({})", function()
            ETB.Filtering:ApplyFilters({})
        end)
        StressCall("11.A46", "Filter:SortItems(nil)", function()
            ETB.Filtering:SortItems(nil)
        end)
        StressCall("11.A47", "Filter:SortItems({})", function()
            ETB.Filtering:SortItems({})
        end)
        -- Reset filters
        pcall(function() ETB.Filtering:ClearFilters() end)
    end

    -- ========================================================================
    -- B) MALFORMED ITEM DATA — Feed garbage items through ADP and consumers
    -- ========================================================================
    print("  |cffc8b88a--- B: Malformed Item Data ---|r")

    local malformedItems = {
        {},                                          -- empty table
        { itemID = nil },                            -- nil itemID
        { itemID = 0 },                              -- zero itemID
        { itemID = -1 },                             -- negative
        { itemID = "not_a_number" },                 -- string itemID
        { itemID = 12345 },                          -- valid ID, no other fields
        { itemID = 12345, visualID = 0 },            -- zero visualID
        { itemID = 12345, visualID = -1 },           -- negative visualID
        { itemID = 12345, name = nil, icon = nil },  -- nil name/icon
        { itemID = 12345, quality = "epic" },        -- string quality
        { itemID = 12345, isCollected = "yes" },     -- string bool
        { itemID = 12345, sourceID = 0 },            -- zero sourceID
        { itemID = 12345, alternatives = "not_table" }, -- wrong alternatives type
        { itemID = 12345, alternatives = {} },       -- empty alternatives
        { name = "No ID Item", quality = 4 },        -- name but no itemID
    }

    -- Feed malformed items to AppearanceDetailPanel
    if ETB.AppearanceDetail then
        for i, badItem in ipairs(malformedItems) do
            StressCall("11.B" .. string.format("%02d", i),
                "ADP:Show(malformed #" .. i .. ")", function()
                ETB.AppearanceDetail:Show(badItem)
            end)
        end
        -- Clean up - hide panel
        pcall(function()
            if ETB.AppearanceDetail.frame and ETB.AppearanceDetail.frame:IsShown() then
                ETB.AppearanceDetail:Hide()
            end
        end)

        -- Feed nil
        StressCall("11.B16", "ADP:Show(nil)", function()
            ETB.AppearanceDetail:Show(nil)
        end)
        StressCall("11.B17", "ADP:PopulateSharedItems(nil)", function()
            ETB.AppearanceDetail:PopulateSharedItems(nil)
        end)
        StressCall("11.B18", "ADP:PreviewSpecificItem(nil)", function()
            ETB.AppearanceDetail:PreviewSpecificItem(nil)
        end)
        StressCall("11.B19", "ADP:PreviewSpecificItem(0)", function()
            ETB.AppearanceDetail:PreviewSpecificItem(0)
        end)
        StressCall("11.B20", "ADP:PreviewSpecificItem(-1)", function()
            ETB.AppearanceDetail:PreviewSpecificItem(-1)
        end)
    end

    -- Feed malformed items to SourcesModal
    if ETB.SourcesModal then
        StressCall("11.B21", "SourcesModal:Show(nil)", function()
            ETB.SourcesModal:Show(nil)
        end)
        StressCall("11.B22", "SourcesModal:Show({})", function()
            ETB.SourcesModal:Show({})
        end)
        StressCall("11.B23", "SourcesModal:Show({itemID=0})", function()
            ETB.SourcesModal:Show({ itemID = 0 })
        end)
        pcall(function()
            if ETB.SourcesModal.frame and ETB.SourcesModal.frame:IsShown() then
                ETB.SourcesModal:Hide()
            end
        end)
    end

    -- Feed malformed items to TooltipEnhancer
    if ETB.TooltipEnhancer then
        StressCall("11.B24", "TE:AddCollectionInfo(nil, nil)", function()
            ETB.TooltipEnhancer:AddCollectionInfo(nil, nil)
        end)
        StressCall("11.B25", "TE:AddSourceInfo(nil, nil)", function()
            ETB.TooltipEnhancer:AddSourceInfo(nil, nil)
        end)
    end

    -- ========================================================================
    -- C) MEMORY PRESSURE — Load all 27 categories, enumerate all items
    -- ========================================================================
    print("  |cffc8b88a--- C: Memory Pressure ---|r")

    collectgarbage("collect")
    collectgarbage("collect")
    local memBefore = collectgarbage("count")

    -- Load ALL 27 categories simultaneously
    local totalItemsLoaded = 0
    local allItemsTable = {}
    StressCall("11.C01", "Load all 27 categories at once", function()
        for catID = 1, 27 do
            local appearances = C_TransmogCollection.GetCategoryAppearances(catID)
            if appearances then
                for _, app in ipairs(appearances) do
                    totalItemsLoaded = totalItemsLoaded + 1
                    allItemsTable[#allItemsTable + 1] = app
                end
            end
        end
    end)

    AddResult("11.C02", string.format("Loaded %d appearances from all categories", totalItemsLoaded),
        totalItemsLoaded > 5000 and "PASS" or "WARN",
        string.format("%d total", totalItemsLoaded), "P1")

    -- Enumerate ALL sources for first 200 appearances
    local sourcesEnumerated = 0
    StressCall("11.C03", "Enumerate sources for 200 appearances", function()
        local count = 0
        for _, app in ipairs(allItemsTable) do
            if count >= 200 then break end
            if app.visualID then
                local sources = C_TransmogCollection.GetAppearanceSources(app.visualID)
                if sources then
                    sourcesEnumerated = sourcesEnumerated + #sources
                end
                count = count + 1
            end
        end
    end)

    AddResult("11.C04", string.format("Enumerated %d sources from 200 appearances", sourcesEnumerated),
        sourcesEnumerated > 0 and "PASS" or "WARN", nil, "P1")

    -- Memory after stress
    collectgarbage("collect")
    collectgarbage("collect")
    local memAfter = collectgarbage("count")
    local memDelta = (memAfter - memBefore) / 1024

    AddResult("11.C05", string.format("Memory delta after stress: %.1fMB", memDelta),
        memDelta < 50 and "PASS" or memDelta < 100 and "WARN" or "FAIL",
        string.format("Before: %.1fMB, After: %.1fMB", memBefore / 1024, memAfter / 1024), "P0")

    -- Release stress data
    allItemsTable = nil
    collectgarbage("collect")

    -- Cache clear stress
    StressCall("11.C06", "ClearVisualIDCache 100x rapid", function()
        for i = 1, 100 do
            ETB:ClearVisualIDCache()
        end
    end)

    if ETB.SourceMapper and ETB.SourceMapper.ClearCache then
        StressCall("11.C07", "SourceMapper:ClearCache rapid", function()
            for i = 1, 50 do
                ETB.SourceMapper:ClearCache()
            end
        end)
    end

    if ETB.BlizzardData and ETB.BlizzardData.InvalidateCollectionCache then
        StressCall("11.C08", "BD:InvalidateCollectionCache 50x", function()
            for i = 1, 50 do
                ETB.BlizzardData:InvalidateCollectionCache()
            end
        end)
    end

    -- ========================================================================
    -- D) RAPID UI CYCLING — Show/Hide/Toggle panels as fast as possible
    -- ========================================================================
    print("  |cffc8b88a--- D: Rapid UI Cycling ---|r")

    -- Get a valid item to test with
    local validItem = nil
    do
        local si = GetSampleItems(200)
        for _, item in ipairs(si) do
            if item.itemID and item.name and item.visualID then
                validItem = item
                break
            end
        end
    end
    if not validItem then
        validItem = { itemID = 16963, name = "Helm of Wrath", quality = 4, visualID = 1 }
    end

    -- ADP rapid cycling
    if ETB.AppearanceDetail then
        StressCall("11.D01", "ADP Show/Hide 50x rapid", function()
            for i = 1, 50 do
                ETB.AppearanceDetail:Show(validItem)
                ETB.AppearanceDetail:Hide()
            end
        end)

        StressCall("11.D02", "ADP Toggle 50x rapid", function()
            for i = 1, 50 do
                ETB.AppearanceDetail:Toggle(validItem)
            end
            -- Ensure hidden after test
            pcall(function() ETB.AppearanceDetail:Hide() end)
        end)

        -- Show with different items rapidly
        StressCall("11.D03", "ADP Show different items 30x", function()
            local items = {}
            do
                local si = GetSampleItems(200)
                local count = 0
                for _, item in ipairs(si) do
                    if count >= 30 then break end
                    items[#items + 1] = item
                    count = count + 1
                end
            end
            for _, item in ipairs(items) do
                ETB.AppearanceDetail:Show(item)
            end
            pcall(function() ETB.AppearanceDetail:Hide() end)
        end)
    end

    -- SourcesModal rapid cycling
    if ETB.SourcesModal then
        StressCall("11.D04", "SourcesModal Show/Hide 30x", function()
            for i = 1, 30 do
                ETB.SourcesModal:Show(validItem)
                ETB.SourcesModal:Hide()
            end
        end)
    end

    -- ========================================================================
    -- E) EVENT STORM — Fire events rapidly
    -- ========================================================================
    print("  |cffc8b88a--- E: Event Storm ---|r")

    if ETB.EventBus then
        StressCall("11.E01", "Fire ETB_COLLECTION_UPDATED 100x", function()
            for i = 1, 100 do
                ETB.EventBus:Fire("ETB_COLLECTION_UPDATED")
            end
        end)

        StressCall("11.E02", "Fire TRANSMOG_COLLECTION_UPDATED 100x", function()
            for i = 1, 100 do
                ETB.EventBus:Fire("TRANSMOG_COLLECTION_UPDATED")
            end
        end)

        StressCall("11.E03", "Fire GET_ITEM_INFO_RECEIVED 100x", function()
            for i = 1, 100 do
                ETB.EventBus:Fire("GET_ITEM_INFO_RECEIVED", 12345 + i, true)
            end
        end)

        StressCall("11.E04", "Fire nonexistent event", function()
            ETB.EventBus:Fire("TOTALLY_FAKE_EVENT_12345")
        end)

        StressCall("11.E05", "Fire event with nil args", function()
            ETB.EventBus:Fire("ETB_COLLECTION_UPDATED", nil, nil, nil)
        end)
    end

    -- ========================================================================
    -- F) BOUNDARY VALUES — Edge item IDs, edge category IDs
    -- ========================================================================
    print("  |cffc8b88a--- F: Boundary Value Testing ---|r")

    local boundaryIDs = { 0, 1, 2, -1, -999, 100000, 999999, 2147483647 }

    -- GetItemInfoInstant with boundary values
    StressCall("11.F01", "GetItemInfoInstant boundary IDs", function()
        for _, id in ipairs(boundaryIDs) do
            GetItemInfoInstant(id) -- Should never crash
        end
    end)

    -- GetItemInfo with boundary values
    StressCall("11.F02", "GetItemInfo boundary IDs", function()
        for _, id in ipairs(boundaryIDs) do
            GetItemInfo(id)
        end
    end)

    -- C_TransmogCollection.GetItemInfo with boundaries
    if C_TransmogCollection and C_TransmogCollection.GetItemInfo then
        StressCall("11.F03", "C_TransmogCollection.GetItemInfo boundaries", function()
            for _, id in ipairs(boundaryIDs) do
                pcall(C_TransmogCollection.GetItemInfo, id)
            end
        end)
    end

    -- Category boundaries
    if C_TransmogCollection and C_TransmogCollection.GetCategoryAppearances then
        StressCall("11.F04", "GetCategoryAppearances boundary catIDs", function()
            local boundaryCats = { 0, -1, 28, 50, 100, 999 }
            for _, catID in ipairs(boundaryCats) do
                pcall(C_TransmogCollection.GetCategoryAppearances, catID)
            end
        end)
    end

    -- GetAppearanceSources with boundaries
    if C_TransmogCollection and C_TransmogCollection.GetAppearanceSources then
        StressCall("11.F05", "GetAppearanceSources boundary visualIDs", function()
            for _, id in ipairs(boundaryIDs) do
                pcall(C_TransmogCollection.GetAppearanceSources, id)
            end
        end)
    end

    -- Blizzard sets boundary
    if C_TransmogSets and C_TransmogSets.GetSetInfo then
        StressCall("11.F06", "C_TransmogSets.GetSetInfo boundaries", function()
            for _, id in ipairs(boundaryIDs) do
                pcall(C_TransmogSets.GetSetInfo, id)
            end
        end)
    end

    -- ========================================================================
    -- G) FILTER COMBINATORIAL — Apply every filter combo rapidly
    -- ========================================================================
    print("  |cffc8b88a--- G: Filter Combinatorial ---|r")

    if ETB.Filtering then
        local collectedValues = { "all", "collected", "uncollected", nil, "", "invalid" }
        local searchValues = { "", "a", "test", nil, "z", "Tier 14", string.rep("x", 200) }
        local qualityValues = { nil, 0, 1, 2, 3, 4, 5, -1, 99 }

        StressCall("11.G01", "Rapid collected filter cycling", function()
            for _, val in ipairs(collectedValues) do
                pcall(ETB.Filtering.SetCollectedFilter, ETB.Filtering, val)
            end
        end)

        StressCall("11.G02", "Rapid search text cycling", function()
            for _, val in ipairs(searchValues) do
                pcall(ETB.Filtering.SetSearchText, ETB.Filtering, val)
            end
        end)

        StressCall("11.G03", "Rapid quality filter cycling", function()
            for _, val in ipairs(qualityValues) do
                pcall(ETB.Filtering.SetQuality, ETB.Filtering, val)
            end
        end)

        -- Apply filters with items while cycling
        StressCall("11.G04", "ApplyFilters while cycling all filters", function()
            local testItems = {}
            do
                local si = GetSampleItems(200)
                local count = 0
                for _, item in ipairs(si) do
                    if count >= 100 then break end
                    testItems[#testItems + 1] = item
                    count = count + 1
                end
            end
            -- Apply with collected filter
            pcall(ETB.Filtering.SetCollectedFilter, ETB.Filtering, "collected")
            pcall(ETB.Filtering.ApplyFilters, ETB.Filtering, testItems)
            -- Apply with uncollected filter
            pcall(ETB.Filtering.SetCollectedFilter, ETB.Filtering, "uncollected")
            pcall(ETB.Filtering.ApplyFilters, ETB.Filtering, testItems)
            -- Apply with search text
            pcall(ETB.Filtering.SetSearchText, ETB.Filtering, "Helm")
            pcall(ETB.Filtering.ApplyFilters, ETB.Filtering, testItems)
        end)

        -- Reset filters after test
        pcall(function() ETB.Filtering:ClearFilters() end)

        -- Sort stress
        StressCall("11.G05", "Sort cycling on item list", function()
            local testItems = {}
            do
                local si = GetSampleItems(200)
                local count = 0
                for _, item in ipairs(si) do
                    if count >= 200 then break end
                    testItems[#testItems + 1] = item
                    count = count + 1
                end
            end
            local sortTypes = { "name", "quality", "collected", "source", nil, "" }
            for _, sortBy in ipairs(sortTypes) do
                pcall(ETB.Filtering.SetSort, ETB.Filtering, sortBy, true)
                pcall(ETB.Filtering.SortItems, ETB.Filtering, testItems)
                pcall(ETB.Filtering.SetSort, ETB.Filtering, sortBy, false)
                pcall(ETB.Filtering.SortItems, ETB.Filtering, testItems)
            end
        end)
    end

    -- ========================================================================
    -- H) BOSS LOOT TOOLTIP STRESS
    -- ========================================================================
    print("  |cffc8b88a--- H: Boss Loot Tooltip Stress ---|r")

    local BLT = ETB.BossLootTooltip
    if BLT then
        StressCall("11.H01", "BLT:IsItemCollected(nil)", function()
            BLT:IsItemCollected(nil)
        end)
        StressCall("11.H02", "BLT:IsItemCollected(0)", function()
            BLT:IsItemCollected(0)
        end)
        StressCall("11.H03", "BLT:CanPlayerUseItem(nil)", function()
            BLT:CanPlayerUseItem(nil)
        end)
        StressCall("11.H04", "BLT:CanPlayerUseItem(0)", function()
            BLT:CanPlayerUseItem(0)
        end)
        StressCall("11.H05", "BLT:GetNPCIDFromGUID(nil)", function()
            BLT:GetNPCIDFromGUID(nil)
        end)
        StressCall("11.H06", "BLT:GetNPCIDFromGUID(\"invalid\")", function()
            BLT:GetNPCIDFromGUID("not-a-guid")
        end)
        StressCall("11.H07", "BLT:GetNPCIDFromGUID(\"\")", function()
            BLT:GetNPCIDFromGUID("")
        end)

        -- Stress AddLootToTooltip with no tooltip
        StressCall("11.H08", "BLT:AddLootToTooltip(nil, nil)", function()
            BLT:AddLootToTooltip(nil, nil)
        end)
        StressCall("11.H09", "BLT:AddLootToTooltip(nil, 0)", function()
            BLT:AddLootToTooltip(nil, 0)
        end)

        -- Iterate entire NPC index to verify structure
        if BLT.npcIndex then
            StressCall("11.H10", "Iterate entire npcIndex without crash", function()
                local npcCount = 0
                local itemCount = 0
                for npcID, items in pairs(BLT.npcIndex) do
                    npcCount = npcCount + 1
                    if type(items) == "table" then
                        for itemID in pairs(items) do
                            itemCount = itemCount + 1
                        end
                    end
                end
            end)
        end
    end

    -- ========================================================================
    -- I) COMMUNITY SETS STRESS
    -- ========================================================================
    print("  |cffc8b88a--- I: Community Sets Stress ---|r")

    if ETB.CommunitySets then
        StressCall("11.I01", "CS:GetAllSets()", function()
            ETB.CommunitySets:GetAllSets()
        end)
        StressCall("11.I02", "CS:GetSet(nil)", function()
            ETB.CommunitySets:GetSet(nil)
        end)
        StressCall("11.I03", "CS:GetSet(0)", function()
            ETB.CommunitySets:GetSet(0)
        end)
        StressCall("11.I04", "CS:GetSet(-1)", function()
            ETB.CommunitySets:GetSet(-1)
        end)
        StressCall("11.I05", "CS:GetSet(99999)", function()
            ETB.CommunitySets:GetSet(99999)
        end)
        StressCall("11.I06", "CS:GetSetCompletion(nil)", function()
            ETB.CommunitySets:GetSetCompletion(nil)
        end)
        StressCall("11.I07", "CS:GetSetCompletion(0)", function()
            ETB.CommunitySets:GetSetCompletion(0)
        end)
        StressCall("11.I08", "CS:IsSetUsable(nil)", function()
            ETB.CommunitySets:IsSetUsable(nil)
        end)
        StressCall("11.I09", "CS:GetSetCount()", function()
            local count = ETB.CommunitySets:GetSetCount()
        end)

        -- UpdateCollectionCounts stress
        if ETB.CommunitySets.UpdateCollectionCounts then
            StressCall("11.I10", "CS:UpdateCollectionCounts 10x", function()
                for i = 1, 10 do
                    ETB.CommunitySets:UpdateCollectionCounts()
                end
            end)
        end
    end

    -- ========================================================================
    -- J) PROFILE MANAGER STRESS
    -- ========================================================================
    print("  |cffc8b88a--- J: Profile Manager Stress ---|r")

    if ETB.ProfileManager then
        StressCall("11.J01", "PM:GetCurrentProfile()", function()
            ETB.ProfileManager:GetCurrentProfile()
        end)
        StressCall("11.J02", "PM:GetProfile(nil)", function()
            ETB.ProfileManager:GetProfile(nil)
        end)
        StressCall("11.J03", "PM:GetProfile(\"\")", function()
            ETB.ProfileManager:GetProfile("")
        end)
        StressCall("11.J04", "PM:GetActivePrimaryArmor()", function()
            ETB.ProfileManager:GetActivePrimaryArmor()
        end)
    end

    -- ========================================================================
    -- K) SETBUILDER STRESS
    -- ========================================================================
    print("  |cffc8b88a--- K: SetBuilder Stress ---|r")

    if ETB.SetBuilder then
        StressCall("11.K01", "SetBuilder module exists", function()
            assert(ETB.SetBuilder ~= nil)
        end)
        -- Don't open the UI (it may steal focus), just test data functions
        if ETB.SetBuilder.ClearCurrentSet then
            StressCall("11.K02", "SB:ClearCurrentSet nil-safe", function()
                ETB.SetBuilder:ClearCurrentSet()
            end)
        end
    end

    -- ========================================================================
    -- L) CROSS-CATEGORY DEDUP STRESS — verify no duplicate visualIDs leak
    -- ========================================================================
    print("  |cffc8b88a--- L: Cross-Category Dedup Verification ---|r")

    StressCall("11.L01", "No duplicate visualIDs within categories", function()
        local totalDupes = 0
        for catID = 1, 27 do
            local ok, appearances = pcall(C_TransmogCollection.GetCategoryAppearances, catID)
            if ok and appearances then
                local seen = {}
                for _, app in ipairs(appearances) do
                    if app.visualID then
                        if seen[app.visualID] then
                            totalDupes = totalDupes + 1
                        end
                        seen[app.visualID] = true
                    end
                end
            end
        end
        assert(totalDupes == 0, totalDupes .. " duplicate visualIDs found")
    end)

    -- ========================================================================
    -- M) RESOLVE VISUAL ID STRESS — Batch resolve many items
    -- ========================================================================
    print("  |cffc8b88a--- M: Batch VisualID Resolution ---|r")

    StressCall("11.M01", "Batch resolve 500 items", function()
        local resolved = 0
        local failed = 0
        do
            local si = GetSampleItems(500)
            local count = 0
            for _, item in ipairs(si) do
                if count >= 500 then break end
                -- Clone item without visualID to force runtime resolution
                local testItem = { itemID = item.itemID }
                local vid = ETB:ResolveVisualID(testItem)
                if vid and vid > 0 then
                    resolved = resolved + 1
                else
                    failed = failed + 1
                end
                count = count + 1
            end
        end
        assert(resolved > 0, "No items resolved")
    end)

    -- Verify cache is populated
    StressCall("11.M02", "Verify cache populated after batch resolve", function()
        -- Resolving same items should be instant (cached)
        local start = debugprofilestop and debugprofilestop() or 0
        do
            local si = GetSampleItems(500)
            local count = 0
            for _, item in ipairs(si) do
                if count >= 500 then break end
                ETB:ResolveVisualIDFromItemID(item.itemID)
                count = count + 1
            end
        end
        local elapsed = debugprofilestop and (debugprofilestop() - start) or 0
        -- Cached lookups should be < 10ms total for 500 items
    end)

    -- ========================================================================
    -- N) PCALL AUDIT — Verify critical paths won't crash on API failure
    -- ========================================================================
    print("  |cffc8b88a--- N: pcall Audit ---|r")

    -- Verify key functions that call Blizzard APIs don't crash even with bad data
    StressCall("11.N01", "GetSourceInfo(nil) via pcall", function()
        local ok, result = pcall(C_TransmogCollection.GetSourceInfo, nil)
        -- Should not crash regardless of result
    end)
    StressCall("11.N02", "GetSourceInfo(0) via pcall", function()
        local ok, result = pcall(C_TransmogCollection.GetSourceInfo, 0)
    end)
    StressCall("11.N03", "PlayerHasTransmog(nil) via pcall", function()
        local ok, result = pcall(C_TransmogCollection.PlayerHasTransmog, nil)
    end)
    StressCall("11.N04", "GetAllSets() via pcall", function()
        local ok, result = pcall(C_TransmogSets.GetAllSets)
    end)

    -- ========================================================================
    -- O) FAVORITES MANAGER STRESS
    -- ========================================================================
    print("  |cffc8b88a--- O: Favorites Manager Stress ---|r")

    if ETB.FavoritesManager then
        StressCall("11.O01", "FavoritesManager handles nil item", function()
            if ETB.FavoritesManager.IsFavorite then
                ETB.FavoritesManager:IsFavorite(nil)
            end
            if ETB.FavoritesManager.ToggleFavorite then
                ETB.FavoritesManager:ToggleFavorite(nil)
            end
        end)
        StressCall("11.O02", "FavoritesManager handles invalid itemID", function()
            if ETB.FavoritesManager.IsFavorite then
                ETB.FavoritesManager:IsFavorite(0)
                ETB.FavoritesManager:IsFavorite(-1)
                ETB.FavoritesManager:IsFavorite(999999999)
            end
        end)
    else
        AddResult("11.O01", "FavoritesManager stress", "SKIP", "Not loaded", "P2")
    end

    -- ========================================================================
    -- SUMMARY
    -- ========================================================================
    print(string.format("\n  |cffc8b88a--- Stress Test Summary: %d tested, %d crashes ---|r", tested, crashes))
    if crashes == 0 then
        print("  |cff00ff00✓ ADDON IS CRASH-PROOF — all edge cases handled safely.|r")
    else
        print(string.format("  |cffff4444✗ %d CRASH(ES) DETECTED — review failures above.|r", crashes))
    end
end

-- ============================================================================
-- SECTION 12: COLOR SEARCH & COLOR FILTERING
-- ============================================================================

local function TestColorSearch()
    SectionHeader(12, "COLOR SEARCH & COLOR FILTERING")
    
    -- 12.01: ColorData module loaded
    Assert("12.01", "ETB.ColorData exists",
        ETB.ColorData ~= nil, "ColorData module not loaded", "P0")
    
    -- 12.02: ColorData has data table
    Assert("12.02", "ColorData.data table exists",
        ETB.ColorData and ETB.ColorData.data ~= nil,
        "ColorData.data is nil", "P0")
    
    -- 12.03: ColorData count > 0
    local colorCount = ETB.ColorData and ETB.ColorData.count or 0
    Assert("12.03", "ColorData has entries (" .. colorCount .. ")",
        colorCount > 10000,
        "Expected 15000+ displayIDs, got " .. colorCount, "P0")
    
    -- 12.04: ColorSearch module loaded
    Assert("12.04", "ETB.ColorSearch exists",
        ETB.ColorSearch ~= nil, "ColorSearch module not loaded", "P0")
    
    -- 12.05: ColorSearch:HasData() returns true
    local hasData = ETB.ColorSearch and ETB.ColorSearch.HasData and ETB.ColorSearch:HasData()
    Assert("12.05", "ColorSearch:HasData() == true",
        hasData == true, "ColorSearch reports no data", "P0")
    
    -- 12.06: Categories defined
    local catCount = ETB.ColorSearch and ETB.ColorSearch.Categories and #ETB.ColorSearch.Categories or 0
    Assert("12.06", "ColorSearch has " .. catCount .. " categories (expect 13)",
        catCount >= 12,
        "Expected 12+ color categories, got " .. catCount, "P1")
    
    -- 12.07: CategoryByID lookup works
    local redCat = ETB.ColorSearch and ETB.ColorSearch.CategoryByID and ETB.ColorSearch.CategoryByID["RED"]
    Assert("12.07", "CategoryByID['RED'] exists",
        redCat ~= nil and redCat.r ~= nil,
        "RED category not found in lookup", "P1")
    
    -- 12.08: Parse a known displayID from data
    -- Pick a displayID that we know exists in the data
    local testDisplayID = nil
    local testRaw = nil
    if ETB.ColorData and ETB.ColorData.data then
        for did, raw in pairs(ETB.ColorData.data) do
            testDisplayID = did
            testRaw = raw
            break
        end
    end
    Assert("12.08", "Can iterate ColorData (first entry: " .. tostring(testDisplayID) .. ")",
        testDisplayID ~= nil and testRaw ~= nil,
        "Could not find any entry in ColorData.data", "P0")
    
    -- 12.09: ParseDisplayColors returns valid data
    local parsed = nil
    if testDisplayID and ETB.ColorSearch then
        parsed = ETB.ColorSearch:ParseDisplayColors(testDisplayID)
    end
    Assert("12.09", "ParseDisplayColors returns table for displayID " .. tostring(testDisplayID),
        parsed ~= nil and type(parsed) == "table" and #parsed >= 2,
        "ParseDisplayColors returned " .. tostring(parsed and #parsed or "nil") .. " colors", "P0")
    
    -- 12.10: Parsed colors have r,g,b fields (0-255)
    local validRGB = false
    if parsed and parsed[1] then
        local c = parsed[1]
        validRGB = (type(c.r) == "number" and type(c.g) == "number" and type(c.b) == "number")
        validRGB = validRGB and (c.r >= 0 and c.r <= 255 and c.g >= 0 and c.g <= 255 and c.b >= 0 and c.b <= 255)
    end
    Assert("12.10", "Parsed color has valid r,g,b (0-255)",
        validRGB,
        "Color fields not valid numbers in 0-255 range", "P1")
    
    -- 12.11: ColorDistance function works
    local dist = ETB.ColorSearch and ETB.ColorSearch:ColorDistance(255, 0, 0, 0, 0, 255)
    Assert("12.11", "ColorDistance(red, blue) = " .. tostring(dist),
        dist ~= nil and dist == 130050,  -- (255-0)^2 + (0-0)^2 + (0-255)^2 = 130050
        "Expected 130050, got " .. tostring(dist), "P1")
    
    -- 12.12: ColorDistance(same, same) = 0
    local dist0 = ETB.ColorSearch and ETB.ColorSearch:ColorDistance(100, 100, 100, 100, 100, 100)
    Assert("12.12", "ColorDistance(same, same) = 0",
        dist0 == 0,
        "Expected 0, got " .. tostring(dist0), "P1")
    
    -- 12.13: ScoreDisplay returns a number
    local score = nil
    if testDisplayID and ETB.ColorSearch then
        score = ETB.ColorSearch:ScoreDisplay(testDisplayID, 200, 40, 40)
    end
    Assert("12.13", "ScoreDisplay returns number (" .. tostring(score) .. ")",
        score ~= nil and type(score) == "number" and score >= 0,
        "ScoreDisplay returned " .. tostring(score), "P1")
    
    -- 12.14: ScoreDisplay for invalid displayID returns max distance
    local badScore = ETB.ColorSearch and ETB.ColorSearch:ScoreDisplay(99999999, 128, 128, 128)
    Assert("12.14", "ScoreDisplay(invalid) returns 195075 (max)",
        badScore == 195075,
        "Expected 195075, got " .. tostring(badScore), "P1")
    
    -- 12.15: DisplayMatchesCategory returns boolean
    local matchResult = nil
    if testDisplayID and ETB.ColorSearch then
        matchResult = ETB.ColorSearch:DisplayMatchesCategory(testDisplayID, "RED")
    end
    Assert("12.15", "DisplayMatchesCategory returns boolean",
        matchResult ~= nil and type(matchResult) == "boolean",
        "Got " .. type(tostring(matchResult)), "P1")
    
    -- 12.16: GetCategoryForDisplay returns a string or nil
    local detectedCat = nil
    if testDisplayID and ETB.ColorSearch then
        detectedCat = ETB.ColorSearch:GetCategoryForDisplay(testDisplayID)
    end
    Assert("12.16", "GetCategoryForDisplay returns category: " .. tostring(detectedCat),
        detectedCat == nil or type(detectedCat) == "string",
        "Got " .. type(tostring(detectedCat)), "P2")
    
    -- 12.17: GetSwatchColor returns r,g,b in 0-1 range
    local sr, sg, sb = nil, nil, nil
    if testDisplayID and ETB.ColorSearch then
        sr, sg, sb = ETB.ColorSearch:GetSwatchColor(testDisplayID)
    end
    Assert("12.17", "GetSwatchColor returns 0-1 RGB",
        sr ~= nil and sg ~= nil and sb ~= nil
        and sr >= 0 and sr <= 1 and sg >= 0 and sg <= 1 and sb >= 0 and sb <= 1,
        string.format("Got r=%.2f g=%.2f b=%.2f", sr or -1, sg or -1, sb or -1), "P2")
    
    -- 12.18: Parse cache populates
    local cacheSize = ETB.ColorSearch and ETB.ColorSearch.parseCacheSize or 0
    Assert("12.18", "Parse cache populated (" .. cacheSize .. " entries)",
        cacheSize > 0,
        "Cache size is 0 after parsing", "P2")
    
    -- 12.19: Test with real item via GetItemInfoInstant
    -- Try a known MoP plate helm (Helm of Wrath, displayID 16808)
    local testItemID = 16866  -- Legplates of Wrath
    local testResult = "no data"
    if ETB.ColorSearch then
        local _, _, _, _, _, _, _, _, _, _, _, _, _, _, did = GetItemInfoInstant(testItemID)
        if did and did > 0 then
            local colors = ETB.ColorSearch:ParseDisplayColors(did)
            if colors then
                testResult = string.format("displayID=%d, %d colors, c1=(%d,%d,%d)",
                    did, #colors, colors[1].r, colors[1].g, colors[1].b)
            else
                testResult = string.format("displayID=%d but no color data", did)
            end
        else
            testResult = "GetItemInfoInstant returned no displayID"
        end
    end
    Assert("12.19", "Real item color lookup: " .. testResult,
        testResult ~= "no data",
        "ColorSearch couldn't process real item", "P1")
    
    -- 12.20: ScoreItem works with real itemID
    local itemScore = ETB.ColorSearch and ETB.ColorSearch:ScoreItem(testItemID, 200, 40, 40)
    Assert("12.20", "ScoreItem(" .. testItemID .. ", red) = " .. tostring(itemScore),
        itemScore ~= nil and type(itemScore) == "number",
        "ScoreItem returned nil", "P1")
    
    -- 12.21: ItemMatchesCategory works
    local itemMatch = ETB.ColorSearch and ETB.ColorSearch:ItemMatchesCategory(testItemID, "RED")
    Assert("12.21", "ItemMatchesCategory(" .. testItemID .. ", RED) = " .. tostring(itemMatch),
        itemMatch ~= nil and type(itemMatch) == "boolean",
        "ItemMatchesCategory returned nil", "P1")
    
    -- 12.22: FilterByCategory produces a subset
    if ETB.ColorSearch then
        local testItems = {}
        local count = 0
        for did, _ in pairs(ETB.ColorData.data) do
            if count >= 100 then break end
            testItems[#testItems + 1] = { itemID = nil, displayID = did }
            count = count + 1
        end
        local filtered = ETB.ColorSearch:FilterByCategory(testItems, "RED")
        Assert("12.22", "FilterByCategory(100 items, RED) -> " .. #filtered .. " matches",
            #filtered >= 0 and #filtered <= #testItems,
            "Filtered count out of range", "P1")
    else
        Assert("12.22", "FilterByCategory test", false, "ColorSearch not loaded", "P1")
    end
    
    -- 12.23: SortByColor doesn't crash
    if ETB.ColorSearch then
        local testItems = {}
        local count = 0
        for did, _ in pairs(ETB.ColorData.data) do
            if count >= 50 then break end
            testItems[#testItems + 1] = { itemID = nil, displayID = did }
            count = count + 1
        end
        local ok, err = pcall(function()
            ETB.ColorSearch:SortByColor(testItems, 200, 40, 40)
        end)
        Assert("12.23", "SortByColor(50 items) doesn't crash",
            ok, "Error: " .. tostring(err), "P1")
    else
        Assert("12.23", "SortByColor test", false, "ColorSearch not loaded", "P1")
    end
    
    -- 12.24: All 13 categories have valid thresholds
    local allValid = true
    local badCats = {}
    if ETB.ColorSearch and ETB.ColorSearch.Categories then
        for _, cat in ipairs(ETB.ColorSearch.Categories) do
            if not cat.id or not cat.name or not cat.r or not cat.g or not cat.b or not cat.threshold then
                allValid = false
                table.insert(badCats, cat.id or "???")
            end
        end
    end
    Assert("12.24", "All categories have id/name/r/g/b/threshold",
        allValid,
        "Invalid categories: " .. table.concat(badCats, ", "), "P1")
    
    -- 12.25: Filtering module has colorFilter field
    Assert("12.25", "ETB.Filtering.filters.colorFilter exists",
        ETB.Filtering and ETB.Filtering.filters and ETB.Filtering.filters.colorFilter == nil,
        "colorFilter field missing from Filtering.filters", "P1")
    
    -- 12.26: SetColorFilter method exists
    Assert("12.26", "ETB.Filtering:SetColorFilter method exists",
        ETB.Filtering and type(ETB.Filtering.SetColorFilter) == "function",
        "SetColorFilter not a function", "P1")
    
    -- 12.27: SetColorFilter / ClearFilters cycle works
    if ETB.Filtering and ETB.Filtering.SetColorFilter then
        ETB.Filtering:SetColorFilter("BLUE")
        local set = ETB.Filtering.filters.colorFilter == "BLUE"
        ETB.Filtering:ClearFilters()
        local cleared = ETB.Filtering.filters.colorFilter == nil
        Assert("12.27", "SetColorFilter(BLUE) then ClearFilters resets to nil",
            set and cleared,
            string.format("set=%s cleared=%s", tostring(set), tostring(cleared)), "P1")
    else
        Assert("12.27", "SetColorFilter cycle", false, "Method missing", "P1")
    end
    
    -- 12.28: Parse cache eviction works (stress test)
    if ETB.ColorSearch then
        local oldMax = ETB.ColorSearch.maxCacheSize
        ETB.ColorSearch.maxCacheSize = 50  -- Temporarily shrink
        ETB.ColorSearch.parseCache = {}
        ETB.ColorSearch.parseCacheSize = 0
        
        local parsed = 0
        for did, _ in pairs(ETB.ColorData.data) do
            if parsed >= 80 then break end
            ETB.ColorSearch:ParseDisplayColors(did)
            parsed = parsed + 1
        end
        
        local finalSize = ETB.ColorSearch.parseCacheSize
        ETB.ColorSearch.maxCacheSize = oldMax  -- Restore
        
        Assert("12.28", "Cache eviction triggered (parsed 80, cache=" .. finalSize .. ", max was 50)",
            finalSize <= 55,  -- Should be ~30-50 after eviction
            "Cache grew to " .. finalSize .. " without evicting", "P1")
    else
        Assert("12.28", "Cache eviction test", false, "ColorSearch not loaded", "P1")
    end
    
    -- 12.29: Batch scoring performance (100 items < 50ms)
    if ETB.ColorSearch then
        local start = debugprofilestop()
        local count = 0
        for did, _ in pairs(ETB.ColorData.data) do
            if count >= 100 then break end
            ETB.ColorSearch:ScoreDisplay(did, 128, 64, 200)
            count = count + 1
        end
        local elapsed = debugprofilestop() - start
        Assert("12.29", string.format("Score 100 displayIDs in %.1fms (target <50ms)", elapsed),
            elapsed < 50,
            string.format("Took %.1fms", elapsed), "P2")
    else
        Assert("12.29", "Performance test", false, "ColorSearch not loaded", "P2")
    end
    
    -- 12.30: GetStats returns valid structure
    if ETB.ColorSearch then
        local stats = ETB.ColorSearch:GetStats()
        Assert("12.30", "GetStats returns valid structure",
            stats ~= nil and stats.totalDisplayIDs > 0 and stats.categories > 0,
            string.format("displayIDs=%s cats=%s cache=%s",
                tostring(stats and stats.totalDisplayIDs), tostring(stats and stats.categories),
                tostring(stats and stats.cacheSize)), "P2")
    else
        Assert("12.30", "GetStats test", false, "ColorSearch not loaded", "P2")
    end
end

-- ============================================================================
-- SECTION 13: CLASS RESTRICTIONS & MY CLASS ONLY FILTER
-- ============================================================================

local function TestClassRestrictions()
    SectionHeader(13, "CLASS RESTRICTIONS & MY CLASS ONLY FILTER")
    
    -- 13.01: ClassRestrictions module loaded
    Assert("13.01", "ETB.ClassRestrictions exists",
        ETB.ClassRestrictions ~= nil, "ClassRestrictions module not loaded", "P0")
    
    -- 13.02: GetPlayerClassID returns a number
    local playerClassID = ETB.ClassRestrictions and ETB.ClassRestrictions:GetPlayerClassID()
    Assert("13.02", "GetPlayerClassID returns " .. tostring(playerClassID),
        playerClassID ~= nil and type(playerClassID) == "number" and playerClassID >= 1 and playerClassID <= 11,
        "Expected classID 1-11, got " .. tostring(playerClassID), "P0")
    
    -- 13.03: IsItemUsableByPlayer(nil) returns true (safe default)
    local nilResult = ETB.ClassRestrictions and ETB.ClassRestrictions:IsItemUsableByPlayer(nil)
    Assert("13.03", "IsItemUsableByPlayer(nil) = true (safe default)",
        nilResult == true, "Got " .. tostring(nilResult), "P1")
    
    -- 13.04: IsItemUsableByClass nil-safe
    local nilClass = ETB.ClassRestrictions and ETB.ClassRestrictions:IsItemUsableByClass(nil, nil)
    Assert("13.04", "IsItemUsableByClass(nil, nil) = true",
        nilClass == true, "Got " .. tostring(nilClass), "P1")
    
    -- 13.05: Test known class-restricted item (16866 = Warrior T2 Legplates of Wrath)
    local is16866Restricted = ETB.ClassRestrictions and ETB.ClassRestrictions:GetItemClassRestrictions(16866)
    if is16866Restricted then
        Assert("13.05", "Item 16866 class restrictions: {" .. table.concat(is16866Restricted, ",") .. "}",
            true, "", "P1")
    else
        Assert("13.05", "Item 16866 class restrictions: nil (no ATT data or unrestricted)",
            true, "This may be expected if ATT data isn't loaded for this item", "P2")
    end
    
    -- 13.06: Test IsItemUsableByClass with known restriction
    if is16866Restricted then
        -- 16866 should be Warrior-only ({1})
        local usableByWarrior = ETB.ClassRestrictions:IsItemUsableByClass(16866, 1)
        local usableByMage = ETB.ClassRestrictions:IsItemUsableByClass(16866, 8)
        Assert("13.06", "Warrior T2 usable by Warrior=" .. tostring(usableByWarrior) .. " Mage=" .. tostring(usableByMage),
            usableByWarrior == true and usableByMage == false,
            string.format("Warrior=%s Mage=%s", tostring(usableByWarrior), tostring(usableByMage)), "P1")
    else
        Assert("13.06", "Class restriction check (skipped: no ATT data for item)", true, "", "P2")
    end
    
    -- 13.07: FormatRestriction returns string or nil
    local formatted = ETB.ClassRestrictions and ETB.ClassRestrictions:FormatRestriction(16866)
    Assert("13.07", "FormatRestriction(16866) = " .. tostring(formatted),
        formatted == nil or type(formatted) == "string",
        "Got type " .. type(tostring(formatted)), "P2")
    
    -- 13.08: PickBestAlternative nil-safe
    local bestNil = ETB.ClassRestrictions and ETB.ClassRestrictions:PickBestAlternative(nil)
    Assert("13.08", "PickBestAlternative(nil) returns nil",
        bestNil == nil, "Got " .. tostring(bestNil), "P1")
    
    -- 13.09: PickBestAlternative with single item
    local singleResult = ETB.ClassRestrictions and ETB.ClassRestrictions:PickBestAlternative({{itemID = 12345, quality = 3, isCollected = true}})
    Assert("13.09", "PickBestAlternative([1 item]) returns that item",
        singleResult ~= nil and singleResult.itemID == 12345,
        "Got " .. tostring(singleResult and singleResult.itemID), "P1")
    
    -- 13.10: PickBestAlternative prefers collected
    local alt1 = {itemID = 11111, quality = 2, isCollected = false}
    local alt2 = {itemID = 22222, quality = 4, isCollected = true}
    local bestCollected = ETB.ClassRestrictions and ETB.ClassRestrictions:PickBestAlternative({alt1, alt2})
    Assert("13.10", "PickBestAlternative prefers collected item",
        bestCollected ~= nil and bestCollected.itemID == 22222,
        "Got " .. tostring(bestCollected and bestCollected.itemID), "P1")
    
    -- 13.11: HasUsableAlternative nil-safe
    local hasUsableNil = ETB.ClassRestrictions and ETB.ClassRestrictions:HasUsableAlternative(nil)
    Assert("13.11", "HasUsableAlternative(nil) = false",
        hasUsableNil == false, "Got " .. tostring(hasUsableNil), "P1")
    
    -- 13.12: Filtering has myClassOnly field
    Assert("13.12", "ETB.Filtering.filters.myClassOnly exists",
        ETB.Filtering and ETB.Filtering.filters and ETB.Filtering.filters.myClassOnly ~= nil,
        "myClassOnly field missing", "P1")
    
    -- 13.13: SetMyClassOnly method exists
    Assert("13.13", "ETB.Filtering:SetMyClassOnly method exists",
        ETB.Filtering and type(ETB.Filtering.SetMyClassOnly) == "function",
        "SetMyClassOnly not a function", "P1")
    
    -- 13.14: Cache works
    if ETB.ClassRestrictions then
        ETB.ClassRestrictions:ClearCache()
        local before = ETB.ClassRestrictions.cacheSize
        ETB.ClassRestrictions:GetItemClassRestrictions(16866)
        ETB.ClassRestrictions:GetItemClassRestrictions(16914)
        ETB.ClassRestrictions:GetItemClassRestrictions(16900)
        local after = ETB.ClassRestrictions.cacheSize
        Assert("13.14", "Cache populated: " .. before .. " -> " .. after,
            after >= before, "Cache didn't grow", "P2")
    else
        Assert("13.14", "Cache test", false, "ClassRestrictions not loaded", "P2")
    end
    
    -- 13.15: ClearCache works
    if ETB.ClassRestrictions then
        ETB.ClassRestrictions:ClearCache()
        Assert("13.15", "ClearCache resets to 0",
            ETB.ClassRestrictions.cacheSize == 0, "Size = " .. ETB.ClassRestrictions.cacheSize, "P2")
    else
        Assert("13.15", "ClearCache test", false, "ClassRestrictions not loaded", "P2")
    end
end

-- ============================================================================
-- REPORT GENERATION
-- ============================================================================

local function GenerateReport()
    local lines = {}
    local _, playerClass = UnitClass("player")
    local playerName = UnitName("player")
    local realmName = GetRealmName()

    table.insert(lines, "╔══════════════════════════════════════════════════════════════╗")
    table.insert(lines, "║  ETB v12.34.0 — Automated Test Report                      ║")
    table.insert(lines, "╠══════════════════════════════════════════════════════════════╣")
    table.insert(lines, string.format("║  Character: %s-%s (%s)", playerName or "?", realmName or "?", playerClass or "?"))
    table.insert(lines, string.format("║  Date: %s", date("%Y-%m-%d %H:%M:%S")))
    table.insert(lines, string.format("║  Duration: %.1f seconds", GetTime() - AutoTest.startTime))
    table.insert(lines, string.format("║  Results: %d PASS | %d FAIL | %d WARN | %d SKIP",
        AutoTest.totalPass, AutoTest.totalFail, AutoTest.totalWarn, AutoTest.totalSkip))
    table.insert(lines, "╚══════════════════════════════════════════════════════════════╝")
    table.insert(lines, "")

    -- Failures first
    local failures = {}
    local warnings = {}
    for _, r in ipairs(AutoTest.results) do
        if r.status == "FAIL" then table.insert(failures, r) end
        if r.status == "WARN" then table.insert(warnings, r) end
    end

    if #failures > 0 then
        table.insert(lines, "=== FAILURES (" .. #failures .. ") ===")
        for _, r in ipairs(failures) do
            table.insert(lines, string.format("  [%s] %s: %s — %s", r.priority, r.id, r.name, r.detail or ""))
        end
        table.insert(lines, "")
    end

    if #warnings > 0 then
        table.insert(lines, "=== WARNINGS (" .. #warnings .. ") ===")
        for _, r in ipairs(warnings) do
            table.insert(lines, string.format("  [%s] %s: %s — %s", r.priority, r.id, r.name, r.detail or ""))
        end
        table.insert(lines, "")
    end

    -- Full results
    table.insert(lines, "=== FULL RESULTS ===")
    for _, r in ipairs(AutoTest.results) do
        local mark = r.status == "PASS" and "✓" or r.status == "FAIL" and "✗" or r.status == "WARN" and "⚠" or "—"
        table.insert(lines, string.format("  %s [%s] %s: %s%s",
            mark, r.priority, r.id, r.name,
            (r.detail and r.detail ~= "") and (" | " .. r.detail) or ""))
    end

    return table.concat(lines, "\n")
end

-- ============================================================================
-- EXPORT (copy to clipboard via editbox)
-- ============================================================================

local function ShowExportFrame(text)
    local f = CreateFrame("Frame", "ETBTestExport", UIParent, "BackdropTemplate")
    f:SetSize(700, 500)
    f:SetPoint("CENTER")
    f:SetBackdrop({ bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background", edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border", edgeSize = 16, insets = { left = 4, right = 4, top = 4, bottom = 4 } })
    f:SetFrameStrata("DIALOG")
    f:EnableMouse(true)
    f:SetMovable(true)
    f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", f.StartMoving)
    f:SetScript("OnDragStop", f.StopMovingOrSizing)

    local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("TOP", 0, -12)
    title:SetText("|cffc8b88aETB Test Report — Ctrl+A then Ctrl+C to copy|r")

    local close = CreateFrame("Button", nil, f, "UIPanelCloseButton")
    close:SetPoint("TOPRIGHT", -2, -2)

    local scroll = CreateFrame("ScrollFrame", nil, f, "UIPanelScrollFrameTemplate")
    scroll:SetPoint("TOPLEFT", 14, -40)
    scroll:SetPoint("BOTTOMRIGHT", -30, 14)

    local eb = CreateFrame("EditBox", nil, scroll)
    eb:SetMultiLine(true)
    eb:SetFontObject("ChatFontNormal")
    eb:SetWidth(640)
    eb:SetAutoFocus(false)
    eb:SetText(text)
    eb:HighlightText()
    scroll:SetScrollChild(eb)

    f:Show()
end

-- ============================================================================
-- RUNNER
-- ============================================================================

local function RunTests(mode)
    AutoTest.results = {}
    AutoTest.totalPass = 0
    AutoTest.totalFail = 0
    AutoTest.totalWarn = 0
    AutoTest.totalSkip = 0
    AutoTest.startTime = GetTime()
    AutoTest.isRunning = true

    print("\n|cffc8b88a╔══════════════════════════════════════════════════╗|r")
    print("|cffc8b88a║  ETB v12.34.0 — Automated Test Suite            ║|r")
    print("|cffc8b88a╠══════════════════════════════════════════════════╣|r")
    print(string.format("|cffc8b88a║  Mode: %s|r", mode or "full"))
    print(string.format("|cffc8b88a║  Character: %s (%s)|r", UnitName("player") or "?", select(2, UnitClass("player")) or "?"))
    print("|cffc8b88a╚══════════════════════════════════════════════════╝|r")

    local sections = {
        { num = 1, fn = TestStartup },
        { num = 2, fn = TestMainBrowser },
        { num = 3, fn = TestGrouping },
        { num = 4, fn = TestCollectionTracking },
        { num = 5, fn = TestCommunitySets },
        { num = 6, fn = TestBlizzardSetsAndSources },
        { num = 7, fn = TestRandomOutfit },
        { num = 8, fn = TestTransmogUI },
        { num = 9, fn = TestBossLootTooltip },
        { num = 10, fn = TestVisualIDResolution },
        { num = 11, fn = TestStress },
        { num = 12, fn = TestColorSearch },
        { num = 13, fn = TestClassRestrictions },
    }

    for _, sec in ipairs(sections) do
        if mode == "quick" and sec.num > 5 then break end
        if mode and mode:match("^%d+$") and sec.num ~= tonumber(mode) then
            -- Skip non-matching sections
        else
            local ok, err = pcall(sec.fn)
            if not ok then
                AddResult(sec.num .. ".ERR", "Section crashed: " .. tostring(err), "FAIL", nil, "P0")
            end
        end
    end

    -- Summary
    local elapsed = GetTime() - AutoTest.startTime
    print("\n|cffc8b88a══════ SUMMARY ══════|r")
    print(string.format("  |cff00ff00PASS: %d|r  |cffff4444FAIL: %d|r  |cffffff00WARN: %d|r  |cff888888SKIP: %d|r  |cff888888(%.1fs)|r",
        AutoTest.totalPass, AutoTest.totalFail, AutoTest.totalWarn, AutoTest.totalSkip, elapsed))

    if AutoTest.totalFail == 0 then
        print("  |cff00ff00✓ ALL TESTS PASSED! Ready for beta testing.|r")
    else
        print(string.format("  |cffff4444✗ %d FAILURES detected. Review results above.|r", AutoTest.totalFail))
    end

    -- Save results
    ETBTestResults = {
        report = GenerateReport(),
        timestamp = time(),
        character = UnitName("player"),
        class = select(2, UnitClass("player")),
        pass = AutoTest.totalPass,
        fail = AutoTest.totalFail,
        warn = AutoTest.totalWarn,
        skip = AutoTest.totalSkip,
    }

    print("\n  |cff888888Type /etbtest report to view full report|r")
    print("  |cff888888Type /etbtest export to copy report to clipboard|r")

    AutoTest.isRunning = false
end

-- ============================================================================
-- SLASH COMMAND
-- ============================================================================

SLASH_ETBTEST1 = "/etbtest"
SlashCmdList["ETBTEST"] = function(msg)
    if not ETB.debug then
        print("|cffc8b88a[ETB]|r Test suite requires debug mode. Type |cff00ccff/etb debug|r to enable.")
        return
    end
    msg = (msg or ""):lower():trim()

    if msg == "" or msg == "full" then
        RunTests("full")
    elseif msg == "quick" then
        RunTests("quick")
    elseif msg:match("^section%s+(%d+)$") or msg:match("^(%d+)$") then
        local num = msg:match("(%d+)")
        RunTests(num)
    elseif msg == "report" then
        if ETBTestResults and ETBTestResults.report then
            print(ETBTestResults.report)
        else
            print("|cffc8b88aNo test results yet. Run /etbtest first.|r")
        end
    elseif msg == "export" then
        if ETBTestResults and ETBTestResults.report then
            ShowExportFrame(ETBTestResults.report)
        else
            print("|cffc8b88aNo test results yet. Run /etbtest first.|r")
        end
    else
        print("|cffc8b88aETB AutoTest v1.0|r")
        print("  /etbtest           — Run full test suite (all sections)")
        print("  /etbtest quick     — Run P0 blockers only (sections 1-5)")
        print("  /etbtest section N — Run specific section (1-13)")
        print("  /etbtest report    — Show last report in chat")
        print("  /etbtest export    — Open copyable report window")
        print("  |cff888888Sections: 1=Startup 2=Browser 3=Grouping 4=Collection|r")
        print("  |cff888888  5=CommSets 6=BlizzSets 7=Outfit 8=TransmogUI|r")
        print("  |cff888888  9=BossLoot 10=VisualID 11=STRESS 12=ColorSearch 13=ClassRestrict|r")
    end
end

ETB:Debug("AutoTest", "AutoTest.lua loaded — /etbtest to run")
