local timer = require("gears.timer")
local gtable = require("gears.table")

local runner = {
    quit_awesome_on_error = os.getenv('TEST_PAUSE_ON_ERRORS') ~= '1',
}

local verbose = os.getenv('VERBOSE') == '1'

-- Was the runner started already?
local running = false

-- This is used if a test causes errors before starting the runner
timer.start_new(1, function()
    if not running then
        io.stderr:write("Error: run_steps() was never called.\n")
        if not runner.quit_awesome_on_error then
            io.stderr:write("Keeping awesome open...\n")
            return  -- keep awesome open on error.
        end
        awesome.quit()
    end
end)

runner.step_kill_clients = function(step)
    if step == 1 then
        for _,c in ipairs(client.get()) do
            c:kill()
        end
    end
    if #client.get() == 0 then
        return true
    end
end

--- Print a message if verbose mode is enabled.
-- @tparam string message The message to print.
function runner.verbose(message)
    if verbose then
        io.stderr:write(message .. "\n")
    end
end

--- When using run_direct(), this function indicates that the test is now done.
-- @tparam[opt=nil] string message An error message explaining the test failure, if it failed.
function runner.done(message)
    if message then
        io.stderr:write("Error: " .. message .. "\n")
        if not runner.quit_awesome_on_error then
            io.stderr:write("Keeping awesome open...\n")
            return
        end
    end

    local client_count = #client.get()
    if client_count > 0 then
        io.stderr:write(string.format(
            "NOTE: there were %d clients left after the test.\n", client_count))

        -- Remove any clients.
        for _,c in ipairs(client.get()) do
            c:kill()
        end
    end

    if not message then
        io.stderr:write("Test finished successfully.\n")
    end
    awesome.quit()
end

--- This function is called to indicate that a test does not use the run_steps()
-- facility, but instead runs something else directly.
function runner.run_direct()
    assert(not running, "API abuse: Test was started twice")
    running = true
end

--- Start some step-wise tests. The given steps are called in order until all
-- succeeded. Each step is a function that can return true/false to indicate
-- success/failure, but can also return nothing if it needs to be called again
-- later.
function runner.run_steps(steps, options)
    options = gtable.crush({
        kill_clients=true,
        wait_per_step=2,  -- how long to wait per step in seconds.
    }, options or {})
    -- Setup timer/timeout to limit waiting for signal and quitting awesome.
    local t = timer({timeout=0})
    local wait=options.wait_per_step / 0.1
    local step=1
    local step_count=0
    runner.run_direct()

    if options.kill_clients then
        -- Add a final step to kill all clients and wait for them to finish.
        -- Ref: https://github.com/awesomeWM/awesome/pull/1904#issuecomment-312793006
        steps[#steps + 1] = runner.step_kill_clients
    end

    t:connect_signal("timeout", function() timer.delayed_call(function()
        local step_func = steps[step]
        step_count = step_count + 1
        local step_as_string = step..'/'..#steps..' (@'..step_count..')'

        io.flush()  -- for "tail -f".
        runner.verbose(string.format('Running step %s..\n', step_as_string))

        -- Call the current step's function.
        local success, result = xpcall(function()
            return step_func(step_count)
        end, debug.traceback)

        if not success then
            runner.done('running function for step '
                        ..step_as_string..': '..tostring(result)..'!')
            t:stop()
        elseif result then
            -- true: test succeeded.
            if step < #steps then
                -- Next step.
                step = step+1
                step_count = 0
                wait = options.wait_per_step / 0.1
                t.timeout = 0
                t:again()
            else
                -- All steps finished, we are done.
                runner.done()
            end
        else
            -- Append filename/lnum of failed step function.
            local step_info = debug.getinfo(step_func)
            local step_loc = string.format("%s:%d", step_info["short_src"], step_info["linedefined"])
            step_as_string = step_as_string .. " ("..step_loc..")"

            if result == false then
                runner.done("Step "..step_as_string.." failed (returned false).")
            else
                -- No result yet, run this step again.
                wait = wait-1
                if wait > 0 then
                    t.timeout = 0.1
                    t:again()
                else
                    runner.done("timeout waiting for signal in step "
                                ..step_as_string..".")
                    t:stop()
                end
            end
        end
    end) end)
    t:start()
end

return runner

-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80
