-- RLAnimalUtilTests.lua
-- Purpose: Tests for RLAnimalUtil utility functions — compare, find, findAndRemove,
--          toKey, toShortKey, toKeyFromIdentifiers, padId, computeCheckDigit,
--          generateUniqueId, getHash
-- Author: Ritter

RLAnimalUtilTests = {}

local Log = RmLogging.getLogger("RLRM")

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

    -- =========================================================================
    -- compare
    -- =========================================================================

    local animalA = { farmId = "123456", uniqueId = "712345", birthday = { country = 1 } }
    local animalB = { farmId = "123456", uniqueId = "712345", birthday = { country = 1 } }

    assert("compare matching animals", RLAnimalUtil.compare(animalA, animalB) == true, true, RLAnimalUtil.compare(animalA, animalB))

    local animalC = { farmId = "999999", uniqueId = "712345", birthday = { country = 1 } }
    assert("compare different farmId", RLAnimalUtil.compare(animalA, animalC) == false, false, RLAnimalUtil.compare(animalA, animalC))

    local animalD = { farmId = "123456", uniqueId = "712345", birthday = { country = 2 } }
    assert("compare different country", RLAnimalUtil.compare(animalA, animalD) == false, false, RLAnimalUtil.compare(animalA, animalD))

    local animalE = { farmId = "123456", uniqueId = "999999", birthday = { country = 1 } }
    assert("compare different uniqueId", RLAnimalUtil.compare(animalA, animalE) == false, false, RLAnimalUtil.compare(animalA, animalE))

    -- Nil-safety (suppress expected warnings during these tests)
    local savedLevel = Log.level
    Log:setLevel(RmLogging.LOG_LEVEL.ERROR)

    local animalNilBday = { farmId = "123456", uniqueId = "712345", birthday = nil }
    assert("compare nil birthday", RLAnimalUtil.compare(animalA, animalNilBday) == false, false, RLAnimalUtil.compare(animalA, animalNilBday))

    assert("compare nil first arg", RLAnimalUtil.compare(nil, animalA) == false, false, RLAnimalUtil.compare(nil, animalA))
    assert("compare nil second arg", RLAnimalUtil.compare(animalA, nil) == false, false, RLAnimalUtil.compare(animalA, nil))

    local animalNilFarmId = { farmId = nil, uniqueId = "712345", birthday = { country = 1 } }
    assert("compare nil farmId", RLAnimalUtil.compare(animalNilFarmId, animalA) == false, false, RLAnimalUtil.compare(animalNilFarmId, animalA))

    local animalNilUniqueId = { farmId = "123456", uniqueId = nil, birthday = { country = 1 } }
    assert("compare nil uniqueId", RLAnimalUtil.compare(animalNilUniqueId, animalA) == false, false, RLAnimalUtil.compare(animalNilUniqueId, animalA))

    Log:setLevel(savedLevel)

    -- =========================================================================
    -- find
    -- =========================================================================

    local animals = {
        { farmId = "111111", uniqueId = "100001", birthday = { country = 1 } },
        { farmId = "222222", uniqueId = "200002", birthday = { country = 2 } },
        { farmId = "333333", uniqueId = "300003", birthday = { country = 3 } },
    }

    assert("find empty table", RLAnimalUtil.find({}, "111111", "100001", 1) == nil, "nil", tostring(RLAnimalUtil.find({}, "111111", "100001", 1)))

    assert("find not in list", RLAnimalUtil.find(animals, "999999", "999999", 9) == nil, "nil", tostring(RLAnimalUtil.find(animals, "999999", "999999", 9)))

    local found = RLAnimalUtil.find(animals, "222222", "200002", 2)
    assert("find in list", found ~= nil and found.farmId == "222222", true, found ~= nil and found.farmId == "222222")

    -- =========================================================================
    -- findAndRemove
    -- =========================================================================

    local removeList = {
        { farmId = "111111", uniqueId = "100001", birthday = { country = 1 } },
        { farmId = "222222", uniqueId = "200002", birthday = { country = 2 } },
        { farmId = "333333", uniqueId = "300003", birthday = { country = 3 } },
    }

    local removed = RLAnimalUtil.findAndRemove(removeList, "222222", "200002", 2)
    assert("findAndRemove returns animal", removed ~= nil and removed.farmId == "222222", true, removed ~= nil and removed.farmId == "222222")
    assert("findAndRemove list shrinks", #removeList == 2, 2, #removeList)

    local notRemoved = RLAnimalUtil.findAndRemove(removeList, "999999", "999999", 9)
    assert("findAndRemove not found returns nil", notRemoved == nil, "nil", tostring(notRemoved))
    assert("findAndRemove list unchanged on miss", #removeList == 2, 2, #removeList)

    -- =========================================================================
    -- toKey / toShortKey
    -- =========================================================================

    assert("toKey format", RLAnimalUtil.toKey("123456", "712345", 1) == "123456 712345 1", "123456 712345 1", RLAnimalUtil.toKey("123456", "712345", 1))
    assert("toShortKey format", RLAnimalUtil.toShortKey("123456", "712345") == "123456 712345", "123456 712345", RLAnimalUtil.toShortKey("123456", "712345"))

    -- Verify toKey matches inline concatenation pattern
    local inlineKey = "123456" .. " " .. "712345" .. " " .. 1
    assert("toKey matches inline concat", RLAnimalUtil.toKey("123456", "712345", 1) == inlineKey, inlineKey, RLAnimalUtil.toKey("123456", "712345", 1))

    -- =========================================================================
    -- toKeyFromIdentifiers
    -- =========================================================================

    -- Flat form (from readStreamIdentifiers)
    local flatIds = { country = 4, farmId = "123456", uniqueId = "712345" }
    assert("toKeyFromIdentifiers flat form", RLAnimalUtil.toKeyFromIdentifiers(flatIds) == "123456 712345 4", "123456 712345 4", RLAnimalUtil.toKeyFromIdentifiers(flatIds))

    -- Nested form (from live Animal)
    local nestedIds = { birthday = { country = 4 }, farmId = "123456", uniqueId = "712345" }
    assert("toKeyFromIdentifiers nested form", RLAnimalUtil.toKeyFromIdentifiers(nestedIds) == "123456 712345 4", "123456 712345 4", RLAnimalUtil.toKeyFromIdentifiers(nestedIds))

    -- Both forms produce same key
    assert("toKeyFromIdentifiers flat == nested", RLAnimalUtil.toKeyFromIdentifiers(flatIds) == RLAnimalUtil.toKeyFromIdentifiers(nestedIds), true, RLAnimalUtil.toKeyFromIdentifiers(flatIds) == RLAnimalUtil.toKeyFromIdentifiers(nestedIds))

    -- No country (nil safety — suppress expected warning)
    local savedLevel2 = Log.level
    Log:setLevel(RmLogging.LOG_LEVEL.ERROR)
    local noCountryIds = { farmId = "123456", uniqueId = "712345" }
    assert("toKeyFromIdentifiers no country returns nil", RLAnimalUtil.toKeyFromIdentifiers(noCountryIds) == nil, "nil", tostring(RLAnimalUtil.toKeyFromIdentifiers(noCountryIds)))
    Log:setLevel(savedLevel2)

    -- =========================================================================
    -- padId
    -- =========================================================================

    assert("padId 1 digit (0)", RLAnimalUtil.padId(0) == "10000", "10000", RLAnimalUtil.padId(0))
    assert("padId 1 digit (9)", RLAnimalUtil.padId(9) == "10009", "10009", RLAnimalUtil.padId(9))
    assert("padId 2 digits (99)", RLAnimalUtil.padId(99) == "10099", "10099", RLAnimalUtil.padId(99))
    assert("padId 3 digits (999)", RLAnimalUtil.padId(999) == "10999", "10999", RLAnimalUtil.padId(999))
    assert("padId 4 digits (9999)", RLAnimalUtil.padId(9999) == "19999", "19999", RLAnimalUtil.padId(9999))
    assert("padId 5 digits no change", RLAnimalUtil.padId(99999) == "99999", "99999", RLAnimalUtil.padId(99999))
    assert("padId 6 digits no change", RLAnimalUtil.padId(100000) == "100000", "100000", RLAnimalUtil.padId(100000))

    -- String input
    assert("padId string input", RLAnimalUtil.padId("42") == "10042", "10042", RLAnimalUtil.padId("42"))

    -- =========================================================================
    -- computeCheckDigit
    -- =========================================================================

    -- Known value: tonumber("123456" .. "12345") % 7 + 1 = 12345612345 % 7 + 1 = 3 + 1 = 4
    local cd1 = RLAnimalUtil.computeCheckDigit("123456", "12345")
    assert("computeCheckDigit known value", cd1 == 4, 4, cd1)

    -- Range: always [1, 7]
    local allInRange = true
    local cdTestCases = {
        { "100000", "10000" },
        { "999999", "99999" },
        { "123456", "11111" },
        { "111111", "12345" },
        { "500000", "50000" },
    }
    for _, tc in ipairs(cdTestCases) do
        local cd = RLAnimalUtil.computeCheckDigit(tc[1], tc[2])
        if cd < 1 or cd > 7 then
            allInRange = false
        end
    end
    assert("computeCheckDigit range always [1, 7]", allInRange, true, allInRange)

    -- Deterministic
    local cd2a = RLAnimalUtil.computeCheckDigit("654321", "54321")
    local cd2b = RLAnimalUtil.computeCheckDigit("654321", "54321")
    assert("computeCheckDigit deterministic", cd2a == cd2b, cd2a, cd2b)

    -- =========================================================================
    -- generateUniqueId
    -- =========================================================================

    -- Integration: generateUniqueId should equal computeCheckDigit(farmHerdId, padId(rawId)) .. padId(rawId)
    local testFarmId = "123456"
    local testRawId = 3

    local paddedId = RLAnimalUtil.padId(testRawId)
    local expectedCheckDigit = RLAnimalUtil.computeCheckDigit(testFarmId, paddedId)
    local expectedUniqueId = expectedCheckDigit .. paddedId

    local actualUniqueId = RLAnimalUtil.generateUniqueId(testFarmId, testRawId)
    assert("generateUniqueId integration", actualUniqueId == expectedUniqueId, expectedUniqueId, actualUniqueId)

    -- Returns string
    assert("generateUniqueId returns string", type(actualUniqueId) == "string", "string", type(actualUniqueId))

    -- Known value: padId(3) = "10003", checkDigit("123456", "10003") = (tonumber("12345610003") % 7) + 1
    -- 12345610003 % 7 = 12345610003 - 1763658571*7 = 12345610003 - 12345609997 = 6, so 6+1 = 7
    local knownUniqueId = RLAnimalUtil.generateUniqueId("123456", 3)
    assert("generateUniqueId known value", knownUniqueId == "710003", "710003", knownUniqueId)

    -- =========================================================================
    -- getHash
    -- =========================================================================

    local hashMock = { age = 10, health = 80, reproduction = 50, subTypeIndex = 3 }
    -- Expected: (100+10) + 1000*(100+80) + 1e6*(100+50) + 1e9*(100+3) = 103150180110
    local hash1 = RLAnimalUtil.getHash(hashMock)
    assert("getHash known values", hash1 == 103150180110, 103150180110, hash1)

    -- Deterministic
    local hash1b = RLAnimalUtil.getHash(hashMock)
    assert("getHash deterministic", hash1 == hash1b, hash1, hash1b)

    -- All zeros
    local hashZero = RLAnimalUtil.getHash({ age = 0, health = 0, reproduction = 0, subTypeIndex = 0 })
    assert("getHash all zeros", hashZero == 100100100100, 100100100100, hashZero)

    -- Different inputs produce different outputs
    local hashMock2 = { age = 20, health = 60, reproduction = 30, subTypeIndex = 1 }
    local hash2 = RLAnimalUtil.getHash(hashMock2)
    assert("getHash different inputs differ", hash1 ~= hash2, "different", hash1 == hash2 and "same" or "different")
end
