-- SPDX-FileCopyrightText: 2021 The CC: Tweaked Developers
--
-- SPDX-License-Identifier: MPL-2.0

describe("The parallel library", function()
    describe("parallel.waitForAny", function()
        it("validates arguments", function()
            expect.error(parallel.waitForAny, ""):eq("bad argument #1 (function expected, got string)")
            expect.error(parallel.waitForAny, function() end, 2):eq("bad argument #2 (function expected, got number)")
        end)

        it("returns immediately with no arguments", function()
            expect(parallel.waitForAny()):eq(0)
        end)

        it("runs functions in parallel", function()
            local entries = {}
            local function a()
                entries[#entries + 1] = "first"
                local s = coroutine.yield()
                entries[#entries + 1] = s
            end
            local function b()
                entries[#entries + 1] = "second"
                local s = coroutine.yield()
                entries[#entries + 1] = s
            end
            os.queueEvent("yield")
            parallel.waitForAny(a, b)
            expect(entries):same({ "first", "second", "yield" })
        end)

        it("accepts an arbitrary number of functions", function()
            local count = 0
            local fns = {}
            for i = 1, 50 do
                fns[i] = function()
                    count = count + 1
                    coroutine.yield()
                end
            end
            os.queueEvent("dummy")
            parallel.waitForAny(table.unpack(fns))
            expect(count):eq(50)
        end)

        it("passes errors to the caller", function()
            local ok, err = pcall(parallel.waitForAny, function() error("Test error") end)
            if ok then fail("Expected function to error") end
            expect(tostring(err)):str_match("Test error$")
        end)

        it("returns the number of the function that exited first", function()
            os.queueEvent("dummy")
            os.queueEvent("dummy")
            expect(parallel.waitForAny(function()
                coroutine.yield()
                coroutine.yield()
            end, function()
                coroutine.yield()
                return
            end, function()
                coroutine.yield()
                coroutine.yield()
            end)):eq(2)
        end)
    end)

    describe("parallel.waitForAll", function()
        it("validates arguments", function()
            expect.error(parallel.waitForAll, ""):eq("bad argument #1 (function expected, got string)")
            expect.error(parallel.waitForAll, function() end, 2):eq("bad argument #2 (function expected, got number)")
        end)

        it("returns immediately with no arguments", function()
            parallel.waitForAll()
        end)

        it("runs functions in parallel", function()
            local entries = {}
            local function a()
                entries[#entries + 1] = "first"
                local s = coroutine.yield()
                entries[#entries + 1] = "a: " .. s
            end
            local function b()
                entries[#entries + 1] = "second"
                local s = coroutine.yield()
                entries[#entries + 1] = "b: " .. s
            end
            os.queueEvent("yield")
            parallel.waitForAll(a, b)
            expect(entries):same({ "first", "second", "a: yield", "b: yield" })
        end)

        it("can spawn new functions", function()
            local entries = {}
            local function a(name)
                entries[#entries + 1] = name
                local s = coroutine.yield()
                entries[#entries + 1] = name .. ": " .. s
            end
            local function b(spawn)
                entries[#entries + 1] = "b"
                spawn(a, "a1")
                spawn(a, "a2")
                local s = coroutine.yield()
                entries[#entries + 1] = "b: " .. s
                spawn(a, "a3")
            end
            os.queueEvent("yield")
            os.queueEvent("yield")
            parallel.waitForAll(b)
            expect(entries):same({
                "b",
                "a1",
                "a2",
                "b: yield",
                "a1: yield",
                "a2: yield",
                "a3",
                "a3: yield",
            })
        end)

        it("cannot spawn outside of parallel", function()
            local spawn
            parallel.waitForAll(function(s) spawn = s end)
            expect.error(spawn, function() end):eq("Cannot spawn new functions outside of waitForAll")
        end)

        it("accepts an arbitrary number of functions", function()
            local count = 0
            local fns = {}
            for i = 1, 50 do
                fns[i] = function()
                    count = count + 1
                    coroutine.yield()
                end
            end
            os.queueEvent("dummy")
            parallel.waitForAll(table.unpack(fns))
            expect(count):eq(50)
        end)

        it("passes errors to the caller", function()
            local ok, err = pcall(parallel.waitForAll, function() error("Test error") end)
            if ok then fail("Expected function to error") end
            expect(tostring(err)):str_match("Test error$")
        end)

        it("completes all functions before exiting", function()
            local exitCount = 0
            os.queueEvent("dummy")
            os.queueEvent("dummy")
            parallel.waitForAll(function()
                coroutine.yield()
                coroutine.yield()
                exitCount = exitCount + 1
            end, function()
                coroutine.yield()
                exitCount = exitCount + 1
                return
            end, function()
                coroutine.yield()
                coroutine.yield()
                exitCount = exitCount + 1
            end)
            expect(exitCount):eq(3)
        end)
    end)

    describe("exceptions", function()
        local try = require "cc.internal.exception".try
        local function check_failure(fn, ...)
            local ok, message, thread = try(fn, ...)
            expect(ok):eq(false)
            expect(message):str_match("/parallel_spec.lua:%d+: Oh no$")
            return thread
        end

        it("throws an exception when within a try", function()
            local expected_thread
            local thread = check_failure(parallel.waitForAny, function()
                expected_thread = coroutine.running()
                error("Oh no")
            end)

            expect(thread):eq(expected_thread)
        end)

        it("throws an exception when within a try (nested)", function()
            local expected_thread
            local thread = check_failure(parallel.waitForAny, function()
                parallel.waitForAny(function()
                    expected_thread = coroutine.running()
                    error("Oh no")
                end)
            end)
            expect(thread):eq(expected_thread)
        end)

        it("throws the raw error when within a pcall", function()
            local expected_thread
            local thread = check_failure(function()
                expected_thread = coroutine.running()

                local ok, err = pcall(parallel.waitForAny, function() error("Oh no") end)
                expect(ok):eq(false)
                expect(err):str_match("/parallel_spec.lua:%d+: Oh no$")
                error(err, 0)
            end)
            expect(thread):eq(expected_thread)
        end)

        it("throws the raw error when within a pcall (nested)", function()
            local expected_thread
            local thread = check_failure(function()
                expected_thread = coroutine.running()

                local ok, err = pcall(parallel.waitForAny, function()
                    parallel.waitForAny(function() error("Oh no") end)
                end)
                expect(ok):eq(false)
                expect(err):str_match("/parallel_spec.lua:%d+: Oh no$")
                error(err, 0)
            end)
            expect(thread):eq(expected_thread)
        end)
    end)
end)
