-- RLHorseTests.lua
-- Purpose: Tests for AnimalHorse module — clamping, processRidingUpdate,
--          sell price formula, getHealthChangeFactor, and duck-typing contract
-- Author: Ritter

RLHorseTests = {}

local Log = RmLogging.getLogger("RLRM")

--- Run all horse-specific tests
---@param assert function Assert closure from RLTestRunner
function RLHorseTests:runAll(assert)
    Log:info("--- Horse Tests ---")

    self:testChangeFitnessClampsAndFloors(assert)
    self:testChangeRidingClampsAndFloors(assert)
    self:testChangeDirtClampsAndFloors(assert)
    self:testProcessRidingUpdateAboveThreshold(assert)
    self:testProcessRidingUpdateBelowThreshold(assert)
    self:testGetHorseSellPrice(assert)
    self:testGetHealthChangeFactor(assert)
    self:testDuckTypingContract(assert)

    -- RLRM-69: setRiding must clamp like other mutators
    self:testSetRidingClampsAndFloors(assert)
    -- RLRM-67: processRidingUpdate division-by-zero at boundary thresholds
    self:testProcessRidingUpdateThresholdZero(assert)
    self:testProcessRidingUpdateThresholdOneOverflow(assert)

    -- RLRM-68: Consistent horse type-check via isHorse()
    self:testIsHorseReturnsTrueForHorse(assert)
    self:testIsHorseReturnsFalseForCow(assert)
    self:testHorseAutoNaming(assert)
    self:testNonHorseNoAutoNaming(assert)
end


-- =========================================================================
-- changeFitness: clamps to 0-100 and floors fractional values
-- =========================================================================

function RLHorseTests:testChangeFitnessClampsAndFloors(assert)
    local horse = RLTestUtils:createTestAnimal("HORSE", { fitness = 50 })
    if horse == nil then return end

    -- Overflow: 50 + 200 -> clamped to 100
    AnimalHorse.changeFitness(horse, 200)
    assert("changeFitness clamps at 100", horse.fitness == 100, 100, horse.fitness)

    -- Underflow: 100 + (-200) -> clamped to 0
    AnimalHorse.changeFitness(horse, -200)
    assert("changeFitness clamps at 0", horse.fitness == 0, 0, horse.fitness)

    -- Fractional: 0 + 50.7 -> floor to 50
    AnimalHorse.changeFitness(horse, 50.7)
    assert("changeFitness floors fractional", horse.fitness == 50, 50, horse.fitness)
end


-- =========================================================================
-- changeRiding: clamps to 0-100 and floors fractional values
-- =========================================================================

function RLHorseTests:testChangeRidingClampsAndFloors(assert)
    local horse = RLTestUtils:createTestAnimal("HORSE", { riding = 50 })
    if horse == nil then return end

    AnimalHorse.changeRiding(horse, 200)
    assert("changeRiding clamps at 100", horse.riding == 100, 100, horse.riding)

    AnimalHorse.changeRiding(horse, -200)
    assert("changeRiding clamps at 0", horse.riding == 0, 0, horse.riding)

    AnimalHorse.changeRiding(horse, 50.7)
    assert("changeRiding floors fractional", horse.riding == 50, 50, horse.riding)
end


-- =========================================================================
-- changeDirt: clamps to 0-100 and floors fractional values
-- =========================================================================

function RLHorseTests:testChangeDirtClampsAndFloors(assert)
    local horse = RLTestUtils:createTestAnimal("HORSE", { dirt = 50 })
    if horse == nil then return end

    AnimalHorse.changeDirt(horse, 200)
    assert("changeDirt clamps at 100", horse.dirt == 100, 100, horse.dirt)

    AnimalHorse.changeDirt(horse, -200)
    assert("changeDirt clamps at 0", horse.dirt == 0, 0, horse.dirt)

    AnimalHorse.changeDirt(horse, 50.7)
    assert("changeDirt floors fractional", horse.dirt == 50, 50, horse.dirt)
end


-- =========================================================================
-- processRidingUpdate: above threshold -> positive fitness, reset, dirt +10
-- =========================================================================

function RLHorseTests:testProcessRidingUpdateAboveThreshold(assert)
    -- Set up horse with riding=80 (factor=0.8) and override threshold to 0.5
    -- so riding is guaranteed above threshold
    local horse = RLTestUtils:createTestAnimal("HORSE", { riding = 80, fitness = 50, dirt = 0 })
    if horse == nil then return end

    -- Override getSubType to inject a controlled ridingThresholdFactor
    local origGetSubType = horse.getSubType
    local mockSubType
    horse.getSubType = function(self)
        if mockSubType == nil then
            mockSubType = {}
            local real = origGetSubType(self)
            for k, v in pairs(real) do mockSubType[k] = v end
            mockSubType.ridingThresholdFactor = 0.5
        end
        return mockSubType
    end

    local savedTimeAdj = g_currentMission.environment.timeAdjustment
    g_currentMission.environment.timeAdjustment = 1.0

    AnimalHorse.processRidingUpdate(horse)

    -- Riding should be reset to 0
    assert("processRidingUpdate resets riding", horse.riding == 0, 0, horse.riding)

    -- Dirt should increase by 10
    assert("processRidingUpdate increases dirt", horse.dirt == 10, 10, horse.dirt)

    -- Above threshold: riding 0.8 > threshold 0.5 -> positive fitness delta (base 25)
    -- factor = (0.8 - 0.5) / (1 - 0.5) = 0.6, delta = 25 * 0.6 * 1.0 = 15
    -- fitness = floor(50 + 15) = 65
    assert("processRidingUpdate above threshold increases fitness",
        horse.fitness == 65, 65, horse.fitness)

    -- Restore
    horse.getSubType = origGetSubType
    g_currentMission.environment.timeAdjustment = savedTimeAdj
end


-- =========================================================================
-- processRidingUpdate: below threshold -> negative fitness, reset, dirt +10
-- =========================================================================

function RLHorseTests:testProcessRidingUpdateBelowThreshold(assert)
    -- Set up horse with riding=0 (factor=0.0), threshold=0.5 -> below threshold
    local horse = RLTestUtils:createTestAnimal("HORSE", { riding = 0, fitness = 50, dirt = 0 })
    if horse == nil then return end

    -- Override getSubType to inject controlled threshold
    local origGetSubType = horse.getSubType
    local mockSubType
    horse.getSubType = function(self)
        if mockSubType == nil then
            mockSubType = {}
            local real = origGetSubType(self)
            for k, v in pairs(real) do mockSubType[k] = v end
            mockSubType.ridingThresholdFactor = 0.5
        end
        return mockSubType
    end

    local savedTimeAdj = g_currentMission.environment.timeAdjustment
    g_currentMission.environment.timeAdjustment = 1.0

    AnimalHorse.processRidingUpdate(horse)

    -- Riding reset (already 0)
    assert("processRidingUpdate below: riding stays 0", horse.riding == 0, 0, horse.riding)

    -- Dirt increases
    assert("processRidingUpdate below: dirt +10", horse.dirt == 10, 10, horse.dirt)

    -- Below threshold: riding=0, factor = 0/0.5 - 1 = -1, delta = 10 * -1 * 1.0 = -10
    -- fitness = floor(50 + (-10)) = 40
    assert("processRidingUpdate below threshold decreases fitness",
        horse.fitness == 40, 40, horse.fitness)

    -- Restore
    horse.getSubType = origGetSubType
    g_currentMission.environment.timeAdjustment = savedTimeAdj
end


-- =========================================================================
-- getHorseSellPrice: formula verification
-- =========================================================================

function RLHorseTests:testGetHorseSellPrice(assert)
    local horse = RLTestUtils:createTestAnimal("HORSE", {
        riding = 50, fitness = 50, dirt = 0, health = 80
    })
    if horse == nil then return end

    local baseSellPrice = 1000
    local meatFactor = 1.0
    local weightFactor = 1.0

    local price = AnimalHorse.getHorseSellPrice(horse, baseSellPrice, meatFactor, weightFactor)

    -- Expected: 1000 * 1.0 * 1.0 * (0.3 + 0.5*0.8 + 0.3*0.5 + 0.2*0.5 - 0.2*0.0)
    -- = 1000 * (0.3 + 0.4 + 0.15 + 0.1 - 0.0) = 1000 * 0.95 = 950
    local expected = 950
    assert("getHorseSellPrice formula correct",
        math.abs(price - expected) < 0.01, expected, price)

    -- Minimum floor: price should never go below sellPrice * 0.05
    local minPrice = AnimalHorse.getHorseSellPrice(horse, baseSellPrice, 0.001, 0.001)
    assert("getHorseSellPrice minimum floor",
        math.abs(minPrice - baseSellPrice * 0.05) < 0.01, baseSellPrice * 0.05, minPrice)
end


-- =========================================================================
-- getHealthChangeFactor: with and without horse cleaning
-- =========================================================================

function RLHorseTests:testGetHealthChangeFactor(assert)
    -- Use dirt=80 so the two branches produce different results
    local horse = RLTestUtils:createTestAnimal("HORSE", { fitness = 50, dirt = 80 })
    if horse == nil then return end

    local foodFactor = 0.8

    -- Save and test with cleaning enabled
    local savedNeedCleaning = Platform.gameplay.needHorseCleaning

    Platform.gameplay.needHorseCleaning = true
    local withCleaning = AnimalHorse.getHealthChangeFactor(horse, foodFactor)
    -- Expected: 0.5*0.8 + 0.4*0.5 + 0.1*(1-0.8) = 0.4 + 0.2 + 0.02 = 0.62
    assert("getHealthChangeFactor with cleaning",
        math.abs(withCleaning - 0.62) < 0.001, 0.62, withCleaning)

    Platform.gameplay.needHorseCleaning = false
    local withoutCleaning = AnimalHorse.getHealthChangeFactor(horse, foodFactor)
    -- Expected: 0.6*0.8 + 0.4*0.5 = 0.48 + 0.2 = 0.68
    assert("getHealthChangeFactor without cleaning",
        math.abs(withoutCleaning - 0.68) < 0.001, 0.68, withoutCleaning)

    -- Verify the two branches actually differ
    assert("cleaning branches produce different results",
        math.abs(withCleaning - withoutCleaning) > 0.01, "different", "same")

    -- Restore
    Platform.gameplay.needHorseCleaning = savedNeedCleaning
end


-- =========================================================================
-- Duck-typing contract: Animal prototype has horse methods
-- =========================================================================

-- =========================================================================
-- RLRM-69: setRiding must clamp to 0-100 and floor (like changeRiding)
-- RED: All three asserts should FAIL before fix (no clamping/flooring)
-- =========================================================================

function RLHorseTests:testSetRidingClampsAndFloors(assert)
    local horse = RLTestUtils:createTestAnimal("HORSE", { riding = 50 })
    if horse == nil then return end

    -- Overflow: 150 -> should clamp to 100
    AnimalHorse.setRiding(horse, 150)
    assert("setRiding clamps at 100", horse.riding == 100, 100, horse.riding)

    -- Underflow: -50 -> should clamp to 0
    AnimalHorse.setRiding(horse, -50)
    assert("setRiding clamps at 0", horse.riding == 0, 0, horse.riding)

    -- Fractional: 50.7 -> should floor to 50
    AnimalHorse.setRiding(horse, 50.7)
    assert("setRiding floors fractional", horse.riding == 50, 50, horse.riding)
end


-- =========================================================================
-- RLRM-67: processRidingUpdate with threshold=0 and riding=0
-- RED: Division by zero — ridingFactor/ridingThresholdFactor = 0/0
-- =========================================================================

function RLHorseTests:testProcessRidingUpdateThresholdZero(assert)
    local horse = RLTestUtils:createTestAnimal("HORSE", { riding = 0, fitness = 50, dirt = 0 })
    if horse == nil then return end

    local origGetSubType = horse.getSubType
    local mockSubType
    horse.getSubType = function(self)
        if mockSubType == nil then
            mockSubType = {}
            local real = origGetSubType(self)
            for k, v in pairs(real) do mockSubType[k] = v end
            mockSubType.ridingThresholdFactor = 0
        end
        return mockSubType
    end

    local savedTimeAdj = g_currentMission.environment.timeAdjustment
    g_currentMission.environment.timeAdjustment = 1.0

    -- Should not crash (division by zero: 0/0)
    local ok, err = pcall(AnimalHorse.processRidingUpdate, horse)
    assert("processRidingUpdate threshold=0 no crash", ok, true, ok)

    -- Fitness should stay unchanged (no threshold = no adjustment)
    -- NaN comparison: NaN ~= 50 so this also catches NaN from 0/0
    assert("processRidingUpdate threshold=0 fitness unchanged",
        horse.fitness == 50, 50, horse.fitness)

    -- Riding still reset, dirt still +10 (side effects should happen)
    assert("processRidingUpdate threshold=0 riding reset", horse.riding == 0, 0, horse.riding)
    assert("processRidingUpdate threshold=0 dirt +10", horse.dirt == 10, 10, horse.dirt)

    horse.getSubType = origGetSubType
    g_currentMission.environment.timeAdjustment = savedTimeAdj
end


-- =========================================================================
-- RLRM-67: processRidingUpdate with threshold=1.0 and ridingFactor>1.0
-- RED: Division by zero — (factor - 1.0) / (1 - 1.0) = x/0
-- We bypass setRiding clamping by setting horse.riding directly to 110
-- =========================================================================

function RLHorseTests:testProcessRidingUpdateThresholdOneOverflow(assert)
    local horse = RLTestUtils:createTestAnimal("HORSE", { fitness = 50, dirt = 0 })
    if horse == nil then return end

    -- Bypass setRiding to simulate unclamped riding > 100
    horse.riding = 110

    local origGetSubType = horse.getSubType
    local mockSubType
    horse.getSubType = function(self)
        if mockSubType == nil then
            mockSubType = {}
            local real = origGetSubType(self)
            for k, v in pairs(real) do mockSubType[k] = v end
            mockSubType.ridingThresholdFactor = 1.0
        end
        return mockSubType
    end

    local savedTimeAdj = g_currentMission.environment.timeAdjustment
    g_currentMission.environment.timeAdjustment = 1.0

    -- Should not crash (division by zero: 0.1 / 0)
    local ok, err = pcall(AnimalHorse.processRidingUpdate, horse)
    assert("processRidingUpdate threshold=1.0 overflow no crash", ok, true, ok)

    -- Fitness should be finite (not inf/nan)
    local fitnessFinite = horse.fitness == horse.fitness and horse.fitness ~= math.huge
        and horse.fitness ~= -math.huge
    assert("processRidingUpdate threshold=1.0 overflow fitness finite",
        fitnessFinite, "finite", tostring(horse.fitness))

    horse.getSubType = origGetSubType
    g_currentMission.environment.timeAdjustment = savedTimeAdj
end


function RLHorseTests:testDuckTypingContract(assert)
    assert("Animal.changeDirt exists", Animal.changeDirt ~= nil, "function", type(Animal.changeDirt))
    assert("Animal.changeFitness exists", Animal.changeFitness ~= nil, "function", type(Animal.changeFitness))
    assert("Animal.changeRiding exists", Animal.changeRiding ~= nil, "function", type(Animal.changeRiding))
    assert("Animal.getFitnessFactor exists", Animal.getFitnessFactor ~= nil, "function", type(Animal.getFitnessFactor))
    assert("Animal.getRidingFactor exists", Animal.getRidingFactor ~= nil, "function", type(Animal.getRidingFactor))
    assert("Animal.getDirtFactor exists", Animal.getDirtFactor ~= nil, "function", type(Animal.getDirtFactor))
    assert("Animal.getDailyRidingTime exists", Animal.getDailyRidingTime ~= nil, "function", type(Animal.getDailyRidingTime))
    assert("Animal.setRiding exists", Animal.setRiding ~= nil, "function", type(Animal.setRiding))
    assert("Animal.resetRiding exists", Animal.resetRiding ~= nil, "function", type(Animal.resetRiding))
    assert("Animal.getHealthChangeFactor exists", Animal.getHealthChangeFactor ~= nil, "function", type(Animal.getHealthChangeFactor))
    -- RLRM-68: isHorse delegate
    assert("Animal.isHorse exists", Animal.isHorse ~= nil, "function", type(Animal.isHorse))
end


-- =========================================================================
-- RLRM-68: isHorse() returns true for horse, false for non-horse
-- RED: Fails until Animal:isHorse() is added
-- =========================================================================

function RLHorseTests:testIsHorseReturnsTrueForHorse(assert)
    local horse = RLTestUtils:createTestAnimal("HORSE")
    if horse == nil then return end

    assert("isHorse returns true for HORSE",
        horse:isHorse() == true, true, horse:isHorse())
end

function RLHorseTests:testIsHorseReturnsFalseForCow(assert)
    local cow = RLTestUtils:createTestAnimal("COW")
    if cow == nil then return end

    assert("isHorse returns false for COW",
        cow:isHorse() == false, false, cow:isHorse())
end


-- =========================================================================
-- RLRM-68: Horse auto-naming regression (constructor gives horses a name)
-- GREEN from start — validates behavior is preserved after refactor
-- =========================================================================

function RLHorseTests:testHorseAutoNaming(assert)
    -- Pass empty name to trigger auto-naming path (constructor: name="" -> nil -> horse check)
    local horse = RLTestUtils:createTestAnimal("HORSE", { name = "" })
    if horse == nil then return end

    -- Horses with no explicit name should get auto-named via animalNameSystem
    assert("horse gets auto-named",
        horse.name ~= nil and horse.name ~= "", "non-empty string", tostring(horse.name))
end

function RLHorseTests:testNonHorseNoAutoNaming(assert)
    -- Pass empty name — cow should NOT get auto-named (only horses do)
    local cow = RLTestUtils:createTestAnimal("COW", { name = "" })
    if cow == nil then return end

    assert("cow with empty name stays nil",
        cow.name == nil, "nil", tostring(cow.name))
end
