--[[
    Commander 3.0 - Test Framework

    Provides assertion library, suite management, and result tracking
    for the automated test system.
]]

local ADDON_NAME, C3 = ...

local TestFramework = {}
C3.TestFramework = TestFramework

-------------------------------------------------------------------------------
-- Result Types
-------------------------------------------------------------------------------

TestFramework.RESULT = {
    PASS = "pass",
    FAIL = "fail",
    SKIP = "skip",
    ERROR = "error",
}

-------------------------------------------------------------------------------
-- Suite Storage
-------------------------------------------------------------------------------

TestFramework._suites = {}
TestFramework._results = {}
TestFramework._suiteOrder = {}

-------------------------------------------------------------------------------
-- Suite Registration
-------------------------------------------------------------------------------

---Create a new test suite
---@param name string Suite name (category)
---@return table suite The suite object
function TestFramework:Suite(name)
    local suite = {
        name = name,
        tests = {},
        beforeAll = nil,
        afterAll = nil,
        beforeEach = nil,
        afterEach = nil,
    }

    self._suites[name] = suite
    table.insert(self._suiteOrder, name)
    return suite
end

---Get a suite by name
---@param name string Suite name
---@return table|nil suite
function TestFramework:GetSuite(name)
    return self._suites[name]
end

---Get all suites in registration order
---@return table suites
function TestFramework:GetAllSuites()
    local ordered = {}
    for _, name in ipairs(self._suiteOrder) do
        if self._suites[name] then
            table.insert(ordered, self._suites[name])
        end
    end
    return ordered
end

-------------------------------------------------------------------------------
-- Test Registration
-------------------------------------------------------------------------------

---Add a test to a suite
---@param suite table The suite object
---@param testName string Test description
---@param testFn function Test function (should throw on failure)
---@param options? table Optional: { skip = bool, skipReason = string, tags = {} }
function TestFramework:Test(suite, testName, testFn, options)
    options = options or {}
    table.insert(suite.tests, {
        name = testName,
        fn = testFn,
        skip = options.skip or false,
        skipReason = options.skipReason,
        tags = options.tags or {},
    })
end

-------------------------------------------------------------------------------
-- Assertions
-------------------------------------------------------------------------------

local Assert = {}
TestFramework.Assert = Assert

function Assert.isTrue(value, message)
    if not value then
        error(message or "Expected true, got " .. tostring(value), 2)
    end
end

function Assert.isFalse(value, message)
    if value then
        error(message or "Expected false, got " .. tostring(value), 2)
    end
end

function Assert.equals(expected, actual, message)
    if expected ~= actual then
        error(message or string.format("Expected %s, got %s",
            tostring(expected), tostring(actual)), 2)
    end
end

function Assert.notEquals(expected, actual, message)
    if expected == actual then
        error(message or string.format("Expected not %s", tostring(expected)), 2)
    end
end

function Assert.notNil(value, message)
    if value == nil then
        error(message or "Expected non-nil value", 2)
    end
end

function Assert.isNil(value, message)
    if value ~= nil then
        error(message or "Expected nil, got " .. tostring(value), 2)
    end
end

function Assert.isType(value, typeName, message)
    if type(value) ~= typeName then
        error(message or string.format("Expected type %s, got %s",
            typeName, type(value)), 2)
    end
end

---Check if object has _type field matching expected
---@param obj any Object to check
---@param expectedType string Expected _type value
---@param message? string Custom error message
function Assert.hasType(obj, expectedType, message)
    if type(obj) ~= "table" then
        error(message or string.format("Expected table with _type %s, got %s",
            expectedType, type(obj)), 2)
    end
    if obj._type ~= expectedType then
        error(message or string.format("Expected _type %s, got %s",
            expectedType, tostring(obj._type)), 2)
    end
end

function Assert.contains(str, pattern, message)
    if type(str) ~= "string" then
        error(message or string.format("Expected string, got %s", type(str)), 2)
    end
    if not str:find(pattern, 1, true) then
        error(message or string.format("Expected '%s' to contain '%s'",
            str, pattern), 2)
    end
end

function Assert.matches(str, pattern, message)
    if type(str) ~= "string" then
        error(message or string.format("Expected string, got %s", type(str)), 2)
    end
    if not str:match(pattern) then
        error(message or string.format("Expected '%s' to match '%s'",
            str, pattern), 2)
    end
end

function Assert.greaterThan(value, threshold, message)
    if not (value > threshold) then
        error(message or string.format("Expected %s > %s",
            tostring(value), tostring(threshold)), 2)
    end
end

function Assert.greaterOrEqual(value, threshold, message)
    if not (value >= threshold) then
        error(message or string.format("Expected %s >= %s",
            tostring(value), tostring(threshold)), 2)
    end
end

function Assert.lessThan(value, threshold, message)
    if not (value < threshold) then
        error(message or string.format("Expected %s < %s",
            tostring(value), tostring(threshold)), 2)
    end
end

function Assert.lessOrEqual(value, threshold, message)
    if not (value <= threshold) then
        error(message or string.format("Expected %s <= %s",
            tostring(value), tostring(threshold)), 2)
    end
end

function Assert.inRange(value, min, max, message)
    if not (value >= min and value <= max) then
        error(message or string.format("Expected %s in range [%s, %s]",
            tostring(value), tostring(min), tostring(max)), 2)
    end
end

function Assert.throws(fn, expectedPattern, message)
    local ok, err = pcall(fn)
    if ok then
        error(message or "Expected function to throw an error", 2)
    end
    if expectedPattern and not tostring(err):find(expectedPattern) then
        error(message or string.format("Expected error matching '%s', got '%s'",
            expectedPattern, tostring(err)), 2)
    end
end

function Assert.doesNotThrow(fn, message)
    local ok, err = pcall(fn)
    if not ok then
        error(message or "Expected function not to throw, got: " .. tostring(err), 2)
    end
end

function Assert.arrayLength(arr, expected, message)
    if type(arr) ~= "table" then
        error(message or "Expected array, got " .. type(arr), 2)
    end
    local len = #arr
    if len ~= expected then
        error(message or string.format("Expected array length %d, got %d",
            expected, len), 2)
    end
end

function Assert.arrayMinLength(arr, minimum, message)
    if type(arr) ~= "table" then
        error(message or "Expected array, got " .. type(arr), 2)
    end
    local len = #arr
    if len < minimum then
        error(message or string.format("Expected array length >= %d, got %d",
            minimum, len), 2)
    end
end

function Assert.arrayMaxLength(arr, maximum, message)
    if type(arr) ~= "table" then
        error(message or "Expected array, got " .. type(arr), 2)
    end
    local len = #arr
    if len > maximum then
        error(message or string.format("Expected array length <= %d, got %d",
            maximum, len), 2)
    end
end

-------------------------------------------------------------------------------
-- Result Tracking
-------------------------------------------------------------------------------

---Record a test result
---@param suiteName string Suite name
---@param testName string Test name
---@param result string One of RESULT.* values
---@param duration number Duration in ms
---@param errMsg? string Error message if failed
function TestFramework:RecordResult(suiteName, testName, result, duration, errMsg)
    if not self._results[suiteName] then
        self._results[suiteName] = {
            passed = 0,
            failed = 0,
            skipped = 0,
            errors = 0,
            tests = {},
        }
    end

    local suiteResult = self._results[suiteName]
    table.insert(suiteResult.tests, {
        name = testName,
        result = result,
        duration = duration,
        error = errMsg,
    })

    if result == self.RESULT.PASS then
        suiteResult.passed = suiteResult.passed + 1
    elseif result == self.RESULT.FAIL then
        suiteResult.failed = suiteResult.failed + 1
    elseif result == self.RESULT.SKIP then
        suiteResult.skipped = suiteResult.skipped + 1
    elseif result == self.RESULT.ERROR then
        suiteResult.errors = suiteResult.errors + 1
    end
end

---Get all recorded results
---@return table results
function TestFramework:GetResults()
    return self._results
end

---Clear all results
function TestFramework:ClearResults()
    self._results = {}
end

---Get total counts across all suites
---@return table totals { passed, failed, skipped, errors, total }
function TestFramework:GetTotals()
    local totals = { passed = 0, failed = 0, skipped = 0, errors = 0 }
    for _, suite in pairs(self._results) do
        totals.passed = totals.passed + suite.passed
        totals.failed = totals.failed + suite.failed
        totals.skipped = totals.skipped + suite.skipped
        totals.errors = totals.errors + suite.errors
    end
    totals.total = totals.passed + totals.failed + totals.skipped + totals.errors
    return totals
end

return TestFramework
