-- RLAIAnimalManagerTests.lua
-- Purpose: Decision logic tests for AIAnimalManager (RLRM-61)
-- Author: Ritter
-- Tests: sell, buy, castrate, insemination decisions via onDayChanged()
-- Strategy: Mock infrastructure + createTestAnimal factory, one decision type per group

RLAIAnimalManagerTests = {}

local Log = RmLogging.getLogger("RLRM")

--- Run all AIAnimalManager decision tests.
--- Order: Edge Cases -> CASTRATE -> SELL -> BUY -> AI (ascending mock complexity)
--- @param assert function Assert closure from RLTestRunner
function RLAIAnimalManagerTests:runAll(assert)
    Log:info("--- AIAnimalManager Decision Tests ---")

    self:testEdgeCases(assert)
    self:testCastrate(assert)
    self:testSell(assert)
    self:testBuy(assert)
    self:testAI(assert)
end

-- =============================================================================
-- EDGE CASES (AC 1-5)
-- =============================================================================

function RLAIAnimalManagerTests:testEdgeCases(assert)
    Log:info("  [Edge Cases]")

    -- ---- AC 1: Server guard ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mockCS = RLTestUtils:getSharedMockClusterSystem()
    local mockHusbandry = {
        getOwnerFarmId = function(self) return 1 end,
        getClusters = function(self) return mockCS.animals end,
        getAnimalTypeIndex = function(self) return AnimalType.COW end,
        addRLMessage = function(self, ...) end,
    }
    local clientMgr = AIAnimalManager.new(mockHusbandry, false)
    clientMgr.settings.sell.enabled = true
    clientMgr.settings.sell.maxAnimals = 5
    local cow = RLTestUtils:createTestAnimal("COW", { gender = "male" })

    clientMgr:onDayChanged()

    assert("EDGE server guard: no events when isServer=false",
        #RLTestUtils:getCapturedEvents() == 0, 0, #RLTestUtils:getCapturedEvents())
    assert("EDGE server guard: no mutations when isServer=false",
        cow.isCastrated == false, false, cow.isCastrated)

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- AC 2: All disabled ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr = RLTestUtils:createTestAIManager()
    local cow2 = RLTestUtils:createTestAnimal("COW", { gender = "male" })

    mgr:onDayChanged()

    assert("EDGE all disabled: no events",
        #RLTestUtils:getCapturedEvents() == 0, 0, #RLTestUtils:getCapturedEvents())
    assert("EDGE all disabled: no mutations",
        cow2.isCastrated == false, false, cow2.isCastrated)

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- AC 3: Empty herd + sell enabled ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr3 = RLTestUtils:createTestAIManager()
    mgr3.settings.sell.enabled = true
    mgr3.settings.sell.maxAnimals = 5

    mgr3:onDayChanged()

    assert("EDGE empty herd: no sell event",
        #RLTestUtils:getCapturedEvents() == 0, 0, #RLTestUtils:getCapturedEvents())

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- AC 4: All-female herd + castrate ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr4 = RLTestUtils:createTestAIManager()
    mgr4.settings.castrate.enabled = true
    mgr4.settings.castrate.age.min = 0
    mgr4.settings.castrate.age.max = 999

    local f1 = RLTestUtils:createTestAnimal("COW", { gender = "female" })
    local f2 = RLTestUtils:createTestAnimal("COW", { gender = "female" })
    local f3 = RLTestUtils:createTestAnimal("COW", { gender = "female" })

    mgr4:onDayChanged()

    local noneCastrated = not f1.isCastrated and not f2.isCastrated and not f3.isCastrated
    assert("EDGE all-female: no castrations",
        noneCastrated, true, noneCastrated)

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- AC 5: All-male herd + sell gender=female ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr5 = RLTestUtils:createTestAIManager()
    mgr5.settings.sell.enabled = true
    mgr5.settings.sell.maxAnimals = 5
    mgr5.settings.sell.gender = "female"

    RLTestUtils:createTestAnimal("COW", { gender = "male" })
    RLTestUtils:createTestAnimal("COW", { gender = "male" })
    RLTestUtils:createTestAnimal("COW", { gender = "male" })

    mgr5:onDayChanged()

    assert("EDGE all-male + sell female: no sell event",
        #RLTestUtils:getCapturedEvents() == 0, 0, #RLTestUtils:getCapturedEvents())

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()
end

-- =============================================================================
-- CASTRATE DECISIONS (AC 6-12)
-- =============================================================================

function RLAIAnimalManagerTests:testCastrate(assert)
    Log:info("  [Castrate Decisions]")

    -- Helper: create a castrate-enabled manager with wide defaults
    local function setupCastrateManager()
        local mgr = RLTestUtils:createTestAIManager()
        mgr.settings.castrate.enabled = true
        mgr.settings.castrate.age.min = 0
        mgr.settings.castrate.age.max = 999
        return mgr
    end

    -- ---- AC 6: Basic castrate — 2 matching males ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr = setupCastrateManager()
    local m1 = RLTestUtils:createTestAnimal("COW", { gender = "male" })
    local m2 = RLTestUtils:createTestAnimal("COW", { gender = "male" })

    mgr:onDayChanged()

    assert("CASTRATE basic: male 1 castrated",
        m1.isCastrated == true, true, m1.isCastrated)
    assert("CASTRATE basic: male 1 fertility=0",
        m1.genetics.fertility == 0, 0, m1.genetics.fertility)
    assert("CASTRATE basic: male 2 castrated",
        m2.isCastrated == true, true, m2.isCastrated)
    assert("CASTRATE basic: male 2 fertility=0",
        m2.genetics.fertility == 0, 0, m2.genetics.fertility)

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- AC 7: Skips females ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr7 = setupCastrateManager()
    local male = RLTestUtils:createTestAnimal("COW", { gender = "male" })
    local female = RLTestUtils:createTestAnimal("COW", { gender = "female" })

    mgr7:onDayChanged()

    assert("CASTRATE skips female: male castrated",
        male.isCastrated == true, true, male.isCastrated)
    assert("CASTRATE skips female: female not castrated",
        female.isCastrated == false, false, female.isCastrated)

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- AC 8: Skips already castrated ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr8 = setupCastrateManager()
    local alreadyCastrated = RLTestUtils:createTestAnimal("COW", {
        gender = "male", isCastrated = true,
        genetics = { health = 1.0, fertility = 0, productivity = 1.0, quality = 1.0, metabolism = 1.0 }
    })
    local uncastrated1 = RLTestUtils:createTestAnimal("COW", { gender = "male" })
    local uncastrated2 = RLTestUtils:createTestAnimal("COW", { gender = "male" })

    mgr8:onDayChanged()

    -- Both uncastrated males should be castrated (loop has no maxAnimals cap)
    assert("CASTRATE skips already castrated: uncastrated males are castrated",
        uncastrated1.isCastrated == true and uncastrated2.isCastrated == true, true,
        "u1=" .. tostring(uncastrated1.isCastrated) .. " u2=" .. tostring(uncastrated2.isCastrated))
    -- Wage proves skip: COW wage=20, each castrate costs 20*0.5=10
    -- 2 castrations = wage 20; if pre-castrated was re-processed, wage would be 30
    assert("CASTRATE skips already castrated: wage reflects 2 not 3 castrations",
        mgr8.wage == 20, 20, mgr8.wage)

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- AC 9: Age filter ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr9 = setupCastrateManager()
    mgr9.settings.castrate.age.min = 12
    mgr9.settings.castrate.age.max = 24

    local tooYoung = RLTestUtils:createTestAnimal("COW", { gender = "male", age = 6 })
    local tooOld = RLTestUtils:createTestAnimal("COW", { gender = "male", age = 36 })
    local inRange = RLTestUtils:createTestAnimal("COW", { gender = "male", age = 18 })

    mgr9:onDayChanged()

    assert("CASTRATE age filter: too young not castrated",
        tooYoung.isCastrated == false, false, tooYoung.isCastrated)
    assert("CASTRATE age filter: too old not castrated",
        tooOld.isCastrated == false, false, tooOld.isCastrated)
    assert("CASTRATE age filter: in range castrated",
        inRange.isCastrated == true, true, inRange.isCastrated)

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- AC 10: Genetics filter (health threshold) ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr10 = setupCastrateManager()
    mgr10.settings.castrate.health.min = 50
    mgr10.settings.castrate.health.max = 175

    local lowHealth = RLTestUtils:createTestAnimal("COW", {
        gender = "male",
        genetics = { health = 0.3, fertility = 1.0, productivity = 1.0, quality = 1.0, metabolism = 1.0 }
    })
    local goodHealth = RLTestUtils:createTestAnimal("COW", {
        gender = "male",
        genetics = { health = 1.0, fertility = 1.0, productivity = 1.0, quality = 1.0, metabolism = 1.0 }
    })

    mgr10:onDayChanged()

    assert("CASTRATE genetics: low health (0.3 < 0.5 min) not castrated",
        lowHealth.isCastrated == false, false, lowHealth.isCastrated)
    assert("CASTRATE genetics: good health (1.0 in range) castrated",
        goodHealth.isCastrated == true, true, goodHealth.isCastrated)

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- AC 11: Mark mode ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr11 = setupCastrateManager()
    mgr11.settings.castrate.mark = true

    local markTarget = RLTestUtils:createTestAnimal("COW", { gender = "male" })

    mgr11:onDayChanged()

    assert("CASTRATE mark mode: not actually castrated",
        markTarget.isCastrated == false, false, markTarget.isCastrated)
    assert("CASTRATE mark mode: AI_MANAGER_CASTRATE mark set",
        markTarget:getMarked("AI_MANAGER_CASTRATE") == true, true, markTarget:getMarked("AI_MANAGER_CASTRATE"))

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- AC 12: Fertility=0 skipped ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr12 = setupCastrateManager()
    local zeroFertility = RLTestUtils:createTestAnimal("COW", {
        gender = "male",
        genetics = { health = 1.0, fertility = 0, productivity = 1.0, quality = 1.0, metabolism = 1.0 }
    })

    mgr12:onDayChanged()

    assert("CASTRATE fertility=0: skipped (not flagged as castrated)",
        zeroFertility.isCastrated == false, false, zeroFertility.isCastrated)

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- Wage accumulation ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgrW = setupCastrateManager()
    RLTestUtils:createTestAnimal("COW", { gender = "male" })
    RLTestUtils:createTestAnimal("COW", { gender = "male" })

    mgrW:onDayChanged()

    assert("CASTRATE wage: accumulated > 0",
        mgrW.wage > 0, ">0", mgrW.wage)

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- RLRM-66: Execute mode (mark=false) must use CASTRATED message keys ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr66, _, mockHusbandry66 = RLTestUtils:createTestAIManager()
    mgr66.settings.castrate.enabled = true
    mgr66.settings.castrate.mark = false
    mgr66.settings.castrate.age.min = 0
    mgr66.settings.castrate.age.max = 999

    -- Capture addRLMessage calls
    local capturedMsgIds = {}
    mockHusbandry66.addRLMessage = function(self, msgId, ...)
        table.insert(capturedMsgIds, msgId)
    end

    RLTestUtils:createTestAnimal("COW", { gender = "male" })
    RLTestUtils:createTestAnimal("COW", { gender = "male" })

    mgr66:onDayChanged()

    -- Execute branch should use AI_MANAGER_CASTRATED_MULTIPLE (2 animals),
    -- NOT AI_MANAGER_MARK_CASTRATE_MULTIPLE
    local hasMarkMsg = false
    local hasCastratedMsg = false
    for _, msgId in ipairs(capturedMsgIds) do
        if string.find(msgId, "MARK_CASTRATE") then hasMarkMsg = true end
        if msgId == "AI_MANAGER_CASTRATED_SINGLE" or msgId == "AI_MANAGER_CASTRATED_MULTIPLE" then
            hasCastratedMsg = true
        end
    end
    assert("RLRM-66: execute mode uses CASTRATED message keys (not MARK)",
        hasCastratedMsg and not hasMarkMsg, true,
        "castrated=" .. tostring(hasCastratedMsg) .. " mark=" .. tostring(hasMarkMsg))

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()
end

-- =============================================================================
-- SELL DECISIONS (AC 13-20)
-- =============================================================================

function RLAIAnimalManagerTests:testSell(assert)
    Log:info("  [Sell Decisions]")

    -- Helper: create a sell-enabled manager with wide defaults
    local function setupSellManager()
        local mgr = RLTestUtils:createTestAIManager()
        mgr.settings.sell.enabled = true
        mgr.settings.sell.maxAnimals = 99
        return mgr
    end

    -- Helper: count sell events and get total animals sold
    local function getSellEventAnimals()
        local events = RLTestUtils:getCapturedEvents()
        for _, event in ipairs(events) do
            if event.animals ~= nil and event.price ~= nil then
                return event.animals
            end
        end
        return nil
    end

    -- ---- AC 13: Basic sell — maxAnimals limits ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr = setupSellManager()
    mgr.settings.sell.maxAnimals = 2

    RLTestUtils:createTestAnimal("COW")
    RLTestUtils:createTestAnimal("COW")
    RLTestUtils:createTestAnimal("COW")

    mgr:onDayChanged()

    local soldAnimals = getSellEventAnimals()
    assert("SELL basic: event contains exactly 2 animals",
        soldAnimals ~= nil and #soldAnimals == 2, 2, soldAnimals and #soldAnimals or 0)

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- AC 14: Gender filter ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr14 = setupSellManager()
    mgr14.settings.sell.gender = "male"

    RLTestUtils:createTestAnimal("COW", { gender = "male" })
    RLTestUtils:createTestAnimal("COW", { gender = "male" })
    RLTestUtils:createTestAnimal("COW", { gender = "female" })
    RLTestUtils:createTestAnimal("COW", { gender = "female" })

    mgr14:onDayChanged()

    local sold14 = getSellEventAnimals()
    local allMale = true
    if sold14 then
        for _, a in ipairs(sold14) do
            if a.gender ~= "male" then allMale = false end
        end
    end
    assert("SELL gender filter: only males in sell event",
        sold14 ~= nil and #sold14 == 2 and allMale, true, sold14 and (#sold14 .. " animals, allMale=" .. tostring(allMale)) or "nil")

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- AC 15: Age filter ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr15 = setupSellManager()
    mgr15.settings.sell.age.min = 12
    mgr15.settings.sell.age.max = 36

    RLTestUtils:createTestAnimal("COW", { age = 6 })   -- too young
    local inRange = RLTestUtils:createTestAnimal("COW", { age = 18 })  -- in range
    RLTestUtils:createTestAnimal("COW", { age = 48 })   -- too old

    mgr15:onDayChanged()

    local sold15 = getSellEventAnimals()
    assert("SELL age filter: only in-range animal sold",
        sold15 ~= nil and #sold15 == 1, 1, sold15 and #sold15 or 0)
    assert("SELL age filter: sold animal is the in-range one",
        sold15 ~= nil and sold15[1] == inRange, true,
        sold15 and ("uniqueId=" .. tostring(sold15[1].uniqueId)) or "nil")

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- AC 16: maxAnimals=1 with 5 matching ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr16 = setupSellManager()
    mgr16.settings.sell.maxAnimals = 1

    for i = 1, 5 do
        RLTestUtils:createTestAnimal("COW")
    end

    mgr16:onDayChanged()

    local sold16 = getSellEventAnimals()
    assert("SELL maxAnimals=1: event contains exactly 1",
        sold16 ~= nil and #sold16 == 1, 1, sold16 and #sold16 or 0)

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- AC 17: Disabled = no event ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr17 = RLTestUtils:createTestAIManager()
    -- sell.enabled is false by default
    RLTestUtils:createTestAnimal("COW")

    mgr17:onDayChanged()

    assert("SELL disabled: no events",
        #RLTestUtils:getCapturedEvents() == 0, 0, #RLTestUtils:getCapturedEvents())

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- AC 18: Mark mode ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr18 = setupSellManager()
    mgr18.settings.sell.mark = true

    local markAnimal = RLTestUtils:createTestAnimal("COW")

    mgr18:onDayChanged()

    assert("SELL mark mode: no broadcastEvent for sell",
        #RLTestUtils:getCapturedEvents() == 0, 0, #RLTestUtils:getCapturedEvents())
    assert("SELL mark mode: AI_MANAGER_SELL mark set",
        markAnimal:getMarked("AI_MANAGER_SELL") == true, true, markAnimal:getMarked("AI_MANAGER_SELL"))

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- AC 19: Disease filter ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr19 = setupSellManager()
    mgr19.settings.sell.diseases = true  -- only sell diseased animals

    local healthy = RLTestUtils:createTestAnimal("COW")
    local diseased = RLTestUtils:createTestAnimal("COW", {
        diseases = { { title = "test_disease", modifyValue = function(self, v) return v * 0.9 end, modifyOutput = function(self, ft, v) return v end } }
    })

    mgr19:onDayChanged()

    local sold19 = getSellEventAnimals()
    assert("SELL disease filter: only diseased animal sold",
        sold19 ~= nil and #sold19 == 1, 1, sold19 and #sold19 or 0)

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- AC 20: Genetics threshold filtering ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr20 = setupSellManager()
    -- Set health range: 50-80 (normalized: 0.50-0.80)
    mgr20.settings.sell.health.min = 50
    mgr20.settings.sell.health.max = 80

    local belowRange = RLTestUtils:createTestAnimal("COW", {
        genetics = { health = 0.3, fertility = 1.0, productivity = 1.0, quality = 1.0, metabolism = 1.0 }
    })
    local inRangeG = RLTestUtils:createTestAnimal("COW", {
        genetics = { health = 0.6, fertility = 1.0, productivity = 1.0, quality = 1.0, metabolism = 1.0 }
    })
    local aboveRange = RLTestUtils:createTestAnimal("COW", {
        genetics = { health = 1.2, fertility = 1.0, productivity = 1.0, quality = 1.0, metabolism = 1.0 }
    })

    mgr20:onDayChanged()

    local sold20 = getSellEventAnimals()
    assert("SELL genetics filter: only in-range genetics animal sold",
        sold20 ~= nil and #sold20 == 1, 1, sold20 and #sold20 or 0)

    -- ---- Wage accumulation ----
    assert("SELL wage: accumulated > 0",
        mgr20.wage > 0, ">0", mgr20.wage)

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()
end

-- =============================================================================
-- BUY DECISIONS (AC 21-27)
-- =============================================================================

function RLAIAnimalManagerTests:testBuy(assert)
    Log:info("  [Buy Decisions]")

    -- Helper: create a buy-enabled manager
    local function setupBuyManager()
        local mgr = RLTestUtils:createTestAIManager()
        mgr.settings.buy.enabled = true
        mgr.settings.buy.maxAnimals = 99
        mgr.settings.buy.budget.type = "fixed"
        mgr.settings.buy.budget.fixed = 50000
        return mgr
    end

    -- Helper: create a mock shop animal with controlled price
    local function createShopAnimal(overrides)
        overrides = overrides or {}
        local animal = RLTestUtils:createTestAnimal("COW", {
            gender = overrides.gender or "female",
            age = overrides.age or 24,
            genetics = overrides.genetics,
            diseases = overrides.diseases or {},
            breed = overrides.breed,
        })
        if animal then
            animal.reserved = false
            -- Override getSellPrice for deterministic pricing
            if overrides.price then
                local price = overrides.price
                animal.getSellPrice = function(self) return price end
            end
        end
        return animal
    end

    -- Helper: get buy event animals
    local function getBuyEventAnimals()
        local events = RLTestUtils:getCapturedEvents()
        for _, event in ipairs(events) do
            if event.animals ~= nil and event.price ~= nil then
                return event.animals, event.price
            end
        end
        return nil, 0
    end

    -- ---- AC 21: Budget fixed ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr21 = setupBuyManager()
    mgr21.settings.buy.budget.fixed = 5000
    RLTestUtils._mockFarmBalance = 10000

    -- Create shop animals (separate from herd clusterSystem)
    local shopCS = RLTestUtils:createMockClusterSystem()
    local shopAnimals = {}
    for i = 1, 3 do
        local sa = RLTestUtils:createTestAnimal("COW", { clusterSystem = shopCS })
        sa.reserved = false
        sa.getSellPrice = function(self) return 2000 end  -- buy price = 2000 * 1.075 = 2150
        table.insert(shopAnimals, sa)
    end
    RLTestUtils._mockShopAnimals = shopAnimals

    mgr21:onDayChanged()

    local bought21, spent21 = getBuyEventAnimals()
    -- Budget 5000, each animal costs 2150, so max 2 animals (4300 <= 5000)
    assert("BUY fixed budget: bought <= budget/price",
        bought21 ~= nil and #bought21 == 2, 2, bought21 and #bought21 or 0)

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- AC 22: Budget percentage ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr22 = setupBuyManager()
    mgr22.settings.buy.budget.type = "percentage"
    mgr22.settings.buy.budget.percentage = 10
    RLTestUtils._mockFarmBalance = 10000

    local shopCS22 = RLTestUtils:createMockClusterSystem()
    local shopAnimals22 = {}
    for i = 1, 3 do
        local sa = RLTestUtils:createTestAnimal("COW", { clusterSystem = shopCS22 })
        sa.reserved = false
        sa.getSellPrice = function(self) return 400 end  -- buy price = 400 * 1.075 = 430
        table.insert(shopAnimals22, sa)
    end
    RLTestUtils._mockShopAnimals = shopAnimals22

    mgr22:onDayChanged()

    -- Budget = 10% of 10000 = 1000. Each animal 430. Max 2 animals (860 <= 1000)
    local bought22 = getBuyEventAnimals()
    assert("BUY percentage budget: bought within 10% budget",
        bought22 ~= nil and #bought22 == 2, 2, bought22 and #bought22 or 0)

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- AC 23: Budget exceeds balance — clamped ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr23 = setupBuyManager()
    mgr23.settings.buy.budget.fixed = 20000
    RLTestUtils._mockFarmBalance = 5000

    local shopCS23 = RLTestUtils:createMockClusterSystem()
    local shopAnimals23 = {}
    for i = 1, 5 do
        local sa = RLTestUtils:createTestAnimal("COW", { clusterSystem = shopCS23 })
        sa.reserved = false
        sa.getSellPrice = function(self) return 2000 end  -- buy price = 2150
        table.insert(shopAnimals23, sa)
    end
    RLTestUtils._mockShopAnimals = shopAnimals23

    mgr23:onDayChanged()

    -- Budget clamped to balance 5000, each 2150 → max 2 (4300 <= 5000)
    local bought23 = getBuyEventAnimals()
    assert("BUY budget clamped to balance: max 2 with balance 5000",
        bought23 ~= nil and #bought23 == 2, 2, bought23 and #bought23 or 0)

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- AC 24: No budget = no buy ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr24 = setupBuyManager()
    mgr24.settings.buy.budget.fixed = 0
    RLTestUtils._mockFarmBalance = 10000

    local shopCS24 = RLTestUtils:createMockClusterSystem()
    local sa24 = RLTestUtils:createTestAnimal("COW", { clusterSystem = shopCS24 })
    sa24.reserved = false
    RLTestUtils._mockShopAnimals = { sa24 }

    mgr24:onDayChanged()

    assert("BUY zero budget: no buy event",
        #RLTestUtils:getCapturedEvents() == 0, 0, #RLTestUtils:getCapturedEvents())

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- AC 25: maxAnimals limit ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr25 = setupBuyManager()
    mgr25.settings.buy.maxAnimals = 2
    RLTestUtils._mockFarmBalance = 100000

    local shopCS25 = RLTestUtils:createMockClusterSystem()
    local shopAnimals25 = {}
    for i = 1, 5 do
        local sa = RLTestUtils:createTestAnimal("COW", { clusterSystem = shopCS25 })
        sa.reserved = false
        sa.getSellPrice = function(self) return 100 end
        table.insert(shopAnimals25, sa)
    end
    RLTestUtils._mockShopAnimals = shopAnimals25

    mgr25:onDayChanged()

    local bought25 = getBuyEventAnimals()
    assert("BUY maxAnimals=2: exactly 2 bought",
        bought25 ~= nil and #bought25 == 2, 2, bought25 and #bought25 or 0)

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- AC 26: Gender filter ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr26 = setupBuyManager()
    mgr26.settings.buy.gender = "female"
    RLTestUtils._mockFarmBalance = 100000

    local shopCS26 = RLTestUtils:createMockClusterSystem()
    local shopAnimals26 = {}
    for i = 1, 2 do
        local sa = RLTestUtils:createTestAnimal("COW", { clusterSystem = shopCS26, gender = "female" })
        sa.reserved = false
        sa.getSellPrice = function(self) return 100 end
        table.insert(shopAnimals26, sa)
    end
    for i = 1, 2 do
        local sa = RLTestUtils:createTestAnimal("COW", { clusterSystem = shopCS26, gender = "male" })
        sa.reserved = false
        sa.getSellPrice = function(self) return 100 end
        table.insert(shopAnimals26, sa)
    end
    RLTestUtils._mockShopAnimals = shopAnimals26

    mgr26:onDayChanged()

    local bought26 = getBuyEventAnimals()
    local allFemale = true
    if bought26 then
        for _, a in ipairs(bought26) do
            if a.gender ~= "female" then allFemale = false end
        end
    end
    assert("BUY gender filter: only females bought",
        bought26 ~= nil and #bought26 == 2 and allFemale, true,
        bought26 and (#bought26 .. " animals, allFemale=" .. tostring(allFemale)) or "nil")

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- AC 27: Disabled = no event ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr27 = RLTestUtils:createTestAIManager()
    -- buy.enabled is false by default

    mgr27:onDayChanged()

    assert("BUY disabled: no events",
        #RLTestUtils:getCapturedEvents() == 0, 0, #RLTestUtils:getCapturedEvents())

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- BUY sort order: cheapest first ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgrSort = setupBuyManager()
    mgrSort.settings.buy.maxAnimals = 10
    mgrSort.settings.buy.budget.fixed = 50000
    RLTestUtils._mockFarmBalance = 50000

    local shopCSSort = RLTestUtils:createMockClusterSystem()
    local shopAnimalsSort = {}
    local prices = { 3000, 1000, 2000 }
    for i = 1, 3 do
        local sa = RLTestUtils:createTestAnimal("COW", { clusterSystem = shopCSSort })
        sa.reserved = false
        local p = prices[i]
        sa.getSellPrice = function(self) return p end
        table.insert(shopAnimalsSort, sa)
    end
    RLTestUtils._mockShopAnimals = shopAnimalsSort

    mgrSort:onDayChanged()

    local boughtSort = getBuyEventAnimals()
    local sortCorrect = boughtSort ~= nil and #boughtSort == 3
    if sortCorrect then
        -- Verify cheapest-first ordering: prices should be ascending
        for i = 2, #boughtSort do
            local prevPrice = boughtSort[i - 1]:getSellPrice()
            local currPrice = boughtSort[i]:getSellPrice()
            if prevPrice > currPrice then
                sortCorrect = false
                break
            end
        end
    end
    assert("BUY sort order: cheapest first",
        sortCorrect, true,
        boughtSort and (#boughtSort .. " animals") or "nil")

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()
end

-- =============================================================================
-- AI INSEMINATION DECISIONS (AC 28-31)
-- =============================================================================

function RLAIAnimalManagerTests:testAI(assert)
    Log:info("  [AI Insemination Decisions]")

    -- Helper: resolve COW type index for dewar compatibility
    local cowTypeIndex = AnimalType.COW

    -- Helper: create a mock dewar with compatible bull
    local function createMockDewar(id, straws)
        return {
            getUniqueId = function(self) return id end,
            straws = straws or 5,
            animal = {
                typeIndex = cowTypeIndex,
                country = 1,
                farmId = "99",
                uniqueId = "bull_" .. id,
                genetics = { health = 1.0, fertility = 1.0, productivity = 1.0, quality = 1.0, metabolism = 1.0 },
                name = "TestBull_" .. id,
                subTypeIndex = RLTestUtils:resolveSubTypeForType("COW") or 1,
                success = 0.95,
            }
        }
    end

    -- Helper: create an AI-enabled manager
    local function setupAIManager()
        local mgr = RLTestUtils:createTestAIManager()
        mgr.settings.ai.enabled = true
        mgr.settings.ai.maxAnimals = 99
        mgr.settings.ai.age.min = 0
        mgr.settings.ai.age.max = 999
        return mgr
    end

    -- Helper: get insemination event items
    local function getInseminationEventItems()
        local events = RLTestUtils:getCapturedEvents()
        for _, event in ipairs(events) do
            if event.items ~= nil then
                return event.items
            end
        end
        return nil
    end

    -- ---- AC 28: No dewars = no insemination ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr28 = setupAIManager()
    RLTestUtils._mockDewars = nil  -- no dewars

    RLTestUtils:createTestAnimal("COW", { gender = "female", age = 24 })

    mgr28:onDayChanged()

    assert("AI no dewars: no insemination event",
        #RLTestUtils:getCapturedEvents() == 0, 0, #RLTestUtils:getCapturedEvents())

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- AC 29: Basic insemination ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr29 = setupAIManager()
    local dewar29 = createMockDewar("dewar_1", 5)
    RLTestUtils._mockDewars = { [cowTypeIndex] = { dewar29 } }

    -- Create eligible female (not pregnant, not inseminated, old enough)
    RLTestUtils:createTestAnimal("COW", { gender = "female", age = 24 })

    mgr29:onDayChanged()

    local items29 = getInseminationEventItems()
    assert("AI basic: insemination event contains 1 pair",
        items29 ~= nil and #items29 == 1, 1, items29 and #items29 or 0)
    if items29 and #items29 > 0 then
        assert("AI basic: dewar ID matches",
            items29[1].dewar == "dewar_1", "dewar_1", items29[1].dewar)
    end

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- AC 30: maxAnimals limit ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr30 = setupAIManager()
    mgr30.settings.ai.maxAnimals = 1
    local dewar30 = createMockDewar("dewar_2", 10)
    RLTestUtils._mockDewars = { [cowTypeIndex] = { dewar30 } }

    for i = 1, 3 do
        RLTestUtils:createTestAnimal("COW", { gender = "female", age = 24 })
    end

    mgr30:onDayChanged()

    local items30 = getInseminationEventItems()
    assert("AI maxAnimals=1: exactly 1 inseminated",
        items30 ~= nil and #items30 == 1, 1, items30 and #items30 or 0)

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- AC 31: Mark mode ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr31 = setupAIManager()
    mgr31.settings.ai.mark = true
    local dewar31 = createMockDewar("dewar_3", 5)
    RLTestUtils._mockDewars = { [cowTypeIndex] = { dewar31 } }

    local markFemale = RLTestUtils:createTestAnimal("COW", { gender = "female", age = 24 })

    mgr31:onDayChanged()

    -- In mark mode, no broadcastEvent for insemination (only bulk message event, but netIsRunning=false)
    assert("AI mark mode: no insemination broadcastEvent",
        #RLTestUtils:getCapturedEvents() == 0, 0, #RLTestUtils:getCapturedEvents())
    assert("AI mark mode: AI_MANAGER_INSEMINATE mark set",
        markFemale:getMarked("AI_MANAGER_INSEMINATE") == true, true, markFemale:getMarked("AI_MANAGER_INSEMINATE"))

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- Genetics sorting: highest genetics first ----
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgrSort = setupAIManager()
    mgrSort.settings.ai.maxAnimals = 1
    local dewarSort = createMockDewar("dewar_sort", 10)
    RLTestUtils._mockDewars = { [cowTypeIndex] = { dewarSort } }

    -- Low genetics total: 0.5*5 = 2.5
    local lowGen = RLTestUtils:createTestAnimal("COW", {
        gender = "female", age = 24,
        genetics = { health = 0.5, fertility = 0.5, productivity = 0.5, quality = 0.5, metabolism = 0.5 }
    })
    -- High genetics total: 1.5*5 = 7.5
    local highGen = RLTestUtils:createTestAnimal("COW", {
        gender = "female", age = 24,
        genetics = { health = 1.5, fertility = 1.5, productivity = 1.5, quality = 1.5, metabolism = 1.5 }
    })

    mgrSort:onDayChanged()

    local itemsSort = getInseminationEventItems()
    -- With maxAnimals=1 and descending sort by genetics, the high-genetics female should be chosen
    local chosenAnimal = itemsSort and itemsSort[1] and itemsSort[1].animal
    assert("AI genetics sort: highest genetics inseminated first",
        chosenAnimal ~= nil and chosenAnimal == highGen, true,
        chosenAnimal == highGen and "highGen" or "other")

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    -- ---- RLRM-65 regression: AI metabolism reads sell.metabolism.max ----
    -- The bug at line 758 uses sell.metabolism.max instead of ai.metabolism.max.
    -- Set sell.metabolism.max narrow (blocking) while ai.metabolism.max stays wide.
    -- If the bug exists, the animal's metabolism (1.0) will be outside sell's range
    -- and the animal will be filtered out (no insemination event).
    RLTestUtils:setupAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()

    local mgr65 = setupAIManager()
    -- AI metabolism range: wide (should allow metabolism=1.0)
    mgr65.settings.ai.metabolism.min = 25
    mgr65.settings.ai.metabolism.max = 175
    -- Sell metabolism range: narrow (would BLOCK metabolism=1.0 if bug reads sell instead of ai)
    mgr65.settings.sell.metabolism.min = 25
    mgr65.settings.sell.metabolism.max = 40  -- 0.40 max — animal's 1.0 exceeds this

    local dewar65 = createMockDewar("dewar_65", 5)
    RLTestUtils._mockDewars = { [cowTypeIndex] = { dewar65 } }

    RLTestUtils:createTestAnimal("COW", { gender = "female", age = 24 })

    mgr65:onDayChanged()

    local items65 = getInseminationEventItems()
    -- RLRM-65: line 758 reads sell.metabolism.max instead of ai.metabolism.max
    -- With fix: metabolismMax = ai.metabolism.max/100 = 1.75
    -- Animal metabolism=1.0 < 1.75, passes filter → 1 item
    assert("AI RLRM-65: ai.metabolism.max used for AI insemination filtering",
        items65 ~= nil and #items65 == 1, 1, items65 and #items65 or 0)

    RLTestUtils:restoreAIManagerTestGlobals()
    RLTestUtils:cleanupTestAnimals()
end
