-- audit_harmony.lua
-- Exhaustively runs the real harmony engine over all 62 real DyeColor dyes and
-- reports every category's picks + flags edge cases (empty / short categories).
-- Run from tests/:  lua audit_harmony.lua   (needs /tmp/dyecolor.csv from wago)
--
-- Not a test_*.lua so run_all.sh ignores it; this is a manual audit tool.

dofile("../Core/VDS_ColorMath.lua")
dofile("../Core/VDS_HarmonyWheel.lua")
dofile("../Core/VDS_Harmony.lua")
dofile("../Data/VDS_ColorMapDB.lua")
local H  = VDS.Harmony
local HW = VDS.HarmonyWheel
local byName = VDS_ColorMapDB.byName

-- packed signed int32 ARGB -> {r,g,b} 0..1
local function unpackRGB(v)
    local u = tonumber(v) % 4294967296
    return { r = (math.floor(u / 65536) % 256) / 255,
             g = (math.floor(u / 256)   % 256) / 255,
             b = (u % 256) / 255 }
end

-- load the 62 real dyes
local infos, order = {}, {}
local f = assert(io.open("/tmp/dyecolor.csv", "r"), "missing /tmp/dyecolor.csv")
f:read("l")  -- header
for line in f:lines() do
    local name, id, _, _, _, start =
        line:match('^"([^"]*)",(%-?%d+),(%-?%d+),(%-?%d+),(%-?%d+),(%-?%d+)')
    if name and id then
        id = tonumber(id)
        infos[id] = { name = name, colorGroup = byName[name], swatchRGB = unpackRGB(start) }
        order[#order + 1] = id
    end
end
f:close()
-- stable order: by group, then name
local GORDER = { Red=1,Orange=2,Yellow=3,Green=4,Teal=5,Blue=6,Purple=7,Pink=8,Brown=9,Grey=10,Black=11,White=12 }
table.sort(order, function(a, b)
    local ga, gb = GORDER[infos[a].colorGroup] or 99, GORDER[infos[b].colorGroup] or 99
    if ga ~= gb then return ga < gb end
    return infos[a].name < infos[b].name
end)

local function names(picks)
    if not picks or #picks == 0 then return "--" end
    local t = {}
    for _, p in ipairs(picks) do t[#t + 1] = infos[p.dyeID] and infos[p.dyeID].name or ("?" .. p.dyeID) end
    return table.concat(t, ", ")
end

-- expected pick counts (the ceiling the engine aims for)
local CHROMA_EXPECT = { complement = 1, analogous = 2, triad = 2, split = 2, clash = 1, neutral = 2, mono = 2 }
local flags = {}
local function flag(dye, cat, got, want)
    flags[#flags + 1] = string.format("%-22s %-11s got %d / want %d", dye, cat, got, want)
end

print("=== HARMONY AUDIT: all 62 dyes through the real engine ===\n")
for _, id in ipairs(order) do
    local a   = infos[id]
    local res = H.forDye(id, infos, {})
    if HW.ANGLE[a.colorGroup] then
        local by = res.byRelationship
        print(string.format("%-22s [%s]", a.name, a.colorGroup))
        print("   Neutral: " .. names(res.neutral))
        print("   Analog : " .. names(by.Analogous) .. "   Compl: " .. names(by.Complement)
            .. "   Clash: " .. names(by.Clash))
        print("   Triad  : " .. names(by.Triad) .. "   Split: " .. names(by.Split)
            .. "   Mono : " .. names(res.mono))
        if #(by.Complement or {}) < 1 then flag(a.name, "complement", #(by.Complement or {}), 1) end
        if #(by.Analogous  or {}) < 2 then flag(a.name, "analogous",  #(by.Analogous  or {}), 2) end
        if #(by.Triad      or {}) < 2 then flag(a.name, "triad",      #(by.Triad      or {}), 2) end
        if #(by.Split      or {}) < 2 then flag(a.name, "split",      #(by.Split      or {}), 2) end
        if #(by.Clash      or {}) < 1 then flag(a.name, "clash",      #(by.Clash      or {}), 1) end
        if #(res.neutral   or {}) < 2 then flag(a.name, "neutral",    #(res.neutral   or {}), 2) end
        if #(res.mono      or {}) < 2 then flag(a.name, "mono",       #(res.mono      or {}), 2) end
    else
        print(string.format("%-22s [%s]  (NEUTRAL anchor)", a.name, a.colorGroup))
        print("   Tones: " .. names(res.tones) .. "   Contrast: " .. names(res.contrast)
            .. "   Accent: " .. names(res.accent))
        if #(res.tones  or {}) < 1 then flag(a.name, "tones",  #(res.tones  or {}), 1) end
        if #(res.accent or {}) < 1 then flag(a.name, "accent", #(res.accent or {}), 1) end
        -- contrast legitimately empty for a truly achromatic anchor; note, don't flag
    end
end

print("\n=== EDGE CASES (category returned fewer picks than the engine aims for) ===")
if #flags == 0 then
    print("  none -- every category filled to its target.")
else
    table.sort(flags)
    for _, line in ipairs(flags) do print("  " .. line) end
    print(string.format("\n  %d short/empty categories across 62 dyes.", #flags))
end
