--[[
    Commander 3.0 - Test Runner

    Provides the 'test' command for running automated tests.
    Usage:
        test              - Run all tests
        test -l           - List available categories
        test pipeline     - Run specific category
        test pipeline -v  - Verbose output
]]

local ADDON_NAME, C3 = ...

local TestRunner = {}
C3.TestRunner = TestRunner

local Framework = C3.TestFramework

-------------------------------------------------------------------------------
-- Runner Configuration
-------------------------------------------------------------------------------

-- Categories in execution order
TestRunner.CATEGORIES = {
    "system",
    "datasources",
    "pipeline",
    "secure",
    "vfs",
    "advanced",
    "intellisense",
}

-------------------------------------------------------------------------------
-- Run Tests
-------------------------------------------------------------------------------

---Run a function with coroutine support (allows yielding without breaking error handling)
---This version runs synchronously - used for --sync mode
---@param fn function Function to run
---@return boolean ok True if function completed without error
---@return string? err Error message if failed
local function runWithYield(fn)
    local co = coroutine.create(fn)
    while coroutine.status(co) ~= "dead" do
        local ok, err = coroutine.resume(co)
        if not ok then
            return false, err
        end
        -- If coroutine yielded, continue resuming
    end
    return true, nil
end

---Run a function with coroutine support, yielding to main thread periodically
---This version yields control back to the Kernel to prevent game freezes
---@param fn function Function to run
---@return boolean ok True if function completed without error
---@return string? err Error message if failed
local function runWithYieldAsync(fn)
    local co = coroutine.create(fn)
    local resumeCount = 0
    while coroutine.status(co) ~= "dead" do
        local ok, err = coroutine.resume(co)
        if not ok then
            return false, err
        end
        -- Yield to Kernel periodically to prevent freezing
        resumeCount = resumeCount + 1
        if resumeCount % 5 == 0 then
            coroutine.yield()
        end
    end
    return true, nil
end

---Run a single test suite
---@param suiteName string Suite name
---@param options? table Options { verbose = bool }
---@return boolean success
function TestRunner:RunSuite(suiteName, options)
    options = options or {}
    local suite = Framework:GetSuite(suiteName)

    if not suite then
        return false, "Suite not found: " .. suiteName
    end

    -- Run beforeAll if defined
    if suite.beforeAll then
        local ok, err = runWithYield(suite.beforeAll)
        if not ok then
            Framework:RecordResult(suiteName, "beforeAll", Framework.RESULT.ERROR, 0, tostring(err))
        end
    end

    -- Run each test
    for _, test in ipairs(suite.tests) do
        -- Skip if marked as skip
        if test.skip then
            Framework:RecordResult(suiteName, test.name, Framework.RESULT.SKIP, 0, test.skipReason)
        else
            -- Run beforeEach
            local shouldRunTest = true
            if suite.beforeEach then
                local ok, err = runWithYield(suite.beforeEach)
                if not ok then
                    Framework:RecordResult(suiteName, test.name, Framework.RESULT.ERROR, 0, "beforeEach: " .. tostring(err))
                    shouldRunTest = false
                end
            end

            if shouldRunTest then
                -- Run the test (using coroutine to allow yields)
                local startTime = debugprofilestop and debugprofilestop() or 0
                local ok, err = runWithYield(test.fn)
                local duration = debugprofilestop and (debugprofilestop() - startTime) or 0

                if ok then
                    Framework:RecordResult(suiteName, test.name, Framework.RESULT.PASS, duration, nil)
                else
                    Framework:RecordResult(suiteName, test.name, Framework.RESULT.FAIL, duration, tostring(err))
                end

                -- Run afterEach
                if suite.afterEach then
                    runWithYield(suite.afterEach)
                end
            end
        end
    end

    -- Run afterAll if defined
    if suite.afterAll then
        runWithYield(suite.afterAll)
    end

    return true
end

---Run all test suites
---@param options? table Options { categories = table, verbose = bool }
---@return table results
function TestRunner:RunAll(options)
    options = options or {}
    Framework:ClearResults()

    for _, category in ipairs(self.CATEGORIES) do
        if not options.categories or options.categories[category] then
            self:RunSuite(category, options)
        end
    end

    return Framework:GetResults()
end

---Run a specific category
---@param category string Category name
---@return table results
---@return boolean success
function TestRunner:RunCategory(category)
    Framework:ClearResults()
    local success = self:RunSuite(category)
    return Framework:GetResults(), success
end

-------------------------------------------------------------------------------
-- Async Test Runner (yields to Kernel to prevent game freezes)
-------------------------------------------------------------------------------

---Run a single test suite asynchronously (yields periodically)
---@param suiteName string Suite name
---@param options? table Options { verbose = bool }
---@return boolean success
function TestRunner:RunSuiteAsync(suiteName, options)
    options = options or {}
    local suite = Framework:GetSuite(suiteName)

    if not suite then
        return false, "Suite not found: " .. suiteName
    end

    -- Run beforeAll if defined
    if suite.beforeAll then
        local ok, err = runWithYieldAsync(suite.beforeAll)
        if not ok then
            Framework:RecordResult(suiteName, "beforeAll", Framework.RESULT.ERROR, 0, tostring(err))
        end
    end

    -- Run each test with yielding between tests
    for testIdx, test in ipairs(suite.tests) do
        -- Yield between tests to prevent freezing
        if testIdx % 3 == 0 then
            coroutine.yield()
        end

        -- Skip if marked as skip
        if test.skip then
            Framework:RecordResult(suiteName, test.name, Framework.RESULT.SKIP, 0, test.skipReason)
        else
            -- Run beforeEach
            local shouldRunTest = true
            if suite.beforeEach then
                local ok, err = runWithYieldAsync(suite.beforeEach)
                if not ok then
                    Framework:RecordResult(suiteName, test.name, Framework.RESULT.ERROR, 0, "beforeEach: " .. tostring(err))
                    shouldRunTest = false
                end
            end

            if shouldRunTest then
                -- Run the test (using coroutine to allow yields)
                local startTime = debugprofilestop and debugprofilestop() or 0
                local ok, err = runWithYieldAsync(test.fn)
                local duration = debugprofilestop and (debugprofilestop() - startTime) or 0

                if ok then
                    Framework:RecordResult(suiteName, test.name, Framework.RESULT.PASS, duration, nil)
                else
                    Framework:RecordResult(suiteName, test.name, Framework.RESULT.FAIL, duration, tostring(err))
                end

                -- Run afterEach
                if suite.afterEach then
                    runWithYieldAsync(suite.afterEach)
                end
            end
        end
    end

    -- Run afterAll if defined
    if suite.afterAll then
        runWithYieldAsync(suite.afterAll)
    end

    return true
end

---Run all test suites asynchronously via the Kernel
---@param options? table Options { categories = table, verbose = bool, onComplete = function }
---@return Task task The Kernel task that can be monitored
function TestRunner:RunAllAsync(options)
    options = options or {}

    return C3._kernel:Run(function()
        Framework:ClearResults()

        for _, category in ipairs(self.CATEGORIES) do
            if not options.categories or options.categories[category] then
                self:RunSuiteAsync(category, options)
            end
            -- Yield between suites
            coroutine.yield()
        end

        -- Call completion callback if provided
        if options.onComplete then
            options.onComplete(Framework:GetResults())
        end
    end, { priority = C3.Kernel.PRIORITY.LOW })
end

-------------------------------------------------------------------------------
-- Result Formatting
-------------------------------------------------------------------------------

---Format test results for display
---@param results table Results from Framework:GetResults()
---@param options? table Options { verbose = bool }
---@return table lines Array of formatted strings
function TestRunner:FormatResults(results, options)
    options = options or {}
    local verbose = options.verbose
    local lines = {}

    local totals = Framework:GetTotals()

    -- Header
    table.insert(lines, "|cFFFFFF00=== Commander3 Test Results ===|r")
    table.insert(lines, "")

    -- Per-suite results (in order)
    for _, suiteName in ipairs(self.CATEGORIES) do
        local suiteResult = results[suiteName]
        if suiteResult then
            local suiteStatus = suiteResult.failed == 0 and suiteResult.errors == 0
            local statusIcon = suiteStatus and "|cFF00FF00+|r" or "|cFFFF4444x|r"
            local total = suiteResult.passed + suiteResult.failed + suiteResult.errors

            table.insert(lines, string.format("%s |cFFFFFFFF%s|r: %d/%d passed",
                statusIcon, suiteName, suiteResult.passed, total))

            -- Show failed/errored tests always, passed only in verbose
            if verbose or suiteResult.failed > 0 or suiteResult.errors > 0 then
                for _, test in ipairs(suiteResult.tests) do
                    if test.result == Framework.RESULT.FAIL then
                        table.insert(lines, string.format("    |cFFFF4444x|r %s", test.name))
                        if test.error then
                            table.insert(lines, string.format("      |cFF888888%s|r", test.error))
                        end
                    elseif test.result == Framework.RESULT.ERROR then
                        table.insert(lines, string.format("    |cFFFF8800!|r %s (error)", test.name))
                        if test.error then
                            table.insert(lines, string.format("      |cFF888888%s|r", test.error))
                        end
                    elseif verbose and test.result == Framework.RESULT.PASS then
                        local durationStr = test.duration > 0
                            and string.format(" (%.1fms)", test.duration) or ""
                        table.insert(lines, string.format("    |cFF00FF00+|r %s%s", test.name, durationStr))
                    elseif verbose and test.result == Framework.RESULT.SKIP then
                        local reason = test.error and (" - " .. test.error) or ""
                        table.insert(lines, string.format("    |cFF888888-|r %s (skipped)%s", test.name, reason))
                    end
                end
            end
        end
    end

    -- Summary
    table.insert(lines, "")
    table.insert(lines, "|cFFFFFF00=== Summary ===|r")

    local passStr = string.format("|cFF00FF00%d passed|r", totals.passed)
    local failStr = totals.failed > 0
        and string.format("|cFFFF4444%d failed|r", totals.failed)
        or "|cFF00FF000 failed|r"

    local summaryParts = { passStr, failStr }

    if totals.skipped > 0 then
        table.insert(summaryParts, string.format("|cFF888888%d skipped|r", totals.skipped))
    end
    if totals.errors > 0 then
        table.insert(summaryParts, string.format("|cFFFF8800%d errors|r", totals.errors))
    end

    table.insert(lines, table.concat(summaryParts, ", ") ..
        string.format(" | Total: %d", totals.total))

    if totals.failed == 0 and totals.errors == 0 then
        table.insert(lines, "")
        table.insert(lines, "|cFF00FF00All tests passed!|r")
    end

    return lines
end

-------------------------------------------------------------------------------
-- Test Command Registration
-------------------------------------------------------------------------------

C3.Command:Define({
    name = "test",
    description = "Run automated tests",
    category = "system",
    acceptsInput = false,
    args = {
        {
            name = "category",
            required = false,
            description = "Test category (system, datasources, pipeline, secure, vfs, advanced, intellisense) or 'all'",
        }
    },
    flags = {
        verbose = { short = "v", description = "Show all test results including passed" },
        list = { short = "l", description = "List available test categories" },
        sync = { short = "s", description = "Run synchronously (may freeze game - use for CI/headless)" },
    },
    execute = function(ctx)
        local category = ctx:GetArg(1)
        local verbose = ctx:HasFlag("verbose") or ctx:HasFlag("v")
        local listOnly = ctx:HasFlag("list") or ctx:HasFlag("l")
        local syncMode = ctx:HasFlag("sync") or ctx:HasFlag("s")

        -- List mode
        if listOnly then
            local lines = { "|cFFFFFF00Available test categories:|r" }
            for _, cat in ipairs(TestRunner.CATEGORIES) do
                local suite = Framework:GetSuite(cat)
                local testCount = suite and #suite.tests or 0
                table.insert(lines, string.format("  |cFF00FFFF%s|r (%d tests)", cat, testCount))
            end
            table.insert(lines, "")
            table.insert(lines, "|cFFAAAAAA Usage:|r test [category] [-v] [-s]")
            table.insert(lines, "|cFFAAAAAA Example:|r test pipeline -v")
            table.insert(lines, "|cFFAAAAAA Sync mode:|r test -s (runs without yielding)")
            return ctx:Text(table.concat(lines, "\n"))
        end

        -- Build category options
        local categoryOpts = nil
        if category and category ~= "all" then
            -- Validate category
            local validCategory = false
            for _, cat in ipairs(TestRunner.CATEGORIES) do
                if cat == category then
                    validCategory = true
                    break
                end
            end

            if not validCategory then
                return ctx:Error(string.format(
                    "Unknown category: %s\nValid categories: %s",
                    category,
                    table.concat(TestRunner.CATEGORIES, ", ")
                ))
            end

            categoryOpts = { [category] = true }
        end

        -- Synchronous mode (for CI/headless or debugging)
        if syncMode then
            local results
            if categoryOpts then
                results = TestRunner:RunCategory(category)
            else
                results = TestRunner:RunAll()
            end
            local lines = TestRunner:FormatResults(results, { verbose = verbose })
            return ctx:Text(table.concat(lines, "\n"))
        end

        -- Async mode (default) - runs via Kernel to prevent game freezes
        local session = ctx.session
        local buffer = session and session._activeBuffer

        -- Show "running" message immediately
        if buffer then
            buffer:AddLine(C3.BufferLine.Text("|cFFFFFF00Running tests...|r"))
        end

        -- Run tests asynchronously
        TestRunner:RunAllAsync({
            categories = categoryOpts,
            verbose = verbose,
            onComplete = function(results)
                -- Output results when complete
                local lines = TestRunner:FormatResults(results, { verbose = verbose })
                if buffer then
                    for _, line in ipairs(lines) do
                        buffer:AddLine(C3.BufferLine.Text(line))
                    end
                end
            end
        })

        -- Return nil since we'll output results via callback
        return nil
    end
})

return TestRunner
