#                Chronos Test Suite
#            (c) Copyright 2021-Present
#         Status Research & Development GmbH
#
#              Licensed under either of
#  Apache License, version 2.0, (LICENSE-APACHEv2)
#              MIT license (LICENSE-MIT)
import std/[strutils, algorithm]
import ".."/chronos/unittest2/asynctests,
       ".."/chronos,
       ".."/chronos/apps/http/[httpserver, httpcommon, httpdebug]
import stew/base10

{.used.}

# Trouble finding this if defined near its use for `data2.sorted`, etc. likely
# related to "generic sandwich" issues.  If any test ever wants to `sort` a
# `seq[(string, seq[string]]` differently, they may need to re-work that test.
proc `<`(a, b: (string, seq[string])): bool = a[0] < b[0]

suite "HTTP server testing suite":
  teardown:
    checkLeaks()

  type
    TooBigTest = enum
      GetBodyTest, ConsumeBodyTest, PostUrlTest, PostMultipartTest
    TestHttpResponse = object
      status: int
      headers: HttpTable
      data: string

    FirstMiddlewareRef = ref object of HttpServerMiddlewareRef
      someInteger: int

    SecondMiddlewareRef = ref object of HttpServerMiddlewareRef
      someString: string

  proc httpClient(address: TransportAddress,
                  data: string): Future[string] {.async.} =
    var transp: StreamTransport
    try:
      transp = await connect(address)
      if len(data) > 0:
        let wres {.used.} = await transp.write(data)
      var rres = await transp.read()
      return bytesToString(rres)
    except CatchableError:
      return "EXCEPTION"
    finally:
      if not(isNil(transp)):
        await closeWait(transp)

  proc httpClient2(transp: StreamTransport,
                   request: string,
                   length: int): Future[TestHttpResponse] {.async.} =
    var buffer = newSeq[byte](4096)
    var sep = @[0x0D'u8, 0x0A'u8, 0x0D'u8, 0x0A'u8]
    let wres = await transp.write(request)
    if wres != len(request):
      raise newException(ValueError, "Unable to write full request")
    let hres = await transp.readUntil(addr buffer[0], len(buffer), sep)
    var hdata = @buffer
    hdata.setLen(hres)
    zeroMem(addr buffer[0], len(buffer))
    await transp.readExactly(addr buffer[0], length)
    let data = bytesToString(buffer.toOpenArray(0, length - 1))
    let (status, headers) =
      block:
        let resp = parseResponse(hdata, false)
        if resp.failed():
          raise newException(ValueError, "Unable to decode response headers")
        var res = HttpTable.init()
        for key, value in resp.headers(hdata):
          res.add(key, value)
        (resp.code, res)
    TestHttpResponse(status: status, headers: headers, data: data)

  proc httpClient3(address: TransportAddress,
                   data: string): Future[TestHttpResponse] {.async.} =
    var
      transp: StreamTransport
      buffer = newSeq[byte](4096)
      sep = @[0x0D'u8, 0x0A'u8, 0x0D'u8, 0x0A'u8]
    try:
      transp = await connect(address)
      if len(data) > 0:
        let wres = await transp.write(data)
        if wres != len(data):
          raise newException(ValueError, "Unable to write full request")
      let hres = await transp.readUntil(addr buffer[0], len(buffer), sep)
      var hdata = @buffer
      hdata.setLen(hres)
      var rres = bytesToString(await transp.read())
      let (status, headers) =
        block:
          let resp = parseResponse(hdata, false)
          if resp.failed():
            raise newException(ValueError, "Unable to decode response headers")
          var res = HttpTable.init()
          for key, value in resp.headers(hdata):
            res.add(key, value)
          (resp.code, res)
      TestHttpResponse(status: status, headers: headers, data: rres)
    finally:
      if not(isNil(transp)):
        await closeWait(transp)

  proc testTooBigBodyChunked(operation: TooBigTest): Future[bool] {.async.} =
    var serverRes = false
    proc process(r: RequestFence): Future[HttpResponseRef] {.
         async: (raises: [CancelledError]).} =
      if r.isOk():
        let request = r.get()
        try:
          case operation
          of GetBodyTest:
            let body {.used.} = await request.getBody()
          of ConsumeBodyTest:
            await request.consumeBody()
          of PostUrlTest:
            let ptable {.used.} = await request.post()
          of PostMultipartTest:
            let ptable {.used.} = await request.post()
          defaultResponse()
        except HttpTransportError as exc:
          defaultResponse(exc)
        except HttpProtocolError as exc:
          if exc.code == Http413:
            serverRes = true
          defaultResponse(exc)
      else:
        defaultResponse()

    let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
    let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process,
                                maxRequestBodySize = 10,
                                socketFlags = socketFlags)
    if res.isErr():
      return false

    let server = res.get()
    server.start()
    let address = server.instance.localAddress()

    let request =
      case operation
      of GetBodyTest, ConsumeBodyTest, PostUrlTest:
        "POST / HTTP/1.1\r\n" &
        "Content-Type: application/x-www-form-urlencoded\r\n" &
        "Transfer-Encoding: chunked\r\n" &
        "Cookie: 2\r\n\r\n" &
        "5\r\na=a&b\r\n5\r\n=b&c=\r\n4\r\nc&d=\r\n4\r\n%D0%\r\n" &
        "2\r\n9F\r\n0\r\n\r\n"
      of PostMultipartTest:
        "POST / HTTP/1.1\r\n" &
        "Host: 127.0.0.1:30080\r\n" &
        "Transfer-Encoding: chunked\r\n" &
        "Content-Type: multipart/form-data; boundary=f98f0\r\n\r\n" &
        "3b\r\n--f98f0\r\nContent-Disposition: form-data; name=\"key1\"" &
        "\r\n\r\nA\r\n\r\n" &
        "3b\r\n--f98f0\r\nContent-Disposition: form-data; name=\"key2\"" &
        "\r\n\r\nB\r\n\r\n" &
        "3b\r\n--f98f0\r\nContent-Disposition: form-data; name=\"key3\"" &
        "\r\n\r\nC\r\n\r\n" &
        "b\r\n--f98f0--\r\n\r\n" &
        "0\r\n\r\n"

    let data = await httpClient(address, request)
    await server.stop()
    await server.closeWait()
    return serverRes and (data.startsWith("HTTP/1.1 413"))

  test "Request headers timeout test":
    proc testTimeout(): Future[bool] {.async.} =
      var serverRes = false
      proc process(r: RequestFence): Future[HttpResponseRef] {.
           async: (raises: [CancelledError]).} =
        if r.isOk():
          let request = r.get()
          try:
            await request.respond(Http200, "TEST_OK", HttpTable.init())
          except HttpWriteError as exc:
            defaultResponse(exc)
        else:
          if r.error.kind == HttpServerError.TimeoutError:
            serverRes = true
          defaultResponse()

      let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
      let res = HttpServerRef.new(initTAddress("127.0.0.1:0"),
                                  process, socketFlags = socketFlags,
                                  httpHeadersTimeout = 100.milliseconds)
      if res.isErr():
        return false

      let server = res.get()
      server.start()
      let address = server.instance.localAddress()
      let data = await httpClient(address, "")
      await server.stop()
      await server.closeWait()
      return serverRes and (data.startsWith("HTTP/1.1 408"))

    check waitFor(testTimeout()) == true

  test "Empty headers test":
    proc testEmpty(): Future[bool] {.async.} =
      var serverRes = false
      proc process(r: RequestFence): Future[HttpResponseRef] {.
           async: (raises: [CancelledError]).} =
        if r.isOk():
          let request = r.get()
          try:
            await request.respond(Http200, "TEST_OK", HttpTable.init())
          except HttpWriteError as exc:
            defaultResponse(exc)
        else:
          if r.error.kind == HttpServerError.ProtocolError:
            serverRes = true
          defaultResponse()

      let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
      let res = HttpServerRef.new(initTAddress("127.0.0.1:0"),
                                  process, socketFlags = socketFlags)
      if res.isErr():
        return false

      let server = res.get()
      server.start()
      let address = server.instance.localAddress()

      let data = await httpClient(address, "\r\n\r\n")
      await server.stop()
      await server.closeWait()
      return serverRes and (data.startsWith("HTTP/1.1 400"))

    check waitFor(testEmpty()) == true

  test "Too big headers test":
    proc testTooBig(): Future[bool] {.async.} =
      var serverRes = false
      proc process(r: RequestFence): Future[HttpResponseRef] {.
           async: (raises: [CancelledError]).} =
        if r.isOk():
          let request = r.get()
          try:
            await request.respond(Http200, "TEST_OK", HttpTable.init())
          except HttpWriteError as exc:
            defaultResponse(exc)
        else:
          if r.error.error == HttpServerError.ProtocolError:
            serverRes = true
          defaultResponse()

      let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
      let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process,
                                  maxHeadersSize = 10,
                                  socketFlags = socketFlags)
      if res.isErr():
        return false

      let server = res.get()
      server.start()
      let address = server.instance.localAddress()

      let data = await httpClient(address, "GET / HTTP/1.1\r\n\r\n")
      await server.stop()
      await server.closeWait()
      return serverRes and (data.startsWith("HTTP/1.1 431"))

    check waitFor(testTooBig()) == true

  test "Too big request body test (content-length)":
    proc testTooBigBody(): Future[bool] {.async.} =
      var serverRes = false
      proc process(r: RequestFence): Future[HttpResponseRef] {.
           async: (raises: [CancelledError]).} =
        if r.isErr():
          if r.error.error == HttpServerError.ProtocolError:
            serverRes = true
        defaultResponse()

      let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
      let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process,
                                  maxRequestBodySize = 10,
                                  socketFlags = socketFlags)
      if res.isErr():
        return false

      let server = res.get()
      server.start()
      let address = server.instance.localAddress()

      let request = "GET / HTTP/1.1\r\nContent-Length: 20\r\n\r\n"
      let data = await httpClient(address, request)
      await server.stop()
      await server.closeWait()
      return serverRes and (data.startsWith("HTTP/1.1 413"))

    check waitFor(testTooBigBody()) == true

  test "Too big request body test (getBody()/chunked encoding)":
    check:
      waitFor(testTooBigBodyChunked(GetBodyTest)) == true

  test "Too big request body test (consumeBody()/chunked encoding)":
    check:
      waitFor(testTooBigBodyChunked(ConsumeBodyTest)) == true

  test "Too big request body test (post()/urlencoded/chunked encoding)":
    check:
      waitFor(testTooBigBodyChunked(PostUrlTest)) == true

  test "Too big request body test (post()/multipart/chunked encoding)":
    check:
      waitFor(testTooBigBodyChunked(PostMultipartTest)) == true

  test "Query arguments test":
    proc testQuery(): Future[bool] {.async.} =
      var serverRes = false
      proc process(r: RequestFence): Future[HttpResponseRef] {.
           async: (raises: [CancelledError]).} =
        if r.isOk():
          let request = r.get()
          var kres = newSeq[string]()
          for k, v in request.query.stringItems():
            kres.add(k & ":" & v)
          sort(kres)
          serverRes = true
          try:
            await request.respond(Http200, "TEST_OK:" & kres.join(":"),
                                  HttpTable.init())
          except HttpWriteError as exc:
            serverRes = false
            defaultResponse(exc)
        else:
          defaultResponse()

      let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
      let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process,
                                  socketFlags = socketFlags)
      if res.isErr():
        return false

      let server = res.get()
      server.start()
      let address = server.instance.localAddress()

      let data1 = await httpClient(address,
                                  "GET /?a=1&a=2&b=3&c=4 HTTP/1.0\r\n\r\n")
      let data2 = await httpClient(address,
              "GET /?a=%D0%9F&%D0%A4=%D0%91&b=%D0%A6&c=%D0%AE HTTP/1.0\r\n\r\n")
      await server.stop()
      await server.closeWait()
      serverRes and
        (data1.find("TEST_OK:a:1:a:2:b:3:c:4") >= 0) and
        (data2.find("TEST_OK:a:П:b:Ц:c:Ю:Ф:Б") >= 0)

    check waitFor(testQuery()) == true

  test "Headers test":
    proc testHeaders(): Future[bool] {.async.} =
      var serverRes = false
      proc process(r: RequestFence): Future[HttpResponseRef] {.
           async: (raises: [CancelledError]).} =
        if r.isOk():
          let request = r.get()
          var kres = newSeq[string]()
          for k, v in request.headers.stringItems():
            kres.add(k & ":" & v)
          sort(kres)
          serverRes = true
          try:
            await request.respond(Http200, "TEST_OK:" & kres.join(":"),
                                  HttpTable.init())
          except HttpWriteError as exc:
            serverRes = false
            defaultResponse(exc)
        else:
          defaultResponse()

      let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
      let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process,
                                  socketFlags = socketFlags)
      if res.isErr():
        return false

      let server = res.get()
      server.start()
      let address = server.instance.localAddress()

      let message =
        "GET / HTTP/1.0\r\n" &
        "Host: www.google.com\r\n" &
        "Content-Type: text/html\r\n" &
        "Expect: 100-continue\r\n" &
        "Cookie: 1\r\n" &
        "Cookie: 2\r\n\r\n"
      let expect = "TEST_OK:content-type:text/html:cookie:1:cookie:2" &
                   ":expect:100-continue:host:www.google.com"
      let data = await httpClient(address, message)
      await server.stop()
      await server.closeWait()
      return serverRes and (data.find(expect) >= 0)

    check waitFor(testHeaders()) == true

  test "POST arguments (urlencoded/content-length) test":
    proc testPostUrl(): Future[bool] {.async.} =
      var serverRes = false
      proc process(r: RequestFence): Future[HttpResponseRef] {.
           async: (raises: [CancelledError]).} =
        if r.isOk():
          var kres = newSeq[string]()
          let request = r.get()
          if request.meth in PostMethods:
            let post =
              try:
                await request.post()
              except HttpProtocolError as exc:
                return defaultResponse(exc)
              except HttpTransportError as exc:
                return defaultResponse(exc)
            for k, v in post.stringItems():
              kres.add(k & ":" & v)
            sort(kres)
          serverRes = true
          try:
            await request.respond(Http200, "TEST_OK:" & kres.join(":"),
                                  HttpTable.init())
          except HttpWriteError as exc:
            serverRes = false
            defaultResponse(exc)
        else:
          defaultResponse()

      let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
      let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process,
                                  socketFlags = socketFlags)
      if res.isErr():
        return false

      let server = res.get()
      server.start()
      let address = server.instance.localAddress()

      let message =
        "POST / HTTP/1.0\r\n" &
        "Content-Type: application/x-www-form-urlencoded\r\n" &
        "Content-Length: 20\r\n" &
        "Cookie: 2\r\n\r\n" &
        "a=a&b=b&c=c&d=%D0%9F"
      let data = await httpClient(address, message)
      let expect = "TEST_OK:a:a:b:b:c:c:d:П"
      await server.stop()
      await server.closeWait()
      return serverRes and (data.find(expect) >= 0)

    check waitFor(testPostUrl()) == true

  test "POST arguments (urlencoded/chunked encoding) test":
    proc testPostUrl2(): Future[bool] {.async.} =
      var serverRes = false
      proc process(r: RequestFence): Future[HttpResponseRef] {.
           async: (raises: [CancelledError]).} =
        if r.isOk():
          var kres = newSeq[string]()
          let request = r.get()
          if request.meth in PostMethods:
            let post =
              try:
                await request.post()
              except HttpProtocolError as exc:
                return defaultResponse(exc)
              except HttpTransportError as exc:
                return defaultResponse(exc)
            for k, v in post.stringItems():
              kres.add(k & ":" & v)
            sort(kres)
          serverRes = true
          try:
            await request.respond(Http200, "TEST_OK:" & kres.join(":"),
                                  HttpTable.init())
          except HttpWriteError as exc:
            serverRes = false
            defaultResponse(exc)
        else:
          defaultResponse()

      let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
      let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process,
                                  socketFlags = socketFlags)
      if res.isErr():
        return false

      let server = res.get()
      server.start()
      let address = server.instance.localAddress()

      let message =
        "POST / HTTP/1.0\r\n" &
        "Content-Type: application/x-www-form-urlencoded\r\n" &
        "Transfer-Encoding: chunked\r\n" &
        "Cookie: 2\r\n\r\n" &
        "5\r\na=a&b\r\n5\r\n=b&c=\r\n4\r\nc&d=\r\n4\r\n%D0%\r\n" &
        "2\r\n9F\r\n0\r\n\r\n"
      let data = await httpClient(address, message)
      let expect = "TEST_OK:a:a:b:b:c:c:d:П"
      await server.stop()
      await server.closeWait()
      return serverRes and (data.find(expect) >= 0)

    check waitFor(testPostUrl2()) == true

  test "POST arguments (multipart/content-length) test":
    proc testPostMultipart(): Future[bool] {.async.} =
      var serverRes = false
      proc process(r: RequestFence): Future[HttpResponseRef] {.
           async: (raises: [CancelledError]).} =
        if r.isOk():
          var kres = newSeq[string]()
          let request = r.get()
          if request.meth in PostMethods:
            let post =
              try:
                await request.post()
              except HttpProtocolError as exc:
                return defaultResponse(exc)
              except HttpTransportError as exc:
                return defaultResponse(exc)
            for k, v in post.stringItems():
              kres.add(k & ":" & v)
            sort(kres)
          serverRes = true
          try:
            await request.respond(Http200, "TEST_OK:" & kres.join(":"),
                                  HttpTable.init())
          except HttpWriteError as exc:
            serverRes = false
            defaultResponse(exc)
        else:
          defaultResponse()

      let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
      let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process,
                                  socketFlags = socketFlags)
      if res.isErr():
        return false

      let server = res.get()
      server.start()
      let address = server.instance.localAddress()

      let message =
        "POST / HTTP/1.0\r\n" &
        "Host: 127.0.0.1:30080\r\n" &
        "User-Agent: curl/7.55.1\r\n" &
        "Accept: */*\r\n" &
        "Content-Length: 343\r\n" &
        "Content-Type: multipart/form-data; " &
        "boundary=------------------------ab5706ba6f80b795\r\n\r\n" &
        "--------------------------ab5706ba6f80b795\r\n" &
        "Content-Disposition: form-data; name=\"key1\"\r\n\r\n" &
        "value1\r\n" &
        "--------------------------ab5706ba6f80b795\r\n" &
        "Content-Disposition: form-data; name=\"key2\"\r\n\r\n" &
        "value2\r\n" &
        "--------------------------ab5706ba6f80b795\r\n" &
        "Content-Disposition: form-data; name=\"key2\"\r\n\r\n" &
        "value4\r\n" &
        "--------------------------ab5706ba6f80b795--\r\n"
      let data = await httpClient(address, message)
      let expect = "TEST_OK:key1:value1:key2:value2:key2:value4"
      await server.stop()
      await server.closeWait()
      return serverRes and (data.find(expect) >= 0)

    check waitFor(testPostMultipart()) == true

  test "POST arguments (multipart/chunked encoding) test":
    proc testPostMultipart2(): Future[bool] {.async.} =
      var serverRes = false
      proc process(r: RequestFence): Future[HttpResponseRef] {.
           async: (raises: [CancelledError]).} =
        if r.isOk():
          var kres = newSeq[string]()
          let request = r.get()
          if request.meth in PostMethods:
            let post =
              try:
                await request.post()
              except HttpProtocolError as exc:
                return defaultResponse(exc)
              except HttpTransportError as exc:
                return defaultResponse(exc)
            for k, v in post.stringItems():
              kres.add(k & ":" & v)
            sort(kres)
          serverRes = true
          try:
            await request.respond(Http200, "TEST_OK:" & kres.join(":"),
                                  HttpTable.init())
          except HttpWriteError as exc:
            serverRes = false
            defaultResponse(exc)
        else:
          serverRes = false
          defaultResponse()

      let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
      let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process,
                                  socketFlags = socketFlags)
      if res.isErr():
        return false

      let server = res.get()
      server.start()
      let address = server.instance.localAddress()

      let message =
        "POST / HTTP/1.0\r\n" &
        "Host: 127.0.0.1:30080\r\n" &
        "Transfer-Encoding: chunked\r\n" &
        "Content-Type: multipart/form-data; boundary=---" &
        "---------------------f98f0e32c55fa2ae\r\n\r\n" &
        "271\r\n" &
        "--------------------------f98f0e32c55fa2ae\r\n" &
        "Content-Disposition: form-data; name=\"key1\"\r\n\r\n" &
        "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" &
        "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n" &
        "--------------------------f98f0e32c55fa2ae\r\n" &
        "Content-Disposition: form-data; name=\"key2\"\r\n\r\n" &
        "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" &
        "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\r\n" &
        "--------------------------f98f0e32c55fa2ae\r\n" &
        "Content-Disposition: form-data; name=\"key2\"\r\n\r\n" &
        "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" &
        "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\r\n" &
        "--------------------------f98f0e32c55fa2ae--\r\n" &
        "\r\n0\r\n\r\n"

      let data = await httpClient(address, message)
      let expect = "TEST_OK:key1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" &
                   "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" &
                   "AAAAA:key2:BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" &
                   "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" &
                   "BBB:key2:CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" &
                   "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"
      await server.stop()
      await server.closeWait()
      return serverRes and (data.find(expect) >= 0)

    check waitFor(testPostMultipart2()) == true

  test "drop() connections test":
    const ClientsCount = 10

    proc testHTTPdrop(): Future[bool] {.async.} =
      var eventWait = newAsyncEvent()
      var eventContinue = newAsyncEvent()
      var count = 0

      proc process(r: RequestFence): Future[HttpResponseRef] {.
           async: (raises: [CancelledError]).} =
        if r.isOk():
          let request = r.get()
          inc(count)
          if count == ClientsCount:
            eventWait.fire()
          await eventContinue.wait()
          try:
            await request.respond(Http404, "", HttpTable.init())
          except HttpWriteError as exc:
            defaultResponse(exc)
        else:
          defaultResponse()

      let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
      let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process,
                                  socketFlags = socketFlags,
                                  maxConnections = 100)
      if res.isErr():
        return false

      let server = res.get()
      server.start()
      let address = server.instance.localAddress()

      var clients: seq[Future[string]]
      let message = "GET / HTTP/1.0\r\nHost: https://127.0.0.1:80\r\n\r\n"
      for i in 0 ..< ClientsCount:
        var clientFut = httpClient(address, message)
        if clientFut.finished():
          return false
        clients.add(clientFut)
      # Waiting for all clients to connect to the server
      await eventWait.wait()
      # Dropping
      await server.closeWait()
      # We are firing second event to unblock client loops, but this loops
      # must be already cancelled.
      eventContinue.fire()
      # Now all clients should be dropped
      discard await allFutures(clients).withTimeout(1.seconds)
      for item in clients:
        if item.read() != "":
          return false
      return true

    check waitFor(testHTTPdrop()) == true

  test "Content-Type multipart boundary test":
    const AllowedCharacters = {
      'a' .. 'z', 'A' .. 'Z', '0' .. '9',
      '\'', '(', ')', '+', '_', ',', '-', '.' ,'/', ':', '=', '?'
    }

    const FailureVectors = [
      "",
      "multipart/byteranges; boundary=A",
      "multipart/form-data;",
      "multipart/form-data; boundary",
      "multipart/form-data; boundary=",
      "multipart/form-data; boundaryMore=A",
      "multipart/form-data; charset=UTF-8; boundary",
      "multipart/form-data; charset=UTF-8; boundary=",
      "multipart/form-data; charset=UTF-8; boundary =",
      "multipart/form-data; charset=UTF-8; boundary= ",
      "multipart/form-data; charset=UTF-8; boundaryMore=",
      "multipart/form-data; charset=UTF-8; boundaryMore=A",
      "multipart/form-data; charset=UTF-8; boundaryMore=AAAAAAAAAAAAAAAAAAAA" &
      "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
    ]

    const SuccessVectors = [
      ("multipart/form-data; boundary=A", "A"),
      ("multipart/form-data; charset=UTF-8; boundary=B", "B"),
      ("multipart/form-data; charset=UTF-8; boundary=--------------------" &
       "--------------------------------------------------", "-----------" &
       "-----------------------------------------------------------"),
      ("multipart/form-data; boundary=--------------------" &
       "--------------------------------------------------", "-----------" &
       "-----------------------------------------------------------"),
      ("multipart/form-data; boundary=--------------------" &
       "--------------------------------------------------; charset=UTF-8",
       "-----------------------------------------------------------------" &
       "-----"),
      ("multipart/form-data; boundary=\"ABCDEFGHIJKLMNOPQRST" &
       "UVWXYZabcdefghijklmnopqrstuvwxyz0123456789'()+_,-.\"; charset=UTF-8",
       "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'()" &
       "+_,-."),
      ("multipart/form-data; boundary=\"ABCDEFGHIJKLMNOPQRST" &
       "UVWXYZabcdefghijklmnopqrstuvwxyz0123456789'()+?=:/\"; charset=UTF-8",
       "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'()" &
       "+?=:/"),
      ("multipart/form-data; charset=UTF-8; boundary=\"ABCDEFGHIJKLMNOPQRST" &
       "UVWXYZabcdefghijklmnopqrstuvwxyz0123456789'()+_,-.\"",
       "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'()" &
       "+_,-."),
      ("multipart/form-data; charset=UTF-8; boundary=\"ABCDEFGHIJKLMNOPQRST" &
       "UVWXYZabcdefghijklmnopqrstuvwxyz0123456789'()+?=:/\"",
       "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'()" &
       "+?=:/"),
      ("multipart/form-data; charset=UTF-8; boundary=0123456789ABCDEFGHIJKL" &
       "MNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+-",
       "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+-"),
      ("multipart/form-data; boundary=0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZa" &
       "bcdefghijklmnopqrstuvwxyz+-; charset=UTF-8",
       "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+-")
    ]
    proc performCheck(ch: openArray[string]): HttpResult[string] =
      let cdata = ? getContentType(ch)
      if cdata.mediaType != MediaType.init("multipart/form-data"):
        return err("Invalid media type")
      getMultipartBoundary(cdata)

    for i in 0 ..< 256:
      let boundary = "multipart/form-data; boundary=\"" & $char(i) & "\""
      if char(i) in AllowedCharacters:
        check performCheck([boundary]).isOk()
      else:
        check performCheck([boundary]).isErr()

    check:
      performCheck([]).isErr()
      performCheck(["multipart/form-data; boundary=A",
                    "multipart/form-data; boundary=B"]).isErr()
    for item in FailureVectors:
      check performCheck([item]).isErr()
    for item in SuccessVectors:
      let res = performCheck([item[0]])
      check:
        res.isOk()
        item[1] == res.get()

  test "HttpTable integer parser test":
    const TestVectors = [
      ("", 0'u64), ("0", 0'u64), ("-0", 0'u64), ("0-", 0'u64),
      ("01", 1'u64), ("001", 1'u64), ("0000000000001", 1'u64),
      ("18446744073709551615", 0xFFFF_FFFF_FFFF_FFFF'u64),
      ("18446744073709551616", 0'u64),
      ("99999999999999999999", 0'u64),
      ("999999999999999999999999999999999999", 0'u64),
      ("FFFFFFFFFFFFFFFF", 0'u64),
      ("0123456789ABCDEF", 0'u64)
    ]
    for i in 0 ..< 256:
      let res = Base10.decode(uint64, [char(i)])
      if char(i) in {'0' .. '9'}:
        check:
          res.isOk()
          res.get() == uint64(i - ord('0'))
      else:
        check res.isErr()

    for item in TestVectors:
      var ht = HttpTable.init([("test", item[0])])
      let value = ht.getInt("test")
      check value == item[1]

  test "HttpTable behavior test":
    var table1 = HttpTable.init()
    var table2 = HttpTable.init([("Header1", "value1"), ("Header2", "value2")])
    check:
      table1.isEmpty() == true
      table2.isEmpty() == false

    table1.add("Header1", "value1")
    table1.add("Header2", "value2")
    table1.add("HEADER2", "VALUE3")
    check:
      table1.getList("HeAdEr2") == @["value2", "VALUE3"]
      table1.getString("HeAdEr2") == "value2,VALUE3"
      table2.getString("HEADER1") == "value1"
      table1.count("HEADER2") == 2
      table1.count("HEADER1") == 1
      table1.getLastString("HEADER1") == "value1"
      table1.getLastString("HEADER2") == "VALUE3"
      "header1" in table1 == true
      "HEADER1" in table1 == true
      "header2" in table1 == true
      "HEADER2" in table1 == true
      "HEADER3" in table1 == false

    var
      data1: seq[tuple[key: string, value: string]]
      data2: seq[tuple[key: string, value: seq[string]]]
    for key, value in table1.stringItems(true):
      data1.add((key, value))
    for key, value in table1.items(true):
      data2.add((key, value))

    check:  # .sorted to not depend upon hash(key)-order
      data1.sorted == sorted(@[("Header2", "value2"), ("Header2", "VALUE3"),
                               ("Header1", "value1")])
      data2.sorted == sorted(@[("Header2", @["value2", "VALUE3"]),
                               ("Header1", @["value1"])])

    table1.set("header2", "value4")
    check:
      table1.getList("header2") == @["value4"]
      table1.getString("header2") == "value4"
      table1.count("header2") == 1
      table1.getLastString("header2") == "value4"

  test "getTransferEncoding() test":
    var encodings = [
      "chunked", "compress", "deflate", "gzip", "identity", "x-gzip"
    ]

    const FlagsVectors = [
      {
        TransferEncodingFlags.Identity, TransferEncodingFlags.Chunked,
        TransferEncodingFlags.Compress, TransferEncodingFlags.Deflate,
        TransferEncodingFlags.Gzip
      },
      {
        TransferEncodingFlags.Identity, TransferEncodingFlags.Compress,
        TransferEncodingFlags.Deflate, TransferEncodingFlags.Gzip
      },
      {
        TransferEncodingFlags.Identity, TransferEncodingFlags.Deflate,
        TransferEncodingFlags.Gzip
      },
      { TransferEncodingFlags.Identity, TransferEncodingFlags.Gzip },
      { TransferEncodingFlags.Identity, TransferEncodingFlags.Gzip },
      { TransferEncodingFlags.Gzip },
      { TransferEncodingFlags.Identity }
    ]

    for i in 0 ..< 7:
      var checkEncodings = @encodings
      if i - 1 >= 0:
        for k in 0 .. (i - 1):
          checkEncodings.delete(0)

      while nextPermutation(checkEncodings):
        let res1 = getTransferEncoding([checkEncodings.join(", ")])
        let res2 = getTransferEncoding([checkEncodings.join(",")])
        let res3 = getTransferEncoding([checkEncodings.join("")])
        let res4 = getTransferEncoding([checkEncodings.join(" ")])
        let res5 = getTransferEncoding([checkEncodings.join(" , ")])
        check:
          res1.isOk()
          res1.get() == FlagsVectors[i]
          res2.isOk()
          res2.get() == FlagsVectors[i]
          res3.isErr()
          res4.isErr()
          res5.isOk()
          res5.get() == FlagsVectors[i]

    check:
      getTransferEncoding([]).tryGet() == { TransferEncodingFlags.Identity }
      getTransferEncoding(["", ""]).tryGet() ==
        { TransferEncodingFlags.Identity }

  test "getContentEncoding() test":
    var encodings = [
      "br", "compress", "deflate", "gzip", "identity", "x-gzip"
    ]

    const FlagsVectors = [
      {
        ContentEncodingFlags.Identity, ContentEncodingFlags.Br,
        ContentEncodingFlags.Compress, ContentEncodingFlags.Deflate,
        ContentEncodingFlags.Gzip
      },
      {
        ContentEncodingFlags.Identity, ContentEncodingFlags.Compress,
        ContentEncodingFlags.Deflate, ContentEncodingFlags.Gzip
      },
      {
        ContentEncodingFlags.Identity, ContentEncodingFlags.Deflate,
        ContentEncodingFlags.Gzip
      },
      { ContentEncodingFlags.Identity, ContentEncodingFlags.Gzip },
      { ContentEncodingFlags.Identity, ContentEncodingFlags.Gzip },
      { ContentEncodingFlags.Gzip },
      { ContentEncodingFlags.Identity }
    ]

    for i in 0 ..< 7:
      var checkEncodings = @encodings
      if i - 1 >= 0:
        for k in 0 .. (i - 1):
          checkEncodings.delete(0)

      while nextPermutation(checkEncodings):
        let res1 = getContentEncoding([checkEncodings.join(", ")])
        let res2 = getContentEncoding([checkEncodings.join(",")])
        let res3 = getContentEncoding([checkEncodings.join("")])
        let res4 = getContentEncoding([checkEncodings.join(" ")])
        let res5 = getContentEncoding([checkEncodings.join(" , ")])
        check:
          res1.isOk()
          res1.get() == FlagsVectors[i]
          res2.isOk()
          res2.get() == FlagsVectors[i]
          res3.isErr()
          res4.isErr()
          res5.isOk()
          res5.get() == FlagsVectors[i]

    check:
      getContentEncoding([]).tryGet() == { ContentEncodingFlags.Identity }
      getContentEncoding(["", ""]).tryGet() == { ContentEncodingFlags.Identity }

  test "queryParams() test":
    const Vectors = [
      ("id=1&id=2&id=3&id=4", {}, "id:1,id:2,id:3,id:4"),
      ("id=1,2,3,4", {}, "id:1,2,3,4"),
      ("id=1%2C2%2C3%2C4", {}, "id:1,2,3,4"),
      ("id=", {}, "id:"),
      ("id=&id=", {}, "id:,id:"),
      ("id=1&id=2&id=3&id=4", {QueryParamsFlag.CommaSeparatedArray},
       "id:1,id:2,id:3,id:4"),
      ("id=1,2,3,4", {QueryParamsFlag.CommaSeparatedArray},
       "id:1,id:2,id:3,id:4"),
      ("id=1%2C2%2C3%2C4", {QueryParamsFlag.CommaSeparatedArray},
       "id:1,id:2,id:3,id:4"),
      ("id=", {QueryParamsFlag.CommaSeparatedArray}, "id:"),
      ("id=&id=", {QueryParamsFlag.CommaSeparatedArray}, "id:,id:"),
      ("id=,", {QueryParamsFlag.CommaSeparatedArray}, "id:,id:"),
      ("id=,,", {QueryParamsFlag.CommaSeparatedArray}, "id:,id:,id:"),
      ("id=1&id=2&id=3,4,5,6&id=7%2C8%2C9%2C10",
       {QueryParamsFlag.CommaSeparatedArray},
       "id:1,id:2,id:3,id:4,id:5,id:6,id:7,id:8,id:9,id:10")
    ]

    proc toString(ht: HttpTable): string =
      var res: seq[string]
      for key, value in ht.items():
        for item in value:
          res.add(key & ":" & item)
      res.join(",")

    for vector in Vectors:
      var table = HttpTable.init()
      for key, value in queryParams(vector[0], vector[1]):
        table.add(key, value)
      check toString(table) == vector[2]

  test "preferredContentType() test":
    const
      jsonMediaType = MediaType.init("application/json")
      sszMediaType = MediaType.init("application/octet-stream")
      plainTextMediaType = MediaType.init("text/plain")
      imageMediaType = MediaType.init("image/jpg")

    proc createRequest(acceptHeader: string): HttpRequestRef =
      let headers = HttpTable.init([("accept", acceptHeader)])
      HttpRequestRef(headers: headers)

    proc createRequest(): HttpRequestRef =
      HttpRequestRef(headers: HttpTable.init())

    var singleHeader = @[
      (
        createRequest("application/json"),
        @[
          "application/json"
        ]
      )
    ]

    var complexHeaders = @[
      (
        createRequest(),
        @[
          "*/*",
          "application/json",
          "application/octet-stream",
          "application/json",
          "application/octet-stream",
          "application/json",
          "image/jpg"
        ]
      ),
      (
        createRequest(""),
        @[
          "*/*",
          "application/json",
          "application/octet-stream",
          "application/json",
          "application/octet-stream",
          "application/json",
          "image/jpg"
        ]
      ),
      (
        createRequest("application/json, application/octet-stream"),
        @[
          "application/json",
          "application/json",
          "application/octet-stream",
          "application/json",
          "application/octet-stream",
          "application/json",
          "application/json"
        ]
      ),
      (
        createRequest("application/octet-stream, application/json"),
        @[
          "application/octet-stream",
          "application/json",
          "application/octet-stream",
          "application/json",
          "application/octet-stream",
          "application/json",
          "application/json"
        ]
      ),
      (
        createRequest("application/json;q=0.9, application/octet-stream"),
        @[
          "application/octet-stream",
          "application/json",
          "application/octet-stream",
          "application/octet-stream",
          "application/octet-stream",
          "application/octet-stream",
          "application/octet-stream"
        ]
      ),
      (
        createRequest("application/json, application/octet-stream;q=0.9"),
        @[
          "application/json",
          "application/json",
          "application/octet-stream",
          "application/json",
          "application/json",
          "application/json",
          "application/json"
        ]
      ),
      (
        createRequest("application/json;q=0.9, application/octet-stream;q=0.8"),
        @[
          "application/json",
          "application/json",
          "application/octet-stream",
          "application/json",
          "application/json",
          "application/json",
          "application/json"
        ]
      ),
       (
        createRequest("application/json;q=0.8, application/octet-stream;q=0.9"),
        @[
          "application/octet-stream",
          "application/json",
          "application/octet-stream",
          "application/octet-stream",
          "application/octet-stream",
          "application/octet-stream",
          "application/octet-stream"
        ]
      ),
      (
      createRequest("text/plain, application/octet-stream, application/json"),
        @[
          "text/plain",
          "application/json",
          "application/octet-stream",
          "application/json",
          "application/octet-stream",
          "application/json",
          "application/json"
        ]
      ),
      (
      createRequest("text/plain, application/json;q=0.8, " &
                    "application/octet-stream;q=0.8"),
        @[
          "text/plain",
          "application/json",
          "application/octet-stream",
          "application/json",
          "application/octet-stream",
          "text/plain",
          "text/plain"
        ]
      ),
      (
      createRequest("text/plain, application/json;q=0.8, " &
                    "application/octet-stream;q=0.5"),
        @[
          "text/plain",
          "application/json",
          "application/octet-stream",
          "application/json",
          "application/json",
          "text/plain",
          "text/plain"
        ]
      ),
      (
       createRequest("text/plain;q=0.8, application/json, " &
                     "application/octet-stream;q=0.8"),
        @[
          "application/json",
          "application/json",
          "application/octet-stream",
          "application/json",
          "application/json",
          "application/json",
          "application/json"
        ]
      ),
      (
      createRequest("text/*, application/json;q=0.8, " &
                    "application/octet-stream;q=0.8"),
        @[
          "text/*",
          "application/json",
          "application/octet-stream",
          "application/json",
          "application/octet-stream",
          "text/plain",
          "text/plain"
        ]
      ),
      (
      createRequest("text/*, application/json;q=0.8, " &
                    "application/octet-stream;q=0.5"),
        @[
          "text/*",
          "application/json",
          "application/octet-stream",
          "application/json",
          "application/json",
          "text/plain",
          "text/plain"
        ]
      ),
      (createRequest("image/jpg, text/plain, application/octet-stream, " &
                     "application/json"),
         @[
            "image/jpg",
            "application/json",
            "application/octet-stream",
            "application/json",
            "application/octet-stream",
            "application/json",
            "image/jpg"
           ]
        ),
        (createRequest("image/jpg;q=1, text/plain;q=0.2, " &
                       "application/octet-stream;q=0.2, " &
                       "application/json;q=0.2"),
         @[
            "image/jpg",
            "application/json",
            "application/octet-stream",
            "application/json",
            "application/octet-stream",
            "application/json",
            "image/jpg"
           ]
        ),
      (
      createRequest("*/*, application/json;q=0.8, " &
                    "application/octet-stream;q=0.5"),
        @[
          "*/*",
          "application/json",
          "application/octet-stream",
          "application/json",
          "application/octet-stream",
          "application/json",
          "image/jpg"
        ]
      ),
      (
        createRequest("*/*"),
        @[
          "*/*",
          "application/json",
          "application/octet-stream",
          "application/json",
          "application/octet-stream",
          "application/json",
          "image/jpg"
        ]
      ),
      (
        createRequest("application/*"),
        @[
          "application/*",
          "application/json",
          "application/octet-stream",
          "application/json",
          "application/octet-stream",
          "application/json",
          "application/json"
        ]
      )
    ]

    for req in singleHeader:
      check $req[0].preferredContentMediaType() == req[1][0]
      let r0 = req[0].preferredContentType()
      let r1 = req[0].preferredContentType(jsonMediaType)
      let r2 = req[0].preferredContentType(sszMediaType)
      let r3 = req[0].preferredContentType(jsonMediaType,
                                           sszMediaType)
      let r4 = req[0].preferredContentType(sszMediaType,
                                           jsonMediaType)
      let r5 = req[0].preferredContentType(jsonMediaType,
                                           sszMediaType,
                                           plainTextMediaType)
      let r6 = req[0].preferredContentType(imageMediaType,
                                           jsonMediaType,
                                           sszMediaType,
                                           plainTextMediaType)
      check:
        r0.isOk() == true
        r1.isOk() == true
        r2.isErr() == true
        r3.isOk() == true
        r4.isOk() == true
        r5.isOk() == true
        r6.isOk() == true
        r0.get() == MediaType.init(req[1][0])
        r1.get() == MediaType.init(req[1][0])
        r3.get() == MediaType.init(req[1][0])
        r4.get() == MediaType.init(req[1][0])
        r5.get() == MediaType.init(req[1][0])
        r6.get() == MediaType.init(req[1][0])

    for req in complexHeaders:
      let r0 = req[0].preferredContentType()
      let r1 = req[0].preferredContentType(jsonMediaType)
      let r2 = req[0].preferredContentType(sszMediaType)
      let r3 = req[0].preferredContentType(jsonMediaType,
                                           sszMediaType)
      let r4 = req[0].preferredContentType(sszMediaType,
                                           jsonMediaType)
      let r5 = req[0].preferredContentType(jsonMediaType,
                                           sszMediaType,
                                           plainTextMediaType)
      let r6 = req[0].preferredContentType(imageMediaType,
                                           jsonMediaType,
                                           sszMediaType,
                                           plainTextMediaType)
      check:
        r0.isOk() == true
        r1.isOk() == true
        r2.isOk() == true
        r3.isOk() == true
        r4.isOk() == true
        r5.isOk() == true
        r6.isOk() == true
        r0.get() == MediaType.init(req[1][0])
        r1.get() == MediaType.init(req[1][1])
        r2.get() == MediaType.init(req[1][2])
        r3.get() == MediaType.init(req[1][3])
        r4.get() == MediaType.init(req[1][4])
        r5.get() == MediaType.init(req[1][5])
        r6.get() == MediaType.init(req[1][6])

  test "SSE server-side events stream test":
    proc testPostMultipart2(): Future[bool] {.async.} =
      var serverRes = false
      proc process(r: RequestFence): Future[HttpResponseRef] {.
           async: (raises: [CancelledError]).} =
        if r.isOk():
          let request = r.get()
          let response = request.getResponse()
          try:
            await response.prepareSSE()
            await response.send("event: event1\r\ndata: data1\r\n\r\n")
            await response.send("event: event2\r\ndata: data2\r\n\r\n")
            await response.sendEvent("event3", "data3")
            await response.sendEvent("event4", "data4")
            await response.send("data: data5\r\n\r\n")
            await response.sendEvent("", "data6")
            await response.finish()
            serverRes = true
            response
          except HttpWriteError as exc:
            serverRes = false
            defaultResponse(exc)
        else:
          defaultResponse()

      let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
      let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process,
                                  socketFlags = socketFlags)
      if res.isErr():
        return false

      let server = res.get()
      server.start()
      let address = server.instance.localAddress()

      let message =
        "GET / HTTP/1.1\r\n" &
        "Host: 127.0.0.1:30080\r\n" &
        "Accept: text/event-stream\r\n" &
        "\r\n"

      let data = await httpClient(address, message)
      let expect = "event: event1\r\ndata: data1\r\n\r\n" &
                   "event: event2\r\ndata: data2\r\n\r\n" &
                   "event: event3\r\ndata: data3\r\n\r\n" &
                   "event: event4\r\ndata: data4\r\n\r\n" &
                   "data: data5\r\n\r\n" &
                   "data: data6\r\n\r\n"
      await server.stop()
      await server.closeWait()
      return serverRes and (data.find(expect) >= 0)

    check waitFor(testPostMultipart2()) == true

  asyncTest "HTTP/1.1 pipeline test":
    const TestMessages = [
      ("GET / HTTP/1.0\r\n\r\n",
       {HttpServerFlags.Http11Pipeline}, false, "close"),
      ("GET / HTTP/1.0\r\nConnection: close\r\n\r\n",
       {HttpServerFlags.Http11Pipeline}, false, "close"),
      ("GET / HTTP/1.0\r\nConnection: keep-alive\r\n\r\n",
       {HttpServerFlags.Http11Pipeline}, false, "close"),
      ("GET / HTTP/1.0\r\n\r\n",
       {}, false, "close"),
      ("GET / HTTP/1.0\r\nConnection: close\r\n\r\n",
       {}, false, "close"),
      ("GET / HTTP/1.0\r\nConnection: keep-alive\r\n\r\n",
       {}, false, "close"),
      ("GET / HTTP/1.1\r\n\r\n",
       {HttpServerFlags.Http11Pipeline}, true, "keep-alive"),
      ("GET / HTTP/1.1\r\nConnection: close\r\n\r\n",
       {HttpServerFlags.Http11Pipeline}, false, "close"),
      ("GET / HTTP/1.1\r\nConnection: keep-alive\r\n\r\n",
       {HttpServerFlags.Http11Pipeline}, true, "keep-alive"),
      ("GET / HTTP/1.1\r\n\r\n",
       {}, false, "close"),
      ("GET / HTTP/1.1\r\nConnection: close\r\n\r\n",
       {}, false, "close"),
      ("GET / HTTP/1.1\r\nConnection: keep-alive\r\n\r\n",
       {}, false, "close")
    ]

    proc process(r: RequestFence): Future[HttpResponseRef] {.
         async: (raises: [CancelledError]).} =
      if r.isOk():
        let request = r.get()
        try:
          await request.respond(Http200, "TEST_OK", HttpTable.init())
        except HttpWriteError as exc:
          defaultResponse(exc)
      else:
        defaultResponse()

    for test in TestMessages:
      let
        socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
        serverFlags = test[1]
        res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process,
                                socketFlags = socketFlags,
                                serverFlags = serverFlags)
      check res.isOk()

      let
        server = res.get()
        address = server.instance.localAddress()

      server.start()
      var transp: StreamTransport

      transp = await connect(address)
      block:
        let response = await transp.httpClient2(test[0], 7)
        check:
          response.data == "TEST_OK"
          response.headers.getString("connection") == test[3]
      # We do this sleeping here just because we running both server and
      # client in single process, so when we received response from server
      # it does not mean that connection has been immediately closed - it
      # takes some more calls, so we trying to get this calls happens.
      await sleepAsync(50.milliseconds)
      let connectionStillAvailable =
        try:
          let response {.used.} = await transp.httpClient2(test[0], 7)
          true
        except CatchableError:
          false

      check connectionStillAvailable == test[2]

      if not(isNil(transp)):
        await transp.closeWait()
      await server.stop()
      await server.closeWait()

  asyncTest "HTTP debug tests":
    const
      TestsCount = 10
      TestRequest = "GET /httpdebug HTTP/1.1\r\nConnection: keep-alive\r\n\r\n"

    proc process(r: RequestFence): Future[HttpResponseRef] {.
         async: (raises: [CancelledError]).} =
      if r.isOk():
        let request = r.get()
        try:
          await request.respond(Http200, "TEST_OK", HttpTable.init())
        except HttpWriteError as exc:
          defaultResponse(exc)
      else:
        defaultResponse()

    proc client(address: TransportAddress,
                data: string): Future[StreamTransport] {.async.} =
      var transp: StreamTransport
      var buffer = newSeq[byte](4096)
      var sep = @[0x0D'u8, 0x0A'u8, 0x0D'u8, 0x0A'u8]
      try:
        transp = await connect(address)
        let wres {.used.} =
          await transp.write(data)
        let hres {.used.} =
          await transp.readUntil(addr buffer[0], len(buffer), sep)
        transp
      except CatchableError:
        if not(isNil(transp)): await transp.closeWait()
        nil

    let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
    let res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process,
                                serverFlags = {HttpServerFlags.Http11Pipeline},
                                socketFlags = socketFlags)
    check res.isOk()

    let server = res.get()
    server.start()
    let address = server.instance.localAddress()

    let info = server.getServerInfo()

    check:
      info.connectionType == ConnectionType.NonSecure
      info.address == address
      info.state == HttpServerState.ServerRunning
      info.flags == {HttpServerFlags.Http11Pipeline}
      info.socketFlags == socketFlags

    var clientFutures: seq[Future[StreamTransport]]
    for i in 0 ..< TestsCount:
      clientFutures.add(client(address, TestRequest))
    await allFutures(clientFutures)

    let connections = server.getConnections()
    check len(connections) == TestsCount
    let currentTime = Moment.now()
    for index, connection in connections.pairs():
      let transp = clientFutures[index].read()
      check:
        connection.remoteAddress.get() == transp.localAddress()
        connection.localAddress.get() == transp.remoteAddress()
        connection.connectionType == ConnectionType.NonSecure
        connection.connectionState == ConnectionState.Alive
        connection.query.get("") == "/httpdebug"
        (currentTime - connection.createMoment.get()) != ZeroDuration
        (currentTime - connection.acceptMoment) != ZeroDuration
    var pending: seq[Future[void]]
    for transpFut in clientFutures:
      pending.add(closeWait(transpFut.read()))
    await allFutures(pending)
    await server.stop()
    await server.closeWait()

  asyncTest "HTTP middleware request filtering test":
    proc init(t: typedesc[FirstMiddlewareRef],
              data: int): HttpServerMiddlewareRef =
      proc shandler(
          middleware: HttpServerMiddlewareRef,
          reqfence: RequestFence,
          nextHandler: HttpProcessCallback2
      ): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} =
        let mw = FirstMiddlewareRef(middleware)
        if reqfence.isErr():
          # Our handler is not supposed to handle request errors, so we
          # call next handler in sequence which could process errors.
          return await nextHandler(reqfence)

        let request = reqfence.get()
        if request.uri.path == "/first":
          # This is request we are waiting for, so we going to process it.
          try:
            await request.respond(Http200, $mw.someInteger)
          except HttpWriteError as exc:
            defaultResponse(exc)
        else:
          # We know nothing about request's URI, so we pass this request to the
          # next handler which could process such request.
          await nextHandler(reqfence)

      HttpServerMiddlewareRef(
        FirstMiddlewareRef(someInteger: data, handler: shandler))

    proc init(t: typedesc[SecondMiddlewareRef],
              data: string): HttpServerMiddlewareRef =
      proc shandler(
          middleware: HttpServerMiddlewareRef,
          reqfence: RequestFence,
          nextHandler: HttpProcessCallback2
      ): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} =
        let mw = SecondMiddlewareRef(middleware)
        if reqfence.isErr():
          # Our handler is not supposed to handle request errors, so we
          # call next handler in sequence which could process errors.
          return await nextHandler(reqfence)

        let request = reqfence.get()

        if request.uri.path == "/second":
          # This is request we are waiting for, so we going to process it.
          try:
            await request.respond(Http200, mw.someString)
          except HttpWriteError as exc:
            defaultResponse(exc)
        else:
          # We know nothing about request's URI, so we pass this request to the
          # next handler which could process such request.
          await nextHandler(reqfence)

      HttpServerMiddlewareRef(
        SecondMiddlewareRef(someString: data, handler: shandler))

    proc process(r: RequestFence): Future[HttpResponseRef] {.
         async: (raises: [CancelledError]).} =
      if r.isOk():
        let request = r.get()
        if request.uri.path == "/test":
          try:
            await request.respond(Http200, "ORIGIN")
          except HttpWriteError as exc:
            defaultResponse(exc)
        else:
          defaultResponse()
      else:
        defaultResponse()

    let
      middlewares = [FirstMiddlewareRef.init(655370),
                     SecondMiddlewareRef.init("SECOND")]
      socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
      res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process,
                              socketFlags = socketFlags,
                              middlewares = middlewares)
    check res.isOk()

    let server = res.get()
    server.start()
    let
      address = server.instance.localAddress()
      req1 = "GET /test HTTP/1.1\r\n\r\n"
      req2 = "GET /first HTTP/1.1\r\n\r\n"
      req3 = "GET /second HTTP/1.1\r\n\r\n"
      req4 = "GET /noway HTTP/1.1\r\n\r\n"
      resp1 = await httpClient3(address, req1)
      resp2 = await httpClient3(address, req2)
      resp3 = await httpClient3(address, req3)
      resp4 = await httpClient3(address, req4)

    check:
      resp1.status == 200
      resp1.data == "ORIGIN"
      resp2.status == 200
      resp2.data == "655370"
      resp3.status == 200
      resp3.data == "SECOND"
      resp4.status == 404

    await server.stop()
    await server.closeWait()

  asyncTest "HTTP middleware request modification test":
    proc init(t: typedesc[FirstMiddlewareRef],
              data: int): HttpServerMiddlewareRef =
      proc shandler(
          middleware: HttpServerMiddlewareRef,
          reqfence: RequestFence,
          nextHandler: HttpProcessCallback2
      ): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} =
        let mw = FirstMiddlewareRef(middleware)
        if reqfence.isErr():
          # Our handler is not supposed to handle request errors, so we
          # call next handler in sequence which could process errors.
          return await nextHandler(reqfence)

        let
          request = reqfence.get()
          modifiedUri = "/modified/" & $mw.someInteger & request.rawPath
        var modifiedHeaders = request.headers
        modifiedHeaders.add("X-Modified", "test-value")

        let res = request.updateRequest(modifiedUri, modifiedHeaders)
        if res.isErr():
          return defaultResponse(res.error)

        # We sending modified request to the next handler.
        await nextHandler(reqfence)

      HttpServerMiddlewareRef(
        FirstMiddlewareRef(someInteger: data, handler: shandler))

    proc process(r: RequestFence): Future[HttpResponseRef] {.
         async: (raises: [CancelledError]).} =
      if r.isOk():
        let request = r.get()
        try:
          await request.respond(Http200, request.rawPath & ":" &
                                request.headers.getString("x-modified"))
        except HttpWriteError as exc:
          defaultResponse(exc)
      else:
        defaultResponse()

    let
      middlewares = [FirstMiddlewareRef.init(655370)]
      socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
      res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process,
                              socketFlags = socketFlags,
                              middlewares = middlewares)
    check res.isOk()

    let server = res.get()
    server.start()
    let
      address = server.instance.localAddress()
      req1 = "GET /test HTTP/1.1\r\n\r\n"
      req2 = "GET /first HTTP/1.1\r\n\r\n"
      req3 = "GET /second HTTP/1.1\r\n\r\n"
      req4 = "GET /noway HTTP/1.1\r\n\r\n"
      resp1 = await httpClient3(address, req1)
      resp2 = await httpClient3(address, req2)
      resp3 = await httpClient3(address, req3)
      resp4 = await httpClient3(address, req4)

    check:
      resp1.status == 200
      resp1.data == "/modified/655370/test:test-value"
      resp2.status == 200
      resp2.data == "/modified/655370/first:test-value"
      resp3.status == 200
      resp3.data == "/modified/655370/second:test-value"
      resp4.status == 200
      resp4.data == "/modified/655370/noway:test-value"

    await server.stop()
    await server.closeWait()

  asyncTest "HTTP middleware request blocking test":
    proc init(t: typedesc[FirstMiddlewareRef],
              data: int): HttpServerMiddlewareRef =
      proc shandler(
          middleware: HttpServerMiddlewareRef,
          reqfence: RequestFence,
          nextHandler: HttpProcessCallback2
      ): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} =
        if reqfence.isErr():
          # Our handler is not supposed to handle request errors, so we
          # call next handler in sequence which could process errors.
          return await nextHandler(reqfence)

        let request = reqfence.get()
        if request.uri.path == "/first":
          # Blocking request by disconnecting remote peer.
          dropResponse()
        elif request.uri.path == "/second":
          # Blocking request by sending HTTP error message with 401 code.
          codeResponse(Http401)
        else:
          # Allow all other requests to be processed by next handler.
          await nextHandler(reqfence)

      HttpServerMiddlewareRef(
        FirstMiddlewareRef(someInteger: data, handler: shandler))

    proc process(r: RequestFence): Future[HttpResponseRef] {.
         async: (raises: [CancelledError]).} =
      if r.isOk():
        let request = r.get()
        try:
          await request.respond(Http200, "ORIGIN")
        except HttpWriteError as exc:
          defaultResponse(exc)
      else:
        defaultResponse()

    let
      middlewares = [FirstMiddlewareRef.init(655370)]
      socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
      res = HttpServerRef.new(initTAddress("127.0.0.1:0"), process,
                              socketFlags = socketFlags,
                              middlewares = middlewares)
    check res.isOk()

    let server = res.get()
    server.start()
    let
      address = server.instance.localAddress()
      req1 = "GET /test HTTP/1.1\r\n\r\n"
      req2 = "GET /first HTTP/1.1\r\n\r\n"
      req3 = "GET /second HTTP/1.1\r\n\r\n"
      resp1 = await httpClient3(address, req1)
      resp3 = await httpClient3(address, req3)

    check:
      resp1.status == 200
      resp1.data == "ORIGIN"
      resp3.status == 401

    let checked =
      try:
        let res {.used.} = await httpClient3(address, req2)
        false
      except TransportIncompleteError:
        true

    check:
      checked == true

    await server.stop()
    await server.closeWait()

  asyncTest "HTTP server - baseUri value test":
    proc process(r: RequestFence): Future[HttpResponseRef] {.
         async: (raises: [CancelledError]).} =
      defaultResponse()

    let
      expectUri2 = "http://www.chronos-test.com/"
      address = initTAddress("127.0.0.1:0")
      socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
      res1 = HttpServerRef.new(address, process,
                               socketFlags = socketFlags)
      res2 = HttpServerRef.new(address, process,
                               socketFlags = socketFlags,
                               serverUri = parseUri(expectUri2))
    check:
      res1.isOk == true
      res2.isOk == true

    let
      server1 = res1.get()
      server2 = res2.get()

    try:
      server1.start()
      server2.start()
      let
        localAddress = server1.instance.localAddress()
        expectUri1 = "http://127.0.0.1:" & $localAddress.port & "/"
      check:
        server1.baseUri == parseUri(expectUri1)
        server2.baseUri == parseUri(expectUri2)
    finally:
      await server1.stop()
      await server1.closeWait()
      await server2.stop()
      await server2.closeWait()
