//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2018 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import XCTest
@testable import NIO
import Dispatch
import NIOConcurrencyHelpers

public final class EventLoopTest : XCTestCase {

    public func testSchedule() throws {
        let eventLoop = EmbeddedEventLoop()

        let scheduled = eventLoop.scheduleTask(in: .seconds(1)) { true }

        var result: Bool?
        scheduled.futureResult.whenSuccess { result = $0 }

        eventLoop.run() // run without time advancing should do nothing
        XCTAssertFalse(scheduled.futureResult.isFulfilled)
        XCTAssertNil(result)

        eventLoop.advanceTime(by: .seconds(1)) // should fire now
        XCTAssertTrue(scheduled.futureResult.isFulfilled)

        XCTAssertNotNil(result)
        XCTAssertTrue(result == true)
    }

    public func testFlatSchedule() throws {
        let eventLoop = EmbeddedEventLoop()

        let scheduled = eventLoop.flatScheduleTask(in: .seconds(1)) {
            eventLoop.makeSucceededFuture(true)
        }

        var result: Bool?
        scheduled.futureResult.whenSuccess { result = $0 }

        eventLoop.run() // run without time advancing should do nothing
        XCTAssertFalse(scheduled.futureResult.isFulfilled)
        XCTAssertNil(result)

        eventLoop.advanceTime(by: .seconds(1)) // should fire now
        XCTAssertTrue(scheduled.futureResult.isFulfilled)

        XCTAssertNotNil(result)
        XCTAssertTrue(result == true)
    }

    public func testScheduleWithDelay() throws {
        let smallAmount: TimeAmount = .milliseconds(100)
        let longAmount: TimeAmount = .seconds(1)
        let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
        defer {
            XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
        }

        // First, we create a server and client channel, but don't connect them.
        let serverChannel = try assertNoThrowWithValue(ServerBootstrap(group: eventLoopGroup)
            .serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
            .bind(host: "127.0.0.1", port: 0).wait())
        let clientBootstrap = ClientBootstrap(group: eventLoopGroup)

        // Now, schedule two tasks: one that takes a while, one that doesn't.
        let nanos: NIODeadline = .now()
        let longFuture = eventLoopGroup.next().scheduleTask(in: longAmount) {
            true
        }.futureResult

        XCTAssertTrue(try assertNoThrowWithValue(try eventLoopGroup.next().scheduleTask(in: smallAmount) {
            true
        }.futureResult.wait()))

        // Ok, the short one has happened. Now we should try connecting them. This connect should happen
        // faster than the final task firing.
        _ = try assertNoThrowWithValue(clientBootstrap.connect(to: serverChannel.localAddress!).wait()) as Channel
        XCTAssertTrue(NIODeadline.now() - nanos < longAmount)

        // Now wait for the long-delayed task.
        XCTAssertTrue(try assertNoThrowWithValue(try longFuture.wait()))
        // Now we're ok.
        XCTAssertTrue(NIODeadline.now() - nanos >= longAmount)
    }

    public func testScheduleCancelled() throws {
        let eventLoop = EmbeddedEventLoop()

        let scheduled = eventLoop.scheduleTask(in: .seconds(1)) { true }

        var result: Bool?
        var error: Error?
        scheduled.futureResult.whenSuccess { result = $0 }
        scheduled.futureResult.whenFailure { error = $0 }

        eventLoop.advanceTime(by: .milliseconds(500)) // advance halfway to firing time
        scheduled.cancel()
        eventLoop.advanceTime(by: .milliseconds(500)) // advance the rest of the way

        XCTAssertTrue(scheduled.futureResult.isFulfilled)
        XCTAssertNil(result)
        XCTAssertEqual(error as? EventLoopError, .cancelled)
    }

    public func testFlatScheduleCancelled() throws {
        let eventLoop = EmbeddedEventLoop()

        let scheduled = eventLoop.flatScheduleTask(in: .seconds(1)) {
            eventLoop.makeSucceededFuture(true)
        }

        var result: Bool?
        var error: Error?
        scheduled.futureResult.whenSuccess { result = $0 }
        scheduled.futureResult.whenFailure { error = $0 }

        eventLoop.advanceTime(by: .milliseconds(500)) // advance halfway to firing time
        scheduled.cancel()
        eventLoop.advanceTime(by: .milliseconds(500)) // advance the rest of the way

        XCTAssertTrue(scheduled.futureResult.isFulfilled)
        XCTAssertNil(result)
        XCTAssertEqual(error as? EventLoopError, .cancelled)
    }

    public func testScheduleRepeatedTask() throws {
        let nanos: NIODeadline = .now()
        let initialDelay: TimeAmount = .milliseconds(5)
        let delay: TimeAmount = .milliseconds(10)
        let count = 5
        let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
        defer {
            XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
        }

        let counter = NIOAtomic<Int>.makeAtomic(value: 0)
        let loop = eventLoopGroup.next()
        let allDone = DispatchGroup()
        allDone.enter()
        loop.scheduleRepeatedTask(initialDelay: initialDelay, delay: delay) { repeatedTask -> Void in
            XCTAssertTrue(loop.inEventLoop)
            let initialValue = counter.load()
            _ = counter.add(1)
            if initialValue == 0 {
                XCTAssertTrue(NIODeadline.now() - nanos >= initialDelay)
            } else if initialValue == count {
                repeatedTask.cancel()
                allDone.leave()
            }
        }

        allDone.wait()

        XCTAssertEqual(counter.load(), count + 1)
        XCTAssertTrue(NIODeadline.now() - nanos >= initialDelay + Int64(count) * delay)
    }

    public func testScheduledTaskThatIsImmediatelyCancelledNeverFires() throws {
        let eventLoop = EmbeddedEventLoop()
        let scheduled = eventLoop.scheduleTask(in: .seconds(1)) { true }

        var result: Bool?
        var error: Error?
        scheduled.futureResult.whenSuccess { result = $0 }
        scheduled.futureResult.whenFailure { error = $0 }

        scheduled.cancel()
        eventLoop.advanceTime(by: .seconds(1))

        XCTAssertTrue(scheduled.futureResult.isFulfilled)
        XCTAssertNil(result)
        XCTAssertEqual(error as? EventLoopError, .cancelled)
    }

    public func testFlatScheduledTaskThatIsImmediatelyCancelledNeverFires() throws {
        let eventLoop = EmbeddedEventLoop()
        let scheduled = eventLoop.flatScheduleTask(in: .seconds(1)) {
            eventLoop.makeSucceededFuture(true)
        }

        var result: Bool?
        var error: Error?
        scheduled.futureResult.whenSuccess { result = $0 }
        scheduled.futureResult.whenFailure { error = $0 }

        scheduled.cancel()
        eventLoop.advanceTime(by: .seconds(1))

        XCTAssertTrue(scheduled.futureResult.isFulfilled)
        XCTAssertNil(result)
        XCTAssertEqual(error as? EventLoopError, .cancelled)
    }

    public func testRepeatedTaskThatIsImmediatelyCancelledNeverFires() throws {
        let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
        defer {
            XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
        }

        let loop = eventLoopGroup.next()
        loop.execute {
            let task = loop.scheduleRepeatedTask(initialDelay: .milliseconds(0), delay: .milliseconds(0)) { task in
                XCTFail()
            }
            task.cancel()
        }
        Thread.sleep(until: .init(timeIntervalSinceNow: 0.1))
    }

    public func testScheduleRepeatedTaskCancelFromDifferentThread() throws {
        let nanos: NIODeadline = .now()
        let initialDelay: TimeAmount = .milliseconds(5)
        let delay: TimeAmount = .milliseconds(0) // this will actually force the race from issue #554 to happen frequently
        let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
        defer {
            XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
        }

        let hasFiredGroup = DispatchGroup()
        let isCancelledGroup = DispatchGroup()
        let loop = eventLoopGroup.next()
        hasFiredGroup.enter()
        isCancelledGroup.enter()
        var isAllowedToFire = true // read/write only on `loop`
        var hasFired = false // read/write only on `loop`
        let repeatedTask = loop.scheduleRepeatedTask(initialDelay: initialDelay, delay: delay) { (_: RepeatedTask) -> Void in
            XCTAssertTrue(loop.inEventLoop)
            if !hasFired {
                // we can only do this once as we can only leave the DispatchGroup once but we might lose a race and
                // the timer might fire more than once (until `shouldNoLongerFire` becomes true).
                hasFired = true
                hasFiredGroup.leave()
            }
            XCTAssertTrue(isAllowedToFire)
        }
        hasFiredGroup.notify(queue: DispatchQueue.global()) {
            repeatedTask.cancel()
            loop.execute {
                // only now do we know that the `cancel` must have gone through
                isAllowedToFire = false
                isCancelledGroup.leave()
            }
        }

        hasFiredGroup.wait()
        XCTAssertTrue(NIODeadline.now() - nanos >= initialDelay)
        isCancelledGroup.wait()
    }

    public func testScheduleRepeatedTaskToNotRetainRepeatedTask() throws {
        let initialDelay: TimeAmount = .milliseconds(5)
        let delay: TimeAmount = .milliseconds(10)
        let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)

        weak var weakRepeated: RepeatedTask?
        try { () -> Void in
            let repeated = eventLoopGroup.next().scheduleRepeatedTask(initialDelay: initialDelay, delay: delay) { (_: RepeatedTask) -> Void in }
            weakRepeated = repeated
            XCTAssertNotNil(weakRepeated)
            repeated.cancel()
            XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
        }()
        assert(weakRepeated == nil, within: .seconds(1))
    }

    public func testScheduleRepeatedTaskToNotRetainEventLoop() throws {
        weak var weakEventLoop: EventLoop? = nil
        try {
            let initialDelay: TimeAmount = .milliseconds(5)
            let delay: TimeAmount = .milliseconds(10)
            let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
            weakEventLoop = eventLoopGroup.next()
            XCTAssertNotNil(weakEventLoop)

            eventLoopGroup.next().scheduleRepeatedTask(initialDelay: initialDelay, delay: delay) { (_: RepeatedTask) -> Void in }
            XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
        }()
        assert(weakEventLoop == nil, within: .seconds(1))
    }

    func testScheduledRepeatedAsyncTask() {
        let eventLoop = EmbeddedEventLoop()
        var counter = 0
        let repeatedTask = eventLoop.scheduleRepeatedAsyncTask(initialDelay: .milliseconds(10),
                                                               delay: .milliseconds(10)) { (_: RepeatedTask) in
            counter += 1
            let p = eventLoop.makePromise(of: Void.self)
            eventLoop.scheduleTask(in: .milliseconds(10)) {
                p.succeed(())
            }
            return p.futureResult
        }
        for _ in 0..<10 {
            // just running shouldn't do anything
            eventLoop.run()
        }
        // t == 0: nothing
        XCTAssertEqual(0, counter)

        // t == 5: nothing
        eventLoop.advanceTime(by: .milliseconds(5))
        XCTAssertEqual(0, counter)

        // t == 10: once
        eventLoop.advanceTime(by: .milliseconds(5))
        XCTAssertEqual(1, counter)

        // t == 15: still once
        eventLoop.advanceTime(by: .milliseconds(5))
        XCTAssertEqual(1, counter)

        // t == 20: still once (because the task takes 10ms to execute)
        eventLoop.advanceTime(by: .milliseconds(5))
        XCTAssertEqual(1, counter)

        // t == 25: still once (because the task takes 10ms to execute)
        eventLoop.advanceTime(by: .milliseconds(5))
        XCTAssertEqual(1, counter)

        // t == 30: twice
        eventLoop.advanceTime(by: .milliseconds(5))
        XCTAssertEqual(2, counter)

        // t == 40: twice
        eventLoop.advanceTime(by: .milliseconds(10))
        XCTAssertEqual(2, counter)

        // t == 50: three times
        eventLoop.advanceTime(by: .milliseconds(10))
        XCTAssertEqual(3, counter)

        // t == 60: three times
        eventLoop.advanceTime(by: .milliseconds(10))
        XCTAssertEqual(3, counter)

        // t == 89: four times
        eventLoop.advanceTime(by: .milliseconds(29))
        XCTAssertEqual(4, counter)

        // t == 90: five times
        eventLoop.advanceTime(by: .milliseconds(1))
        XCTAssertEqual(5, counter)

        repeatedTask.cancel()

        eventLoop.run()
        XCTAssertEqual(5, counter)

        eventLoop.advanceTime(by: .hours(10))
        XCTAssertEqual(5, counter)
    }

    public func testEventLoopGroupMakeIterator() throws {
        let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
        defer {
            XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
        }

        var counter = 0
        var innerCounter = 0
        eventLoopGroup.makeIterator().forEach { loop in
            counter += 1
            loop.makeIterator().forEach { _ in
                innerCounter += 1
            }
        }

        XCTAssertEqual(counter, System.coreCount)
        XCTAssertEqual(innerCounter, System.coreCount)
    }

    public func testEventLoopMakeIterator() throws {
        let eventLoop = EmbeddedEventLoop()
        let iterator = eventLoop.makeIterator()
        defer {
            XCTAssertNoThrow(try eventLoop.syncShutdownGracefully())
        }

        var counter = 0
        iterator.forEach { loop in
            XCTAssertTrue(loop === eventLoop)
            counter += 1
        }

        XCTAssertEqual(counter, 1)
    }

    public func testMultipleShutdown() throws {
        // This test catches a regression that causes it to intermittently fail: it reveals bugs in synchronous shutdown.
        // Do not ignore intermittent failures in this test!
        let threads = 8
        let numBytes = 256
        let group = MultiThreadedEventLoopGroup(numberOfThreads: threads)

        // Create a server channel.
        let serverChannel = try assertNoThrowWithValue(ServerBootstrap(group: group)
            .serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
            .bind(host: "127.0.0.1", port: 0).wait())

        // We now want to connect to it. To try to slow this stuff down, we're going to use a multiple of the number
        // of event loops.
        for _ in 0..<(threads * 5) {
            let clientChannel = try assertNoThrowWithValue(ClientBootstrap(group: group)
                .connect(to: serverChannel.localAddress!)
                .wait())

            var buffer = clientChannel.allocator.buffer(capacity: numBytes)
            for i in 0..<numBytes {
                buffer.writeInteger(UInt8(i % 256))
            }

            try clientChannel.writeAndFlush(NIOAny(buffer)).wait()
        }

        // We should now shut down gracefully.
        try group.syncShutdownGracefully()
    }

    public func testShuttingDownFailsRegistration() throws {
        // This test catches a regression where the selectable event loop would allow a socket registration while
        // it was nominally "shutting down". To do this, we take advantage of the fact that the event loop attempts
        // to cleanly shut down all the channels before it actually closes. We add a custom channel that we can use
        // to wedge the event loop in the "shutting down" state, ensuring that we have plenty of time to attempt the
        // registration.
        class WedgeOpenHandler: ChannelDuplexHandler {
            typealias InboundIn = Any
            typealias OutboundIn = Any
            typealias OutboundOut = Any

            private let promiseRegisterCallback: (EventLoopPromise<Void>) -> Void

            var closePromise: EventLoopPromise<Void>? = nil
            private let channelActivePromise: EventLoopPromise<Void>?

            init(channelActivePromise: EventLoopPromise<Void>? = nil, _ promiseRegisterCallback: @escaping (EventLoopPromise<Void>) -> Void) {
                self.promiseRegisterCallback = promiseRegisterCallback
                self.channelActivePromise = channelActivePromise
            }

            func channelActive(context: ChannelHandlerContext) {
                self.channelActivePromise?.succeed(())
            }

            func close(context: ChannelHandlerContext, mode: CloseMode, promise: EventLoopPromise<Void>?) {
                guard self.closePromise == nil else {
                    XCTFail("Attempted to create duplicate close promise")
                    return
                }
                XCTAssertTrue(context.channel.isActive)
                self.closePromise = context.eventLoop.makePromise()
                self.closePromise!.futureResult.whenSuccess {
                    context.close(mode: mode, promise: promise)
                }
                promiseRegisterCallback(self.closePromise!)
            }
        }

        let promiseQueue = DispatchQueue(label: "promiseQueue")
        var promises: [EventLoopPromise<Void>] = []

        let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
        defer {
            XCTAssertThrowsError(try group.syncShutdownGracefully()) { error in
                XCTAssertEqual(.shutdown, error as? EventLoopError)
            }
        }
        let loop = group.next() as! SelectableEventLoop

        let serverChannelUp = group.next().makePromise(of: Void.self)
        let serverChannel = try assertNoThrowWithValue(ServerBootstrap(group: group)
            .childChannelInitializer { channel in
                channel.pipeline.addHandler(WedgeOpenHandler(channelActivePromise: serverChannelUp) { promise in
                    promiseQueue.sync { promises.append(promise) }
                })
            }
            .bind(host: "127.0.0.1", port: 0).wait())
        defer {
            XCTAssertFalse(serverChannel.isActive)
        }
        let connectPromise = loop.makePromise(of: Void.self)

        // We're going to create and register a channel, but not actually attempt to do anything with it.
        let wedgeHandler = WedgeOpenHandler { promise in
            promiseQueue.sync { promises.append(promise) }
        }
        let channel = try SocketChannel(eventLoop: loop, protocolFamily: .inet)
        try channel.eventLoop.submit {
            channel.pipeline.addHandler(wedgeHandler).flatMap {
                channel.register()
            }.flatMap {
                // connecting here to stop epoll from throwing EPOLLHUP at us
                channel.connect(to: serverChannel.localAddress!)
            }.cascade(to: connectPromise)
        }.wait()

        // Wait for the connect to complete.
        XCTAssertNoThrow(try connectPromise.futureResult.wait())

        XCTAssertNoThrow(try serverChannelUp.futureResult.wait())

        let g = DispatchGroup()
        let q = DispatchQueue(label: "\(#file)/\(#line)")
        g.enter()
        // Now we're going to start closing the event loop. This should not immediately succeed.
        loop.initiateClose(queue: q) { result in
            func workaroundSR9815() {
                XCTAssertNoThrow(try result.get())
            }
            workaroundSR9815()
            g.leave()
        }

        // Now we're going to attempt to register a new channel. This should immediately fail.
        let newChannel = try SocketChannel(eventLoop: loop, protocolFamily: .inet)

        XCTAssertThrowsError(try newChannel.register().wait()) { error in
            XCTAssertEqual(.shutdown, error as? EventLoopError)
        }

        // Confirm that the loop still hasn't closed.
        XCTAssertEqual(.timedOut, g.wait(timeout: .now()))

        // Now let it close.
        promiseQueue.sync {
            promises.forEach { $0.succeed(()) }
        }
        XCTAssertNoThrow(g.wait())
    }

    public func testEventLoopThreads() throws {
        var counter = 0
        let body: ThreadInitializer = { t in
            counter += 1
        }
        let threads: [ThreadInitializer] = [body, body]

        let group = MultiThreadedEventLoopGroup(threadInitializers: threads)

        XCTAssertEqual(2, counter)
        XCTAssertNoThrow(try group.syncShutdownGracefully())
    }

    public func testEventLoopPinned() throws {
        #if os(Linux) || os(Android)
            let target = NIOThread.current.affinity.cpuIds.first!
            let body: ThreadInitializer = { t in
                let set = LinuxCPUSet(target)
                t.affinity = set
                XCTAssertEqual(set, t.affinity)
            }
            let threads: [ThreadInitializer] = [body, body]

            let group = MultiThreadedEventLoopGroup(threadInitializers: threads)

            XCTAssertNoThrow(try group.syncShutdownGracefully())
        #endif
    }

    public func testEventLoopPinnedCPUIdsConstructor() throws {
        #if os(Linux) || os(Android)
            let target = NIOThread.current.affinity.cpuIds.first!
            let group = MultiThreadedEventLoopGroup(pinnedCPUIds: [target])
            let eventLoop = group.next()
            let set = try eventLoop.submit {
                NIOThread.current.affinity
            }.wait()

            XCTAssertEqual(LinuxCPUSet(target), set)
            XCTAssertNoThrow(try group.syncShutdownGracefully())
        #endif
    }

    public func testCurrentEventLoop() throws {
        class EventLoopHolder {
            weak var loop: EventLoop?
            init(_ loop: EventLoop) {
                self.loop = loop
            }
        }

        func assertCurrentEventLoop0() throws -> EventLoopHolder {
            let group = MultiThreadedEventLoopGroup(numberOfThreads: 2)

            let loop1 = group.next()
            let currentLoop1 = try loop1.submit {
                MultiThreadedEventLoopGroup.currentEventLoop
            }.wait()
            XCTAssertTrue(loop1 === currentLoop1)

            let loop2 = group.next()
            let currentLoop2 = try loop2.submit {
                MultiThreadedEventLoopGroup.currentEventLoop
            }.wait()
            XCTAssertTrue(loop2 === currentLoop2)
            XCTAssertFalse(loop1 === loop2)

            let holder = EventLoopHolder(loop2)
            XCTAssertNotNil(holder.loop)
            XCTAssertNil(MultiThreadedEventLoopGroup.currentEventLoop)
            XCTAssertNoThrow(try group.syncShutdownGracefully())
            return holder
        }

        let holder = try assertCurrentEventLoop0()

        // We loop as the Thread used by SelectableEventLoop may not be gone yet.
        // In the next major version we should ensure to join all threads and so be sure all are gone when
        // syncShutdownGracefully returned.
        var tries = 0
        while holder.loop != nil {
            XCTAssertTrue(tries < 5, "Reference to EventLoop still alive after 5 seconds")
            sleep(1)
            tries += 1
        }
    }

    public func testShutdownWhileScheduledTasksNotReady() throws {
        let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
        let eventLoop = group.next()
        _ = eventLoop.scheduleTask(in: .hours(1)) { }
        try group.syncShutdownGracefully()
    }

    public func testCloseFutureNotifiedBeforeUnblock() throws {
        class AssertHandler: ChannelInboundHandler {
            typealias InboundIn = Any

            let groupIsShutdown = NIOAtomic.makeAtomic(value: false)
            let removed = NIOAtomic.makeAtomic(value: false)

            public func handlerRemoved(context: ChannelHandlerContext) {
                XCTAssertFalse(groupIsShutdown.load())
                XCTAssertTrue(removed.compareAndExchange(expected: false, desired: true))
            }
        }

        let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
        let eventLoop = group.next()
        let assertHandler = AssertHandler()
        let serverSocket = try assertNoThrowWithValue(ServerBootstrap(group: group)
            .bind(host: "localhost", port: 0).wait())
        let channel = try assertNoThrowWithValue(SocketChannel(eventLoop: eventLoop as! SelectableEventLoop,
                                                               protocolFamily: serverSocket.localAddress!.protocol))
        XCTAssertNoThrow(try channel.pipeline.addHandler(assertHandler).wait() as Void)
        XCTAssertNoThrow(try channel.eventLoop.flatSubmit {
            channel.register().flatMap {
                channel.connect(to: serverSocket.localAddress!)
            }
        }.wait() as Void)
        let closeFutureFulfilledEventually = NIOAtomic<Bool>.makeAtomic(value: false)
        XCTAssertFalse(channel.closeFuture.isFulfilled)
        channel.closeFuture.whenSuccess {
            XCTAssertTrue(closeFutureFulfilledEventually.compareAndExchange(expected: false, desired: true))
        }
        XCTAssertNoThrow(try group.syncShutdownGracefully())
        XCTAssertTrue(assertHandler.groupIsShutdown.compareAndExchange(expected: false, desired: true))
        XCTAssertTrue(assertHandler.removed.load())
        XCTAssertFalse(channel.isActive)
        XCTAssertTrue(closeFutureFulfilledEventually.load())
    }

    public func testScheduleMultipleTasks() throws {
        let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
        defer {
            XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
        }
        var array = Array<(Int, NIODeadline)>()
        let scheduled1 = eventLoopGroup.next().scheduleTask(in: .milliseconds(500)) {
            array.append((1, .now()))
        }

        let scheduled2 = eventLoopGroup.next().scheduleTask(in: .milliseconds(100)) {
            array.append((2, .now()))
        }

        let scheduled3 = eventLoopGroup.next().scheduleTask(in: .milliseconds(1000)) {
            array.append((3, .now()))
        }

        var result = try eventLoopGroup.next().scheduleTask(in: .milliseconds(1000)) {
            array
        }.futureResult.wait()

        XCTAssertTrue(scheduled1.futureResult.isFulfilled)
        XCTAssertTrue(scheduled2.futureResult.isFulfilled)
        XCTAssertTrue(scheduled3.futureResult.isFulfilled)

        let first = result.removeFirst()
        XCTAssertEqual(2, first.0)
        let second = result.removeFirst()
        XCTAssertEqual(1, second.0)
        let third = result.removeFirst()
        XCTAssertEqual(3, third.0)

        XCTAssertTrue(first.1 < second.1)
        XCTAssertTrue(second.1 < third.1)

        XCTAssertTrue(result.isEmpty)

    }

    public func testRepeatedTaskThatIsImmediatelyCancelledNotifies() throws {
        let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
        defer {
            XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
        }

        let loop = eventLoopGroup.next()
        let promise1: EventLoopPromise<Void> = loop.makePromise()
        let promise2: EventLoopPromise<Void> = loop.makePromise()
        let expect1 = XCTestExpectation(description: "Initializer promise was fulfilled")
        let expect2 = XCTestExpectation(description: "Cancellation-specific promise was fulfilled")
        promise1.futureResult.whenSuccess { expect1.fulfill() }
        promise2.futureResult.whenSuccess { expect2.fulfill() }
        loop.execute {
            let task = loop.scheduleRepeatedTask(initialDelay: .milliseconds(0), delay: .milliseconds(0), notifying: promise1) { task in
                XCTFail()
            }
            task.cancel(promise: promise2)
        }
        Thread.sleep(until: .init(timeIntervalSinceNow: 0.1))
        let res = XCTWaiter.wait(for: [expect1, expect2], timeout: 1.0)
        XCTAssertEqual(res, .completed)
    }

    public func testRepeatedTaskThatIsCancelledAfterRunningAtLeastTwiceNotifies() throws {
        let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
        defer {
            XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
        }

        let loop = eventLoopGroup.next()
        let promise1: EventLoopPromise<Void> = loop.makePromise()
        let promise2: EventLoopPromise<Void> = loop.makePromise()
        let expectRuns = XCTestExpectation(description: "Repeated task has run")
        expectRuns.expectedFulfillmentCount = 2
        let task = loop.scheduleRepeatedTask(initialDelay: .milliseconds(0), delay: .milliseconds(10), notifying: promise1) { task in
            expectRuns.fulfill()
        }
        XCTAssertEqual(XCTWaiter.wait(for: [expectRuns], timeout: 0.05), .completed)
        let expect1 = XCTestExpectation(description: "Initializer promise was fulfilled")
        let expect2 = XCTestExpectation(description: "Cancellation-specific promise was fulfilled")
        promise1.futureResult.whenSuccess { expect1.fulfill() }
        promise2.futureResult.whenSuccess { expect2.fulfill() }
        task.cancel(promise: promise2)
        XCTAssertEqual(XCTWaiter.wait(for: [expect1, expect2], timeout: 1.0), .completed)
    }

    public func testRepeatedTaskThatCancelsItselfNotifiesOnlyWhenFinished() throws {
        let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
        defer {
            XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
        }

        let loop = eventLoopGroup.next()
        let promise1: EventLoopPromise<Void> = loop.makePromise()
        let promise2: EventLoopPromise<Void> = loop.makePromise()
        let semaphore = DispatchSemaphore(value: 0)
        loop.scheduleRepeatedTask(initialDelay: .milliseconds(0), delay: .milliseconds(0), notifying: promise1) { task -> Void in
            task.cancel(promise: promise2)
            semaphore.wait()
        }
        let expectFail1 = XCTestExpectation(description: "Initializer promise was wrongly fulfilled")
        let expectFail2 = XCTestExpectation(description: "Cancellation-specific promise was wrongly fulfilled")
        let expect1 = XCTestExpectation(description: "Initializer promise was fulfilled")
        let expect2 = XCTestExpectation(description: "Cancellation-specific promise was fulfilled")
        promise1.futureResult.whenSuccess { expectFail1.fulfill(); expect1.fulfill() }
        promise2.futureResult.whenSuccess { expectFail2.fulfill(); expect2.fulfill() }
        XCTAssertEqual(XCTWaiter.wait(for: [expectFail1, expectFail2], timeout: 0.5), .timedOut)
        semaphore.signal()
        XCTAssertEqual(XCTWaiter.wait(for: [expect1, expect2], timeout: 0.5), .completed)
    }

    func testCancelledScheduledTasksDoNotHoldOnToRunClosure() {
        let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
        defer {
            XCTAssertNoThrow(try group.syncShutdownGracefully())
        }

        class Thing {}

        weak var weakThing: Thing? = nil

        func make() -> Scheduled<Never> {
            let aThing = Thing()
            weakThing = aThing
            return group.next().scheduleTask(in: .hours(1)) {
                preconditionFailure("this should definitely not run: \(aThing)")
            }
        }

        let scheduled = make()
        scheduled.cancel()
        assert(weakThing == nil, within: .seconds(1))
        XCTAssertThrowsError(try scheduled.futureResult.wait()) { error in
            XCTAssertEqual(EventLoopError.cancelled, error as? EventLoopError)
        }
    }

    func testIllegalCloseOfEventLoopFails() {
        // Vapor 3 closes EventLoops directly which is illegal and makes the `shutdownGracefully` of the owning
        // MultiThreadedEventLoopGroup never succeed.
        let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
        defer {
            XCTAssertNoThrow(try group.syncShutdownGracefully())
        }
        XCTAssertThrowsError(try group.next().syncShutdownGracefully()) { error in
            switch error {
            case EventLoopError.unsupportedOperation:
                () // expected
            default:
                XCTFail("illegal shutdown threw wrong error \(error)")
            }
        }
    }

    func testSubtractingDeadlineFromPastAndFuturesDeadlinesWorks() {
        let older = NIODeadline.now()
        Thread.sleep(until: Date().addingTimeInterval(0.02))
        let newer = NIODeadline.now()

        XCTAssertLessThan(older - newer, .nanoseconds(0))
        XCTAssertGreaterThan(newer - older, .nanoseconds(0))
    }

    func testCallingSyncShutdownGracefullyMultipleTimesShouldNotHang() throws {
        let elg = MultiThreadedEventLoopGroup(numberOfThreads: 4)
        try elg.syncShutdownGracefully()
        try elg.syncShutdownGracefully()
        try elg.syncShutdownGracefully()
    }

    func testCallingShutdownGracefullyMultipleTimesShouldExecuteAllCallbacks() throws {
        let elg = MultiThreadedEventLoopGroup(numberOfThreads: 4)
        let condition: ConditionLock<Int> = ConditionLock(value: 0)
        elg.shutdownGracefully { _ in
            if condition.lock(whenValue: 0, timeoutSeconds: 1) {
                condition.unlock(withValue: 1)
            }
        }
        elg.shutdownGracefully { _ in
            if condition.lock(whenValue: 1, timeoutSeconds: 1) {
                condition.unlock(withValue: 2)
            }
        }
        elg.shutdownGracefully { _ in
            if condition.lock(whenValue: 2, timeoutSeconds: 1) {
                condition.unlock(withValue: 3)
            }
        }

        guard condition.lock(whenValue: 3, timeoutSeconds: 1) else {
            XCTFail("Not all shutdown callbacks have been executed")
            return
        }
        condition.unlock()
    }

    func testEdgeCasesNIODeadlineMinusNIODeadline() {
        let smallestPossibleDeadline = NIODeadline.uptimeNanoseconds(.min)
        let largestPossibleDeadline = NIODeadline.uptimeNanoseconds(.max)
        let distantFuture = NIODeadline.distantFuture
        let distantPast = NIODeadline.distantPast
        let zeroDeadline = NIODeadline.uptimeNanoseconds(0)
        let nowDeadline = NIODeadline.now()

        let allDeadlines = [smallestPossibleDeadline, largestPossibleDeadline, distantPast, distantFuture,
                            zeroDeadline, nowDeadline]

        for deadline1 in allDeadlines {
            for deadline2 in allDeadlines {
                if deadline1 > deadline2 {
                    XCTAssertGreaterThan(deadline1 - deadline2, TimeAmount.nanoseconds(0))
                } else if deadline1 < deadline2 {
                    XCTAssertLessThan(deadline1 - deadline2, TimeAmount.nanoseconds(0))
                } else {
                    // they're equal.
                    XCTAssertEqual(deadline1 - deadline2, TimeAmount.nanoseconds(0))
                }
            }
        }
    }

    func testEdgeCasesNIODeadlinePlusTimeAmount() {
        let smallestPossibleTimeAmount = TimeAmount.nanoseconds(.min)
        let largestPossibleTimeAmount = TimeAmount.nanoseconds(.max)
        let zeroTimeAmount = TimeAmount.nanoseconds(0)

        let smallestPossibleDeadline = NIODeadline.uptimeNanoseconds(.min)
        let largestPossibleDeadline = NIODeadline.uptimeNanoseconds(.max)
        let distantFuture = NIODeadline.distantFuture
        let distantPast = NIODeadline.distantPast
        let zeroDeadline = NIODeadline.uptimeNanoseconds(0)
        let nowDeadline = NIODeadline.now()

        for timeAmount in [smallestPossibleTimeAmount, largestPossibleTimeAmount, zeroTimeAmount] {
            for deadline in [smallestPossibleDeadline, largestPossibleDeadline, distantPast, distantFuture,
                             zeroDeadline, nowDeadline] {
                let (partial, overflow) = Int64(deadline.uptimeNanoseconds).addingReportingOverflow(timeAmount.nanoseconds)
                let expectedValue: UInt64
                if overflow {
                    XCTAssertGreaterThanOrEqual(timeAmount.nanoseconds, 0)
                    XCTAssertGreaterThanOrEqual(deadline.uptimeNanoseconds, 0)
                    // we cap at distantFuture torwards +inf
                    expectedValue = NIODeadline.distantFuture.uptimeNanoseconds
                } else if partial < 0 {
                    // we cap at 0 towards -inf
                    expectedValue = 0
                } else {
                    // otherwise we have a result
                    expectedValue = .init(partial)
                }
                XCTAssertEqual((deadline + timeAmount).uptimeNanoseconds, expectedValue)
            }
        }
    }

    func testEdgeCasesNIODeadlineMinusTimeAmount() {
        let smallestPossibleTimeAmount = TimeAmount.nanoseconds(.min)
        let largestPossibleTimeAmount = TimeAmount.nanoseconds(.max)
        let zeroTimeAmount = TimeAmount.nanoseconds(0)

        let smallestPossibleDeadline = NIODeadline.uptimeNanoseconds(.min)
        let largestPossibleDeadline = NIODeadline.uptimeNanoseconds(.max)
        let distantFuture = NIODeadline.distantFuture
        let distantPast = NIODeadline.distantPast
        let zeroDeadline = NIODeadline.uptimeNanoseconds(0)
        let nowDeadline = NIODeadline.now()

        for timeAmount in [smallestPossibleTimeAmount, largestPossibleTimeAmount, zeroTimeAmount] {
            for deadline in [smallestPossibleDeadline, largestPossibleDeadline, distantPast, distantFuture,
                             zeroDeadline, nowDeadline] {
                let (partial, overflow) = Int64(deadline.uptimeNanoseconds).subtractingReportingOverflow(timeAmount.nanoseconds)
                let expectedValue: UInt64
                if overflow {
                    XCTAssertLessThan(timeAmount.nanoseconds, 0)
                    XCTAssertGreaterThanOrEqual(deadline.uptimeNanoseconds, 0)
                    // we cap at distantFuture torwards +inf
                    expectedValue = NIODeadline.distantFuture.uptimeNanoseconds
                } else if partial < 0 {
                    // we cap at 0 towards -inf
                    expectedValue = 0
                } else {
                    // otherwise we have a result
                    expectedValue = .init(partial)
                }
                XCTAssertEqual((deadline - timeAmount).uptimeNanoseconds, expectedValue)
            }
        }
    }

    func testSuccessfulFlatSubmit() {
        let eventLoop = EmbeddedEventLoop()
        let future = eventLoop.flatSubmit {
            eventLoop.makeSucceededFuture(1)
        }
        eventLoop.run()
        XCTAssertNoThrow(XCTAssertEqual(1, try future.wait()))
    }

    func testFailingFlatSubmit() {
        enum TestError: Error { case failed }

        let eventLoop = EmbeddedEventLoop()
        let future = eventLoop.flatSubmit { () -> EventLoopFuture<Int> in
            eventLoop.makeFailedFuture(TestError.failed)
        }
        eventLoop.run()
        XCTAssertThrowsError(try future.wait()) { error in
            XCTAssertEqual(.failed, error as? TestError)
        }
    }

    func testSchedulingTaskOnTheEventLoopWithinTheEventLoopsOnlyTask() {
        let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1)
        defer {
            XCTAssertNoThrow(try elg.syncShutdownGracefully())
        }

        let el = elg.next()
        let g = DispatchGroup()
        g.enter()
        el.execute {
            // We're the last and only task running, scheduling another task here makes sure that despite not waking
            // up the selector, we will still run this task.
            el.execute {
                g.leave()
            }
        }
        g.wait()
    }

    func testSchedulingTaskOnTheEventLoopWithinTheEventLoopsOnlyIOOperation() {
        final class ExecuteSomethingOnEventLoop: ChannelInboundHandler {
            typealias InboundIn = ByteBuffer

            static let numberOfInstances = NIOAtomic<Int>.makeAtomic(value: 0)
            let groupToNotify: DispatchGroup

            init(groupToNotify: DispatchGroup) {
                XCTAssertEqual(0, ExecuteSomethingOnEventLoop.numberOfInstances.add(1))
                self.groupToNotify = groupToNotify
            }

            func channelRead(context: ChannelHandlerContext, data: NIOAny) {
                context.eventLoop.execute {
                    self.groupToNotify.leave()
                }
            }
        }

        let elg1 = MultiThreadedEventLoopGroup(numberOfThreads: 1)
        let elg2 = MultiThreadedEventLoopGroup(numberOfThreads: 1)
        defer {
            XCTAssertNoThrow(try elg1.syncShutdownGracefully())
            XCTAssertNoThrow(try elg2.syncShutdownGracefully())
        }

        let g = DispatchGroup()
        g.enter()
        var maybeServer: Channel?
        XCTAssertNoThrow(maybeServer = try ServerBootstrap(group: elg2)
            .serverChannelOption(ChannelOptions.socketOption(.so_reuseaddr), value: 1)
            .serverChannelOption(ChannelOptions.autoRead, value: false)
            .serverChannelOption(ChannelOptions.maxMessagesPerRead, value: 1)
            .childChannelInitializer { channel in
                channel.pipeline.addHandler(ExecuteSomethingOnEventLoop(groupToNotify: g))
            }
            .bind(to: .init(ipAddress: "127.0.0.1", port: 0))
            .wait())
        maybeServer?.read() // this should accept one client

        var maybeClient: Channel?
        XCTAssertNoThrow(maybeClient = try ClientBootstrap(group: elg1)
            .connect(to: maybeServer?.localAddress ?? SocketAddress(unixDomainSocketPath: "/dev/null/does/not/exist"))
            .wait())

        guard let client = maybeClient else {
            XCTFail("couldn't connect")
            return
        }

        var buffer = client.allocator.buffer(capacity: 1)
        buffer.writeString("X")

        // Now let's trigger a channelRead in the accepted channel which should schedule running an EventLoop task
        // with no outstanding operations on the EventLoop (no IO, nor tasks left to do).
        XCTAssertNoThrow(try client.writeAndFlush(buffer).wait())

        // The executed task should've notified this DispatchGroup
        g.wait()
    }

    func testCancellingTheLastOutstandingTask() {
        let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1)
        defer {
            XCTAssertNoThrow(try elg.syncShutdownGracefully())
        }

        let el = elg.next()
        let task = el.scheduleTask(in: .milliseconds(10)) {}
        task.cancel()
        // sleep for 10ms which should have the above scheduled (and cancelled) task have caused an unnecessary wakeup.
        Thread.sleep(forTimeInterval: 0.015 /* 15 ms */)
    }

    func testSchedulingTaskOnTheEventLoopWithinTheEventLoopsOnlyScheduledTask() {
        let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1)
        defer {
            XCTAssertNoThrow(try elg.syncShutdownGracefully())
        }

        let el = elg.next()
        let g = DispatchGroup()
        g.enter()
        el.scheduleTask(in: .nanoseconds(10) /* something non-0 */) {
            el.execute {
                g.leave()
            }
        }
        g.wait()
    }

    func testSelectableEventLoopDescription() {
        let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1)
        defer {
            XCTAssertNoThrow(try elg.syncShutdownGracefully())
        }

        let el: EventLoop = elg.next()
        let expectedPrefix = "SelectableEventLoop { selector = Selector { descriptor ="
        let expectedContains = "thread = NIOThread(name = NIO-ELT-"
        let expectedSuffix = " }"
        let desc = el.description
        XCTAssert(el.description.starts(with: expectedPrefix), desc)
        XCTAssert(el.description.reversed().starts(with: expectedSuffix.reversed()), desc)
        // let's check if any substring contains the `expectedContains`
        XCTAssert(desc.indices.contains { startIndex in
            desc[startIndex...].starts(with: expectedContains)
        }, desc)
    }

    func testMultiThreadedEventLoopGroupDescription() {
        let elg: EventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
        defer {
            XCTAssertNoThrow(try elg.syncShutdownGracefully())
        }

        XCTAssert(elg.description.starts(with: "MultiThreadedEventLoopGroup { threadPattern = NIO-ELT-"),
                  elg.description)
    }

    func testSafeToExecuteTrue() {
        let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1)
        defer {
            XCTAssertNoThrow(try elg.syncShutdownGracefully())
        }
        let loop = elg.next() as! SelectableEventLoop
        XCTAssertTrue(loop.testsOnly_validExternalStateToScheduleTasks)
        XCTAssertTrue(loop.testsOnly_validExternalStateToScheduleTasks)
    }

    func testSafeToExecuteFalse() {
        let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1)
        let loop = elg.next() as! SelectableEventLoop
        try? elg.syncShutdownGracefully()
        XCTAssertFalse(loop.testsOnly_validExternalStateToScheduleTasks)
        XCTAssertFalse(loop.testsOnly_validExternalStateToScheduleTasks)
    }

    func testTakeOverThreadAndAlsoTakeItBack() {
        let currentNIOThread = NIOThread.current
        let currentNSThread = Thread.current
        let lock = Lock()
        var hasBeenShutdown = false
        let allDoneGroup = DispatchGroup()
        allDoneGroup.enter()
        MultiThreadedEventLoopGroup.withCurrentThreadAsEventLoop { loop in
            XCTAssertEqual(currentNIOThread, NIOThread.current)
            XCTAssertEqual(currentNSThread, Thread.current)
            XCTAssert(loop === MultiThreadedEventLoopGroup.currentEventLoop)
            loop.shutdownGracefully(queue: DispatchQueue.global()) { error in
                XCTAssertNil(error)
                lock.withLock {
                    hasBeenShutdown = error == nil
                }
                allDoneGroup.leave()
            }
        }
        allDoneGroup.wait()
        XCTAssertTrue(lock.withLock { hasBeenShutdown })
    }

    func testThreadTakeoverUnsetsCurrentEventLoop() {
        XCTAssertNil(MultiThreadedEventLoopGroup.currentEventLoop)

        MultiThreadedEventLoopGroup.withCurrentThreadAsEventLoop { el in
            XCTAssert(el === MultiThreadedEventLoopGroup.currentEventLoop)
            el.shutdownGracefully { error in
                XCTAssertNil(error)
            }
        }

        XCTAssertNil(MultiThreadedEventLoopGroup.currentEventLoop)
    }

    func testWeCanDoTrulySingleThreadedNetworking() {
        final class SaveReceivedByte: ChannelInboundHandler {
            typealias InboundIn = ByteBuffer

            // For once, we don't need thread-safety as we're taking the calling thread :)
            var received: UInt8? = nil
            var readCalls: Int = 0
            var allDonePromise: EventLoopPromise<Void>? = nil

            func channelRead(context: ChannelHandlerContext, data: NIOAny) {
                self.readCalls += 1
                XCTAssertEqual(1, self.readCalls)

                var data = self.unwrapInboundIn(data)
                XCTAssertEqual(1, data.readableBytes)

                XCTAssertNil(self.received)
                self.received = data.readInteger()

                self.allDonePromise?.succeed(())

                context.close(promise: nil)
            }
        }

        let receiveHandler = SaveReceivedByte() // There'll be just one connection, we can share.
        MultiThreadedEventLoopGroup.withCurrentThreadAsEventLoop { loop in
            ServerBootstrap(group: loop)
                .serverChannelOption(ChannelOptions.socket(.init(SOL_SOCKET), .init(SO_REUSEADDR)), value: 1)
                .childChannelInitializer { accepted in
                    accepted.pipeline.addHandler(receiveHandler)
                }
                .bind(host: "127.0.0.1", port: 0)
                .flatMap { serverChannel in
                    ClientBootstrap(group: loop).connect(to: serverChannel.localAddress!).flatMap { clientChannel in
                        var buffer = clientChannel.allocator.buffer(capacity: 1)
                        buffer.writeString("J")
                        return clientChannel.writeAndFlush(buffer)
                    }.flatMap {
                        XCTAssertNil(receiveHandler.allDonePromise)
                        receiveHandler.allDonePromise = loop.makePromise()
                        return receiveHandler.allDonePromise!.futureResult
                    }.flatMap {
                        serverChannel.close()
                    }
                }.whenComplete { (result: Result<Void, Error>) -> Void in
                    func workaroundSR9815withAUselessFunction() {
                        XCTAssertNoThrow(try result.get())
                    }
                    workaroundSR9815withAUselessFunction()

                    // All done, let's return back into the calling thread.
                    loop.shutdownGracefully { error in
                        XCTAssertNil(error)
                    }
                }
        }

        // All done, the EventLoop is terminated so we should be able to check the results.
        XCTAssertEqual(UInt8(ascii: "J"), receiveHandler.received)
    }

    func testWeFailOutstandingScheduledTasksOnELShutdown() {
        let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
        let scheduledTask = group.next().scheduleTask(in: .hours(24)) {
            XCTFail("We lost the 24 hour race and aren't even in Le Mans.")
        }
        let waiter = DispatchGroup()
        waiter.enter()
        scheduledTask.futureResult.map {
            XCTFail("didn't expect success")
        }.whenFailure { error in
            XCTAssertEqual(.shutdown, error as? EventLoopError)
            waiter.leave()
        }

        XCTAssertNoThrow(try group.syncShutdownGracefully())
        waiter.wait()
    }

    func testSchedulingTaskOnFutureFailedByELShutdownDoesNotMakeUsExplode() {
        let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
        let scheduledTask = group.next().scheduleTask(in: .hours(24)) {
            XCTFail("Task was scheduled in 24 hours, yet it executed.")
        }
        let waiter = DispatchGroup()
        waiter.enter() // first scheduled task
        waiter.enter() // scheduled task in the first task's whenFailure.
        scheduledTask.futureResult.map {
            XCTFail("didn't expect success")
        }.whenFailure { error in
            XCTAssertEqual(.shutdown, error as? EventLoopError)
            group.next().execute {} // This previously blew up
            group.next().scheduleTask(in: .hours(24)) {
                XCTFail("Task was scheduled in 24 hours, yet it executed.")
            }.futureResult.map {
                XCTFail("didn't expect success")
            }.whenFailure { error in
                XCTAssertEqual(.shutdown, error as? EventLoopError)
                waiter.leave()
            }
            waiter.leave()
        }

        XCTAssertNoThrow(try group.syncShutdownGracefully())
        waiter.wait()
    }

    func testEventLoopGroupProvider() {
        let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
        defer {
            XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully())
        }

        let provider = NIOEventLoopGroupProvider.shared(eventLoopGroup)

        if case .shared(let sharedEventLoopGroup) = provider {
            XCTAssertTrue(sharedEventLoopGroup is MultiThreadedEventLoopGroup)
            XCTAssertTrue(sharedEventLoopGroup === eventLoopGroup)
        } else {
            XCTFail("Not the same")
        }
    }

    // Test that scheduling a task at the maximum value doesn't crash.
    // (Crashing resulted from an EINVAL/IOException thrown by the kevent
    // syscall when the timeout value exceeded the maximum supported by
    // the Darwin kernel #1056).
    public func testScheduleMaximum() {
        let eventLoop = EmbeddedEventLoop()
        let maxAmount: TimeAmount = .nanoseconds(.max)
        let scheduled = eventLoop.scheduleTask(in: maxAmount) { true }

        var result: Bool?
        var error: Error?
        scheduled.futureResult.whenSuccess { result = $0 }
        scheduled.futureResult.whenFailure { error = $0 }

        scheduled.cancel()

        XCTAssertTrue(scheduled.futureResult.isFulfilled)
        XCTAssertNil(result)
        XCTAssertEqual(error as? EventLoopError, .cancelled)
    }

    func testEventLoopsWithPreSucceededFuturesCacheThem() {
        let el = EventLoopWithPreSucceededFuture()
        defer {
            XCTAssertNoThrow(try el.syncShutdownGracefully())
        }

        let future1 = el.makeSucceededFuture(())
        let future2 = el.makeSucceededFuture(())
        let future3 = el.makeSucceededVoidFuture()

        XCTAssert(future1 === future2)
        XCTAssert(future2 === future3)
    }

    func testEventLoopsWithoutPreSucceededFuturesDoNotCacheThem() {
        let el = EventLoopWithoutPreSucceededFuture()
        defer {
            XCTAssertNoThrow(try el.syncShutdownGracefully())
        }

        let future1 = el.makeSucceededFuture(())
        let future2 = el.makeSucceededFuture(())
        let future3 = el.makeSucceededVoidFuture()

        XCTAssert(future1 !== future2)
        XCTAssert(future2 !== future3)
        XCTAssert(future1 !== future3)
    }

    func testSelectableEventLoopHasPreSucceededFuturesOnlyOnTheEventLoop() {
        let elg = MultiThreadedEventLoopGroup(numberOfThreads: 1)
        defer {
            XCTAssertNoThrow(try elg.syncShutdownGracefully())
        }

        let el = elg.next()

        let futureOutside1 = el.makeSucceededVoidFuture()
        let futureOutside2 = el.makeSucceededFuture(())
        XCTAssert(futureOutside1 !== futureOutside2)

        XCTAssertNoThrow(try el.submit {
            let futureInside1 = el.makeSucceededVoidFuture()
            let futureInside2 = el.makeSucceededFuture(())

            XCTAssert(futureOutside1 !== futureInside1)
            XCTAssert(futureInside1 === futureInside2)
        }.wait())
    }

    func testMakeCompletedFuture() {
        let eventLoop = EmbeddedEventLoop()
        defer {
            XCTAssertNoThrow(try eventLoop.syncShutdownGracefully())
        }

        XCTAssertEqual(try eventLoop.makeCompletedFuture(.success("foo")).wait(), "foo")

        struct DummyError: Error {}
        let future = eventLoop.makeCompletedFuture(Result<String, Error>.failure(DummyError()))
        XCTAssertThrowsError(try future.wait()) { error in
            XCTAssertTrue(error is DummyError)
        }
    }

    func testMakeCompletedVoidFuture() {
        let eventLoop = EventLoopWithPreSucceededFuture()
        defer {
            XCTAssertNoThrow(try eventLoop.syncShutdownGracefully())
        }

        let future1 = eventLoop.makeCompletedFuture(.success(()))
        let future2 = eventLoop.makeSucceededVoidFuture()
        let future3 = eventLoop.makeSucceededFuture(())
        XCTAssert(future1 === future2)
        XCTAssert(future2 === future3)
    }
}

fileprivate class EventLoopWithPreSucceededFuture: EventLoop {
    var inEventLoop: Bool {
        return true
    }

    func execute(_ task: @escaping () -> Void) {
        preconditionFailure("not implemented")
    }

    func submit<T>(_ task: @escaping () throws -> T) -> EventLoopFuture<T> {
        preconditionFailure("not implemented")
    }

    @discardableResult
    func scheduleTask<T>(deadline: NIODeadline, _ task: @escaping () throws -> T) -> Scheduled<T> {
        preconditionFailure("not implemented")
    }

    @discardableResult
    func scheduleTask<T>(in: TimeAmount, _ task: @escaping () throws -> T) -> Scheduled<T> {
        preconditionFailure("not implemented")
    }

    func preconditionInEventLoop(file: StaticString, line: UInt) {
        preconditionFailure("not implemented")
    }

    func preconditionNotInEventLoop(file: StaticString, line: UInt) {
        preconditionFailure("not implemented")
    }

    var _succeededVoidFuture: EventLoopFuture<Void>?
    func makeSucceededVoidFuture() -> EventLoopFuture<Void> {
        guard self.inEventLoop, let voidFuture = self._succeededVoidFuture else {
            return self.makeSucceededFuture(())
        }
        return voidFuture
    }

    init() {
        self._succeededVoidFuture = EventLoopFuture(eventLoop: self, value: (), file: "n/a", line: 0)
    }

    func shutdownGracefully(queue: DispatchQueue, _ callback: @escaping (Error?) -> Void) {
        self._succeededVoidFuture = nil
        queue.async {
            callback(nil)
        }
    }
}

fileprivate class EventLoopWithoutPreSucceededFuture: EventLoop {
    var inEventLoop: Bool {
        return true
    }

    func execute(_ task: @escaping () -> Void) {
        preconditionFailure("not implemented")
    }

    func submit<T>(_ task: @escaping () throws -> T) -> EventLoopFuture<T> {
        preconditionFailure("not implemented")
    }

    @discardableResult
    func scheduleTask<T>(deadline: NIODeadline, _ task: @escaping () throws -> T) -> Scheduled<T> {
        preconditionFailure("not implemented")
    }

    @discardableResult
    func scheduleTask<T>(in: TimeAmount, _ task: @escaping () throws -> T) -> Scheduled<T> {
        preconditionFailure("not implemented")
    }

    func preconditionInEventLoop(file: StaticString, line: UInt) {
        preconditionFailure("not implemented")
    }

    func preconditionNotInEventLoop(file: StaticString, line: UInt) {
        preconditionFailure("not implemented")
    }

    func shutdownGracefully(queue: DispatchQueue, _ callback: @escaping (Error?) -> Void) {
        queue.async {
            callback(nil)
        }
    }
}
