//===----------------------------------------------------------------------===//
//
// This source file is part of the AsyncHTTPClient open source project
//
// Copyright (c) 2021 Apple Inc. and the AsyncHTTPClient project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of AsyncHTTPClient project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

@testable import AsyncHTTPClient
import Logging
import NIOConcurrencyHelpers
import NIOCore
import NIOEmbedded
import NIOHTTP1
import NIOPosix
import NIOSSL
import NIOTestUtils
import XCTest

class HTTP2ConnectionTests: XCTestCase {
    func testCreateNewConnectionFailureClosedIO() {
        let embedded = EmbeddedChannel()

        XCTAssertNoThrow(try embedded.connect(to: SocketAddress(ipAddress: "127.0.0.1", port: 3000)).wait())
        XCTAssertNoThrow(try embedded.close().wait())
        // to really destroy the channel we need to tick once
        embedded.embeddedEventLoop.run()
        let logger = Logger(label: "test.http2.connection")

        XCTAssertThrowsError(try HTTP2Connection.start(
            channel: embedded,
            connectionID: 0,
            delegate: TestHTTP2ConnectionDelegate(),
            configuration: .init(),
            logger: logger
        ).wait())
    }

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

        let httpBin = HTTPBin(.http2(compress: false))
        defer { XCTAssertNoThrow(try httpBin.shutdown()) }

        let connectionCreator = TestConnectionCreator()
        let delegate = TestHTTP2ConnectionDelegate()
        var maybeHTTP2Connection: HTTP2Connection?
        XCTAssertNoThrow(maybeHTTP2Connection = try connectionCreator.createHTTP2Connection(
            to: httpBin.port,
            delegate: delegate,
            on: eventLoop
        )
        )
        guard let http2Connection = maybeHTTP2Connection else {
            return XCTFail("Expected to have an HTTP2 connection here.")
        }

        var maybeRequest: HTTPClient.Request?
        var maybeRequestBag: RequestBag<ResponseAccumulator>?
        XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "https://localhost:\(httpBin.port)"))
        XCTAssertNoThrow(maybeRequestBag = try RequestBag(
            request: XCTUnwrap(maybeRequest),
            eventLoopPreference: .indifferent,
            task: .init(eventLoop: eventLoop, logger: .init(label: "test")),
            redirectHandler: nil,
            connectionDeadline: .distantFuture,
            requestOptions: .forTests(),
            delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
        ))
        guard let requestBag = maybeRequestBag else {
            return XCTFail("Expected to have a request bag at this point")
        }

        http2Connection.executeRequest(requestBag)

        XCTAssertEqual(delegate.hitStreamClosed, 0)
        var maybeResponse: HTTPClient.Response?
        XCTAssertNoThrow(maybeResponse = try requestBag.task.futureResult.wait())
        XCTAssertEqual(maybeResponse?.status, .ok)
        XCTAssertEqual(maybeResponse?.version, .http2)
        XCTAssertEqual(delegate.hitStreamClosed, 1)
    }

    func testEveryDoneRequestLeadsToAStreamAvailableCall() {
        class NeverRespondChannelHandler: ChannelInboundHandler {
            typealias InboundIn = HTTPServerRequestPart
            typealias OutboundOut = HTTPServerResponsePart

            init() {}

            func channelRead(context: ChannelHandlerContext, data: NIOAny) {}
        }

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

        let httpBin = HTTPBin(.http2(compress: false))
        defer { XCTAssertNoThrow(try httpBin.shutdown()) }

        let connectionCreator = TestConnectionCreator()
        let delegate = TestHTTP2ConnectionDelegate()
        var maybeHTTP2Connection: HTTP2Connection?
        XCTAssertNoThrow(maybeHTTP2Connection = try connectionCreator.createHTTP2Connection(
            to: httpBin.port,
            delegate: delegate,
            on: eventLoop
        ))
        guard let http2Connection = maybeHTTP2Connection else {
            return XCTFail("Expected to have an HTTP2 connection here.")
        }
        defer { XCTAssertNoThrow(try http2Connection.close().wait()) }

        var futures = [EventLoopFuture<HTTPClient.Response>]()

        XCTAssertEqual(delegate.hitStreamClosed, 0)

        for _ in 0..<100 {
            var maybeRequest: HTTPClient.Request?
            var maybeRequestBag: RequestBag<ResponseAccumulator>?
            XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "https://localhost:\(httpBin.port)"))
            XCTAssertNoThrow(maybeRequestBag = try RequestBag(
                request: XCTUnwrap(maybeRequest),
                eventLoopPreference: .indifferent,
                task: .init(eventLoop: eventLoop, logger: .init(label: "test")),
                redirectHandler: nil,
                connectionDeadline: .distantFuture,
                requestOptions: .forTests(),
                delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
            ))
            guard let requestBag = maybeRequestBag else {
                return XCTFail("Expected to have a request bag at this point")
            }

            http2Connection.executeRequest(requestBag)

            futures.append(requestBag.task.futureResult)
        }

        for future in futures {
            XCTAssertNoThrow(try future.wait())
        }

        XCTAssertEqual(delegate.hitStreamClosed, 100)
        XCTAssertTrue(http2Connection.channel.isActive)
    }

    func testCancelAllRunningRequests() {
        class NeverRespondChannelHandler: ChannelInboundHandler {
            typealias InboundIn = HTTPServerRequestPart
            typealias OutboundOut = HTTPServerResponsePart

            init() {}

            func channelRead(context: ChannelHandlerContext, data: NIOAny) {}
        }

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

        let httpBin = HTTPBin(.http2(compress: false), handlerFactory: { _ in NeverRespondChannelHandler() })
        defer { XCTAssertNoThrow(try httpBin.shutdown()) }

        let connectionCreator = TestConnectionCreator()
        let delegate = TestHTTP2ConnectionDelegate()
        var maybeHTTP2Connection: HTTP2Connection?
        XCTAssertNoThrow(maybeHTTP2Connection = try connectionCreator.createHTTP2Connection(
            to: httpBin.port,
            delegate: delegate,
            on: eventLoop
        )
        )
        guard let http2Connection = maybeHTTP2Connection else {
            return XCTFail("Expected to have an HTTP2 connection here.")
        }

        var futures = [EventLoopFuture<HTTPClient.Response>]()

        for _ in 0..<100 {
            var maybeRequest: HTTPClient.Request?
            var maybeRequestBag: RequestBag<ResponseAccumulator>?
            XCTAssertNoThrow(maybeRequest = try HTTPClient.Request(url: "https://localhost:\(httpBin.port)"))
            XCTAssertNoThrow(maybeRequestBag = try RequestBag(
                request: XCTUnwrap(maybeRequest),
                eventLoopPreference: .indifferent,
                task: .init(eventLoop: eventLoop, logger: .init(label: "test")),
                redirectHandler: nil,
                connectionDeadline: .distantFuture,
                requestOptions: .forTests(),
                delegate: ResponseAccumulator(request: XCTUnwrap(maybeRequest))
            ))
            guard let requestBag = maybeRequestBag else {
                return XCTFail("Expected to have a request bag at this point")
            }

            http2Connection.executeRequest(requestBag)

            XCTAssertEqual(delegate.hitStreamClosed, 0)

            futures.append(requestBag.task.futureResult)
        }

        http2Connection.shutdown()

        for future in futures {
            XCTAssertThrowsError(try future.wait()) {
                XCTAssertEqual($0 as? HTTPClientError, .cancelled)
            }
        }

        XCTAssertNoThrow(try http2Connection.closeFuture.wait())
    }
}

class TestConnectionCreator {
    enum Error: Swift.Error {
        case alreadyCreatingAnotherConnection
        case wantedHTTP2ConnectionButGotHTTP1
        case wantedHTTP1ConnectionButGotHTTP2
    }

    enum State {
        case idle
        case waitingForHTTP1Connection(EventLoopPromise<HTTP1Connection>)
        case waitingForHTTP2Connection(EventLoopPromise<HTTP2Connection>)
    }

    private var state: State = .idle
    private let lock = Lock()

    init() {}

    func createHTTP1Connection(
        to port: Int,
        delegate: HTTP1ConnectionDelegate,
        connectionID: HTTPConnectionPool.Connection.ID = 0,
        on eventLoop: EventLoop,
        logger: Logger = .init(label: "test")
    ) throws -> HTTP1Connection {
        let request = try! HTTPClient.Request(url: "https://localhost:\(port)")

        var tlsConfiguration = TLSConfiguration.makeClientConfiguration()
        tlsConfiguration.certificateVerification = .none
        var config = HTTPClient.Configuration()
        config.httpVersion = .automatic
        let factory = HTTPConnectionPool.ConnectionFactory(
            key: .init(request),
            tlsConfiguration: tlsConfiguration,
            clientConfiguration: config,
            sslContextCache: .init()
        )

        let promise = try self.lock.withLock { () -> EventLoopPromise<HTTP1Connection> in
            guard case .idle = self.state else {
                throw Error.alreadyCreatingAnotherConnection
            }

            let promise = eventLoop.makePromise(of: HTTP1Connection.self)
            self.state = .waitingForHTTP1Connection(promise)
            return promise
        }

        factory.makeConnection(
            for: self,
            connectionID: connectionID,
            http1ConnectionDelegate: delegate,
            http2ConnectionDelegate: EmptyHTTP2ConnectionDelegate(),
            deadline: .now() + .seconds(2),
            eventLoop: eventLoop,
            logger: logger
        )

        return try promise.futureResult.wait()
    }

    func createHTTP2Connection(
        to port: Int,
        delegate: HTTP2ConnectionDelegate,
        connectionID: HTTPConnectionPool.Connection.ID = 0,
        on eventLoop: EventLoop,
        logger: Logger = .init(label: "test")
    ) throws -> HTTP2Connection {
        let request = try! HTTPClient.Request(url: "https://localhost:\(port)")

        var tlsConfiguration = TLSConfiguration.makeClientConfiguration()
        tlsConfiguration.certificateVerification = .none
        var config = HTTPClient.Configuration()
        config.httpVersion = .automatic
        let factory = HTTPConnectionPool.ConnectionFactory(
            key: .init(request),
            tlsConfiguration: tlsConfiguration,
            clientConfiguration: config,
            sslContextCache: .init()
        )

        let promise = try self.lock.withLock { () -> EventLoopPromise<HTTP2Connection> in
            guard case .idle = self.state else {
                throw Error.alreadyCreatingAnotherConnection
            }

            let promise = eventLoop.makePromise(of: HTTP2Connection.self)
            self.state = .waitingForHTTP2Connection(promise)
            return promise
        }

        factory.makeConnection(
            for: self,
            connectionID: connectionID,
            http1ConnectionDelegate: EmptyHTTP1ConnectionDelegate(),
            http2ConnectionDelegate: delegate,
            deadline: .now() + .seconds(2),
            eventLoop: eventLoop,
            logger: logger
        )

        return try promise.futureResult.wait()
    }
}

extension TestConnectionCreator: HTTPConnectionRequester {
    enum EitherPromiseWrapper<SucceedType, FailType> {
        case succeed(EventLoopPromise<SucceedType>, SucceedType)
        case fail(EventLoopPromise<FailType>, Error)

        func complete() {
            switch self {
            case .succeed(let promise, let success):
                promise.succeed(success)
            case .fail(let promise, let error):
                promise.fail(error)
            }
        }
    }

    func http1ConnectionCreated(_ connection: HTTP1Connection) {
        let wrapper = self.lock.withLock {
            () -> (EitherPromiseWrapper<HTTP1Connection, HTTP2Connection>) in

            switch self.state {
            case .waitingForHTTP1Connection(let promise):
                return .succeed(promise, connection)

            case .waitingForHTTP2Connection(let promise):
                return .fail(promise, Error.wantedHTTP2ConnectionButGotHTTP1)

            case .idle:
                preconditionFailure("Invalid state")
            }
        }
        wrapper.complete()
    }

    func http2ConnectionCreated(_ connection: HTTP2Connection, maximumStreams: Int) {
        let wrapper = self.lock.withLock {
            () -> (EitherPromiseWrapper<HTTP2Connection, HTTP1Connection>) in

            switch self.state {
            case .waitingForHTTP1Connection(let promise):
                return .fail(promise, Error.wantedHTTP1ConnectionButGotHTTP2)

            case .waitingForHTTP2Connection(let promise):
                return .succeed(promise, connection)

            case .idle:
                preconditionFailure("Invalid state")
            }
        }
        wrapper.complete()
    }

    enum FailPromiseWrapper<Type1, Type2> {
        case type1(EventLoopPromise<Type1>)
        case type2(EventLoopPromise<Type2>)

        func fail(_ error: Swift.Error) {
            switch self {
            case .type1(let eventLoopPromise):
                eventLoopPromise.fail(error)
            case .type2(let eventLoopPromise):
                eventLoopPromise.fail(error)
            }
        }
    }

    func failedToCreateHTTPConnection(_: HTTPConnectionPool.Connection.ID, error: Swift.Error) {
        let wrapper = self.lock.withLock {
            () -> (FailPromiseWrapper<HTTP1Connection, HTTP2Connection>) in

            switch self.state {
            case .waitingForHTTP1Connection(let promise):
                return .type1(promise)

            case .waitingForHTTP2Connection(let promise):
                return .type2(promise)

            case .idle:
                preconditionFailure("Invalid state")
            }
        }
        wrapper.fail(error)
    }
}

class TestHTTP2ConnectionDelegate: HTTP2ConnectionDelegate {
    var hitStreamClosed: Int {
        self.lock.withLock { self._hitStreamClosed }
    }

    var hitGoAwayReceived: Int {
        self.lock.withLock { self._hitGoAwayReceived }
    }

    var hitConnectionClosed: Int {
        self.lock.withLock { self._hitConnectionClosed }
    }

    var maxStreamSetting: Int {
        self.lock.withLock { self._maxStreamSetting }
    }

    private let lock = Lock()
    private var _hitStreamClosed: Int = 0
    private var _hitGoAwayReceived: Int = 0
    private var _hitConnectionClosed: Int = 0
    private var _maxStreamSetting: Int = 100

    init() {}

    func http2Connection(_: HTTP2Connection, newMaxStreamSetting: Int) {}

    func http2ConnectionStreamClosed(_: HTTP2Connection, availableStreams: Int) {
        self.lock.withLockVoid {
            self._hitStreamClosed += 1
        }
    }

    func http2ConnectionGoAwayReceived(_: HTTP2Connection) {
        self.lock.withLockVoid {
            self._hitGoAwayReceived += 1
        }
    }

    func http2ConnectionClosed(_: HTTP2Connection) {
        self.lock.withLockVoid {
            self._hitConnectionClosed += 1
        }
    }
}

final class EmptyHTTP2ConnectionDelegate: HTTP2ConnectionDelegate {
    func http2Connection(_: HTTP2Connection, newMaxStreamSetting: Int) {
        preconditionFailure("Unimplemented")
    }

    func http2ConnectionStreamClosed(_: HTTP2Connection, availableStreams: Int) {
        preconditionFailure("Unimplemented")
    }

    func http2ConnectionGoAwayReceived(_: HTTP2Connection) {
        preconditionFailure("Unimplemented")
    }

    func http2ConnectionClosed(_: HTTP2Connection) {
        preconditionFailure("Unimplemented")
    }
}

final class EmptyHTTP1ConnectionDelegate: HTTP1ConnectionDelegate {
    func http1ConnectionReleased(_: HTTP1Connection) {
        preconditionFailure("Unimplemented")
    }

    func http1ConnectionClosed(_: HTTP1Connection) {
        preconditionFailure("Unimplemented")
    }
}
