-- RLTestUtils.lua
-- Purpose: Shared utilities for RLRM test suite
-- Author: Ritter
-- Reference: Fresh/scripts/tests/RmTestUtils.lua (adapted, no createContainer)

RLTestUtils = {}
RLTestUtils.TEST_PREFIX = "rl_test_"
--- Fake farm ID for test animals. Deliberately outside the game's generated range
--- (game uses 100000-999999 via math.random). Avoids collision with real farm IDs.
RLTestUtils.TEST_FARM_ID = "0"

local Log = RmLogging.getLogger("RLRM")

-- =============================================================================
-- TEST UTILITIES
-- =============================================================================

--- Clean up test state and reset test isolation.
--- Calls cleanupTestAnimals() then resets testAnimalPrefix.
function RLTestUtils:cleanup()
    self:cleanupTestAnimals()
    -- CRITICAL: Disable test isolation - restore normal operation
    RealisticLivestock.testAnimalPrefix = nil
    Log:trace("TEST_CLEANUP: Reset testAnimalPrefix to nil (normal mode restored)")
end

-- =============================================================================
-- MOCK CLUSTER SYSTEM
-- =============================================================================

--- Create a minimal mock clusterSystem with all methods Animal uses.
--- Provides: animals array, owner with spec structure, and all methods
--- referenced by self.clusterSystem in RealisticLivestock_Animal.lua (36 refs).
--- @return table mockClusterSystem Mock with animals array, owner, and required methods
function RLTestUtils:createMockClusterSystem()
    local mock = {
        animals = {},
        currentAnimalId = 0,
        isServer = true,
        needsUpdate = false,
    }
    -- Owner with minimal spec structure
    mock.owner = {
        ownerFarmId = 1,
        isServer = true,
        spec_husbandryMilk = nil,
        addRLMessage = nil,
        getOwnerFarmId = function(self) return self.ownerFarmId end,
    }
    -- Self-reference: owner.spec_husbandryAnimals.clusterSystem = mock
    -- Include getOwnerFarmId on nested spec in case code accesses it via spec path
    mock.owner.spec_husbandryAnimals = {
        clusterSystem = mock,
        getOwnerFarmId = function(self) return 1 end,
    }
    -- Methods
    mock.getAnimals = function(self) return self.animals end
    mock.getClusters = function(self) return self.animals end
    mock.addCluster = function(self, animal) table.insert(self.animals, animal) end
    mock.addPendingRemoveCluster = function(self, animal) end  -- no-op
    mock.setDirty = function(self) end  -- no-op
    mock.getNextAnimalId = function(self)
        self.currentAnimalId = self.currentAnimalId + 1
        return self.currentAnimalId
    end
    Log:trace("TEST: Created mock clusterSystem")
    return mock
end

--- Get or create the shared mock clusterSystem (reused across a test run).
--- @return table mockClusterSystem
function RLTestUtils:getSharedMockClusterSystem()
    if self._sharedMockCS == nil then
        self._sharedMockCS = self:createMockClusterSystem()
    end
    return self._sharedMockCS
end

-- =============================================================================
-- SUBTYPE RESOLUTION AND DEFAULTS
-- =============================================================================

--- Resolve the first subTypeIndex matching the given animal type name.
--- Maps "COW" -> first matching subTypeIndex via g_currentMission.animalSystem.
--- SubType indices are dynamic (depend on loaded mods/maps), so must resolve at runtime.
--- @param animalTypeName string Animal type (COW, PIG, SHEEP, CHICKEN, HORSE)
--- @return number|nil subTypeIndex The resolved subtype index, or nil if not found
function RLTestUtils:resolveSubTypeForType(animalTypeName)
    if g_currentMission == nil or g_currentMission.animalSystem == nil then
        Log:error("TEST: Cannot resolve subType -- g_currentMission.animalSystem is nil")
        return nil
    end
    local animalSystem = g_currentMission.animalSystem
    local typeIndex = AnimalType[animalTypeName]
    if typeIndex == nil then
        Log:warning("TEST: Unknown animal type '%s'", animalTypeName)
        return nil
    end
    for i = 1, #animalSystem.subTypes do
        if animalSystem:getTypeIndexBySubTypeIndex(i) == typeIndex then
            Log:trace("TEST: Resolved %s -> subTypeIndex %d", animalTypeName, i)
            return i
        end
    end
    Log:warning("TEST: No subType found for type '%s' (typeIndex=%d)", animalTypeName, typeIndex)
    return nil
end

--- Default config values per animal type.
--- Age = reasonable adult age. Health = mid-range. Gender = female (most common test case).
RLTestUtils.TYPE_DEFAULTS = {
    COW     = { age = 24, health = 80, gender = "female" },
    PIG     = { age = 12, health = 80, gender = "female" },
    SHEEP   = { age = 12, health = 80, gender = "female" },
    CHICKEN = { age = 6,  health = 80, gender = "female" },
    HORSE   = { age = 24, health = 80, gender = "female" },
}

--- Standard deterministic genetics for test animals (all 1.0 multipliers).
--- Bypasses random generation for reproducible tests.
RLTestUtils.DEFAULT_GENETICS = {
    health = 1.0, fertility = 1.0, productivity = 1.0,
    quality = 1.0, metabolism = 1.0
}

-- =============================================================================
-- TEST ANIMAL FACTORY
-- =============================================================================

--- Create a test animal with sensible defaults and optional overrides.
--- Uses Animal.new({config}) from RLRM-58. Sets test-prefixed uniqueId for isolation.
--- Animals are inserted into a mock clusterSystem and tracked for cleanup.
---
--- IMPORTANT: Uses explicit nil checks (not `or`) for numeric/boolean fields to allow
--- overrides of age=0, health=0, isParent=false, etc.
---
--- @param animalType string Animal type name (COW, PIG, SHEEP, CHICKEN, HORSE)
--- @param overrides table|nil Optional config overrides (any Animal.new config field)
--- @return table|nil animal The created Animal object, or nil on failure
function RLTestUtils:createTestAnimal(animalType, overrides)
    overrides = overrides or {}

    -- Resolve subTypeIndex for this animal type
    local subTypeIndex = overrides.subTypeIndex
        or self:resolveSubTypeForType(animalType)
    if subTypeIndex == nil then
        Log:error("TEST: Cannot create test animal -- failed to resolve subType for '%s'", animalType)
        return nil
    end

    -- Auto-increment test ID
    self._testAnimalCounter = (self._testAnimalCounter or 0) + 1
    local uniqueId = overrides.uniqueId
        or (self.TEST_PREFIX .. self._testAnimalCounter)

    -- Get or create shared mock clusterSystem
    local clusterSystem = overrides.clusterSystem or self:getSharedMockClusterSystem()

    -- Shallow-copy default genetics so tests don't mutate the shared table
    local genetics = overrides.genetics
    if genetics == nil then
        genetics = {}
        for k, v in pairs(self.DEFAULT_GENETICS) do genetics[k] = v end
    end

    -- Type defaults
    local defaults = self.TYPE_DEFAULTS[animalType] or self.TYPE_DEFAULTS.COW

    -- Helper: nil-safe override (allows 0, false as valid overrides)
    local function val(key, default)
        if overrides[key] ~= nil then return overrides[key] end
        return default
    end

    -- All Animal.new fields, grouped by factory-default vs pass-through.
    -- Core fields: factory provides explicit defaults via val().
    -- Pass-through fields: nil unless overridden (constructor applies its defaults).
    local config = {
        subTypeIndex = subTypeIndex,
        uniqueId = uniqueId,
        farmId = val("farmId", self.TEST_FARM_ID),
        clusterSystem = clusterSystem,
        age = val("age", defaults.age),
        health = val("health", defaults.health),
        gender = val("gender", defaults.gender),
        genetics = genetics,
        name = val("name", "Test" .. animalType),
        -- Additional overridable fields (constructor defaults apply if nil)
        monthsSinceLastBirth = overrides.monthsSinceLastBirth,
        reproduction = overrides.reproduction,
        isParent = overrides.isParent,
        isPregnant = overrides.isPregnant,
        isLactating = overrides.isLactating,
        motherId = overrides.motherId,
        fatherId = overrides.fatherId,
        isCastrated = overrides.isCastrated,
        diseases = overrides.diseases,
        marks = overrides.marks,
        weight = overrides.weight,
        variation = overrides.variation,
        impregnatedBy = overrides.impregnatedBy,
        children = overrides.children,
        monitor = overrides.monitor,
        insemination = overrides.insemination,
        pos = overrides.pos,
        dirt = overrides.dirt,
        fitness = overrides.fitness,
        riding = overrides.riding,
        recentlyBoughtByAI = overrides.recentlyBoughtByAI,
    }

    -- Create the animal
    local animal = Animal.new(config)
    if animal == nil then
        Log:error("TEST: Animal.new returned nil for type '%s'", animalType)
        return nil
    end

    -- Insert into clusterSystem registry
    table.insert(clusterSystem.animals, animal)

    -- Track for cleanup
    self._testAnimals = self._testAnimals or {}
    table.insert(self._testAnimals, { animal = animal, clusterSystem = clusterSystem })

    Log:debug("TEST: Created test %s [%s] age=%d health=%.0f gender=%s",
        animalType, uniqueId, animal.age, animal.health, animal.gender)

    return animal
end

-- =============================================================================
-- MOCK DISEASE TYPE
-- =============================================================================

--- Create a mock disease type with known test values and optional overrides.
--- Decouples tests from XML disease data. Follows same pattern as createMockClusterSystem.
--- Returns a disease type table (not a Disease object) — callers use Disease.new(dt) to create instances.
---
--- IMPORTANT: Uses explicit nil checks (not `or`) for `recovery` and `treatment` fields
--- to allow suppression via `overrides.recovery = false` or `overrides.treatment = false`.
--- Pass `false` (not `nil`) to suppress these fields, since `nil` is indistinguishable from "not provided".
--- @param overrides table|nil Optional overrides for any disease type field
--- @return table mockDiseaseType
function RLTestUtils:createMockDiseaseType(overrides)
    overrides = overrides or {}

    -- Helper: nil-safe override for suppressible fields.
    -- Pass false to suppress (set nil), pass nil to use default, pass value to override.
    local function suppressible(key, default)
        if overrides[key] == false then return nil end
        if overrides[key] ~= nil then return overrides[key] end
        return default
    end

    local dt = {
        title = overrides.title or "test_disease",
        key = overrides.key or "rl_test_disease",
        name = overrides.name or "Test Disease",
        animals = overrides.animals or { [AnimalType.COW] = true },
        value = overrides.value or 0.85,
        transmission = overrides.transmission or 0.05,
        immunity = overrides.immunity or 6,
        recovery = suppressible("recovery", 3),
        prerequisites = overrides.prerequisites or {},
        probability = overrides.probability or { {age = 0, value = 0}, {age = 12, value = 0.01} },
        fatality = overrides.fatality or { {time = 2, value = 0}, {time = 5, value = 0.1}, {time = 100, value = 0.5} },
        output = overrides.output or { milk = 0 },
        treatment = suppressible("treatment", { cost = 100, duration = 2 }),
        carrier = overrides.carrier or nil,
        genetic = overrides.genetic or nil,
    }
    Log:trace("TEST: Created mock disease type '%s'", dt.title)
    return dt
end

-- =============================================================================
-- AIANIMALMANAGER TEST INFRASTRUCTURE
-- =============================================================================

--- Saved globals for AIAnimalManager test isolation.
--- @type table|nil
RLTestUtils._savedAIGlobals = nil

--- Captured broadcastEvent calls during AIAnimalManager tests.
RLTestUtils._capturedEvents = {}

--- Configurable mock farm balance for BUY tests.
RLTestUtils._mockFarmBalance = 100000

--- Configurable mock shop animals for BUY tests.
RLTestUtils._mockShopAnimals = {}

--- Configurable mock dewars for AI tests. Keyed by animalTypeIndex.
RLTestUtils._mockDewars = nil

--- Save and patch all globals needed for AIAnimalManager testing.
--- Must be paired with restoreAIManagerTestGlobals() — even on error.
--- Safe to call multiple times (restores first if already set up).
function RLTestUtils:setupAIManagerTestGlobals()
    if self._savedAIGlobals ~= nil then
        self:restoreAIManagerTestGlobals()
    end

    self._savedAIGlobals = {
        broadcastEvent = g_server.broadcastEvent,
        netIsRunning = g_server.netIsRunning,
        formatMoney = g_i18n.formatMoney,
        getFarmById = g_farmManager.getFarmById,
        getDewarsByFarm = g_dewarManager and g_dewarManager.getDewarsByFarm or nil,
        getSaleAnimalsByTypeIndex = g_currentMission.animalSystem.getSaleAnimalsByTypeIndex,
        getAnimalTransportFee = g_currentMission.animalSystem.getAnimalTransportFee,
        sellValidate = AIAnimalSellEvent.validate,
        buyValidate = AIAnimalBuyEvent.validate,
    }

    -- Spy: capture broadcastEvent calls
    self._capturedEvents = {}
    g_server.broadcastEvent = function(serverSelf, event, ...)
        table.insert(RLTestUtils._capturedEvents, event)
    end
    -- Prevent bulk message event at end of onDayChanged
    g_server.netIsRunning = false

    -- Identity money formatter
    g_i18n.formatMoney = function(i18nSelf, val, ...)
        return tostring(val)
    end

    -- Mock farm with configurable balance
    g_farmManager.getFarmById = function(fmSelf, farmId)
        return {
            getBalance = function(farmSelf) return RLTestUtils._mockFarmBalance end,
            changeBalance = function(farmSelf, amount) end,
        }
    end

    -- Mock dewar manager
    if g_dewarManager then
        g_dewarManager.getDewarsByFarm = function(dmSelf, farmId)
            return RLTestUtils._mockDewars
        end
    end

    -- Mock shop animals for BUY
    g_currentMission.animalSystem.getSaleAnimalsByTypeIndex = function(asSelf, typeIndex)
        return RLTestUtils._mockShopAnimals
    end

    -- Mock transport fee (return 0 for deterministic price calculations)
    g_currentMission.animalSystem.getAnimalTransportFee = function(asSelf, subTypeIndex, age)
        return 0
    end

    -- Mock event validation (always pass)
    AIAnimalSellEvent.validate = function(...) return nil end
    AIAnimalBuyEvent.validate = function(...) return nil end

    Log:trace("TEST: AIAnimalManager globals patched")
end

--- Restore all globals saved by setupAIManagerTestGlobals().
--- Safe to call even if setup was never called (no-op). Idempotent.
function RLTestUtils:restoreAIManagerTestGlobals()
    if self._savedAIGlobals == nil then return end
    local saved = self._savedAIGlobals

    g_server.broadcastEvent = saved.broadcastEvent
    g_server.netIsRunning = saved.netIsRunning
    g_i18n.formatMoney = saved.formatMoney
    g_farmManager.getFarmById = saved.getFarmById
    if g_dewarManager and saved.getDewarsByFarm then
        g_dewarManager.getDewarsByFarm = saved.getDewarsByFarm
    end
    g_currentMission.animalSystem.getSaleAnimalsByTypeIndex = saved.getSaleAnimalsByTypeIndex
    g_currentMission.animalSystem.getAnimalTransportFee = saved.getAnimalTransportFee
    AIAnimalSellEvent.validate = saved.sellValidate
    AIAnimalBuyEvent.validate = saved.buyValidate

    self._savedAIGlobals = nil
    self._capturedEvents = {}
    self._mockFarmBalance = 100000
    self._mockShopAnimals = {}
    self._mockDewars = nil

    Log:trace("TEST: AIAnimalManager globals restored")
end

--- Return captured broadcastEvent calls.
--- @return table Array of event objects captured by the spy
function RLTestUtils:getCapturedEvents()
    return self._capturedEvents
end

--- Create an AIAnimalManager instance with a mock husbandry for testing.
--- All decision types are disabled by default (matching AIAnimalManager.new defaults).
--- Returns the manager, mock clusterSystem, and mock husbandry for full test control.
--- @return table aiManager, table mockClusterSystem, table mockHusbandry
function RLTestUtils:createTestAIManager()
    local mockCS = self: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,
        getNumOfFreeAnimalSlots = function(self) return 100 end,
    }
    local mgr = AIAnimalManager.new(mockHusbandry, true)
    return mgr, mockCS, mockHusbandry
end

-- =============================================================================
-- TEST ANIMAL CLEANUP
-- =============================================================================

--- Remove all test animals from their clusterSystems and reset tracking state.
--- Uses array-clear approach (not Animal:delete()) to avoid index-shift bugs
--- when removing multiple animals from the same array.
--- Safe to call even if no test animals were created. Idempotent.
function RLTestUtils:cleanupTestAnimals()
    if self._testAnimals and #self._testAnimals > 0 then
        -- Collect unique clusterSystems to clear
        local clusterSystems = {}
        for _, entry in ipairs(self._testAnimals) do
            clusterSystems[entry.clusterSystem] = true
        end
        -- Clear each clusterSystem's animals array
        local csCount = 0
        for cs, _ in pairs(clusterSystems) do
            cs.animals = {}
            csCount = csCount + 1
        end
        Log:debug("TEST_CLEANUP: Removed %d test animals from %d clusterSystems",
            #self._testAnimals, csCount)
    end
    self._testAnimals = {}
    self._testAnimalCounter = 0
    self._sharedMockCS = nil
end
