-- RLFormulaTests.lua
-- Purpose: Tests for constants (DAYS_PER_MONTH, MAP_TO_AREA_CODE, AREA_CODES,
--          ALPHABET, START_YEAR, NUM_CHARACTERS) and functions (formatAge,
--          getMapCountryCode, getMapCountryIndex)
-- Author: Ritter

RLFormulaTests = {}

local Log = RmLogging.getLogger("RLRM")

--- Run formula and constants validation tests
---@param assert function Assert closure from RLTestRunner
function RLFormulaTests:runAll(assert)
    Log:info("--- Formula & Constants Tests ---")

    local RL = RealisticLivestock
    local C = RLConstants

    -- =========================================================================
    -- DAYS_PER_MONTH
    -- =========================================================================

    local monthCount = 0
    local daySum = 0
    local allPositiveInts = true
    for i = 1, 12 do
        if C.DAYS_PER_MONTH[i] ~= nil then
            monthCount = monthCount + 1
            daySum = daySum + C.DAYS_PER_MONTH[i]
            if C.DAYS_PER_MONTH[i] <= 0 or C.DAYS_PER_MONTH[i] ~= math.floor(C.DAYS_PER_MONTH[i]) then
                allPositiveInts = false
            end
        end
    end

    assert("DAYS_PER_MONTH has 12 entries", monthCount == 12, 12, monthCount)
    assert("DAYS_PER_MONTH all positive integers", allPositiveInts, true, allPositiveInts)
    assert("DAYS_PER_MONTH sum is 365", daySum == 365, 365, daySum)
    assert("DAYS_PER_MONTH Jan=31", C.DAYS_PER_MONTH[1] == 31, 31, C.DAYS_PER_MONTH[1])
    assert("DAYS_PER_MONTH Feb=28", C.DAYS_PER_MONTH[2] == 28, 28, C.DAYS_PER_MONTH[2])
    assert("DAYS_PER_MONTH Apr=30", C.DAYS_PER_MONTH[4] == 30, 30, C.DAYS_PER_MONTH[4])

    -- =========================================================================
    -- MAP_TO_AREA_CODE — all values are valid keys in AREA_CODES (1-16)
    -- =========================================================================

    local allValid = true
    local invalidMap = nil
    for mapName, areaCode in pairs(C.MAP_TO_AREA_CODE) do
        if type(areaCode) ~= "number" or areaCode < 1 or areaCode > 16 or C.AREA_CODES[areaCode] == nil then
            allValid = false
            invalidMap = mapName
            break
        end
    end

    assert("MAP_TO_AREA_CODE all values valid AREA_CODES keys",
        allValid, "all valid", invalidMap and ("invalid: " .. invalidMap) or "all valid")
    assert("MAP_TO_AREA_CODE Riverbend Springs=2",
        C.MAP_TO_AREA_CODE["Riverbend Springs"] == 2, 2, C.MAP_TO_AREA_CODE["Riverbend Springs"])
    assert("MAP_TO_AREA_CODE Riverview=1",
        C.MAP_TO_AREA_CODE["Riverview"] == 1, 1, C.MAP_TO_AREA_CODE["Riverview"])
    assert("MAP_TO_AREA_CODE Oberschwaben=6",
        C.MAP_TO_AREA_CODE["Oberschwaben"] == 6, 6, C.MAP_TO_AREA_CODE["Oberschwaben"])
    assert("MAP_TO_AREA_CODE Zielonka=5",
        C.MAP_TO_AREA_CODE["Zielonka"] == 5, 5, C.MAP_TO_AREA_CODE["Zielonka"])

    -- =========================================================================
    -- AREA_CODES — entries 1-16, each has .code and .country
    -- =========================================================================

    local areaCodeCount = 0
    local allHaveFields = true
    for i = 1, 16 do
        local entry = C.AREA_CODES[i]
        if entry ~= nil then
            areaCodeCount = areaCodeCount + 1
            if type(entry.code) ~= "string" or #entry.code ~= 2 then
                allHaveFields = false
            end
            if type(entry.country) ~= "string" or #entry.country == 0 then
                allHaveFields = false
            end
        end
    end

    assert("AREA_CODES has 16 entries (no gaps)", areaCodeCount == 16, 16, areaCodeCount)
    assert("AREA_CODES all have .code (2-char) and .country", allHaveFields, true, allHaveFields)
    assert("AREA_CODES [1]=UK", C.AREA_CODES[1].code == "UK", "UK", C.AREA_CODES[1].code)
    assert("AREA_CODES [2]=US", C.AREA_CODES[2].code == "US", "US", C.AREA_CODES[2].code)
    assert("AREA_CODES [6]=DE", C.AREA_CODES[6].code == "DE", "DE", C.AREA_CODES[6].code)

    -- =========================================================================
    -- ALPHABET — 38 entries, values 0-37, no duplicates
    -- =========================================================================

    local alphaCount = 0
    local valueSeen = {}
    local hasDuplicates = false
    for _, v in pairs(RL.ALPHABET) do
        alphaCount = alphaCount + 1
        if valueSeen[v] then
            hasDuplicates = true
        end
        valueSeen[v] = true
    end

    -- Check 0-37 coverage
    local fullCoverage = true
    for i = 0, 37 do
        if not valueSeen[i] then
            fullCoverage = false
            break
        end
    end

    assert("ALPHABET has 38 entries", alphaCount == 38, 38, alphaCount)
    assert("ALPHABET no duplicate values", not hasDuplicates, true, not hasDuplicates)
    assert("ALPHABET covers 0-37", fullCoverage, true, fullCoverage)
    assert("ALPHABET ['0']=0", RL.ALPHABET["0"] == 0, 0, RL.ALPHABET["0"])
    assert("ALPHABET ['A']=10", RL.ALPHABET["A"] == 10, 10, RL.ALPHABET["A"])
    assert("ALPHABET ['Z']=35", RL.ALPHABET["Z"] == 35, 35, RL.ALPHABET["Z"])
    assert("ALPHABET ['-']=37", RL.ALPHABET["-"] == 37, 37, RL.ALPHABET["-"])

    -- =========================================================================
    -- START_YEAR and NUM_CHARACTERS
    -- =========================================================================

    assert("START_YEAR FULL=2024", C.START_YEAR.FULL == 2024, 2024, C.START_YEAR.FULL)
    assert("START_YEAR PARTIAL=24", C.START_YEAR.PARTIAL == 24, 24, C.START_YEAR.PARTIAL)
    assert("NUM_CHARACTERS=64", RL.NUM_CHARACTERS == 64, 64, RL.NUM_CHARACTERS)

    -- =========================================================================
    -- MARKS — 5 entries, each with key/active/priority/text, unique priorities
    -- =========================================================================

    local expectedMarks = { "AI_MANAGER_SELL", "AI_MANAGER_CASTRATE", "AI_MANAGER_DISEASE", "AI_MANAGER_INSEMINATE", "PLAYER" }
    local markCount = 0
    for _ in pairs(C.MARKS) do markCount = markCount + 1 end
    assert("MARKS has 5 entries", markCount == 5, 5, markCount)

    local allMarksValid = true
    local invalidMark = nil
    for _, name in ipairs(expectedMarks) do
        local mark = C.MARKS[name]
        if mark == nil then
            allMarksValid = false
            invalidMark = name .. " missing"
            break
        end
        if type(mark.key) ~= "string" or mark.key ~= name then
            allMarksValid = false
            invalidMark = name .. " bad key"
            break
        end
        if type(mark.active) ~= "boolean" then
            allMarksValid = false
            invalidMark = name .. " bad active"
            break
        end
        if type(mark.priority) ~= "number" or mark.priority ~= math.floor(mark.priority) then
            allMarksValid = false
            invalidMark = name .. " bad priority"
            break
        end
        if type(mark.text) ~= "string" or #mark.text == 0 then
            allMarksValid = false
            invalidMark = name .. " bad text"
            break
        end
    end
    assert("MARKS all entries have key/active/priority/text",
        allMarksValid, "all valid", invalidMark or "all valid")

    -- Verify all priorities are unique (prevents non-deterministic getHighestPriorityMark)
    local prioritySeen = {}
    local duplicatePriority = false
    for name, mark in pairs(C.MARKS) do
        if prioritySeen[mark.priority] then
            duplicatePriority = true
            invalidMark = name .. " duplicates priority " .. mark.priority
            break
        end
        prioritySeen[mark.priority] = name
    end
    assert("MARKS all priorities unique", not duplicatePriority,
        "all unique", invalidMark or "all unique")

    assert("MARKS PLAYER has highest priority (1)",
        C.MARKS["PLAYER"].priority == 1, 1, C.MARKS["PLAYER"].priority)

    -- =========================================================================
    -- formatAge
    -- =========================================================================

    local age0 = RL.formatAge(0)
    assert("formatAge(0) starts with '0'",
        string.sub(age0, 1, 1) == "0", "0", string.sub(age0, 1, 1))

    local age1 = RL.formatAge(1)
    assert("formatAge(1) starts with '1'",
        string.sub(age1, 1, 1) == "1", "1", string.sub(age1, 1, 1))

    local age12 = RL.formatAge(12)
    assert("formatAge(12) starts with '1' (1 year)",
        string.sub(age12, 1, 1) == "1", "1", string.sub(age12, 1, 1))
    assert("formatAge(12) contains comma",
        string.find(age12, ",") ~= nil, "contains ','", age12)

    local age25 = RL.formatAge(25)
    assert("formatAge(25) starts with '2' (2 years)",
        string.sub(age25, 1, 1) == "2", "2", string.sub(age25, 1, 1))
    assert("formatAge(25) contains comma",
        string.find(age25, ",") ~= nil, "contains ','", age25)

    -- =========================================================================
    -- getMapCountryCode / getMapCountryIndex — save/restore mapAreaCode
    -- =========================================================================

    local savedMapAreaCode = RL.mapAreaCode

    RL.mapAreaCode = 1
    assert("getMapCountryCode areaCode=1 returns UK",
        RL.getMapCountryCode() == "UK", "UK", RL.getMapCountryCode())

    RL.mapAreaCode = 2
    assert("getMapCountryCode areaCode=2 returns US",
        RL.getMapCountryCode() == "US", "US", RL.getMapCountryCode())

    RL.mapAreaCode = nil
    assert("getMapCountryCode areaCode=nil returns UK (default)",
        RL.getMapCountryCode() == "UK", "UK", RL.getMapCountryCode())

    RL.mapAreaCode = 5
    assert("getMapCountryIndex areaCode=5 returns 5",
        RL.getMapCountryIndex() == 5, 5, RL.getMapCountryIndex())

    RL.mapAreaCode = nil
    assert("getMapCountryIndex areaCode=nil returns 1 (default)",
        RL.getMapCountryIndex() == 1, 1, RL.getMapCountryIndex())

    -- Restore
    RL.mapAreaCode = savedMapAreaCode

    -- =========================================================================
    -- Extraction proof: constants moved, no backward-compat aliases
    -- =========================================================================

    assert("RealisticLivestock.AREA_CODES removed", RL.AREA_CODES == nil, "nil", tostring(RL.AREA_CODES))
    assert("RealisticLivestock.DAYS_PER_MONTH removed", RL.DAYS_PER_MONTH == nil, "nil", tostring(RL.DAYS_PER_MONTH))
    assert("RealisticLivestock.START_YEAR removed", RL.START_YEAR == nil, "nil", tostring(RL.START_YEAR))
    assert("RealisticLivestock.MARKS removed", RL.MARKS == nil, "nil", tostring(RL.MARKS))
    assert("RealisticLivestock.MAP_TO_AREA_CODE removed", RL.MAP_TO_AREA_CODE == nil, "nil", tostring(RL.MAP_TO_AREA_CODE))
    assert("RLConstants.AREA_CODES exists", C.AREA_CODES ~= nil, "table", tostring(C.AREA_CODES))
    assert("RLConstants.DAYS_PER_MONTH exists", C.DAYS_PER_MONTH ~= nil, "table", tostring(C.DAYS_PER_MONTH))
    assert("RLConstants.START_YEAR exists", C.START_YEAR ~= nil, "table", tostring(C.START_YEAR))
    assert("RLConstants.MARKS exists", C.MARKS ~= nil, "table", tostring(C.MARKS))
    assert("RLConstants.MAP_TO_AREA_CODE exists", C.MAP_TO_AREA_CODE ~= nil, "table", tostring(C.MAP_TO_AREA_CODE))
end
