-- WoWTetris Automated Test System
-- Tests all classes, ultimates, racials, sabotages, and shop items
-- Logs any errors or missing functions

local addonName, addon = ...

addon.TestSystem = {}
local TS = addon.TestSystem

-- Test results storage
TS.results = {
    passed = 0,
    failed = 0,
    errors = {},
    warnings = {},
    tested = {}
}

-- Initialize test environment
function TS:InitTestEnvironment()
    -- Save original state
    self.originalState = {
        selectedClass = addon.selectedClass,
        selectedRace = addon.selectedRace,
        selectedUltimate = addon.selectedUltimate,
        testMode = addon.testMode,
        gameState = addon.gameState and {
            score = addon.gameState.score,
            level = addon.gameState.level,
            isPlaying = addon.gameState.isPlaying,
        }
    }
    
    -- Enable test mode
    addon.testMode = true
    
    -- Reset test-related variables
    addon.forcedPieceCount = 0
    addon.forcedPieceType = nil
    addon.timedEffects = {}
    
    -- Initialize db if needed (for GetClassStats)
    addon.db = addon.db or {}
    addon.db.classStats = addon.db.classStats or {}
    
    -- Set a test class for functions that need it
    addon.selectedClass = addon.selectedClass or "WARRIOR"
    
    -- Ensure board constants are set
    addon.BOARD_WIDTH = addon.BOARD_WIDTH or 10
    addon.BOARD_HEIGHT = addon.BOARD_HEIGHT or 20
    
    -- Initialize game state - ALWAYS ensure rows/cols are set
    addon.gameState = addon.gameState or {}
    addon.gameState.board = {}
    addon.gameState.rows = 20
    addon.gameState.cols = 10
    addon.gameState.score = addon.gameState.score or 1000
    addon.gameState.level = addon.gameState.level or 5
    addon.gameState.isPlaying = true
    addon.gameState.isPaused = false
    addon.gameState.currentPiece = {x = 5, y = 10, type = "T", rotation = 0}
    addon.gameState.nextPieces = {"I", "O", "T"}
    addon.gameState.heldPiece = nil
    addon.gameState.canHold = true
    addon.gameState.combo = 0
    addon.gameState.gameTime = 60
    addon.gameState.totalLines = 10
    addon.gameState.scoreMultiplier = 1.0
    addon.gameState.speedMultiplier = 1.0
    
    -- Initialize empty board
    for r = 1, 20 do
        addon.gameState.board[r] = {}
        for c = 1, 10 do
            addon.gameState.board[r][c] = nil
        end
    end
    
    -- Add some test blocks
    for c = 1, 8 do
        addon.gameState.board[20][c] = {type = "I"}
        addon.gameState.board[19][c] = {type = "O"}
    end
    
    -- Add some garbage blocks for testing
    addon.gameState.board[18][1] = {type = "G", isGarbage = true}
    addon.gameState.board[18][2] = {type = "G", isGarbage = true}
    
    print("|cff00ff00[TestSystem]|r Test environment initialized")
end

-- Restore original state
function TS:RestoreState()
    if self.originalState then
        addon.selectedClass = self.originalState.selectedClass
        addon.selectedRace = self.originalState.selectedRace
        addon.selectedUltimate = self.originalState.selectedUltimate
        addon.testMode = self.originalState.testMode
    end
end

-- Log error
function TS:LogError(category, item, error)
    table.insert(self.results.errors, {
        category = category,
        item = item,
        error = error,
        timestamp = date("%H:%M:%S")
    })
    self.results.failed = self.results.failed + 1
    -- Only show in chat if verbose mode (errors will show in popup)
    if self.verboseMode then
        print("|cffff0000[TEST FAILED]|r " .. category .. ": " .. item .. " - " .. error)
    end
end

-- Log warning
function TS:LogWarning(category, item, warning)
    table.insert(self.results.warnings, {
        category = category,
        item = item,
        warning = warning
    })
    -- Warnings not shown in chat
end

-- Log success
function TS:LogSuccess(category, item)
    self.results.passed = self.results.passed + 1
    table.insert(self.results.tested, category .. ": " .. item)
    -- Success not shown in chat (too spammy)
end

-- Safe function call with error handling
function TS:SafeCall(func, ...)
    local success, result = pcall(func, ...)
    return success, result
end

-- Test if a function exists
function TS:FunctionExists(funcName)
    local parts = {strsplit(":", funcName)}
    if #parts == 2 then
        local obj = parts[1] == "addon" and addon or _G[parts[1]]
        return obj and type(obj[parts[2]]) == "function"
    end
    return type(addon[funcName]) == "function"
end

-- ============================================================
-- TEST: All Ultimate Functions
-- ============================================================
function TS:TestUltimateFunctions()
    local ultFunctions = {
        {name = "Ult_ClearBottom", args = {2}, desc = "Clear bottom X rows"},
        {name = "Ult_ClearTop", args = {2}, desc = "Clear top X rows"},
        {name = "Ult_ClearRandomRows", args = {2}, desc = "Clear X random rows"},
        {name = "Ult_DestroyRandom", args = {5}, desc = "Destroy X random blocks"},
        {name = "Ult_ClearCurrentColumn", args = {}, desc = "Clear column where piece is"},
        {name = "Ult_ClearCurrentRow", args = {}, desc = "Clear row where piece is"},
        {name = "Ult_ClearColumns", args = {2}, desc = "Clear X random columns"},
        {name = "Ult_AreaClear", args = {3}, desc = "Clear XxX area around piece"},
        {name = "Ult_Earthquake", args = {}, desc = "Make all blocks fall to fill gaps"},
        {name = "Ult_ClearCross", args = {}, desc = "Clear row AND column at piece"},
        {name = "Ult_ClearMostFilled", args = {2}, desc = "Clear X most-filled rows"},
        {name = "Ult_ClearIncompleteRows", args = {}, desc = "Clear incomplete rows from bottom"},
        {name = "Ult_ClearAlternateRows", args = {}, desc = "Clear every other row"},
        {name = "Ult_FillGaps", args = {3}, desc = "Fill gaps in bottom X rows"},
        {name = "Ult_FillAllGaps", args = {}, desc = "Fill ALL single-block gaps"},
        {name = "Ult_WildGrowth", args = {}, desc = "Complete rows >70% filled"},
        {name = "Ult_TransformToI", args = {}, desc = "Transform current piece to I-piece"},
        {name = "Ult_TransformPiece", args = {}, desc = "Transform piece to random type"},
        {name = "Ult_FreeSwap", args = {}, desc = "Free hold swap"},
        {name = "Ult_ChooseNextPieces", args = {3}, desc = "Set next X pieces to I-pieces"},
        {name = "Ult_VerticalStrike", args = {4}, desc = "Clear vertical area"},
        {name = "Ult_DestroyByType", args = {}, desc = "Destroy all of 1 random block type"},
        {name = "Ult_DeathAndDecay", args = {}, desc = "Clear 1 row/sec over time"},
        {name = "Ult_SoulReaper", args = {}, desc = "If board >60% full, clear rows"},
        {name = "Ult_ArmyOfDead", args = {}, desc = "Clear blocks over time"},
        {name = "Ult_Ascendance", args = {}, desc = "Clear rows + columns"},
        {name = "Ult_ShatterAdjacent", args = {}, desc = "Shatter blocks touching piece"},
        {name = "Ult_CorruptRows", args = {3}, desc = "Clear rows over time"},
        {name = "Ult_Doom", args = {}, desc = "Delayed massive clear"},
        {name = "Ult_SwipeRows", args = {2}, desc = "Clear rows at piece height"},
        {name = "Ult_Starfall", args = {}, desc = "Clear blocks over time"},
        {name = "Ult_Incarnation", args = {}, desc = "Clear all except bottom rows"},
        {name = "Ult_WakeOfAshes", args = {}, desc = "Clear rows + set next piece"},
        {name = "Ult_PlaceBomb", args = {3}, desc = "Place bomb that explodes on land"},
        {name = "Ult_Avatar", args = {}, desc = "Supercharge next 3 pieces"},
        {name = "Ult_ClearRow", args = {10}, desc = "Clear specific row number"},
        {name = "Ult_ClearColumn", args = {5}, desc = "Clear specific column number"},
        {name = "Ult_StormEarthFire", args = {}, desc = "3 clones clear row/col/row"},
        {name = "Ult_Metamorphosis", args = {}, desc = "Transform + area clear"},
        {name = "Ult_DeepBreath", args = {}, desc = "Clear middle rows"},
        {name = "Ult_RectClear", args = {2, 3}, desc = "Clear rectangular area"},
        {name = "Ult_RemoveGarbageBlocks", args = {}, desc = "Remove all garbage blocks"},
        {name = "Ult_ClearAdjacentColumns", args = {}, desc = "Clear columns near piece"},
        {name = "Ult_SetForcedPieces", args = {3, "I"}, desc = "Force next X pieces to be type"},
        {name = "Ult_SetTimedEffect", args = {"test", 5}, desc = "Set temporary effect"},
        {name = "Ult_StartDoT", args = {2, 3}, desc = "Clear blocks over time"},
        {name = "Ult_ClearSequentialRows", args = {3}, desc = "Clear rows one by one"},
        {name = "Ult_ClearCenterColumns", args = {3}, desc = "Clear center columns"},
        {name = "Ult_ClearAllButTop", args = {3}, desc = "Clear all except top X rows"},
        {name = "Ult_ClearAllButBottom", args = {3}, desc = "Clear all except bottom X rows"},
        {name = "Ult_ConvokeSpirits", args = {}, desc = "8 random effects"},
        {name = "Ult_TargetedAreaClear", args = {4}, desc = "Clear area at piece position"},
    }
    
    for _, func in ipairs(ultFunctions) do
        if not addon[func.name] then
            self:LogError("Missing Function", func.name, 
                "The function '" .. func.name .. "' does not exist.\n" ..
                "This function should: " .. func.desc .. "\n" ..
                "Add this function to Classes.lua")
        else
            local success, err = self:SafeCall(addon[func.name], addon, unpack(func.args))
            if success then
                self:LogSuccess("Ultimate Function", func.name)
            else
                self:LogError("Function Error", func.name,
                    "Function crashed when called.\n" ..
                    "What it does: " .. func.desc .. "\n" ..
                    "Lua Error: " .. tostring(err))
            end
        end
    end
end

-- ============================================================
-- TEST: All Classes and Their Ultimates
-- ============================================================
function TS:TestClasses()
    local allClasses = {"WARRIOR", "PALADIN", "HUNTER", "ROGUE", "PRIEST", 
                        "DEATHKNIGHT", "SHAMAN", "MAGE", "WARLOCK", "DRUID",
                        "MONK", "DEMONHUNTER", "EVOKER"}
    
    for _, className in ipairs(allClasses) do
        local classData = addon.CLASSES[className]
        
        -- Check class exists
        if not classData then
            self:LogError("Class Missing", className, 
                "The class '" .. className .. "' is not defined in addon.CLASSES table.\n" ..
                "Fix: Add " .. className .. " to the CLASSES table in Classes.lua")
        else
            self:LogSuccess("Class Definition", className)
        end
        
        -- Check class has ultimates
        local ultimates = addon.ULTIMATES[className]
        if not ultimates then
            self:LogError("Class Has No Ultimates", className,
                "Class '" .. className .. "' has no ultimates defined.\n" ..
                "Fix: Add " .. className .. " = { ... } to the ULTIMATES table in Classes.lua")
        else
            for i, ult in ipairs(ultimates) do
                local ultName = ult.name or ("Ultimate #" .. i)
                
                -- Check ultimate has required fields
                if not ult.name then
                    self:LogError("Ultimate Missing Name", className .. " Ultimate #" .. i,
                        "Ultimate #" .. i .. " for " .. className .. " has no name.\n" ..
                        "Fix: Add 'name = \"Ability Name\"' to the ultimate definition")
                elseif not ult.icon then
                    self:LogError("Ultimate Missing Icon", className .. ": " .. ultName,
                        "Ultimate '" .. ultName .. "' for " .. className .. " has no icon.\n" ..
                        "Fix: Add 'icon = \"Interface\\\\Icons\\\\IconName\"' to the ultimate")
                elseif not ult.description then
                    self:LogError("Ultimate Missing Description", className .. ": " .. ultName,
                        "Ultimate '" .. ultName .. "' for " .. className .. " has no description.\n" ..
                        "Fix: Add 'description = \"What this does\"' to the ultimate")
                elseif not ult.execute then
                    self:LogError("Ultimate Missing Execute", className .. ": " .. ultName,
                        "Ultimate '" .. ultName .. "' for " .. className .. " has no execute function!\n" ..
                        "This ultimate will do NOTHING when used.\n" ..
                        "Fix: Add 'execute = function() WoWTetris:Ult_Something() end' to the ultimate")
                else
                    -- Test execute function
                    addon.selectedClass = className
                    local success, err = self:SafeCall(ult.execute)
                    if success then
                        self:LogSuccess("Ultimate Works", className .. ": " .. ultName)
                    else
                        self:LogError("Ultimate Crashes", className .. ": " .. ultName,
                            "Ultimate '" .. ultName .. "' crashes when activated!\n" ..
                            "Class: " .. className .. "\n" ..
                            "Unlock Level: " .. (ult.unlockLevel or "?") .. "\n" ..
                            "Lua Error: " .. tostring(err) .. "\n" ..
                            "Fix: Check the execute function in Classes.lua")
                    end
                end
            end
        end
        
        -- Check class has PvP abilities
        local pvpAbilities = addon.PVP_ABILITIES and addon.PVP_ABILITIES[className]
        if not pvpAbilities then
            self:LogWarning("No PvP Abilities", className, "No PvP sabotage abilities defined")
        else
            self:LogSuccess("PvP Abilities", className .. " (" .. #pvpAbilities .. " abilities)")
        end
    end
end

-- ============================================================
-- TEST: All PvP Sabotage Abilities
-- ============================================================
function TS:TestPvPSabotages()
    if not addon.PVP_ABILITIES then
        self:LogError("PvP System Missing", "addon.PVP_ABILITIES",
            "The PVP_ABILITIES table does not exist!\n" ..
            "All PvP sabotages are broken.\n" ..
            "Fix: Add 'addon.PVP_ABILITIES = { ... }' to Multiplayer.lua")
        return
    end
    
    -- Check if sabotage handler exists
    if not addon.ApplySabotageEffect then
        self:LogError("PvP System Missing", "ApplySabotageEffect",
            "The ApplySabotageEffect function does not exist!\n" ..
            "PvP sabotages won't actually do anything.\n" ..
            "Fix: Add ApplySabotageEffect function to Multiplayer.lua")
    end
    
    local allClasses = {"WARRIOR", "PALADIN", "HUNTER", "ROGUE", "PRIEST", 
                        "DEATHKNIGHT", "SHAMAN", "MAGE", "WARLOCK", "DRUID",
                        "MONK", "DEMONHUNTER", "EVOKER"}
    
    -- Expected sabotage effect types that should be handled
    local knownEffects = {
        "garbage", "instantDrop", "blackout", "hidePreview", "reverseControls",
        "disableRotation", "disableHold", "ultimateDisabled", "scatter",
        "pushUp", "shuffle", "transformPiece"
    }
    
    for _, className in ipairs(allClasses) do
        local abilities = addon.PVP_ABILITIES[className]
        
        if not abilities then
            self:LogError("PvP Abilities Missing", className,
                "Class '" .. className .. "' has no PvP sabotage abilities!\n" ..
                "Fix: Add " .. className .. " = { ... } to PVP_ABILITIES in Multiplayer.lua")
        else
            for i, ability in ipairs(abilities) do
                local abilityName = ability.name or ("Ability #" .. i)
                
                -- Check required fields
                if not ability.name then
                    self:LogError("PvP Ability Missing Name", className .. " #" .. i,
                        "PvP ability #" .. i .. " for " .. className .. " has no name.\n" ..
                        "Fix: Add 'name = \"Ability Name\"' to the ability")
                elseif not ability.icon then
                    self:LogError("PvP Ability Missing Icon", className .. ": " .. abilityName,
                        "PvP ability '" .. abilityName .. "' has no icon.\n" ..
                        "Will show blank square in PvP UI.\n" ..
                        "Fix: Add 'icon = \"Interface\\\\Icons\\\\IconName\"'")
                elseif not ability.description then
                    self:LogError("PvP Ability Missing Description", className .. ": " .. abilityName,
                        "PvP ability '" .. abilityName .. "' has no description.\n" ..
                        "Players won't know what it does.\n" ..
                        "Fix: Add 'description = \"What this does to opponent\"'")
                else
                    -- Check if description mentions known effects
                    local desc = ability.description:lower()
                    local hasKnownEffect = false
                    
                    -- Check for common effect keywords
                    local effectKeywords = {
                        "garbage", "row", "instant", "drop", "black", "hide", "preview",
                        "reverse", "control", "rotation", "hold", "ultimate", "disable",
                        "scatter", "block", "push", "shuffle", "transform", "freeze", "stun"
                    }
                    
                    for _, keyword in ipairs(effectKeywords) do
                        if desc:find(keyword) then
                            hasKnownEffect = true
                            break
                        end
                    end
                    
                    if not hasKnownEffect then
                        self:LogWarning("PvP Ability Unknown Effect", className .. ": " .. abilityName,
                            "Can't determine what effect this ability has.\n" ..
                            "Description: " .. ability.description)
                    end
                    
                    self:LogSuccess("PvP Ability Defined", className .. ": " .. abilityName)
                end
                
                -- Check unlock level
                if not ability.unlockLevel then
                    self:LogWarning("PvP Ability No Level", className .. ": " .. abilityName,
                        "No unlock level specified, defaults to 1")
                end
            end
            
            -- Check we have expected number of abilities (should be 5 per class)
            if #abilities < 5 then
                self:LogWarning("PvP Abilities Incomplete", className,
                    "Only " .. #abilities .. " PvP abilities defined (expected 5)")
            elseif #abilities > 5 then
                self:LogSuccess("PvP Abilities", className .. " has " .. #abilities .. " abilities (extra!)")
            end
        end
    end
    
    -- Test sabotage effect application
    local testEffects = {
        {name = "blackout", duration = 3, desc = "Hide opponent's board"},
        {name = "hidePreview", duration = 5, desc = "Hide next piece preview"},
        {name = "reverseControls", duration = 4, desc = "Swap left/right controls"},
        {name = "disableRotation", duration = 5, desc = "Can't rotate pieces"},
        {name = "disableHold", duration = 8, desc = "Can't hold pieces"},
        {name = "ultimateDisabled", duration = 10, desc = "Can't use ultimate"},
    }
    
    if addon.ApplySabotageEffect then
        for _, effect in ipairs(testEffects) do
            local success, err = self:SafeCall(addon.ApplySabotageEffect, addon, effect.name, effect.duration)
            if success then
                self:LogSuccess("Sabotage Effect Works", effect.name .. " (" .. effect.desc .. ")")
            else
                self:LogError("Sabotage Effect Crashes", effect.name,
                    "Effect '" .. effect.name .. "' crashes when applied!\n" ..
                    "What it does: " .. effect.desc .. "\n" ..
                    "Lua Error: " .. tostring(err) .. "\n" ..
                    "Fix: Check ApplySabotageEffect in Multiplayer.lua")
            end
        end
    end
    
    -- Test helper functions used by sabotages (just check they exist, don't call them to avoid side effects)
    local sabotageHelpers = {
        {name = "AddGarbageRow", desc = "Add garbage row to board", critical = true},
        {name = "ForceInstantDrop", desc = "Force current piece to drop instantly", critical = true},
        {name = "ScatterBlocks", desc = "Scatter random blocks on board", critical = false},
        {name = "ShuffleBoard", desc = "Shuffle blocks on board", critical = false},
        {name = "PushBoardUp", desc = "Push all blocks up", critical = false},
    }
    
    for _, helper in ipairs(sabotageHelpers) do
        if addon[helper.name] then
            self:LogSuccess("Sabotage Helper Exists", helper.name)
        else
            if helper.critical then
                self:LogError("Sabotage Helper Missing", helper.name,
                    "Critical function '" .. helper.name .. "' does not exist!\n" ..
                    "Many sabotages will be broken.\n" ..
                    "What it should do: " .. helper.desc)
            else
                self:LogWarning("Sabotage Helper Missing", helper.name,
                    "Optional function '" .. helper.name .. "' does not exist.\n" ..
                    "What it should do: " .. helper.desc)
            end
        end
    end
end

-- ============================================================
-- TEST: All Races and Their Abilities
-- ============================================================
function TS:TestRaces()
    if not addon.RACES then
        self:LogError("Races System Missing", "addon.RACES",
            "The RACES table does not exist!\n" ..
            "Fix: Add 'addon.RACES = { ... }' to Classes.lua")
        return
    end
    
    local expectedRaces = {"HUMAN", "NIGHTELF", "DWARF", "GNOME", "DRAENEI", "WORGEN",
                           "ORC", "UNDEAD", "TAUREN", "TROLL", "BLOODELF", "GOBLIN", 
                           "PANDAREN", "DRACTHYR"}
    
    for _, raceName in ipairs(expectedRaces) do
        local raceData = addon.RACES[raceName]
        
        if not raceData then
            self:LogError("Race Missing", raceName,
                "Race '" .. raceName .. "' is not defined.\n" ..
                "Fix: Add " .. raceName .. " = { ... } to addon.RACES in Classes.lua")
        else
            -- Check required fields
            if not raceData.name then
                self:LogError("Race Missing Name", raceName,
                    "Race '" .. raceName .. "' has no display name.\n" ..
                    "Fix: Add 'name = \"" .. raceName:sub(1,1) .. raceName:sub(2):lower() .. "\"' to the race")
            end
            
            if not raceData.icon then
                self:LogError("Race Missing Icon", raceName,
                    "Race '" .. raceName .. "' has no icon - will show blank square.\n" ..
                    "Fix: Add 'icon = \"Interface\\\\Icons\\\\Achievement_Character_" .. raceName:sub(1,1) .. raceName:sub(2):lower() .. "_Male\"'")
            end
            
            if not raceData.ability then
                self:LogError("Race Has No Ability", raceName,
                    "Race '" .. raceName .. "' has no racial ability!\n" ..
                    "Fix: Add 'ability = { name = \"...\", description = \"...\" }' to the race")
            else
                if not raceData.ability.name then
                    self:LogError("Racial Ability Missing Name", raceName,
                        "Racial ability for '" .. raceName .. "' has no name.\n" ..
                        "Fix: Add 'name = \"Ability Name\"' inside the ability table")
                end
                if not raceData.ability.description then
                    self:LogError("Racial Ability Missing Description", raceName,
                        "Racial ability for '" .. raceName .. "' has no description.\n" ..
                        "Fix: Add 'description = \"What this does\"' inside the ability table")
                end
            end
            
            if not raceData.abilityType then
                self:LogError("Race Missing Type", raceName,
                    "Race '" .. raceName .. "' doesn't specify if ability is PvP or Solo.\n" ..
                    "Fix: Add 'abilityType = \"pvp\"' or 'abilityType = \"solo\"' to the race")
            end
            
            if not raceData.classes or #raceData.classes == 0 then
                self:LogError("Race Has No Classes", raceName,
                    "Race '" .. raceName .. "' has no class restrictions - can't be selected!\n" ..
                    "Fix: Add 'classes = {\"WARRIOR\", \"MAGE\", ...}' to the race")
            end
            
            -- Test racial execution
            if raceData.ability and raceData.abilityType == "solo" then
                addon.selectedRace = raceName
                if not addon.ExecuteRacialEffect then
                    self:LogError("Racial System Broken", "ExecuteRacialEffect",
                        "The function ExecuteRacialEffect does not exist!\n" ..
                        "All solo racial abilities are broken.\n" ..
                        "Fix: Add ExecuteRacialEffect function to Multiplayer.lua")
                else
                    local success, err = self:SafeCall(addon.ExecuteRacialEffect, addon, raceName)
                    if success then
                        self:LogSuccess("Racial Works", raceName .. ": " .. (raceData.ability.name or "Unknown"))
                    else
                        self:LogError("Racial Crashes", raceName .. ": " .. (raceData.ability.name or "Unknown"),
                            "Racial ability crashes when activated!\n" ..
                            "Race: " .. raceName .. "\n" ..
                            "Ability: " .. (raceData.ability.name or "Unknown") .. "\n" ..
                            "Type: " .. (raceData.abilityType or "Unknown") .. "\n" ..
                            "Lua Error: " .. tostring(err) .. "\n" ..
                            "Fix: Check ExecuteRacialEffect in Multiplayer.lua for this race")
                    end
                end
            elseif raceData.abilityType == "pvp" then
                -- PvP racial - verify handler exists
                if not addon.HandleRacialSabotage then
                    self:LogError("PvP Racial System Broken", "HandleRacialSabotage",
                        "HandleRacialSabotage function missing!\n" ..
                        "All PvP racial abilities are broken.\n" ..
                        "Fix: Add HandleRacialSabotage function to Multiplayer.lua")
                else
                    self:LogSuccess("PvP Racial", raceName .. ": " .. (raceData.ability and raceData.ability.name or "Unknown"))
                end
            end
        end
    end
end

-- ============================================================
-- TEST: Shop Items
-- ============================================================
function TS:TestShopItems()
    if not addon.SHOP_ITEMS then
        self:LogError("Shop System Missing", "addon.SHOP_ITEMS",
            "The SHOP_ITEMS table does not exist!\n" ..
            "The entire shop system is broken.\n" ..
            "Fix: Add 'addon.SHOP_ITEMS = { ultimates = {...}, sabotage = {...} }' to Shop.lua")
        return
    end
    
    -- Test shop ultimates
    if addon.SHOP_ITEMS.ultimates then
        for _, item in ipairs(addon.SHOP_ITEMS.ultimates) do
            if not item.id then
                self:LogError("Shop Ultimate Missing ID", item.name or "Unknown",
                    "Shop ultimate has no ID - can't be purchased!\n" ..
                    "Fix: Add 'id = \"ult_xxx_#\"' to the item")
            elseif not item.name then
                self:LogError("Shop Ultimate Missing Name", item.id,
                    "Shop ultimate '" .. item.id .. "' has no display name.\n" ..
                    "Fix: Add 'name = \"Ability Name\"' to the item")
            elseif not item.forClass then
                self:LogError("Shop Ultimate No Class", item.name,
                    "Shop ultimate '" .. item.name .. "' has no class restriction!\n" ..
                    "It won't appear in any class's ultimate list.\n" ..
                    "Fix: Add 'forClass = \"WARRIOR\"' (or other class) to the item")
            else
                -- Test execution
                addon.selectedClass = item.forClass
                if not addon.ExecuteShopUltimate then
                    self:LogError("Shop System Broken", "ExecuteShopUltimate",
                        "ExecuteShopUltimate function missing!\n" ..
                        "All shop ultimates are broken.\n" ..
                        "Fix: Add ExecuteShopUltimate function to Classes.lua")
                else
                    local success, err = self:SafeCall(addon.ExecuteShopUltimate, addon, item.id)
                    if success then
                        self:LogSuccess("Shop Ultimate Works", item.forClass .. ": " .. item.name)
                    else
                        self:LogError("Shop Ultimate Crashes", item.forClass .. ": " .. item.name,
                            "Shop ultimate crashes when activated!\n" ..
                            "ID: " .. item.id .. "\n" ..
                            "Class: " .. item.forClass .. "\n" ..
                            "Cost: " .. (item.cost or "?") .. " " .. (item.currency or "coins") .. "\n" ..
                            "Lua Error: " .. tostring(err) .. "\n" ..
                            "Fix: Check ExecuteShopUltimate in Classes.lua for ID '" .. item.id .. "'")
                    end
                end
            end
        end
    else
        self:LogWarning("No Shop Ultimates", "SHOP_ITEMS.ultimates", "No shop ultimates defined")
    end
    
    -- Test shop sabotages
    if addon.SHOP_ITEMS.sabotage then
        for _, item in ipairs(addon.SHOP_ITEMS.sabotage) do
            if not item.id then
                self:LogError("Shop Sabotage Missing ID", item.name or "Unknown",
                    "Shop sabotage has no ID.\n" ..
                    "Fix: Add 'id = \"sab_xxx_#\"' to the item")
            elseif not item.name then
                self:LogError("Shop Sabotage Missing Name", item.id,
                    "Shop sabotage '" .. item.id .. "' has no name.\n" ..
                    "Fix: Add 'name = \"Ability Name\"' to the item")
            else
                self:LogSuccess("Shop Sabotage", item.name)
            end
        end
    end
    
    -- Test shop classes
    if addon.SHOP_ITEMS.classes then
        for _, item in ipairs(addon.SHOP_ITEMS.classes) do
            if not addon.CLASSES[item.id] then
                self:LogError("Shop Class Invalid", item.id,
                    "Shop class '" .. item.id .. "' doesn't exist in CLASSES table!\n" ..
                    "Players can buy it but can't play it.\n" ..
                    "Fix: Add " .. item.id .. " to addon.CLASSES in Classes.lua")
            else
                self:LogSuccess("Shop Class", item.name or item.id)
            end
        end
    end
end

-- ============================================================
-- TEST: Timed Effects System
-- ============================================================
function TS:TestTimedEffects()
    -- Test SetTimedEffect
    if not addon.Ult_SetTimedEffect then
        self:LogError("Timed Effects", "Ult_SetTimedEffect", "Function missing")
        return
    end
    
    -- Test HasTimedEffect
    if not addon.HasTimedEffect then
        self:LogError("Timed Effects", "HasTimedEffect", "Function missing")
        return
    end
    
    -- Set a test effect
    addon:Ult_SetTimedEffect("testEffect", 10)
    
    -- Check if it's active
    if addon:HasTimedEffect("testEffect") then
        self:LogSuccess("Timed Effects", "Set and check working")
    else
        self:LogError("Timed Effects", "HasTimedEffect", "Effect not detected after setting")
    end
    
    -- Test effect that should not exist
    if addon:HasTimedEffect("nonExistentEffect") then
        self:LogError("Timed Effects", "HasTimedEffect", "False positive for non-existent effect")
    else
        self:LogSuccess("Timed Effects", "Non-existent effect correctly returns false")
    end
end

-- ============================================================
-- TEST: Forced Pieces System
-- ============================================================
function TS:TestForcedPieces()
    -- Reset forced pieces
    addon.forcedPieceCount = 0
    addon.forcedPieceType = nil
    
    -- Test SetForcedPieces
    if not addon.Ult_SetForcedPieces then
        self:LogError("Forced Pieces", "Ult_SetForcedPieces", "Function missing")
        return
    end
    
    addon:Ult_SetForcedPieces(5, "I")
    
    if addon.forcedPieceCount == 5 and addon.forcedPieceType == "I" then
        self:LogSuccess("Forced Pieces", "SetForcedPieces working")
    else
        self:LogError("Forced Pieces", "SetForcedPieces", "Values not set correctly")
    end
    
    -- Test GetNextPiece respects forced pieces
    if addon.GetNextPiece then
        local piece = addon:GetNextPiece()
        if piece == "I" then
            self:LogSuccess("Forced Pieces", "GetNextPiece returns forced type")
        else
            self:LogError("Forced Pieces", "GetNextPiece", "Did not return forced piece type, got: " .. tostring(piece))
        end
        
        -- Check count decreased
        if addon.forcedPieceCount == 4 then
            self:LogSuccess("Forced Pieces", "Count decrements correctly")
        else
            self:LogError("Forced Pieces", "Count", "Expected 4, got: " .. tostring(addon.forcedPieceCount))
        end
    end
    
    -- Reset
    addon.forcedPieceCount = 0
    addon.forcedPieceType = nil
end

-- ============================================================
-- VALIDATION TEST HELPERS
-- These functions verify that abilities do what they claim
-- ============================================================

-- Count total blocks on board
function TS:CountBlocks()
    local state = addon.gameState
    local count = 0
    for r = 1, state.rows do
        for c = 1, state.cols do
            if state.board[r][c] then
                count = count + 1
            end
        end
    end
    return count
end

-- Count blocks in a specific row
function TS:CountBlocksInRow(row)
    local state = addon.gameState
    local count = 0
    for c = 1, state.cols do
        if state.board[row][c] then
            count = count + 1
        end
    end
    return count
end

-- Count blocks in a specific column
function TS:CountBlocksInColumn(col)
    local state = addon.gameState
    local count = 0
    for r = 1, state.rows do
        if state.board[r][col] then
            count = count + 1
        end
    end
    return count
end

-- Check if row is completely empty
function TS:IsRowEmpty(row)
    return self:CountBlocksInRow(row) == 0
end

-- Check if row is completely full
function TS:IsRowFull(row)
    return self:CountBlocksInRow(row) == addon.gameState.cols
end

-- Count garbage blocks
function TS:CountGarbageBlocks()
    local state = addon.gameState
    local count = 0
    for r = 1, state.rows do
        for c = 1, state.cols do
            if state.board[r][c] and state.board[r][c].isGarbage then
                count = count + 1
            end
        end
    end
    return count
end

-- Fill board with test pattern (for validation)
function TS:FillTestBoard(pattern)
    local state = addon.gameState
    
    -- Ensure rows and cols are set
    state.rows = state.rows or 20
    state.cols = state.cols or 10
    
    -- Ensure board exists
    if not state.board then
        state.board = {}
    end
    
    -- Clear board first - ensure each row is a fresh table
    for r = 1, state.rows do
        state.board[r] = {}
        for c = 1, state.cols do
            state.board[r][c] = nil
        end
    end
    
    if pattern == "full" then
        -- Fill entire board (200 blocks for 20x10)
        for r = 1, state.rows do
            for c = 1, state.cols do
                state.board[r][c] = {type = "T"}
            end
        end
    elseif pattern == "bottom_half" then
        -- Fill bottom 10 rows (100 blocks)
        for r = 11, state.rows do
            for c = 1, state.cols do
                state.board[r][c] = {type = "T"}
            end
        end
    elseif pattern == "scattered" then
        -- Random scattered blocks
        for r = 10, state.rows do
            for c = 1, state.cols do
                if math.random() > 0.5 then
                    state.board[r][c] = {type = "T"}
                end
            end
        end
    elseif pattern == "garbage" then
        -- Fill bottom 6 rows with garbage blocks (60 blocks)
        for r = 15, state.rows do
            for c = 1, state.cols do
                state.board[r][c] = {type = "G", isGarbage = true}
            end
        end
    elseif pattern == "gaps" then
        -- Rows with gaps in middle
        for r = 15, state.rows do
            for c = 1, state.cols do
                if c ~= 5 and c ~= 6 then  -- Leave gaps in middle
                    state.board[r][c] = {type = "T"}
                end
            end
        end
    elseif pattern == "full_sparse" then
        -- Fill EVERY row with some blocks (not complete rows to avoid line clears)
        -- This ensures ultimates that target top/middle/bottom all have blocks to clear
        for r = 1, state.rows do
            for c = 1, state.cols do
                -- Fill 7 out of 10 columns per row (leaves gaps to prevent line clears)
                if c <= 7 then
                    state.board[r][c] = {type = "T"}
                end
            end
        end
    end
end

-- ============================================================
-- VALIDATION: Test Ultimate Effects Match Descriptions
-- ============================================================
function TS:ValidateUltimates()
    local validationErrors = {}
    
    -- Disable automatic line clearing during validation to get accurate counts
    addon.skipLineClear = true
    
    -- Test: Ult_ClearBottom(n) - should clear exactly n bottom rows
    self:FillTestBoard("bottom_half")
    local beforeCount = self:CountBlocks()  -- Should be 100
    addon:Ult_ClearBottom(3)
    local afterCount = self:CountBlocks()
    local expectedCleared = 3 * addon.gameState.cols  -- 3 rows * 10 cols = 30
    local actualCleared = beforeCount - afterCount
    if actualCleared < 25 or actualCleared > 35 then
        table.insert(validationErrors, {
            name = "Ult_ClearBottom(3)",
            expected = "Clear ~30 blocks (3 rows)",
            actual = "Cleared " .. actualCleared .. " blocks",
            severity = "ERROR"
        })
    end
    
    -- Test: Ult_ClearTop(n)
    self:FillTestBoard("bottom_half")
    beforeCount = self:CountBlocks()
    addon:Ult_ClearTop(2)
    afterCount = self:CountBlocks()
    actualCleared = beforeCount - afterCount
    -- Top 2 rows of bottom_half are empty, so this should clear 0 in that pattern
    -- Use scattered instead
    self:FillTestBoard("scattered")
    beforeCount = self:CountBlocks()
    addon:Ult_ClearTop(2)
    afterCount = self:CountBlocks()
    actualCleared = beforeCount - afterCount
    -- Just check it doesn't crash and clears something reasonable
    if actualCleared < 0 then
        table.insert(validationErrors, {
            name = "Ult_ClearTop(2)",
            expected = "Clear top 2 rows",
            actual = "Cleared " .. actualCleared .. " blocks (negative?)",
            severity = "ERROR"
        })
    end
    
    -- Test: Ult_DestroyRandom(n) - should destroy exactly n blocks
    self:FillTestBoard("bottom_half")
    beforeCount = self:CountBlocks()
    addon:Ult_DestroyRandom(15)
    afterCount = self:CountBlocks()
    actualCleared = beforeCount - afterCount
    if actualCleared < 12 or actualCleared > 18 then
        table.insert(validationErrors, {
            name = "Ult_DestroyRandom(15)",
            expected = "Destroy ~15 blocks",
            actual = "Destroyed " .. actualCleared .. " blocks",
            severity = "ERROR"
        })
    end
    
    -- Test: Ult_AreaClear(n) - should clear n x n area
    self:FillTestBoard("bottom_half")
    addon.gameState.currentPiece = {x = 5, y = 15, type = "T", rotation = 0}  -- Position in filled area
    beforeCount = self:CountBlocks()
    addon:Ult_AreaClear(5)
    afterCount = self:CountBlocks()
    actualCleared = beforeCount - afterCount
    if actualCleared < 15 or actualCleared > 30 then
        table.insert(validationErrors, {
            name = "Ult_AreaClear(5)",
            expected = "Clear ~25 blocks (5x5 area)",
            actual = "Cleared " .. actualCleared .. " blocks",
            severity = "WARNING"
        })
    end
    
    -- Re-enable line clearing
    addon.skipLineClear = false
    
    -- Test: Ult_RemoveGarbageBlocks - should remove ALL garbage
    self:FillTestBoard("garbage")
    local garbageBefore = self:CountGarbageBlocks()
    addon:Ult_RemoveGarbageBlocks()
    local garbageAfter = self:CountGarbageBlocks()
    if garbageAfter ~= 0 then
        table.insert(validationErrors, {
            name = "Ult_RemoveGarbageBlocks",
            expected = "Remove ALL garbage blocks",
            actual = garbageAfter .. " garbage blocks remain",
            severity = "ERROR"
        })
    end
    
    -- Test: Ult_FillGaps(n) - should fill gaps in bottom n rows
    -- Just verify it runs without crashing - the actual gap filling is hard to test
    -- because it depends on ClearLines behavior
    self:FillTestBoard("gaps")
    local success, err = self:SafeCall(addon.Ult_FillGaps, addon, 6)
    if not success then
        table.insert(validationErrors, {
            name = "Ult_FillGaps(6)",
            expected = "Function should run without error",
            actual = "Error: " .. tostring(err),
            severity = "ERROR"
        })
    end
    
    -- Test: Ult_SetForcedPieces - should set forced pieces
    addon.forcedPieceCount = 0
    addon.forcedPieceType = nil
    addon:Ult_SetForcedPieces(5, "I")
    if addon.forcedPieceCount ~= 5 or addon.forcedPieceType ~= "I" then
        table.insert(validationErrors, {
            name = "Ult_SetForcedPieces(5, 'I')",
            expected = "Set 5 forced I-pieces",
            actual = "Count: " .. tostring(addon.forcedPieceCount) .. ", Type: " .. tostring(addon.forcedPieceType),
            severity = "ERROR"
        })
    end
    
    -- Test: Ult_StartDoT - should clear blocks over time
    self:FillTestBoard("full")
    beforeCount = self:CountBlocks()
    addon:Ult_StartDoT(3, 3)  -- 3 blocks/sec for 3 seconds = 9 blocks total
    -- Note: DoT is async, we just verify it starts without error
    
    -- Test: Timed effects
    addon.timedEffects = {}
    addon:Ult_SetTimedEffect("testBonus", 10)
    if not addon:HasTimedEffect("testBonus") then
        table.insert(validationErrors, {
            name = "Ult_SetTimedEffect",
            expected = "Effect should be active",
            actual = "Effect not detected",
            severity = "ERROR"
        })
    end
    
    return validationErrors
end

-- ============================================================
-- VALIDATION: Test All Class Ultimates Match Their Descriptions
-- ============================================================
function TS:ValidateClassUltimates()
    local errors = {}
    
    -- Disable automatic line clearing during validation to get accurate counts
    addon.skipLineClear = true
    
    for className, ultimates in pairs(addon.ULTIMATES) do
        for i, ult in ipairs(ultimates) do
            local desc = ult.description:lower()
            
            -- TEST 1: Icon/Texture exists
            if not ult.icon or ult.icon == "" then
                table.insert(errors, {
                    class = className,
                    ultimate = ult.name,
                    problem = "Missing icon",
                    details = "Ultimate has no icon defined"
                })
            end
            
            -- Parse expected effect from description
            local expectedRows = desc:match("clear (%d+) row") or desc:match("(%d+) row")
            -- Skip "all of X" patterns like "Destroy all of 1 random block type"
            local expectedBlocks = nil
            if not desc:match("all of %d+") then
                expectedBlocks = desc:match("destroy (%d+)") or desc:match("clear (%d+) block") or desc:match("(%d+) random block")
            end
            local expectedArea = desc:match("(%d+)x%1 area") or desc:match("(%d+)x(%d+)")
            local expectedFill = desc:match("fill") and true or false
            local expectedIPiece = desc:match("i%-piece") and true or false
            
            -- Check for timed effect abilities
            local expectedTimedEffect = desc:match("for (%d+) sec") or desc:match("(%d+) seconds")
            local expectedImmune = desc:match("immune") or desc:match("no blocks land")
            local expectedSlow = desc:match("slow")
            local expectedPause = desc:match("pause") or desc:match("freeze")
            
            -- Skip DoT/timed abilities from block clear checks (they clear over time, not instantly)
            local isDoT = desc:match("per sec") or desc:match("/sec") or desc:match("over time") or desc:match("for %d+ sec")
            
            if not isDoT then
                -- Skip FILL abilities from block clear checks
                local isFillAbility = desc:match("fill") and not desc:match("most%-filled")
                
                -- Skip conditional abilities that may not trigger
                local isConditional = desc:match("if ") or desc:match("when ")
                
                -- Use full board pattern to test ALL areas (top, middle, bottom)
                self:FillTestBoard("full_sparse")
                local beforeBlocks = self:CountBlocks()
                
                -- Reset forced piece tracking
                addon.forcedPieceCount = 0
                addon.forcedPieceType = nil
                
                -- Save nextPieces state
                local nextPiecesBefore = {}
                if addon.gameState.nextPieces then
                    for idx, p in ipairs(addon.gameState.nextPieces) do
                        nextPiecesBefore[idx] = p
                    end
                end
                
                addon.gameState.currentPiece = {x = 5, y = 10, type = "T", rotation = 0}
                
                -- Execute the ultimate
                local success, err = self:SafeCall(ult.execute)
                
                if not success then
                    table.insert(errors, {
                        class = className,
                        ultimate = ult.name,
                        problem = "Crashes on execution",
                        details = tostring(err)
                    })
                else
                    local afterBlocks = self:CountBlocks()
                    local blocksCleared = beforeBlocks - afterBlocks
                    
                    -- Check if nextPieces were modified (alternative I-piece system)
                    local nextPiecesChanged = false
                    local iPiecesSet = 0
                    if addon.gameState.nextPieces then
                        for idx, p in ipairs(addon.gameState.nextPieces) do
                            if p == "I" and (not nextPiecesBefore[idx] or nextPiecesBefore[idx] ~= "I") then
                                iPiecesSet = iPiecesSet + 1
                                nextPiecesChanged = true
                            end
                        end
                    end
                    
                    -- Validate based on description (skip fill and conditional abilities)
                    if expectedRows and not isFillAbility and not isConditional then
                        local rowCount = tonumber(expectedRows)
                        local expectedCleared = rowCount * 10  -- Approximate
                        -- Allow 50% tolerance for partial rows, area clears, etc.
                        if blocksCleared < expectedCleared * 0.3 then
                            table.insert(errors, {
                                class = className,
                                ultimate = ult.name,
                                problem = "Clears fewer blocks than described",
                                details = "Description says '" .. expectedRows .. " rows' but only cleared " .. blocksCleared .. " blocks"
                            })
                        end
                    end
                    
                    if expectedBlocks then
                        local blockCount = tonumber(expectedBlocks)
                        -- Allow 50% tolerance
                        if blocksCleared < blockCount * 0.5 or blocksCleared > blockCount * 2 then
                            table.insert(errors, {
                                class = className,
                                ultimate = ult.name,
                                problem = "Block count mismatch",
                                details = "Description says '" .. expectedBlocks .. " blocks' but cleared " .. blocksCleared
                            })
                        end
                    end
                    
                    -- Check I-pieces (both systems)
                    -- Skip "transform" abilities - they transform the CURRENT piece, not set NEXT pieces
                    local isTransform = desc:match("transform") and desc:match("piece")
                    if expectedIPiece and not isTransform then
                        local hasForcedPieces = addon.forcedPieceCount > 0 and addon.forcedPieceType == "I"
                        local hasNextPiecesSet = iPiecesSet > 0
                        
                        if not hasForcedPieces and not hasNextPiecesSet then
                            table.insert(errors, {
                                class = className,
                                ultimate = ult.name,
                                problem = "Should set I-pieces but didn't",
                                details = "Description mentions I-pieces but neither forcedPieceCount nor nextPieces were set"
                            })
                        end
                    end
                end
            end  -- end if not isDoT
        end
    end
    
    -- Re-enable line clearing
    addon.skipLineClear = false
    
    return errors
end

-- ============================================================
-- VALIDATION: Test Shop Ultimates (including timed effects like Darkness)
-- ============================================================
function TS:ValidateShopUltimates()
    local errors = {}
    
    if not addon.SHOP_ULTIMATES then return errors end
    
    for _, item in ipairs(addon.SHOP_ULTIMATES) do
        local desc = item.description:lower()
        
        -- Check icon exists
        if not item.icon or item.icon == "" then
            table.insert(errors, {
                class = item.forClass or "UNKNOWN",
                ability = item.name,
                problem = "Missing icon",
                details = "Shop ultimate has no icon defined"
            })
        end
        
        -- Check for timed effect abilities
        local expectedDuration = desc:match("for (%d+) sec") or desc:match("(%d+) seconds")
        local expectedImmune = desc:match("immune") or desc:match("no blocks land")
        local expectedSlow = desc:match("slow")
        
        if expectedDuration then
            -- This is a timed effect ability - test it sets the effect
            addon.timedEffects = {}
            
            -- Execute the shop ultimate
            if addon.ExecuteShopUltimate then
                local success, err = self:SafeCall(addon.ExecuteShopUltimate, addon, item.id)
                
                if not success then
                    table.insert(errors, {
                        class = item.forClass or "UNKNOWN",
                        ability = item.name,
                        problem = "Crashes on execution",
                        details = tostring(err)
                    })
                else
                    -- Check if a timed effect was set
                    local hasTimedEffect = false
                    if addon.timedEffects then
                        for k, v in pairs(addon.timedEffects) do
                            hasTimedEffect = true
                            break
                        end
                    end
                    
                    if not hasTimedEffect then
                        table.insert(errors, {
                            class = item.forClass or "UNKNOWN",
                            ability = item.name,
                            problem = "Timed effect not applied",
                            details = "Description says '" .. expectedDuration .. " sec' but no timed effect was set. The ability does nothing!"
                        })
                    end
                end
            end
        end
    end
    
    return errors
end

-- ============================================================
-- VALIDATION: Test Piece Textures (check for blank/missing)
-- ============================================================
function TS:ValidatePieceTextures()
    local errors = {}
    
    -- Known-good texture prefixes that exist in all WoW versions
    local knownGoodPrefixes = {
        "Ability_", "Spell_", "INV_", "Achievement_", "ClassIcon_",
        "ability_", "spell_", "inv_"
    }
    
    -- Textures known to NOT exist (add problematic ones here)
    local knownBadTextures = {
        "Spell_Fire_FelImmolation",  -- Doesn't exist in retail
        "Spell_Fire_FelFlameRing",   -- Doesn't exist - DH O-piece was blank
    }
    
    -- Check all classes have valid piece textures
    local allClasses = {"WARRIOR", "PALADIN", "HUNTER", "ROGUE", "PRIEST", 
                        "DEATHKNIGHT", "SHAMAN", "MAGE", "WARLOCK", "DRUID",
                        "MONK", "DEMONHUNTER", "EVOKER"}
    
    local pieceTypes = {"I", "O", "T", "S", "Z", "J", "L"}
    
    for _, className in ipairs(allClasses) do
        if addon.CLASS_PIECE_TEXTURES and addon.CLASS_PIECE_TEXTURES[className] then
            local textures = addon.CLASS_PIECE_TEXTURES[className]
            for _, pieceType in ipairs(pieceTypes) do
                local texture = textures[pieceType]
                if not texture or texture == "" then
                    table.insert(errors, {
                        class = className,
                        ability = "Piece " .. pieceType,
                        problem = "Missing texture",
                        details = "No texture defined for " .. pieceType .. " piece"
                    })
                elseif texture:match("^Interface\\Icons\\$") or texture == "Interface\\Icons\\" then
                    table.insert(errors, {
                        class = className,
                        ability = "Piece " .. pieceType,
                        problem = "Empty texture path",
                        details = "Texture path is incomplete: " .. texture
                    })
                else
                    -- Check against known bad textures
                    for _, badTex in ipairs(knownBadTextures) do
                        if texture:find(badTex) then
                            table.insert(errors, {
                                class = className,
                                ability = "Piece " .. pieceType,
                                problem = "Known broken texture",
                                details = "Texture '" .. badTex .. "' doesn't exist in WoW. Will show blank!"
                            })
                        end
                    end
                    
                    -- Check for suspicious lowercase ability_ prefixes (may not exist)
                    local iconName = texture:match("Interface\\Icons\\(.+)")
                    if iconName then
                        local hasGoodPrefix = false
                        for _, prefix in ipairs(knownGoodPrefixes) do
                            if iconName:sub(1, #prefix) == prefix then
                                hasGoodPrefix = true
                                break
                            end
                        end
                        if not hasGoodPrefix then
                            -- Not an error, just a warning we can't show
                            -- table.insert(errors, {...}) 
                        end
                    end
                end
            end
        else
            table.insert(errors, {
                class = className,
                ability = "All Pieces",
                problem = "No textures defined",
                details = "CLASS_PIECE_TEXTURES[" .. className .. "] is missing entirely"
            })
        end
    end
    
    return errors
end

-- ============================================================
-- VALIDATION: Test PvP Sabotage Effects Match Descriptions
-- ============================================================
function TS:ValidatePvPSabotages()
    local errors = {}
    
    if not addon.PVP_ABILITIES then return errors end
    
    local allClasses = {"WARRIOR", "PALADIN", "HUNTER", "ROGUE", "PRIEST", 
                        "DEATHKNIGHT", "SHAMAN", "MAGE", "WARLOCK", "DRUID",
                        "MONK", "DEMONHUNTER", "EVOKER"}
    
    for _, className in ipairs(allClasses) do
        local abilities = addon.PVP_ABILITIES[className]
        if not abilities then abilities = {} end
        
        for i, ability in ipairs(abilities) do
            local desc = ability.description:lower()
            local abilityName = ability.name or ("Ability #" .. i)
            
            -- Parse expected effects from description
            local expectedGarbage = desc:match("(%d+) garbage")
            local expectedScatter = desc:match("scatter (%d+)") or desc:match("(%d+) block")
            local expectedInstantDrop = desc:match("instant drop") or desc:match("force.*drop")
            local expectedBlackout = desc:match("hide") or desc:match("black")
            local expectedDisable = desc:match("disable")
            local expectedReverse = desc:match("reverse")
            local expectedPush = desc:match("push (%d+)")
            local expectedShuffle = desc:match("shuffle")
            local expectedDuration = desc:match("(%d+) sec") or desc:match("(%d+)s")
            
            -- Set up test environment
            self:FillTestBoard("bottom_half")
            local beforeBlocks = self:CountBlocks()
            local beforeGarbage = self:CountGarbageBlocks()
            addon.pvpEffects = {}
            
            -- We can't fully test PvP sabotages without a mock opponent,
            -- but we can validate the description makes sense
            local issues = {}
            
            -- Check for conflicting or unclear descriptions
            if expectedGarbage then
                local garbageCount = tonumber(expectedGarbage)
                if garbageCount > 1 then
                    table.insert(issues, "Garbage rows should be capped at 1 (description says " .. garbageCount .. ")")
                end
            end
            
            if expectedScatter then
                local scatterCount = tonumber(expectedScatter)
                if scatterCount > 4 then
                    table.insert(issues, "Scattered blocks should be capped at 4 (description says " .. scatterCount .. ")")
                end
            end
            
            -- Check if description mentions freeze/stun (should be instant drop now)
            if desc:match("freeze") or desc:match("stun") then
                if not expectedInstantDrop then
                    table.insert(issues, "Description mentions freeze/stun but should use 'instant drop' instead (per rebalance)")
                end
            end
            
            -- Check if description mentions speed effects (removed in rebalance)
            if desc:match("speed") and desc:match("faster") then
                table.insert(issues, "Description mentions speed increase but speed effects were removed in rebalance")
            end
            
            -- Check duration is reasonable (15s+ is concerning for PvP)
            if expectedDuration then
                local duration = tonumber(expectedDuration)
                if duration and duration > 15 then
                    table.insert(issues, "Duration of " .. duration .. "s may be too long for balanced PvP")
                end
            end
            
            -- Report any issues found
            for _, issue in ipairs(issues) do
                table.insert(errors, {
                    class = className,
                    ability = abilityName,
                    problem = "Description/Balance Issue",
                    details = issue
                })
            end
        end
    end
    
    return errors
end

-- ============================================================
-- VALIDATION: Test Racial Abilities Match Descriptions
-- ============================================================
function TS:ValidateRacials()
    local errors = {}
    
    if not addon.RACES then return errors end
    
    for raceName, raceData in pairs(addon.RACES) do
        if raceData.ability and raceData.abilityType == "solo" then
            local desc = raceData.ability.description:lower()
            local abilityName = raceData.ability.name or "Unknown"
            
            -- Set up test environment
            self:FillTestBoard("bottom_half")
            local beforeBlocks = self:CountBlocks()
            addon.forcedPieceCount = 0
            addon.forcedPieceType = nil
            addon.timedEffects = {}
            
            -- Save ultimate charge state for Blood Elf test
            local chargeUsedBefore = addon.ultimateChargeUsed or 0
            
            -- Execute the racial
            if addon.ExecuteRacialEffect then
                local success, err = self:SafeCall(addon.ExecuteRacialEffect, addon, raceName)
                
                if success then
                    local afterBlocks = self:CountBlocks()
                    local blocksCleared = beforeBlocks - afterBlocks
                    
                    -- Validate based on description
                    local expectedRows = desc:match("clear.-(%d+) row")
                    local expectedBlocks = desc:match("(%d+) block")
                    local expectedGarbageRemoval = desc:match("garbage") and desc:match("remove")
                    local expectedIPieces = desc:match("(%d+).-i%-piece")
                    local expectedFill = desc:match("fill")
                    local expectedScore = desc:match("score") or desc:match("%%")
                    local expectedCharge = desc:match("charge") and desc:match("ultimate")
                    local expectedFullCharge = desc:match("fully charge")
                    local expectedSlow = desc:match("slow") and desc:match("drop")
                    local expectedFaster = desc:match("faster") and desc:match("charge")
                    
                    -- Check garbage removal
                    if expectedGarbageRemoval then
                        self:FillTestBoard("garbage")
                        local garbageBefore = self:CountGarbageBlocks()
                        addon:ExecuteRacialEffect(raceName)
                        local garbageAfter = self:CountGarbageBlocks()
                        if garbageAfter > 0 and garbageBefore > 0 then
                            table.insert(errors, {
                                class = raceName,
                                ability = abilityName,
                                problem = "Doesn't remove all garbage",
                                details = "Description says remove garbage but " .. garbageAfter .. " remain"
                            })
                        end
                    end
                    
                    -- Check I-pieces
                    if expectedIPieces then
                        local count = tonumber(expectedIPieces)
                        if addon.forcedPieceCount ~= count or addon.forcedPieceType ~= "I" then
                            table.insert(errors, {
                                class = raceName,
                                ability = abilityName,
                                problem = "I-pieces not set correctly",
                                details = "Expected " .. count .. " I-pieces, got " .. tostring(addon.forcedPieceCount)
                            })
                        end
                    end
                    
                    -- Check Blood Elf "fully charge ultimate" (special case - not a timed effect)
                    if expectedFullCharge then
                        local chargeUsedAfter = addon.ultimateChargeUsed or 0
                        local cooldownAfter = addon.ultimateCooldown or 999
                        -- Blood Elf sets ultimateCooldown = 0 AND ultimateChargeUsed to a negative value
                        -- Either check should pass
                        local cooldownCleared = cooldownAfter == 0
                        local chargeModified = chargeUsedAfter < chargeUsedBefore
                        
                        if not cooldownCleared and not chargeModified then
                            table.insert(errors, {
                                class = raceName,
                                ability = abilityName,
                                problem = "Ultimate not fully charged",
                                details = "Description says 'fully charge ultimate' but neither cooldown (=" .. tostring(cooldownAfter) .. ") nor charge was modified"
                            })
                        end
                        -- Skip timed effect check for Blood Elf
                    elseif expectedScore or expectedSlow or expectedFaster then
                        -- Check timed effects for score/slow/charge speed bonuses
                        local hasTimedEffect = false
                        if addon.timedEffects then
                            for k, v in pairs(addon.timedEffects) do
                                hasTimedEffect = true
                                break
                            end
                        end
                        if not hasTimedEffect then
                            table.insert(errors, {
                                class = raceName,
                                ability = abilityName,
                                problem = "Timed effect not applied",
                                details = "Description mentions score/slow/charge bonus but no timed effect set"
                            })
                        end
                    end
                else
                    table.insert(errors, {
                        class = raceName,
                        ability = abilityName,
                        problem = "Crashes on execution",
                        details = tostring(err)
                    })
                end
            end
        end
    end
    
    return errors
end

-- ============================================================
-- TEST: Core Game Functions
-- ============================================================
function TS:TestCoreFunctions()
    -- Functions that need no arguments
    local noArgFunctions = {
        "GetDropSpeed", "GetTimeScoreMultiplier", "GetAffixScoreMultiplier",
        "GetUltimateCharge", "GetUltimateChargeRequired", "IsUltimateCharged",
        "ApplyGravity", "RenderBoard", "AddGarbageRow"
    }
    
    for _, funcName in ipairs(noArgFunctions) do
        if addon[funcName] then
            local success, err = self:SafeCall(addon[funcName], addon)
            if success then
                self:LogSuccess("Core Function", funcName)
            else
                self:LogError("Core Function", funcName, tostring(err))
            end
        else
            self:LogError("Core Function", funcName, "Function does not exist")
        end
    end
    
    -- Functions that need specific arguments
    -- GetClassStats(classKey, raceKey)
    if addon.GetClassStats then
        local success, err = self:SafeCall(addon.GetClassStats, addon, "WARRIOR", nil)
        if success then
            self:LogSuccess("Core Function", "GetClassStats")
        else
            self:LogError("Core Function", "GetClassStats", tostring(err))
        end
    else
        self:LogError("Core Function", "GetClassStats", "Function does not exist")
    end
    
    -- GetLevelRank(level)
    if addon.GetLevelRank then
        local success, err = self:SafeCall(addon.GetLevelRank, addon, 10)
        if success then
            self:LogSuccess("Core Function", "GetLevelRank")
        else
            self:LogError("Core Function", "GetLevelRank", tostring(err))
        end
    else
        self:LogError("Core Function", "GetLevelRank", "Function does not exist")
    end
    
    -- UpdateUltimateCooldown(elapsed)
    if addon.UpdateUltimateCooldown then
        local success, err = self:SafeCall(addon.UpdateUltimateCooldown, addon, 0.1)
        if success then
            self:LogSuccess("Core Function", "UpdateUltimateCooldown")
        else
            self:LogError("Core Function", "UpdateUltimateCooldown", tostring(err))
        end
    else
        self:LogError("Core Function", "UpdateUltimateCooldown", "Function does not exist")
    end
end

-- ============================================================
-- RUN ALL TESTS
-- ============================================================
function TS:RunAllTests()
    print("|cff00ff00[TestSystem]|r Running automated tests...")
    
    -- Reset results
    self.results = {
        passed = 0,
        failed = 0,
        errors = {},
        warnings = {},
        tested = {}
    }
    self.verboseMode = false  -- Set to true to see failures in chat too
    
    -- Suppress print messages during testing
    addon.suppressTestPrints = true
    
    -- Initialize test environment
    self:InitTestEnvironment()
    
    -- Run all test suites
    self:TestCoreFunctions()
    self:TestUltimateFunctions()
    self:TestClasses()
    self:TestPvPSabotages()
    self:TestRaces()
    self:TestShopItems()
    self:TestTimedEffects()
    self:TestForcedPieces()
    
    -- Run VALIDATION tests (check effects match descriptions)
    self:RunValidationTests()
    
    -- Re-enable prints
    addon.suppressTestPrints = false
    
    -- Restore state
    self:RestoreState()
    
    -- Print summary
    self:PrintSummary()
end

-- ============================================================
-- RUN VALIDATION TESTS
-- ============================================================
function TS:RunValidationTests()
    -- Validate ultimate functions work as described
    local ultErrors = self:ValidateUltimates()
    for _, err in ipairs(ultErrors) do
        self:LogError("Validation: " .. err.name, err.expected,
            "EFFECT MISMATCH!\n" ..
            "Expected: " .. err.expected .. "\n" ..
            "Actual: " .. err.actual .. "\n" ..
            "The ability doesn't do what its description says!")
    end
    
    -- Validate class ultimates match descriptions
    local classErrors = self:ValidateClassUltimates()
    for _, err in ipairs(classErrors) do
        self:LogError("Ultimate Validation: " .. err.class .. " - " .. err.ultimate, err.problem,
            "DESCRIPTION MISMATCH!\n" ..
            "Problem: " .. err.problem .. "\n" ..
            "Details: " .. err.details .. "\n" ..
            "The ability effect doesn't match the tooltip!")
    end
    
    -- Validate shop ultimates (timed effects like Darkness)
    local shopErrors = self:ValidateShopUltimates()
    for _, err in ipairs(shopErrors) do
        self:LogError("Shop Ultimate: " .. err.class .. " - " .. err.ability, err.problem,
            "SHOP ULTIMATE BUG!\n" ..
            "Problem: " .. err.problem .. "\n" ..
            "Details: " .. err.details .. "\n" ..
            "Check ExecuteShopUltimate in Classes.lua")
    end
    
    -- Validate piece textures
    local textureErrors = self:ValidatePieceTextures()
    for _, err in ipairs(textureErrors) do
        self:LogError("Texture: " .. err.class .. " - " .. err.ability, err.problem,
            "TEXTURE ISSUE!\n" ..
            "Problem: " .. err.problem .. "\n" ..
            "Details: " .. err.details .. "\n" ..
            "Check CLASS_PIECE_TEXTURES in Pieces.lua")
    end
    
    -- Validate PvP sabotages match descriptions and balance rules
    local pvpErrors = self:ValidatePvPSabotages()
    for _, err in ipairs(pvpErrors) do
        self:LogError("PvP Sabotage: " .. err.class .. " - " .. err.ability, err.problem,
            "PVP BALANCE ISSUE!\n" ..
            "Problem: " .. err.problem .. "\n" ..
            "Details: " .. err.details .. "\n" ..
            "Check description and balance rules in Multiplayer.lua")
    end
    
    -- Validate racial abilities match descriptions
    local racialErrors = self:ValidateRacials()
    for _, err in ipairs(racialErrors) do
        self:LogError("Racial Validation: " .. err.class .. " - " .. err.ability, err.problem,
            "RACIAL EFFECT MISMATCH!\n" ..
            "Problem: " .. err.problem .. "\n" ..
            "Details: " .. err.details .. "\n" ..
            "Check ExecuteRacialEffect in Multiplayer.lua")
    end
end

-- ============================================================
-- PRINT SUMMARY
-- ============================================================
function TS:PrintSummary()
    print("|cff00ff00[TestSystem]|r Tests complete! " .. self.results.passed .. " passed, " .. self.results.failed .. " failed.")
    
    -- Show popup window with failed tests
    if #self.results.errors > 0 then
        self:ShowResultsWindow()
    else
        print("|cff00ff00[TestSystem]|r All tests passed! No errors found.")
    end
end

-- ============================================================
-- RESULTS POPUP WINDOW
-- ============================================================
function TS:ShowResultsWindow()
    -- Create window if it doesn't exist
    if not self.resultsFrame then
        local f = CreateFrame("Frame", "WoWTetrisTestResults", UIParent, "BackdropTemplate")
        f:SetSize(700, 500)
        f:SetPoint("CENTER")
        f:SetBackdrop({
            bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
            edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Gold-Border",
            tile = true, tileSize = 32, edgeSize = 32,
            insets = {left = 8, right = 8, top = 8, bottom = 8}
        })
        f:SetMovable(true)
        f:EnableMouse(true)
        f:RegisterForDrag("LeftButton")
        f:SetScript("OnDragStart", f.StartMoving)
        f:SetScript("OnDragStop", f.StopMovingOrSizing)
        f:SetFrameStrata("DIALOG")
        
        -- Title
        local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
        title:SetPoint("TOP", 0, -15)
        title:SetText("|cffff0000WoW Tetris - Test Failures|r")
        f.title = title
        
        -- Summary text
        local summary = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        summary:SetPoint("TOP", title, "BOTTOM", 0, -10)
        f.summary = summary
        
        -- Scroll frame for error list
        local scrollFrame = CreateFrame("ScrollFrame", nil, f, "UIPanelScrollFrameTemplate")
        scrollFrame:SetPoint("TOPLEFT", 15, -70)
        scrollFrame:SetPoint("BOTTOMRIGHT", -35, 50)
        
        -- Content frame inside scroll
        local content = CreateFrame("Frame", nil, scrollFrame)
        content:SetSize(640, 1)  -- Height will be set dynamically
        scrollFrame:SetScrollChild(content)
        f.content = content
        
        -- Close button
        local closeBtn = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
        closeBtn:SetSize(100, 25)
        closeBtn:SetPoint("BOTTOM", 0, 15)
        closeBtn:SetText("Close")
        closeBtn:SetScript("OnClick", function() f:Hide() end)
        
        -- X button in corner
        local xBtn = CreateFrame("Button", nil, f, "UIPanelCloseButton")
        xBtn:SetPoint("TOPRIGHT", -5, -5)
        
        -- Copy button
        local copyBtn = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
        copyBtn:SetSize(120, 25)
        copyBtn:SetPoint("BOTTOMLEFT", 15, 15)
        copyBtn:SetText("Copy to Chat")
        copyBtn:SetScript("OnClick", function()
            print("|cffff0000=== TEST FAILURES ===|r")
            for i, err in ipairs(TS.results.errors) do
                print("|cffff0000[" .. i .. "]|r " .. err.category .. ": " .. err.item)
                -- Print first line of error only
                local firstLine = err.error:match("^([^\n]+)")
                print("    " .. (firstLine or err.error))
            end
        end)
        
        self.resultsFrame = f
    end
    
    local f = self.resultsFrame
    
    -- Update summary
    local passRate = self.results.passed + self.results.failed > 0 
        and math.floor((self.results.passed / (self.results.passed + self.results.failed)) * 100) 
        or 0
    f.summary:SetText("|cff00ff00Passed: " .. self.results.passed .. "|r  |cffff0000Failed: " .. self.results.failed .. "|r  |cff00ffffPass Rate: " .. passRate .. "%|r")
    
    -- Clear old content
    for _, child in pairs({f.content:GetChildren()}) do
        child:Hide()
        child:SetParent(nil)
    end
    
    -- Populate error list
    local yOffset = 0
    
    for i, err in ipairs(self.results.errors) do
        -- Calculate row height based on error message length
        local errorLines = select(2, err.error:gsub("\n", "\n")) + 1
        local rowHeight = 30 + (errorLines * 14)
        
        local row = CreateFrame("Frame", nil, f.content, "BackdropTemplate")
        row:SetSize(630, rowHeight)
        row:SetPoint("TOPLEFT", 5, -yOffset)
        row:SetBackdrop({
            bgFile = "Interface\\ChatFrame\\ChatFrameBackground",
            edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
            tile = true, tileSize = 16, edgeSize = 12,
            insets = {left = 2, right = 2, top = 2, bottom = 2}
        })
        row:SetBackdropColor(0.2, 0.05, 0.05, 0.95)
        row:SetBackdropBorderColor(0.5, 0.15, 0.15, 1)
        
        -- Error number with red X
        local numText = row:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
        numText:SetPoint("TOPLEFT", 8, -8)
        numText:SetText("|cffff0000X|r |cffffffff#" .. i .. "|r")
        
        -- Category + Item (What failed)
        local itemText = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        itemText:SetPoint("TOPLEFT", 50, -8)
        itemText:SetText("|cffffd700" .. err.category .. ":|r |cffffffff" .. err.item .. "|r")
        itemText:SetWidth(570)
        itemText:SetJustifyH("LEFT")
        
        -- Error details (multi-line)
        local errText = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        errText:SetPoint("TOPLEFT", 50, -26)
        errText:SetText("|cffaaaaaa" .. err.error .. "|r")
        errText:SetWidth(570)
        errText:SetJustifyH("LEFT")
        errText:SetSpacing(2)
        
        yOffset = yOffset + rowHeight + 5
    end
    
    -- Set content height
    f.content:SetHeight(math.max(yOffset, 100))
    
    -- Update title with count
    f.title:SetText("|cffff0000WoW Tetris - " .. #self.results.errors .. " Test Failures|r")
    
    f:Show()
end

-- ============================================================
-- SLASH COMMAND
-- ============================================================
SLASH_TETRISTEST1 = "/tetristest"
SlashCmdList["TETRISTEST"] = function(msg)
    if msg == "all" or msg == "" then
        addon.TestSystem:RunAllTests()
    elseif msg == "classes" then
        addon.TestSystem:InitTestEnvironment()
        addon.TestSystem:TestClasses()
        addon.TestSystem:PrintSummary()
    elseif msg == "races" then
        addon.TestSystem:InitTestEnvironment()
        addon.TestSystem:TestRaces()
        addon.TestSystem:PrintSummary()
    elseif msg == "shop" then
        addon.TestSystem:InitTestEnvironment()
        addon.TestSystem:TestShopItems()
        addon.TestSystem:PrintSummary()
    elseif msg == "ultimates" then
        addon.TestSystem:InitTestEnvironment()
        addon.TestSystem:TestUltimateFunctions()
        addon.TestSystem:PrintSummary()
    elseif msg == "pvp" then
        addon.TestSystem:InitTestEnvironment()
        addon.TestSystem:TestPvPSabotages()
        addon.TestSystem:PrintSummary()
    elseif msg == "validate" then
        addon.TestSystem:InitTestEnvironment()
        addon.TestSystem.results = {passed = 0, failed = 0, errors = {}, warnings = {}, tested = {}}
        addon.TestSystem:RunValidationTests()
        addon.TestSystem:PrintSummary()
    else
        print("|cff00ff00[WoW Tetris Test System]|r")
        print("Usage: /tetristest [option]")
        print("  all - Run all tests (default)")
        print("  classes - Test all classes and their ultimates")
        print("  races - Test all races and their abilities")
        print("  shop - Test all shop items")
        print("  ultimates - Test all ultimate functions")
        print("  pvp - Test all PvP sabotage abilities")
        print("  |cffff0000validate|r - Verify effects match descriptions!")
    end
end

print("|cff00ff00[WoW Tetris]|r Test System loaded. Type /tetristest to run tests.")
