//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2019 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 NIO
import NIOHTTP1
import NIOTestUtils
import XCTest

class NIOHTTP1TestServerTest: XCTestCase {
    private var group: EventLoopGroup!
    private let allocator = ByteBufferAllocator()

    override func setUp() {
        self.group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
    }

    override func tearDown() {
        XCTAssertNoThrow(try self.group.syncShutdownGracefully())
        self.group = nil
    }

    func connect(serverPort: Int, responsePromise: EventLoopPromise<String>) throws -> EventLoopFuture<Channel> {
        let bootstrap = ClientBootstrap(group: self.group)
            .channelOption(ChannelOptions.socket(SocketOptionLevel(SOL_SOCKET), SO_REUSEADDR), value: 1)
            .channelInitializer { channel in
                channel.pipeline.addHTTPClientHandlers(position: .first,
                                                       leftOverBytesStrategy: .fireError).flatMap {
                    channel.pipeline.addHandler(AggregateBodyHandler())
                }.flatMap {
                    channel.pipeline.addHandler(TestHTTPHandler(responsePromise: responsePromise))
                }
        }
        return bootstrap.connect(host: "127.0.0.1", port: serverPort)
    }

    private func sendRequest(channel: Channel, uri: String, message: String) {
        var requestBuffer = allocator.buffer(capacity: message.utf8.count)
        requestBuffer.writeString(message)
        var headers = HTTPHeaders()
        headers.add(name: "Content-Type", value: "text/plain; charset=utf-8")
        headers.add(name: "Content-Length", value: "\(requestBuffer.readableBytes)")

        let requestHead = HTTPRequestHead(version: HTTPVersion(major: 1, minor: 1),
                                          method: .GET,
                                          uri: uri,
                                          headers: headers)

        channel.write(NIOAny(HTTPClientRequestPart.head(requestHead)), promise: nil)
        channel.write(NIOAny(HTTPClientRequestPart.body(.byteBuffer(requestBuffer))), promise: nil)
        channel.writeAndFlush(NIOAny(HTTPClientRequestPart.end(nil)), promise: nil)
    }

    private func sendRequestTo(_ url: URL, body: String) throws -> EventLoopFuture<String> {
        let responsePromise = self.group.next().makePromise(of: String.self)
        let channel = try connect(serverPort: url.port!, responsePromise: responsePromise).wait()
        sendRequest(channel: channel, uri: url.path, message: body)
        return responsePromise.futureResult
    }

    func testTheExampleInTheDocs() {
        // Setup the test environment.
        let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
        let allocator = ByteBufferAllocator()
        let testServer = NIOHTTP1TestServer(group: group)
        defer {
            XCTAssertNoThrow(try testServer.stop())
            XCTAssertNoThrow(try group.syncShutdownGracefully())
        }

        // Use your library to send a request to the server.
        let requestBody = "ping"
        var requestComplete: EventLoopFuture<String>!
        XCTAssertNoThrow(requestComplete = try sendRequestTo(
            URL(string: "http://127.0.0.1:\(testServer.serverPort)/some-route")!,
            body: requestBody))

        // Assert the server received the expected request.
        // Use custom methods if you only want some specific assertions on part
        // of the request.
        XCTAssertNoThrow(XCTAssertEqual(.head(.init(version: .init(major: 1, minor: 1),
                                                    method: .GET,
                                                    uri: "/some-route",
                                                    headers: .init([
                                                        ("Content-Type", "text/plain; charset=utf-8"),
                                                        ("Content-Length", "4")]))),
                                        try testServer.readInbound()))
        var requestBuffer = allocator.buffer(capacity: 128)
        requestBuffer.writeString(requestBody)
        XCTAssertNoThrow(XCTAssertEqual(.body(requestBuffer),
                                        try testServer.readInbound()))
        XCTAssertNoThrow(XCTAssertEqual(.end(nil),
                                        try testServer.readInbound()))

        // Make the server send a response to the client.
        let responseBody = "pong"
        var responseBuffer = allocator.buffer(capacity: 128)
        responseBuffer.writeString(responseBody)
        XCTAssertNoThrow(try testServer.writeOutbound(.head(.init(version: .init(major: 1, minor: 1), status: .ok))))
        XCTAssertNoThrow(try testServer.writeOutbound(.body(.byteBuffer(responseBuffer))))
        XCTAssertNoThrow(try testServer.writeOutbound(.end(nil)))

        // Assert that the client received the response from the server.
        XCTAssertNoThrow(XCTAssertEqual(responseBody, try requestComplete.wait()))
    }

    func testSimpleRequest() {
        let uri = "/request"
        let requestMessage = "request message"
        let responseMessage = "response message"
        let testServer = NIOHTTP1TestServer(group: self.group)
        defer {
            XCTAssertNoThrow(try testServer.stop())
        }

        // Establish the connection and send the request
        let responsePromise = self.group.next().makePromise(of: String.self)
        var channel: Channel!
        XCTAssertNoThrow(channel = try self.connect(serverPort: testServer.serverPort, responsePromise: responsePromise).wait())

        // Send a request to the server
        self.sendRequest(channel: channel, uri: uri, message: requestMessage)
        let response = responsePromise.futureResult

        // Assert we received the expected request
        XCTAssertNoThrow(try testServer.readInbound().assertHead(expectedURI: uri))
        XCTAssertNoThrow(try testServer.readInbound().assertBody(expectedMessage: requestMessage))
        XCTAssertNoThrow(try testServer.readInbound().assertEnd())

        // Send the response to the client
        var responseBuffer = allocator.buffer(capacity: responseMessage.utf8.count)
        responseBuffer.writeString(responseMessage)
        XCTAssertNoThrow(try testServer.writeOutbound(.head(.init(version: .init(major: 1, minor: 1), status: .ok))))
        XCTAssertNoThrow(try testServer.writeOutbound(.body(.byteBuffer(responseBuffer))))
        XCTAssertNoThrow(try testServer.writeOutbound(.end(nil)))

        // Verify that the client got what the server sent
        XCTAssertNoThrow(XCTAssertEqual(responseMessage, try response.wait()))
    }

    func testConcurrentRequests() {
        let testServer = NIOHTTP1TestServer(group: self.group)
        defer {
            XCTAssertNoThrow(try testServer.stop())
        }

        // Establish two "concurrent" requests
        let request1URI = "/request1"
        let request1Message = "Request #1"
        let response1Promise = self.group.next().makePromise(of: String.self)
        var channel1: Channel!
        XCTAssertNoThrow(channel1 = try self.connect(serverPort: testServer.serverPort, responsePromise: response1Promise).wait())

        let request2URI = "/request2"
        let request2Message = "Request #2"
        let response2Promise = self.group.next().makePromise(of: String.self)
        var channel2: Channel!
        XCTAssertNoThrow(channel2 = try self.connect(serverPort: testServer.serverPort, responsePromise: response2Promise).wait())

        // Both channels are connected to the server. Request on `channel1`
        // connected connection first so `testServer` will handle it completely
        // before moving on to the request on `channel2`.

        // Send a request to the server using the second channel (Accepted but the server is not handling it)
        self.sendRequest(channel: channel2, uri: request2URI, message: request2Message)

        // Check that nothing happened. The server is blocked waiting for the first
        // request to complete so it times out and throws when we try to read from it
        // before the first request completes.
        XCTAssertThrowsError(try testServer.readInbound(deadline: .now() + .milliseconds(5)))

        // Send a request to the server using the second channel (Currently handled by the server)
        self.sendRequest(channel: channel1, uri: request1URI, message: request1Message)

        // Assert we received the expected request from client1
        XCTAssertNoThrow(try testServer.readInbound().assertHead(expectedURI: request1URI))
        XCTAssertNoThrow(try testServer.readInbound().assertBody(expectedMessage: request1Message))
        XCTAssertNoThrow(try testServer.readInbound().assertEnd())

        // Send the response to client1
        let response1Message = "Response #1"
        var response1Buffer = allocator.buffer(capacity: response1Message.utf8.count)
        response1Buffer.writeString(response1Message)
        XCTAssertNoThrow(try testServer.writeOutbound(.head(.init(version: .init(major: 1, minor: 1), status: .ok))))
        XCTAssertNoThrow(try testServer.writeOutbound(.body(.byteBuffer(response1Buffer))))
        XCTAssertNoThrow(try testServer.writeOutbound(.end(nil)))

        // Assert we received the expected request from client2
        XCTAssertNoThrow(try testServer.readInbound().assertHead(expectedURI: request2URI))
        XCTAssertNoThrow(try testServer.readInbound().assertBody(expectedMessage: request2Message))
        XCTAssertNoThrow(try testServer.readInbound().assertEnd())

        // Send the response to client2
        let response2Message = "Response #2"
        var response2Buffer = allocator.buffer(capacity: response2Message.utf8.count)
        response2Buffer.writeString(response2Message)
        XCTAssertNoThrow(try testServer.writeOutbound(.head(.init(version: .init(major: 1, minor: 1), status: .ok))))
        XCTAssertNoThrow(try testServer.writeOutbound(.body(.byteBuffer(response2Buffer))))
        XCTAssertNoThrow(try testServer.writeOutbound(.end(nil)))

        // Verify that the each client got their own response
        XCTAssertNoThrow(XCTAssertEqual(response1Message, try response1Promise.futureResult.wait()))
        XCTAssertNoThrow(XCTAssertEqual(response2Message, try response2Promise.futureResult.wait()))
    }

    func testTestWebServerCanBeReleased() {
        weak var weakTestServer: NIOHTTP1TestServer? = nil
        func doIt() {
            let testServer = NIOHTTP1TestServer(group: self.group)
            weakTestServer = testServer
            XCTAssertNoThrow(try testServer.stop())
        }
        doIt()
        assert(weakTestServer == nil, within: .milliseconds(500))
    }

    func testStopClosesAcceptedChannel() {
        let testServer = NIOHTTP1TestServer(group: self.group)

        let responsePromise = self.group.next().makePromise(of: String.self)
        var channel: Channel!
        XCTAssertNoThrow(channel = try self.connect(serverPort: testServer.serverPort,
                                                    responsePromise: responsePromise).wait())
        self.sendRequest(channel: channel, uri: "/uri", message: "hello")

        XCTAssertNoThrow(try testServer.readInbound().assertHead(expectedURI: "/uri"))
        XCTAssertNoThrow(try testServer.readInbound().assertBody(expectedMessage: "hello"))
        XCTAssertNoThrow(try testServer.readInbound().assertEnd())

        XCTAssertNoThrow(try testServer.stop())
        XCTAssertNotNil(channel)
        XCTAssertNoThrow(try channel.closeFuture.wait())
    }
}

private final class TestHTTPHandler: ChannelInboundHandler {
    public typealias InboundIn = HTTPClientResponsePart
    public typealias OutboundOut = HTTPClientRequestPart

    private let responsePromise: EventLoopPromise<String>

    init(responsePromise: EventLoopPromise<String>) {
        self.responsePromise = responsePromise
    }

    public func handlerRemoved(context: ChannelHandlerContext) {
        struct HandlerRemovedBeforeReceivingFullRequestError: Error {}
        self.responsePromise.fail(HandlerRemovedBeforeReceivingFullRequestError())
    }

    public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
        switch self.unwrapInboundIn(data) {
        case .head(let responseHead):
            guard case .ok = responseHead.status else {
                self.responsePromise.fail(ResponseError.badStatus)
                return
            }
        case .body(var byteBuffer):
            // We're using AggregateBodyHandler so we see all the body content at once
            if let response = byteBuffer.readString(length: byteBuffer.readableBytes) {
                self.responsePromise.succeed(response)
            } else {
                self.responsePromise.fail(ResponseError.missingResponse)
            }
        case .end:
            context.close(promise: nil)
        }
    }

    public func errorCaught(context: ChannelHandlerContext, error: Error) {
        self.responsePromise.fail(error)
        context.close(promise: nil)
    }
}

extension HTTPServerRequestPart {
    func assertHead(expectedURI: String, file: StaticString = #file, line: UInt = #line) {
        switch self {
        case .head(let head):
            XCTAssertEqual(.GET, head.method)
            XCTAssertEqual(expectedURI, head.uri)
            XCTAssertEqual("text/plain; charset=utf-8", head.headers["Content-Type"].first)
        default:
            XCTFail("Expected head, got \(self)", file: file, line: line)
        }
    }

    func assertBody(expectedMessage: String, file: StaticString = #file, line: UInt = #line) {
        switch self {
        case .body(let buffer):
            // Note that the test server coalesces the body parts for us.
            XCTAssertEqual(expectedMessage,
                           String(decoding: buffer.readableBytesView, as: Unicode.UTF8.self))
        default:
            XCTFail("Expected body, got \(self)", file: file, line: line)
        }
    }

    func assertEnd(file: StaticString = #file, line: UInt = #line) {
        switch self {
        case .end(_):
            ()
        default:
            XCTFail("Expected end, got \(self)", file: file, line: line)
        }
    }
}

private final class AggregateBodyHandler: ChannelInboundHandler {
    typealias InboundIn = HTTPClientResponsePart
    typealias InboundOut = HTTPClientResponsePart

    var receivedSoFar: ByteBuffer? = nil

    func channelRead(context: ChannelHandlerContext, data: NIOAny) {
        switch self.unwrapInboundIn(data) {
        case .head:
            context.fireChannelRead(data)
        case .body(var buffer):
            if self.receivedSoFar == nil {
                self.receivedSoFar = buffer
            } else {
                self.receivedSoFar?.writeBuffer(&buffer)
            }
        case .end:
            if let receivedSoFar = self.receivedSoFar {
                context.fireChannelRead(self.wrapInboundOut(.body(receivedSoFar)))
            }
            context.fireChannelRead(data)
        }
    }
}

private enum ResponseError: Error {
    case badStatus
    case missingResponse
}

func assert(_ condition: @autoclosure () -> Bool, within time: TimeAmount, testInterval: TimeAmount? = nil, _ message: String = "condition not satisfied in time", file: StaticString = #file, line: UInt = #line) {
    let testInterval = testInterval ?? TimeAmount.nanoseconds(time.nanoseconds / 5)
    let endTime = NIODeadline.now() + time

    repeat {
        if condition() { return }
        usleep(UInt32(testInterval.nanoseconds / 1000))
    } while (NIODeadline.now() < endTime)

    if !condition() {
        XCTFail(message, file: file, line: line)
    }
}
