local addonName, addon = ...

addon.UI = addon.UI or {}
local UI = addon.UI
local LT = addon.LootTracker
local LootDB = LT and LT.DB
local Query = LT and LT.Query
local Tracks = LT and LT.UpgradeTracks
local Upgrade = LT and LT.Upgrade
local Crafting = LT and LT.Crafting

local function Lang() return addon.Localization and addon.Localization:GetLanguage() or "en" end

local SOURCES = {
    { key = "KEYS", en = "KEYS", ru = "Ключи", slots = 2 },
    { key = "RAID", en = "RAID", ru = "Рейд", slots = 2 },
    { key = "VOID_CORE", en = "VOID CORE ROLL", ru = "Бросок Сердечника Бездны", slots = 2 },
    { key = "CRAFT", en = "CRAFT", ru = "Крафт", slots = 2 },
}

local PRIORITIES = {
    { key = "HIGH", label = "HIGH" },
    { key = "BIS", label = "BIS" },
}

local BLIZZARD_CHECK_TEXTURE = "Interface\\Buttons\\UI-CheckBox-Check"
local MENU_CHECK = "|TInterface\\Buttons\\UI-CheckBox-Check:16:16:0:0|t "

local LEFT_SLOTS = {
    { key = "head",      slotID = 0,  en = "HEAD",      ru = "Голова" },
    { key = "neck",      slotID = 1,  en = "NECK",      ru = "Шея" },
    { key = "shoulder",  slotID = 2,  en = "SHOULDER",  ru = "Плечи" },
    { key = "back",      slotID = 3,  en = "BACK",      ru = "Спина" },
    { key = "chest",     slotID = 4,  en = "CHEST",     ru = "Тело" },
    { key = "shirt",     slotID = nil,en = "SHIRT",     ru = "Рубашка" },
    { key = "tabard",    slotID = nil,en = "TABARD",    ru = "Гербовая накидка" },
    { key = "wrist",     slotID = 5,  en = "WRIST",     ru = "Запястья" },
}

local RIGHT_SLOTS = {
    { key = "hands",     slotID = 6,  en = "HANDS",     ru = "Кисти рук" },
    { key = "waist",     slotID = 7,  en = "WAIST",     ru = "Пояс" },
    { key = "legs",      slotID = 8,  en = "LEGS",      ru = "Ноги" },
    { key = "feet",      slotID = 9,  en = "FEET",      ru = "Ступни" },
    { key = "finger1",   slotID = 12, en = "RING 1",    ru = "Кольцо 1", duplicateIndex = 1 },
    { key = "finger2",   slotID = 12, en = "RING 2",    ru = "Кольцо 2", duplicateIndex = 2 },
    { key = "trinket1",  slotID = 13, en = "TRINKET 1", ru = "Тринкет 1", duplicateIndex = 1 },
    { key = "trinket2",  slotID = 13, en = "TRINKET 2", ru = "Тринкет 2", duplicateIndex = 2 },
}


local WEAPON_SLOTS = {
    { key = "mainhand", slotID = 10, en = "MAIN HAND", ru = "Основная рука" },
    { key = "offhand",  slotID = 11, en = "OFF HAND",  ru = "Левая рука" },
}

local function SlotLabel(slot)
    local translated = UI:LT(slot.en)
    if translated and translated ~= slot.en then
        return slot.en .. "\n" .. translated
    end
    return slot.en
end

local function SourceLabel(source)
    return UI:LT(source.en)
end

local function GetEffectiveTrackedSlotID(item)
    if not item then return nil end

    local sourceType = tostring(item.sourceType or ""):upper()
    local trackType = tostring(item.trackType or ""):upper()
    local isCrafted = sourceType == "CRAFTED" or sourceType == "CRAFT"
        or trackType == "CRAFTED" or trackType == "CRAFT"

    -- Older versions stored crafted items with the fallback slot 14 because
    -- Query.Data only knows dungeon/raid items. Resolve crafted equipment from
    -- the live Blizzard item API instead, which is the same source used by the
    -- crafted browser.
    if isCrafted and Crafting and Crafting.GetItemSlotID then
        local craftSlot = tonumber(Crafting:GetItemSlotID(item.itemID))
        if craftSlot and craftSlot ~= 14 then
            return craftSlot
        end
    end

    return tonumber(item.slotID)
end

local function GetTrackedSourceBucket(item)
    local sourceType = tostring(item and item.sourceType or ""):upper()
    local trackType = tostring(item and item.trackType or ""):upper()

    if sourceType == "KEYS" or sourceType == "DUNGEON" or trackType == "DUNGEON" then
        return "KEYS"
    end
    if sourceType == "RAIDS" or sourceType == "RAID" or trackType == "RAID" then
        return "RAID"
    end
    if sourceType == "VOID_CORE" or sourceType == "VOID_CORE_ROLL"
        or trackType == "VOID_CORE" or trackType == "VOID_CORE_ROLL" then
        return "VOID_CORE"
    end
    if sourceType == "CRAFTED" or sourceType == "CRAFT" or trackType == "CRAFTED" or trackType == "CRAFT" then
        return "CRAFT"
    end
    return nil
end

local function GetTrackedItemLevel(item)
    if not item then return nil end
    local direct = tonumber(item.itemLevel)
    if direct then return direct end

    local rank = tonumber(item.trackRank) or 1
    local trackType = tostring(item.trackType or ""):upper()
    if Tracks and trackType == "DUNGEON" and Tracks.GetDungeonEntry then
        local entry = Tracks:GetDungeonEntry(item.trackKey or "champion", rank)
        return entry and tonumber(entry.ilvl) or nil
    end
    if Tracks and trackType == "RAID" and Tracks.GetRaidEntry then
        local entry = Tracks:GetRaidEntry(item.trackKey or "normal", rank)
        return entry and tonumber(entry.ilvl) or nil
    end
    return nil
end

local function GetItemLevelOptions()
    local values = {}
    if not Tracks then return values end

    -- Equipment Loot Test follows the Mythic+ upgrade tracks shown to the user:
    -- CHAMPION -> HERO -> MYTH. Keep every rank as its own menu entry so the
    -- dropdown matches Blizzard-style track wording instead of showing bare numbers.
    for _, key in ipairs(Tracks.dungeonOrder or {}) do
        for rank, entry in ipairs((Tracks.dungeon and Tracks.dungeon[key]) or {}) do
            table.insert(values, {
                ilvl = tonumber(entry.ilvl),
                bonusId = tonumber(entry.bonusId),
                label = (entry.groupLabel or tostring(key):upper()) .. "  " .. (entry.label or tostring(entry.ilvl or "")),
                trackKey = key,
                rank = rank,
                entry = entry,
            })
        end
    end
    return values
end

local function CreatePanel(parent)
    local panel = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    panel:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = true,
        tileSize = 16,
        edgeSize = 12,
        insets = { left = 3, right = 3, top = 3, bottom = 3 },
    })
    panel:SetBackdropColor(0.02, 0.015, 0.03, 0.94)
    panel:SetBackdropBorderColor(unpack(UI.COLORS.border))
    return panel
end

local function CreateDropdown(parent, width, kind)
    local dropdown = UI:CreateBlizzardDropdown(
        parent,
        width,
        "SELECT",
        function(_, rootDescription)
            local entries = UI:GetLootSelectorEntries(kind) or {}
            for _, entry in ipairs(entries) do
                local wrapped = {}
                for k, v in pairs(entry) do wrapped[k] = v end
                local original = entry.func
                wrapped.func = function()
                    if original then original() end
                    C_Timer.After(0, function()
                        if UI.RefreshEquipmentLootTest then UI:RefreshEquipmentLootTest() end
                    end)
                end
                UI:AddBlizzardMenuEntry(rootDescription, wrapped)
            end
        end
    )
    dropdown:SetHeight(28)
    dropdown.Kind = kind
    return dropdown
end

local function CreateItemLevelDropdown(parent, width)
    local dropdown = UI:CreateBlizzardDropdown(
        parent,
        width,
        "ALL ITEM LEVELS",
        function(_, rootDescription)
            UI:AddBlizzardMenuEntry(rootDescription, {
                text = (UI.EquipmentTestItemLevel and "" or MENU_CHECK) .. "ALL ITEM LEVELS",
                func = function()
                    UI.EquipmentTestItemLevel = nil
                    WhatsNextUserData = WhatsNextUserData or {}
                    WhatsNextUserData.equipmentLootTest = WhatsNextUserData.equipmentLootTest or {}
                    WhatsNextUserData.equipmentLootTest.itemLevel = nil
                    WhatsNextUserData.equipmentLootTest.itemLevelLabel = nil
                    WhatsNextUserData.equipmentLootTest.itemLevelBonusID = nil
                    WhatsNextUserData.equipmentLootTest.trackKey = nil
                    WhatsNextUserData.equipmentLootTest.trackRank = nil
                    UI.EquipmentTestItemLevelLabel = nil
                    UI.EquipmentTestItemLevelBonusID = nil
                    UI.EquipmentTestTrackKey = nil
                    UI.EquipmentTestTrackRank = nil
                    UI:RefreshEquipmentLootTest()
                end,
            })
            for _, option in ipairs(GetItemLevelOptions()) do
                UI:AddBlizzardMenuEntry(rootDescription, {
                    text = ((tonumber(UI.EquipmentTestItemLevel) == option.ilvl and UI.EquipmentTestItemLevelLabel == option.label) and MENU_CHECK or "") .. option.label,
                    func = function()
                        UI.EquipmentTestItemLevel = option.ilvl
                        UI.EquipmentTestItemLevelLabel = option.label
                        UI.EquipmentTestItemLevelBonusID = option.bonusId
                        UI.EquipmentTestTrackKey = option.trackKey
                        UI.EquipmentTestTrackRank = option.rank
                        WhatsNextUserData = WhatsNextUserData or {}
                        WhatsNextUserData.equipmentLootTest = WhatsNextUserData.equipmentLootTest or {}
                        WhatsNextUserData.equipmentLootTest.itemLevel = option.ilvl
                        WhatsNextUserData.equipmentLootTest.itemLevelLabel = option.label
                        WhatsNextUserData.equipmentLootTest.itemLevelBonusID = option.bonusId
                        WhatsNextUserData.equipmentLootTest.trackKey = option.trackKey
                        WhatsNextUserData.equipmentLootTest.trackRank = option.rank
                        UI:RefreshEquipmentLootTest()
                    end,
                })
            end
        end
    )
    dropdown:SetHeight(28)
    return dropdown
end

local function GetSelectedPreviewEntry()
    local key = UI.EquipmentTestTrackKey
    local rank = tonumber(UI.EquipmentTestTrackRank)
    if key and rank and Tracks and Tracks.GetDungeonEntry then
        local entry = Tracks:GetDungeonEntry(key, rank)
        if entry then
            return entry
        end
    end

    if UI.EquipmentTestItemLevel then
        return {
            ilvl = tonumber(UI.EquipmentTestItemLevel),
            bonusId = tonumber(UI.EquipmentTestItemLevelBonusID),
            groupLabel = UI.EquipmentTestItemLevelLabel or "TRACK",
            label = tostring(UI.EquipmentTestItemLevel),
        }
    end

    return nil
end

local function SetItemTooltip(button, itemID)
    button.ItemID = itemID
    button:SetScript("OnEnter", function(self)
        local tracked = LootDB and LootDB:GetTrackedItem(UI.LootCharacterKey, UI.LootSpecID, itemID)
        local previewEntry = GetSelectedPreviewEntry()
        local tooltipEntry = previewEntry
        local itemLink = nil

        if Upgrade then
            if previewEntry and Upgrade.BuildPreviewItemLink then
                -- The Item Level selected on Equipment Loot Test is authoritative.
                -- Never mix the saved My Loot bonusIDs into this temporary preview.
                itemLink = Upgrade:BuildPreviewItemLink(
                    itemID,
                    UI.LootSpecID,
                    previewEntry
                )
            elseif Upgrade.BuildTrackedItemLink then
                -- ALL ITEM LEVELS: show exactly the track saved on this item,
                -- identical to My Loot.
                itemLink, tooltipEntry = Upgrade:BuildTrackedItemLink(
                    itemID,
                    UI.LootSpecID,
                    tracked
                )
            end
        end

        -- This is the same tooltip path used by the old Loot Browser/My Loot.
        -- Blizzard reads the generated item link and displays the real resulting
        -- item level and stats; no tooltip text is rewritten manually.
        if UI.ShowLootItemTooltip then
            UI:ShowLootItemTooltip(self, itemLink, tracked, tooltipEntry)
        else
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            if itemLink then
                GameTooltip:SetHyperlink(itemLink)
            elseif GameTooltip.SetItemByID then
                GameTooltip:SetItemByID(itemID)
            end
            local info = Query and Query.GetItemInfo and Query:GetItemInfo(itemID) or nil
            if UI.AddItemNameEquivalentToTooltip then
                UI:AddItemNameEquivalentToTooltip(itemID, info and info.name or nil)
            end
            GameTooltip:Show()
        end
    end)
    button:SetScript("OnLeave", function()
        GameTooltip:Hide()
    end)
end
function UI:OpenEquipmentTestItemMenu(anchor, item)
    if not anchor or not item or not item.itemID or not LootDB then return end

    local itemID = tonumber(item.itemID)
    local tracked = LootDB:GetTrackedItem(self.LootCharacterKey, self.LootSpecID, itemID)
    if not tracked then return end

    local entries = {}

    local isCoreCopy = item.isVoidCoreVariant == true
    local obtained = isCoreCopy and tracked.voidCoreObtained == true or tracked.obtained == true
    table.insert(entries, {
        text = obtained and (MENU_CHECK .. "|cff72e572" .. UI:T("OBTAINED", "OBTAINED") .. "|r") or UI:T("MARK_OBTAINED", "MARK AS OBTAINED"),
        func = function()
            if isCoreCopy then
                LootDB:SetVoidCoreObtained(self.LootCharacterKey, self.LootSpecID, itemID, not obtained)
            else
                LootDB:SetObtained(self.LootCharacterKey, self.LootSpecID, itemID, not obtained)
            end
            self:RefreshEquipmentLootTest()
            if self.RefreshLootTracker then self:RefreshLootTracker() end
        end,
    })

    if isCoreCopy then
        table.insert(entries, { text = UI:T("VOID_CORE_COPY", "VOID CORE — SEPARATE COPY"), disabled = true })
        table.insert(entries, {
            text = UI:T("VOID_CORE_REMOVE", "REMOVE VOID CORE ROLL"),
            func = function()
                LootDB:SetVoidCore(self.LootCharacterKey, self.LootSpecID, itemID, false)
            end,
        })
        self:OpenBlizzardContextMenu(anchor, entries)
        return
    end

    table.insert(entries, { text = "PRIORITY", disabled = true })
    for _, priority in ipairs(PRIORITIES) do
        table.insert(entries, {
            text = ((tracked.priority == priority.key) and MENU_CHECK or "") .. priority.label,
            func = function()
                LootDB:SetPriority(self.LootCharacterKey, self.LootSpecID, itemID, priority.key)
                self:RefreshEquipmentLootTest()
                if self.RefreshLootTracker then self:RefreshLootTracker() end
            end,
        })
    end

    table.insert(entries, { text = " ", disabled = true })
    table.insert(entries, {
        text = (tracked.voidCore and MENU_CHECK or "")
            .. UI:T("VOID_CORE_ROLL", "VOID CORE ROLL"),
        func = function()
            LootDB:SetVoidCore(self.LootCharacterKey, self.LootSpecID, itemID, not tracked.voidCore)
            self:RefreshEquipmentLootTest()
            if self.RefreshLootTracker then self:RefreshLootTracker() end
        end,
    })

    table.insert(entries, {
        text = "|cffff5a5a" .. UI:T("REMOVE_FROM_MY_LOOT", "REMOVE FROM MY LOOT") .. "|r",
        func = function()
            LootDB:UntrackItem(self.LootCharacterKey, self.LootSpecID, itemID)
            self:RefreshEquipmentLootTest()
            if self.RefreshLootTracker then self:RefreshLootTracker() end
        end,
    })

    self:OpenBlizzardContextMenu(anchor, entries)
end

local EQUIP_CELL_WIDTH = 102
local EQUIP_CELL_HEIGHT = 50
local EQUIP_CELL_GAP = 4
local EQUIP_LABEL_WIDTH = 92
local EQUIP_LABEL_GAP = 8
local EQUIP_SOURCE_GRID_WIDTH = (#SOURCES * EQUIP_CELL_WIDTH) + ((#SOURCES - 1) * EQUIP_CELL_GAP)
local EQUIP_ROW_WIDTH = EQUIP_LABEL_WIDTH + EQUIP_LABEL_GAP + EQUIP_SOURCE_GRID_WIDTH

local function CreateSourceCell(parent, sourceKey)
    local cell = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    cell:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Buttons\\WHITE8X8",
        edgeSize = 1,
    })
    cell:SetBackdropColor(0.015, 0.015, 0.02, 0.88)
    cell:SetBackdropBorderColor(0.28, 0.10, 0.34, 0.85)
    cell.sourceKey = sourceKey
    cell.ItemButtons = {}
    return cell
end

local function CreateSlotRow(parent, slot, side)
    local row = CreateFrame("Frame", nil, parent)
    row:SetSize(EQUIP_ROW_WIDTH, 54)
    row.Slot = slot
    row.Side = side

    local label = row:CreateFontString(nil, "OVERLAY")
    label:SetFontObject(GameFontNormalSmall)
    label:SetWidth(EQUIP_LABEL_WIDTH)
    label:SetJustifyH(side == "LEFT" and "RIGHT" or "LEFT")
    label:SetJustifyV("MIDDLE")
    label:SetWordWrap(true)
    label:SetText(SlotLabel(slot))
    label:SetTextColor(unpack(UI.COLORS.text))
    row.Label = label

    local cells = {}
    for index, source in ipairs(SOURCES) do
        local cell = CreateSourceCell(row, source.key)
        cell.Source = source
        cell:SetSize(EQUIP_CELL_WIDTH, EQUIP_CELL_HEIGHT)
        cells[index] = cell
    end
    row.Cells = cells

    if side == "LEFT" then
        label:SetPoint("LEFT", row, "LEFT", 0, 0)
        cells[1]:SetPoint("LEFT", label, "RIGHT", EQUIP_LABEL_GAP, 0)
        for i = 2, #cells do
            cells[i]:SetPoint("LEFT", cells[i-1], "RIGHT", EQUIP_CELL_GAP, 0)
        end
    else
        cells[1]:SetPoint("LEFT", row, "LEFT", 0, 0)
        for i = 2, #cells do
            cells[i]:SetPoint("LEFT", cells[i-1], "RIGHT", EQUIP_CELL_GAP, 0)
        end
        label:SetPoint("LEFT", cells[#cells], "RIGHT", EQUIP_LABEL_GAP, 0)
    end

    return row
end

local function ClearCell(cell)
    for _, button in ipairs(cell.ItemButtons or {}) do
        button:Hide()
        button:SetParent(nil)
    end
    cell.ItemButtons = {}
    if cell.MoreText then
        cell.MoreText:SetText("")
    end
end

local function PopulateCell(cell, items, emptyText)
    ClearCell(cell)
    items = items or {}

    if #items == 0 and emptyText then
        if not cell.EmptyText then
            cell.EmptyText = cell:CreateFontString(nil, "OVERLAY")
            cell.EmptyText:SetFontObject(GameFontHighlightSmall)
            cell.EmptyText:SetPoint("CENTER")
            cell.EmptyText:SetWidth(76)
            cell.EmptyText:SetJustifyH("CENTER")
            cell.EmptyText:SetTextColor(unpack(UI.COLORS.muted))
        end
        cell.EmptyText:SetText(emptyText)
        cell.EmptyText:Show()
    elseif cell.EmptyText then
        cell.EmptyText:Hide()
    end

    local maxSlots = (cell.Source and cell.Source.slots) or 3
    local size = 44
    local gap = 4
    local leftInset = 4

    local function MakeItemButton(slotFrame, item)
        local info = Query and Query:GetItemInfo(item.itemID) or nil
        local button = CreateFrame("Button", nil, slotFrame)
        button:SetAllPoints(slotFrame)

        local icon = button:CreateTexture(nil, "ARTWORK")
        icon:SetPoint("TOPLEFT", button, "TOPLEFT", 2, -2)
        icon:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", -2, 2)
        icon:SetTexture((info and info.icon) or 134400)
        -- Only the icon is desaturated while the item is not obtained.
        -- Borders, item-level text, cells and Blizzard tooltip remain unchanged.
        if icon.SetDesaturated then
            icon:SetDesaturated(not item.obtained)
        end
        button.Icon = icon

        local displayObtained = item.obtained == true

        local border = button:CreateTexture(nil, "OVERLAY")
        border:SetPoint("TOPLEFT", button, "TOPLEFT", -1, 1)
        border:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", 1, -1)
        border:SetTexture("Interface\\Buttons\\UI-ActionButton-Border")
        border:SetBlendMode("ADD")
        border:SetAlpha(displayObtained and 1 or 0.45)

        if displayObtained then
            -- High-contrast Blizzard-native obtained badge. Keep it separate from
            -- the item art so the check cannot disappear into a bright icon.
            local badge = CreateFrame("Frame", nil, button, "BackdropTemplate")
            badge:SetSize(24, 24)
            badge:SetPoint("TOPRIGHT", button, "TOPRIGHT", 4, 4)
            badge:SetFrameLevel(button:GetFrameLevel() + 8)
            badge:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1 })
            badge:SetBackdropColor(0.02, 0.02, 0.02, 0.96)
            badge:SetBackdropBorderColor(0.25, 1.00, 0.25, 1.00)
            local check = badge:CreateTexture(nil, "OVERLAY")
            check:SetTexture("Interface\\Buttons\\UI-CheckBox-Check")
            check:SetSize(28, 28)
            check:SetPoint("CENTER")
        end

        local selectedIlvl = tonumber(UI.EquipmentTestItemLevel)
        local shownIlvl = selectedIlvl or GetTrackedItemLevel(item)
        if shownIlvl then
            local ilvl = button:CreateFontString(nil, "OVERLAY")
            ilvl:SetFontObject(GameFontNormalSmall)
            ilvl:SetPoint("BOTTOM", button, "BOTTOM", 0, 1)
            ilvl:SetText(tostring(shownIlvl))
            ilvl:SetTextColor(1, 0.82, 0)
            button.ItemLevelText = ilvl
        end

        button:SetScript("OnClick", function(self)
            UI:OpenEquipmentTestItemMenu(self, item)
        end)
        SetItemTooltip(button, item.itemID)
        return button
    end

    if maxSlots >= 2 then
        -- Equipment Test intentionally shows only explicit HIGH and BIS priorities.
        -- WANTED remains stored in My Loot but is hidden from this test view.
        local byPriority = { HIGH = {}, BIS = {} }
        for _, item in ipairs(items) do
            local priority = tostring(item.priority or "WANTED"):upper()
            if priority == "HIGH" or priority == "BIS" then
                table.insert(byPriority[priority], item)
            end
        end
        for _, list in pairs(byPriority) do
            table.sort(list, function(a, b) return (tonumber(a.itemID) or 0) < (tonumber(b.itemID) or 0) end)
        end

        local order = { "HIGH", "BIS" }
        for i, priority in ipairs(order) do
            local slotFrame = CreateFrame("Frame", nil, cell, "BackdropTemplate")
            slotFrame:SetSize(size + 2, size + 2)
            slotFrame:SetPoint("LEFT", cell, "LEFT", leftInset + ((i - 1) * (size + gap)), 0)
            slotFrame:SetBackdrop({ bgFile = "Interface\\Buttons\\WHITE8X8", edgeFile = "Interface\\Buttons\\WHITE8X8", edgeSize = 1 })
            slotFrame:SetBackdropColor(0.025, 0.02, 0.035, 0.80)
            slotFrame:SetBackdropBorderColor(0.18, 0.09, 0.22, 0.75)
            table.insert(cell.ItemButtons, slotFrame)

            local item = byPriority[priority][1]
            if item then
                MakeItemButton(slotFrame, item)
                if #byPriority[priority] > 1 then
                    local more = slotFrame:CreateFontString(nil, "OVERLAY")
                    more:SetFontObject(GameFontNormalSmall)
                    more:SetPoint("TOPRIGHT", slotFrame, "TOPRIGHT", -1, -1)
                    more:SetText("+" .. tostring(#byPriority[priority] - 1))
                    more:SetTextColor(unpack(UI.COLORS.gold))
                end
            else
                local hint = slotFrame:CreateFontString(nil, "OVERLAY")
                hint:SetFontObject(GameFontHighlightSmall)
                hint:SetPoint("CENTER")
                hint:SetText(priority == "BIS" and "B" or "H")
                hint:SetTextColor(0.33, 0.30, 0.38)
            end
        end
    end
end

local function SplitDuplicateSlots(items, slotID)
    local result = { [1] = {}, [2] = {} }
    local byPriority = { HIGH = {}, BIS = {} }
    for _, item in ipairs(items) do
        if GetEffectiveTrackedSlotID(item) == slotID then
            local key = tostring(item.priority or "HIGH"):upper()
            if key ~= "BIS" then key = "HIGH" end
            byPriority[key] = byPriority[key] or {}
            table.insert(byPriority[key], item)
        end
    end
    for _, priority in ipairs(PRIORITIES) do
        local list = byPriority[priority.key] or {}
        table.sort(list, function(a, b) return (tonumber(a.itemID) or 0) < (tonumber(b.itemID) or 0) end)
        for index, item in ipairs(list) do
            local bucket = ((index - 1) % 2) + 1
            table.insert(result[bucket], item)
        end
    end
    return result
end

function UI:InitializeEquipmentLootTest()
    if self.EquipmentLootTestFrame then return end

    local parent = self.PageHost or self.MainFrame
    if not parent then return end

    local frame = CreateFrame("Frame", "WhatsNextEquipmentLootTestFrame", parent, "BackdropTemplate")
    self.EquipmentLootTestFrame = frame
    frame:SetAllPoints(parent)
    self:ApplyPanelBackdrop(frame)
    frame:Hide()

    local title = frame:CreateFontString(nil, "OVERLAY")
    title:SetFontObject(GameFontNormalLarge)
    title:SetPoint("TOPLEFT", frame, "TOPLEFT", 16, -14)
    title:SetText(UI:T("EQUIPMENT_LOOT_TEST", "EQUIPMENT LOOT TEST"))
    title:SetTextColor(unpack(self.COLORS.gold))

    local subtitle = frame:CreateFontString(nil, "OVERLAY")
    subtitle:SetFontObject(GameFontHighlightSmall)
    subtitle:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -5)
    subtitle:SetPoint("RIGHT", frame, "RIGHT", -18, 0)
    subtitle:SetJustifyH("LEFT")
    subtitle:SetText(UI:T("EXPERIMENTAL_LOOT", "Equipment plan by source."))
    subtitle:SetTextColor(unpack(self.COLORS.muted))

    local top = CreatePanel(frame)
    top:SetPoint("TOPLEFT", frame, "TOPLEFT", 14, -58)
    top:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -14, -58)
    top:SetHeight(74)

    local charLabel = top:CreateFontString(nil, "OVERLAY")
    charLabel:SetFontObject(GameFontNormalSmall)
    charLabel:SetPoint("TOPLEFT", top, "TOPLEFT", 10, -8)
    charLabel:SetText(UI:T("CHARACTER", "CHARACTER"))
    charLabel:SetTextColor(unpack(self.COLORS.muted))

    local charDD = CreateDropdown(top, 220, "character")
    self.EquipmentTestCharacterDropdown = charDD
    charDD:SetPoint("TOPLEFT", top, "TOPLEFT", 8, -27)

    local specLabel = top:CreateFontString(nil, "OVERLAY")
    specLabel:SetFontObject(GameFontNormalSmall)
    specLabel:SetPoint("TOPLEFT", top, "TOPLEFT", 244, -8)
    specLabel:SetText(UI:T("SPECIALIZATION", "SPECIALIZATION"))
    specLabel:SetTextColor(unpack(self.COLORS.muted))

    local specDD = CreateDropdown(top, 190, "spec")
    self.EquipmentTestSpecDropdown = specDD
    specDD:SetPoint("TOPLEFT", top, "TOPLEFT", 242, -27)

    local ilvlLabel = top:CreateFontString(nil, "OVERLAY")
    ilvlLabel:SetFontObject(GameFontNormalSmall)
    ilvlLabel:SetPoint("TOPLEFT", top, "TOPLEFT", 446, -8)
    ilvlLabel:SetText(UI:T("ITEM_LEVEL", "ITEM LEVEL"))
    ilvlLabel:SetTextColor(unpack(self.COLORS.muted))

    local ilvlDD = CreateItemLevelDropdown(top, 160)
    self.EquipmentTestItemLevelDropdown = ilvlDD
    ilvlDD:SetPoint("TOPLEFT", top, "TOPLEFT", 444, -27)

    local hint = top:CreateFontString(nil, "OVERLAY")
    hint:SetFontObject(GameFontHighlightSmall)
    hint:SetPoint("TOPLEFT", top, "TOPLEFT", 628, -13)
    hint:SetPoint("RIGHT", top, "RIGHT", -12, 0)
    hint:SetJustifyH("LEFT")
    hint:SetWordWrap(true)
    hint:SetText(UI:T("EQUIPMENT_SHARED_HELP", "Shared loot data: Loot Browser, My Loot and this view use the same tracked items."))
    hint:SetTextColor(unpack(self.COLORS.muted))

    local body = CreateFrame("Frame", nil, frame)
    self.EquipmentLootTestBody = body
    body:SetPoint("TOPLEFT", top, "BOTTOMLEFT", 0, -10)
    body:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -14, 14)

    -- No character model in the test view: use the full body width for loot.
    self.EquipmentTestModelPanel = nil
    self.EquipmentTestPlayerModel = nil
    self.EquipmentTestModelNote = nil

    local centerGap = 24
    local left = CreateFrame("Frame", nil, body)
    self.EquipmentTestLeft = left
    left:SetPoint("TOPLEFT", body, "TOPLEFT", 0, 0)
    left:SetPoint("BOTTOMLEFT", body, "BOTTOMLEFT", 0, 118)
    left:SetPoint("RIGHT", body, "CENTER", -(centerGap / 2), 0)

    local right = CreateFrame("Frame", nil, body)
    self.EquipmentTestRight = right
    right:SetPoint("TOPRIGHT", body, "TOPRIGHT", 0, 0)
    right:SetPoint("BOTTOMRIGHT", body, "BOTTOMRIGHT", 0, 118)
    right:SetPoint("LEFT", body, "CENTER", (centerGap / 2), 0)

    local centerDivider = body:CreateTexture(nil, "BORDER")
    centerDivider:SetTexture("Interface\\Buttons\\WHITE8X8")
    centerDivider:SetWidth(1)
    centerDivider:ClearAllPoints()
    centerDivider:SetPoint("TOP", body, "TOP", 0, 0)
    centerDivider:SetPoint("BOTTOM", body, "BOTTOM", 0, 0)
    centerDivider:SetVertexColor(0.38, 0.14, 0.46, 0.85)
    self.EquipmentTestCenterDivider = centerDivider

    local function CreateColumnHeaders(parent, side)
        local header = CreateFrame("Frame", nil, parent)
        header:SetSize(EQUIP_ROW_WIDTH, 44)
        header:SetPoint("TOP", parent, "TOP", 0, 0)

        local x = side == "LEFT" and (EQUIP_LABEL_WIDTH + EQUIP_LABEL_GAP) or 0
        for _, source in ipairs(SOURCES) do
            local text = header:CreateFontString(nil, "OVERLAY")
            text:SetFontObject(GameFontNormalSmall)
            text:SetWidth(EQUIP_CELL_WIDTH)
            text:SetHeight(40)
            text:SetJustifyH("CENTER")
            text:SetJustifyV("TOP")
            text:SetWordWrap(true)
            text:SetPoint("TOPLEFT", header, "TOPLEFT", x, -2)
            text:SetText(SourceLabel(source))
            text:SetTextColor(unpack(UI.COLORS.gold))
            x = x + EQUIP_CELL_WIDTH + EQUIP_CELL_GAP
        end
    end

    CreateColumnHeaders(left, "LEFT")
    CreateColumnHeaders(right, "RIGHT")

    self.EquipmentTestRows = {}
    for index, slot in ipairs(LEFT_SLOTS) do
        local row = CreateSlotRow(left, slot, "LEFT")
        row:SetPoint("TOP", left, "TOP", 0, -48 - ((index - 1) * 56))
        self.EquipmentTestRows[slot.key] = row
    end
    for index, slot in ipairs(RIGHT_SLOTS) do
        local row = CreateSlotRow(right, slot, "RIGHT")
        row:SetPoint("TOP", right, "TOP", 0, -48 - ((index - 1) * 56))
        self.EquipmentTestRows[slot.key] = row
    end

    local weaponPanel = CreateFrame("Frame", nil, body)
    self.EquipmentTestWeaponPanel = weaponPanel
    weaponPanel:SetPoint("BOTTOMLEFT", body, "BOTTOMLEFT", 0, 0)
    weaponPanel:SetPoint("BOTTOMRIGHT", body, "BOTTOMRIGHT", 0, 0)
    weaponPanel:SetHeight(66)

    -- Weapons follow the exact same two-half geometry as the equipment above.
    -- No centered title is used here, so no text can overlap the middle divider.
    local weaponGap = centerGap
    local weaponLeft = CreateFrame("Frame", nil, weaponPanel)
    weaponLeft:SetPoint("TOPLEFT", weaponPanel, "TOPLEFT", 0, 0)
    weaponLeft:SetPoint("BOTTOMLEFT", weaponPanel, "BOTTOMLEFT", 0, 0)
    weaponLeft:SetPoint("RIGHT", weaponPanel, "CENTER", -(weaponGap / 2), 0)

    local weaponRight = CreateFrame("Frame", nil, weaponPanel)
    weaponRight:SetPoint("TOPRIGHT", weaponPanel, "TOPRIGHT", 0, 0)
    weaponRight:SetPoint("BOTTOMRIGHT", weaponPanel, "BOTTOMRIGHT", 0, 0)
    weaponRight:SetPoint("LEFT", weaponPanel, "CENTER", (weaponGap / 2), 0)


    self.EquipmentTestWeaponRows = {}

    local mainHand = CreateSlotRow(weaponLeft, WEAPON_SLOTS[1], "LEFT")
    mainHand:SetPoint("TOP", weaponLeft, "TOP", 0, -8)
    self.EquipmentTestRows[WEAPON_SLOTS[1].key] = mainHand
    self.EquipmentTestWeaponRows[WEAPON_SLOTS[1].key] = mainHand

    local offHand = CreateSlotRow(weaponRight, WEAPON_SLOTS[2], "RIGHT")
    offHand:SetPoint("TOP", weaponRight, "TOP", 0, -8)
    self.EquipmentTestRows[WEAPON_SLOTS[2].key] = offHand
    self.EquipmentTestWeaponRows[WEAPON_SLOTS[2].key] = offHand

    frame:SetScript("OnShow", function()
        UI:RefreshEquipmentLootTest()
    end)
end

function UI:RefreshEquipmentLootTest()
    if not self.EquipmentLootTestFrame then return end
    if not LootDB then return end

    self:EnsureLootTrackerSelection()
    if self.EquipmentTestItemLevel == nil and WhatsNextUserData and WhatsNextUserData.equipmentLootTest then
        self.EquipmentTestItemLevel = tonumber(WhatsNextUserData.equipmentLootTest.itemLevel)
        self.EquipmentTestItemLevelLabel = WhatsNextUserData.equipmentLootTest.itemLevelLabel
        self.EquipmentTestItemLevelBonusID = tonumber(WhatsNextUserData.equipmentLootTest.itemLevelBonusID)
        self.EquipmentTestTrackKey = WhatsNextUserData.equipmentLootTest.trackKey
        self.EquipmentTestTrackRank = tonumber(WhatsNextUserData.equipmentLootTest.trackRank)
    end

    if self.EquipmentTestCharacterDropdown then
        self:SetBlizzardDropdownText(self.EquipmentTestCharacterDropdown, self:GetLootSelectorText("character"))
    end
    if self.EquipmentTestSpecDropdown then
        self:SetBlizzardDropdownText(self.EquipmentTestSpecDropdown, self:GetLootSelectorText("spec"))
    end
    if self.EquipmentTestItemLevelDropdown then
        self:SetBlizzardDropdownText(
            self.EquipmentTestItemLevelDropdown,
            self.EquipmentTestItemLevel and (self.EquipmentTestItemLevelLabel or tostring(self.EquipmentTestItemLevel)) or UI:T("ALL_ITEM_LEVELS", "ALL ITEM LEVELS")
        )
    end

    local trackedItems = LootDB:GetTrackedItems(self.LootCharacterKey, self.LootSpecID) or {}

    local function GetSlotItems(slot)
        local result = {}
        if slot.slotID == nil then return result end

        for _, item in ipairs(trackedItems) do
            local priority = tostring(item.priority or "WANTED"):upper()
            if GetEffectiveTrackedSlotID(item) == tonumber(slot.slotID) and (priority == "HIGH" or priority == "BIS") then
                table.insert(result, item)
            end
        end

        return result
    end

    -- Rings and trinkets are interchangeable equipment slots.  Their two
    -- visual rows must therefore be filled independently for every
    -- source+priority cell:
    --   row 1 = first item in that exact cell
    --   row 2 = second item in that exact cell
    -- This prevents two BIS rings/trinkets from occupying the same button.
    local function SelectDuplicateRowItems(grouped, duplicateIndex)
        if not duplicateIndex then return grouped end

        local selected = { KEYS = {}, RAID = {}, VOID_CORE = {}, CRAFT = {} }

        for _, source in ipairs(SOURCES) do
            local sourceItems = grouped[source.key] or {}
            local byPriority = { HIGH = {}, BIS = {} }

            for _, item in ipairs(sourceItems) do
                local priority = tostring(item.priority or "HIGH"):upper()
                if priority ~= "BIS" then priority = "HIGH" end
                table.insert(byPriority[priority], item)
            end

            for _, priority in ipairs({ "HIGH", "BIS" }) do
                table.sort(byPriority[priority], function(a, b)
                    local aid = tonumber(a.itemID) or 0
                    local bid = tonumber(b.itemID) or 0
                    if aid ~= bid then return aid < bid end

                    -- Keep ordering stable when the same item ID can exist as
                    -- different tracked variants.
                    local av = a.isVoidCoreVariant and 1 or 0
                    local bv = b.isVoidCoreVariant and 1 or 0
                    return av < bv
                end)

                local item = byPriority[priority][duplicateIndex]
                if item then
                    table.insert(selected[source.key], item)
                end
            end
        end

        return selected
    end

    local function RefreshRow(slot)
        local row = self.EquipmentTestRows and self.EquipmentTestRows[slot.key]
        if not row then return end

        local grouped = { KEYS = {}, RAID = {}, VOID_CORE = {}, CRAFT = {} }
        for _, item in ipairs(GetSlotItems(slot)) do
            local bucket = GetTrackedSourceBucket(item)
            if bucket and grouped[bucket] then
                table.insert(grouped[bucket], item)
            end

            -- Void Core is independent from the original source. A HIGH/BIS item
            -- selected from Keys, Raid or Craft therefore appears in its normal
            -- source column and, when marked, also in the Void Core column.
            if item.voidCore and bucket ~= "VOID_CORE" then
                -- Void Core is represented as a separate tracked copy. It keeps
                -- the same item/priority/tooltip metadata, but has its own obtained state.
                local coreCopy = {}
                for key, value in pairs(item) do coreCopy[key] = value end
                coreCopy.isVoidCoreVariant = true
                coreCopy.obtained = item.voidCoreObtained == true
                coreCopy.obtainedAt = item.voidCoreObtainedAt
                table.insert(grouped.VOID_CORE, coreCopy)
            end
        end

        for _, source in ipairs(SOURCES) do
            table.sort(grouped[source.key], function(a, b)
                local ap = a.priority == "BIS" and 3 or (a.priority == "HIGH" and 2 or 1)
                local bp = b.priority == "BIS" and 3 or (b.priority == "HIGH" and 2 or 1)
                if ap ~= bp then return ap > bp end
                return (tonumber(a.itemID) or 0) < (tonumber(b.itemID) or 0)
            end)
        end

        -- RING 1/2 and TRINKET 1/2 share the same Blizzard equipment slot.
        -- Split them per displayed source and per HIGH/BIS cell, not globally.
        grouped = SelectDuplicateRowItems(grouped, slot.duplicateIndex)

        PopulateCell(row.Cells[1], grouped.KEYS)
        PopulateCell(row.Cells[2], grouped.RAID)
        PopulateCell(row.Cells[3], grouped.VOID_CORE, UI:T("RESERVED", "reserved"))
        PopulateCell(row.Cells[4], grouped.CRAFT)
    end

    for _, slot in ipairs(LEFT_SLOTS) do RefreshRow(slot) end
    for _, slot in ipairs(RIGHT_SLOTS) do RefreshRow(slot) end
    for _, slot in ipairs(WEAPON_SLOTS) do RefreshRow(slot) end

end
