-- RLBreedingMathTests.lua
-- Purpose: Tests for BreedingMath functions — clamp, erf, normal_cdf,
--          inv_norm_cdf, sd_from_poutside, randn, breedOffspring
-- Author: Ritter

RLBreedingMathTests = {}

local Log = RmLogging.getLogger("RLRM")

--- Run all BreedingMath tests
---@param assert function Assert closure from RLTestRunner
function RLBreedingMathTests:runAll(assert)
    Log:info("--- BreedingMath Tests ---")

    local BM = BreedingMath

    -- =========================================================================
    -- SD_CONST
    -- =========================================================================

    assert("SD_CONST == 0.15", math.abs(BM.SD_CONST - 0.15) < 0.001, 0.15, BM.SD_CONST)

    -- =========================================================================
    -- clamp
    -- =========================================================================

    assert("clamp within range", BM.clamp(1.0, 0.25, 1.75) == 1.0, 1.0, BM.clamp(1.0, 0.25, 1.75))
    assert("clamp below lo", BM.clamp(0.1, 0.25, 1.75) == 0.25, 0.25, BM.clamp(0.1, 0.25, 1.75))
    assert("clamp above hi", BM.clamp(2.0, 0.25, 1.75) == 1.75, 1.75, BM.clamp(2.0, 0.25, 1.75))
    assert("clamp exact lo boundary", BM.clamp(0.25, 0.25, 1.75) == 0.25, 0.25, BM.clamp(0.25, 0.25, 1.75))
    assert("clamp exact hi boundary", BM.clamp(1.75, 0.25, 1.75) == 1.75, 1.75, BM.clamp(1.75, 0.25, 1.75))

    -- =========================================================================
    -- erf
    -- =========================================================================

    assert("erf(0) ~ 0.0", math.abs(BM.erf(0)) < 1e-6, 0.0, BM.erf(0))

    local erf1 = BM.erf(1)
    assert("erf(1) ~ 0.8427", math.abs(erf1 - 0.8427) < 0.001, 0.8427, erf1)

    local erfNeg1 = BM.erf(-1)
    assert("erf(-1) ~ -0.8427", math.abs(erfNeg1 - (-0.8427)) < 0.001, -0.8427, erfNeg1)

    local erf3 = BM.erf(3)
    assert("erf(3) ~ 0.9999", math.abs(erf3 - 0.9999) < 0.001, 0.9999, erf3)

    -- =========================================================================
    -- normal_cdf
    -- =========================================================================

    assert("normal_cdf(0) ~ 0.5", math.abs(BM.normal_cdf(0) - 0.5) < 1e-6, 0.5, BM.normal_cdf(0))

    local ncdf1 = BM.normal_cdf(1)
    assert("normal_cdf(1) ~ 0.8413", math.abs(ncdf1 - 0.8413) < 0.001, 0.8413, ncdf1)

    local ncdfNeg1 = BM.normal_cdf(-1)
    assert("normal_cdf(-1) ~ 0.1587", math.abs(ncdfNeg1 - 0.1587) < 0.001, 0.1587, ncdfNeg1)

    local ncdfLarge = BM.normal_cdf(5)
    assert("normal_cdf(5) ~ 1.0", ncdfLarge > 0.999, ">0.999", ncdfLarge)

    -- =========================================================================
    -- inv_norm_cdf
    -- =========================================================================

    local inv05 = BM.inv_norm_cdf(0.5)
    assert("inv_norm_cdf(0.5) ~ 0.0", math.abs(inv05) < 0.001, 0.0, inv05)

    local inv84 = BM.inv_norm_cdf(0.8413)
    assert("inv_norm_cdf(0.8413) ~ 1.0", math.abs(inv84 - 1.0) < 0.01, 1.0, inv84)

    -- Lower tail (p < 0.02425)
    local inv01 = BM.inv_norm_cdf(0.01)
    assert("inv_norm_cdf(0.01) ~ -2.326 (lower tail)",
        math.abs(inv01 - (-2.326)) < 0.01, -2.326, inv01)

    -- Upper tail (p > 0.97575) — validates sign-bug fix
    local inv99 = BM.inv_norm_cdf(0.99)
    assert("inv_norm_cdf(0.99) ~ +2.326 (upper tail, sign-fix)",
        math.abs(inv99 - 2.326) < 0.01, 2.326, inv99)

    -- Roundtrip: normal_cdf(inv_norm_cdf(p)) ~ p
    for _, p in ipairs({0.1, 0.3, 0.7, 0.9}) do
        local roundtrip = BM.normal_cdf(BM.inv_norm_cdf(p))
        assert(string.format("roundtrip p=%.1f", p),
            math.abs(roundtrip - p) < 0.001, p, roundtrip)
    end

    -- =========================================================================
    -- sd_from_poutside
    -- =========================================================================

    assert("sd_from_poutside delta=0 returns nil",
        BM.sd_from_poutside(0, 0.5) == nil, "nil", tostring(BM.sd_from_poutside(0, 0.5)))

    local sd1 = BM.sd_from_poutside(1.0, 0.5)
    assert("sd_from_poutside delta=1.0 p=0.5 positive finite",
        sd1 ~= nil and sd1 > 0 and sd1 < math.huge, ">0", tostring(sd1))

    local sdSmallP = BM.sd_from_poutside(0.5, 0.1)
    local sdLargeP = BM.sd_from_poutside(0.5, 0.9)
    assert("sd_from_poutside p=0.1 < p=0.9 (smaller p needs tighter SD)",
        sdSmallP < sdLargeP, "sdSmallP < sdLargeP",
        string.format("%.4f vs %.4f", sdSmallP, sdLargeP))

    -- =========================================================================
    -- randn — statistical test (200 samples)
    -- =========================================================================

    local sum = 0
    local allFinite = true
    local N_RANDN = 200
    for _ = 1, N_RANDN do
        local v = BM.randn()
        sum = sum + v
        if v ~= v or v == math.huge or v == -math.huge then
            allFinite = false
        end
    end
    local meanRandn = sum / N_RANDN

    assert("randn mean ~ 0.0 (±0.5)", math.abs(meanRandn) < 0.5, "~0.0", meanRandn)
    assert("randn all values finite", allFinite, true, allFinite)

    -- =========================================================================
    -- breedOffspring — default opts, parents (0.5, 1.5)
    -- =========================================================================

    local N_BREED = 100
    local sumBreed = 0
    local allInRange = true
    for _ = 1, N_BREED do
        local child = BM.breedOffspring(0.5, 1.5)
        sumBreed = sumBreed + child
        if child < 0.25 or child > 1.75 then
            allInRange = false
        end
    end
    local meanBreed = sumBreed / N_BREED

    assert("breedOffspring default all in [0.25, 1.75]", allInRange, true, allInRange)
    assert("breedOffspring default mean ~ 1.0 (±0.3)",
        math.abs(meanBreed - 1.0) < 0.3, "~1.0", meanBreed)

    -- =========================================================================
    -- breedOffspring — swapped parent order (1.5, 0.5)
    -- =========================================================================

    local sumSwapped = 0
    local allSwappedInRange = true
    for _ = 1, N_BREED do
        local child = BM.breedOffspring(1.5, 0.5)
        sumSwapped = sumSwapped + child
        if child < 0.25 or child > 1.75 then
            allSwappedInRange = false
        end
    end
    local meanSwapped = sumSwapped / N_BREED

    assert("breedOffspring swapped parents all in [0.25, 1.75]",
        allSwappedInRange, true, allSwappedInRange)
    assert("breedOffspring swapped parents mean ~ 1.0 (±0.3)",
        math.abs(meanSwapped - 1.0) < 0.3, "~1.0", meanSwapped)

    -- =========================================================================
    -- breedOffspring — identical parents (1.0, 1.0)
    -- =========================================================================

    local sumIdent = 0
    local allIdentInRange = true
    for _ = 1, N_BREED do
        local child = BM.breedOffspring(1.0, 1.0)
        sumIdent = sumIdent + child
        if child < 0.25 or child > 1.75 then
            allIdentInRange = false
        end
    end
    local meanIdent = sumIdent / N_BREED

    assert("breedOffspring identical parents all in [0.25, 1.75]",
        allIdentInRange, true, allIdentInRange)
    assert("breedOffspring identical parents mean ~ 1.0 (±0.3)",
        math.abs(meanIdent - 1.0) < 0.3, "~1.0", meanIdent)

    -- =========================================================================
    -- breedOffspring — identical parents with explicit p_outside
    -- =========================================================================

    local sumIdentP = 0
    local allIdentPInRange = true
    for _ = 1, N_BREED do
        local child = BM.breedOffspring(1.0, 1.0, { p_outside = 0.3 })
        sumIdentP = sumIdentP + child
        if child < 0.25 or child > 1.75 then
            allIdentPInRange = false
        end
    end
    local meanIdentP = sumIdentP / N_BREED

    assert("breedOffspring identical+p_outside all in [0.25, 1.75]",
        allIdentPInRange, true, allIdentPInRange)
    assert("breedOffspring identical+p_outside mean ~ 1.0 (±0.3)",
        math.abs(meanIdentP - 1.0) < 0.3, "~1.0", meanIdentP)

    -- =========================================================================
    -- breedOffspring — clamp=false, large SD (should exceed bounds)
    -- =========================================================================

    local anyOutside = false
    for _ = 1, N_BREED do
        local child = BM.breedOffspring(0.3, 1.7, { sd = 2.0, clamp = false })
        if child < 0.25 or child > 1.75 then
            anyOutside = true
            break
        end
    end

    assert("breedOffspring clamp=false at least one outside [0.25, 1.75]",
        anyOutside, true, anyOutside)

    -- =========================================================================
    -- breedOffspring — probabilities table
    -- =========================================================================

    local _, probs = BM.breedOffspring(0.5, 1.5)

    assert("breedOffspring probs.sd > 0",
        probs.sd ~= nil and probs.sd > 0, ">0", tostring(probs.sd))
    assert("breedOffspring probs.above_best exists",
        probs.above_best ~= nil, "not nil", tostring(probs.above_best))
    assert("breedOffspring probs.between exists",
        probs.between ~= nil, "not nil", tostring(probs.between))
    assert("breedOffspring probs.below_worst exists",
        probs.below_worst ~= nil, "not nil", tostring(probs.below_worst))

    local probSum = probs.above_best + probs.between + probs.below_worst
    assert("breedOffspring probs sum ~ 1.0",
        math.abs(probSum - 1.0) < 0.001, 1.0, probSum)
end
