/*
 * Copyright 2021 - 2023 Sporta Technologies PVT LTD & the ZIO HTTP contributors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package zio.http.endpoint

import scala.annotation.nowarn

import zio._
import zio.test._

import zio.stream.ZStream

import zio.schema.{DeriveSchema, Schema}

import zio.http.Header.ContentType
import zio.http.Method._
import zio.http._
import zio.http.codec._
import zio.http.endpoint.EndpointSpec.{extractStatus, testEndpoint, testEndpointWithHeaders}

@nowarn("msg=possible missing interpolator")
@nowarn("msg=dead code")
object RequestSpec extends ZIOHttpSpec {
  def spec = suite("RequestSpec")(
    suite("handler")(
      test("simple request with header") {
        check(Gen.int, Gen.int, Gen.uuid) { (userId, postId, correlationId) =>
          val testRoutes = testEndpointWithHeaders(
            Routes(
              Endpoint(GET / "users" / int("userId"))
                .header(HeaderCodec.headerAs[java.util.UUID]("X-Correlation-ID"))
                .out[String]
                .implementHandler {
                  Handler.fromFunction { case (userId, correlationId) =>
                    s"path(users, $userId) header(correlationId=$correlationId)"
                  }
                },
              Endpoint(GET / "users" / int("userId") / "posts" / int("postId"))
                .header[java.util.UUID]("X-Correlation-ID")
                .out[String]
                .implementHandler {
                  Handler.fromFunction { case (userId, postId, correlationId) =>
                    s"path(users, $userId, posts, $postId) header(correlationId=$correlationId)"
                  }
                },
            ),
          ) _
          testRoutes(
            s"/users/$userId",
            List("X-Correlation-ID" -> correlationId.toString),
            s"path(users, $userId) header(correlationId=$correlationId)",
          ) &&
          testRoutes(
            s"/users/$userId/posts/$postId",
            List("X-Correlation-ID" -> correlationId.toString),
            s"path(users, $userId, posts, $postId) header(correlationId=$correlationId)",
          )
        }
      },
      test("simple request with header with multiple values") {
        check(Gen.int, Gen.listOfN(3)(Gen.uuid)) { (userId, correlationId) =>
          val testRoutes = testEndpointWithHeaders(
            Routes(
              Endpoint(GET / "users" / int("userId"))
                .header(HeaderCodec.headerAs[Chunk[java.util.UUID]]("X-Correlation-ID"))
                .out[String]
                .implementHandler {
                  Handler.fromFunction { case (userId, correlationId) =>
                    s"path(users, $userId) header(correlationId=${correlationId.mkString(",")})"
                  }
                },
            ),
          ) _
          testRoutes(
            s"/users/$userId",
            correlationId.map(uuid => "X-Correlation-ID" -> uuid.toString),
            s"path(users, $userId) header(correlationId=${correlationId.mkString(",")})",
          )
        }
      },
      test("simple request with header with multiple values non empty") {
        check(Gen.int, Gen.listOfN(3)(Gen.uuid)) { (userId, correlationId) =>
          val testRoutes = testEndpointWithHeaders(
            Routes(
              Endpoint(GET / "users" / int("userId"))
                .header(HeaderCodec.headerAs[NonEmptyChunk[java.util.UUID]]("X-Correlation-ID"))
                .out[String]
                .implementHandler {
                  Handler.fromFunction { case (userId, correlationId) =>
                    s"path(users, $userId) header(correlationId=${correlationId.mkString(",")})"
                  }
                },
            ),
          ) _
          testRoutes(
            s"/users/$userId",
            correlationId.map(uuid => "X-Correlation-ID" -> uuid.toString),
            s"path(users, $userId) header(correlationId=${correlationId.mkString(",")})",
          )
        }
      },
      test("custom content type") {
        check(Gen.int) { id =>
          val endpoint =
            Endpoint(GET / "posts")
              .query(HttpCodec.query[String]("id"))
              .out[Int](MediaType.text.`plain`)
          val routes   =
            endpoint.implementHandler {
              Handler.succeed(id)
            }

          for {
            response <- routes.toRoutes.runZIO(
              Request
                .get(URL.decode(s"/posts?id=$id").toOption.get)
                .addHeader(Header.Accept(MediaType.text.`plain`)),
            )
            contentType = response.header(Header.ContentType)
          } yield assertTrue(extractStatus(response).code == 200) &&
            assertTrue(contentType == Some(ContentType(MediaType.text.`plain`)))
        }
      },
      test("custom out header") {
        val endpoint =
          Endpoint(GET / "posts")
            .out[String](MediaType.text.plain)
            .outHeader(HttpCodec.age)
        val routes   = endpoint.implementAs(("test", Header.Age(1.minute))).toRoutes
        for {
          response <- routes.runZIO(Request.get(URL.decode("/posts").toOption.get))
          age = response.header(Header.Age)
          body <- response.body.asString.orDie
        } yield assertTrue(age.contains(Header.Age(1.minute)), body == "test")
      },
      test("custom out header for only one output") {
        val endpoint    =
          Endpoint(GET / "posts")
            .out[String](MediaType.text.plain)
            .outHeader(HttpCodec.age)
            .out[Int](MediaType.text.plain)
        val routesRight = endpoint.implementAs(Right(("test", Header.Age(1.minute)))).toRoutes
        val routesLeft  = endpoint.implementAs(Left(1)).toRoutes
        for {
          responseRight <- routesRight.runZIO(Request.get(URL.decode("/posts").toOption.get))
          ageRight = responseRight.header(Header.Age)
          bodyRight    <- responseRight.body.asString.orDie
          responseLeft <- routesLeft.runZIO(Request.get(URL.decode("/posts").toOption.get))
          ageLeft = responseLeft.header(Header.Age)
          bodyLeft <- responseLeft.body.asString.orDie
        } yield assertTrue(
          ageRight.contains(Header.Age(1.minute)),
          bodyRight == "test",
          ageLeft.isEmpty,
          bodyLeft == "1",
        )
      },
      test("custom out header with outCodec") {
        val endpoint =
          Endpoint(GET / "posts")
            .outCodec((HttpCodec.content[String](MediaType.text.plain) | HttpCodec.content[Int]) ++ HttpCodec.age)
        val routes   = endpoint.implementAs((Left("test"), Header.Age(1.minute))).toRoutes
        for {
          response <- routes.runZIO(Request.get(URL.decode("/posts").toOption.get))
          age = response.header(Header.Age)
          body <- response.body.asString.orDie
        } yield assertTrue(age.contains(Header.Age(1.minute)), body == "test")
      },
      test("multiple Accept headers") {
        check(Gen.int) { id =>
          val endpoint =
            Endpoint(GET / "posts")
              .query(HttpCodec.query[String]("id"))
              .out[Int](MediaType.text.`plain`)
          val routes   =
            endpoint.implementHandler {
              Handler.succeed(id)
            }

          for {
            response <- routes.toRoutes.runZIO(
              Request
                .get(URL.decode(s"/posts?id=$id").toOption.get)
                .addHeader(Header.Accept(MediaType.application.`json`, MediaType.text.`plain`)),
            )
            contentType = response.header(Header.ContentType)
          } yield assertTrue(extractStatus(response).code == 200) &&
            assertTrue(contentType == Some(ContentType(MediaType.text.`plain`)))
        }
      },
      test("content negotiation with multiple output codecs") {
        val jsonCodec = HttpCodec.content[String](MediaType.application.`json`)
        val textCodec = HttpCodec.content[String](MediaType.text.`plain`)
        val combined  = jsonCodec | textCodec
        val endpoint  = Endpoint(GET / "negotiate").outCodec(combined)
        val routes    = endpoint.implementAs("hello").toRoutes

        for {
          jsonRes <- routes.runZIO(
            Request
              .get(URL.decode("/negotiate").toOption.get)
              .addHeader(Header.Accept(MediaType.application.`json`)),
          )
          jsonContentType = jsonRes.header(Header.ContentType)
          textRes <- routes.runZIO(
            Request
              .get(URL.decode("/negotiate").toOption.get)
              .addHeader(Header.Accept(MediaType.text.`plain`)),
          )
          textContentType = textRes.header(Header.ContentType)
          textPreferredRes <- routes.runZIO(
            Request
              .get(URL.decode("/negotiate").toOption.get)
              .addHeader(
                Header.Accept(
                  Header.Accept.MediaTypeWithQFactor(MediaType.text.`plain`, None),
                  Header.Accept.MediaTypeWithQFactor(MediaType.application.`json`, Some(0.5)),
                ),
              ),
          )
          textPreferredContentType = textPreferredRes.header(Header.ContentType)
        } yield assertTrue(
          jsonContentType == Some(ContentType(MediaType.application.`json`)),
          textContentType == Some(ContentType(MediaType.text.`plain`)),
          textPreferredContentType == Some(ContentType(MediaType.text.`plain`)),
        )
      },
      test("custom status code") {
        check(Gen.int) { id =>
          val endpoint =
            Endpoint(GET / "posts")
              .query(HttpCodec.query[String]("id"))
              .out[Int](Status.NotFound)
          val routes   =
            endpoint.implementHandler {
              Handler.succeed(id)
            }

          for {
            response <- routes.toRoutes.runZIO(
              Request.get(URL.decode(s"/posts?id=$id").toOption.get),
            )
          } yield assertTrue(extractStatus(response).code == 404)
        }
      },
      suite("bad request for failed codec")(
        test("query codec") {
          check(Gen.int, Gen.boolean) { (id, notAnId) =>
            val endpoint =
              Endpoint(GET / "posts")
                .query(HttpCodec.query[Int]("id"))
                .out[Int]
            val routes   = endpoint.implementHandler { Handler.succeed(id) }
            for {
              response <- routes.toRoutes.runZIO(
                Request.get(url"/posts?id=$notAnId").addHeader(Header.Accept(MediaType.application.`json`)),
              )
              contentType = response.header(Header.ContentType)
            } yield assertTrue(
              extractStatus(response).code == 400,
              contentType.map(_.mediaType).contains(MediaType.application.`json`),
            )
          }
        },
        test("header codec") {
          check(Gen.int, Gen.alphaNumericString) { (id, notACorrelationId) =>
            val endpoint =
              Endpoint(GET / "posts")
                .header(HeaderCodec.headerAs[java.util.UUID]("X-Correlation-ID"))
                .out[Int]
            val routes   =
              endpoint.implementHandler {
                Handler.succeed(id)
              }

            for {
              response <- routes.toRoutes.runZIO(
                Request.get(URL.decode(s"/posts").toOption.get).addHeader("X-Correlation-ID", notACorrelationId),
              )
            } yield assertTrue(extractStatus(response).code == 400)
          }
        },
      ),
      test("bad request for missing header")(
        check(Gen.int) { id =>
          val endpoint =
            Endpoint(GET / "posts")
              .header(HeaderCodec.headerAs[java.util.UUID]("X-Correlation-ID"))
              .out[Int]
          val routes   =
            endpoint.implementHandler {
              Handler.succeed(id)
            }

          for {
            response <- routes.toRoutes.runZIO(
              Request.get(URL.decode(s"/posts").toOption.get),
            )
          } yield assertTrue(extractStatus(response).code == 400)
        },
      ),
      test("out of order api") {
        check(Gen.int, Gen.int, Gen.alphaNumericString, Gen.int(1, Int.MaxValue)) { (userId, postId, name, age) =>
          val testRoutes = testEndpoint(
            Routes(
              Endpoint(GET / "users" / int("userId"))
                .out[String]
                .implementHandler {
                  Handler.fromFunction { userId =>
                    s"path(users, $userId)"
                  }
                },
              Endpoint(GET / "users" / int("userId") / "posts" / int("postId"))
                .query(HttpCodec.query[String]("name"))
                .query(HttpCodec.query[String]("age"))
                .out[String]
                .implementHandler {
                  Handler.fromFunction { case (userId, postId, name, age) =>
                    s"path(users, $userId, posts, $postId) query(name=$name, age=$age)"
                  }
                },
            ),
          ) _
          testRoutes(s"/users/$userId", s"path(users, $userId)") &&
          testRoutes(
            s"/users/$userId/posts/$postId?name=$name&age=$age",
            s"path(users, $userId, posts, $postId) query(name=$name, age=$age)",
          )
        }
      },
      test("fallback") {
        check(Gen.int, Gen.alphaNumericString) { (userId, username) =>
          val testRoutes = testEndpoint(
            Routes(
              Endpoint(GET / "users")
                .query(HttpCodec.query[Int]("userId") | HttpCodec.query[String]("userId"))
                .out[String]
                .implementHandler {
                  Handler.fromFunction { userId =>
                    val value = userId.fold(_.toString, identity)
                    s"path(users) query(userId=$value)"
                  }
                },
            ),
          ) _
          testRoutes(s"/users?userId=$userId", s"path(users) query(userId=$userId)") &&
          testRoutes(s"/users?userId=$username", s"path(users) query(userId=$username)")
        }
      },
      test("broad api") {
        check(Gen.int, Gen.int, Gen.int, Gen.int) { (userId, postId, commentId, replyId) =>
          val broadUsers                       =
            Endpoint(GET / "users").out[String](Doc.p("Created user id")).implementHandler {
              Handler.succeed("path(users)")
            }
          val broadUsersId                     =
            Endpoint(GET / "users" / int("userId")).out[String].implementHandler {
              Handler.fromFunction { userId =>
                s"path(users, $userId)"
              }
            }
          val boardUsersPosts                  =
            Endpoint(GET / "users" / int("userId") / "posts")
              .out[String]
              .implementHandler {
                Handler.fromFunction { userId =>
                  s"path(users, $userId, posts)"
                }
              }
          val boardUsersPostsId                =
            Endpoint(GET / "users" / int("userId") / "posts" / int("postId"))
              .out[String]
              .implementHandler {
                Handler.fromFunction { case (userId, postId) =>
                  s"path(users, $userId, posts, $postId)"
                }
              }
          val boardUsersPostsComments          =
            Endpoint(
              GET /
                "users" / int("userId") / "posts" / int("postId") / "comments",
            )
              .out[String]
              .implementHandler {
                Handler.fromFunction { case (userId, postId) =>
                  s"path(users, $userId, posts, $postId, comments)"
                }
              }
          val boardUsersPostsCommentsId        =
            Endpoint(
              GET /
                "users" / int("userId") / "posts" / int("postId") / "comments" / int("commentId"),
            )
              .out[String]
              .implementHandler {
                Handler.fromFunction { case (userId, postId, commentId) =>
                  s"path(users, $userId, posts, $postId, comments, $commentId)"
                }
              }
          val broadPosts                       =
            Endpoint(GET / "posts").out[String].implementHandler(Handler.succeed("path(posts)"))
          val broadPostsId                     =
            Endpoint(GET / "posts" / int("postId")).out[String].implementHandler {
              Handler.fromFunction { postId =>
                s"path(posts, $postId)"
              }
            }
          val boardPostsComments               =
            Endpoint(GET / "posts" / int("postId") / "comments")
              .out[String]
              .implementHandler {
                Handler.fromFunction { postId =>
                  s"path(posts, $postId, comments)"
                }
              }
          val boardPostsCommentsId             =
            Endpoint(GET / "posts" / int("postId") / "comments" / int("commentId"))
              .out[String]
              .implementHandler {
                Handler.fromFunction { case (postId, commentId) =>
                  s"path(posts, $postId, comments, $commentId)"
                }
              }
          val broadComments                    =
            Endpoint(GET / "comments").out[String].implementHandler(Handler.succeed("path(comments)"))
          val broadCommentsId                  =
            Endpoint(GET / "comments" / int("commentId")).out[String].implementHandler {
              Handler.fromFunction { commentId =>
                s"path(comments, $commentId)"
              }
            }
          val broadUsersComments               =
            Endpoint(GET / "users" / int("userId") / "comments")
              .out[String]
              .implementHandler {
                Handler.fromFunction { userId =>
                  s"path(users, $userId, comments)"
                }
              }
          val broadUsersCommentsId             =
            Endpoint(GET / "users" / int("userId") / "comments" / int("commentId"))
              .out[String]
              .implementHandler {
                Handler.fromFunction { case (userId, commentId) =>
                  s"path(users, $userId, comments, $commentId)"
                }
              }
          val boardUsersPostsCommentsReplies   =
            Endpoint(
              GET /
                "users" / int("userId") / "posts" / int("postId") / "comments" / int("commentId") / "replies",
            )
              .out[String]
              .implementHandler {
                Handler.fromFunction { case (userId, postId, commentId) =>
                  s"path(users, $userId, posts, $postId, comments, $commentId, replies)"
                }
              }
          val boardUsersPostsCommentsRepliesId =
            Endpoint(
              GET /
                "users" / int("userId") / "posts" / int("postId") / "comments" / int("commentId") /
                "replies" / int("replyId"),
            )
              .out[String]
              .implementHandler {
                Handler.fromFunction { case (userId, postId, commentId, replyId) =>
                  s"path(users, $userId, posts, $postId, comments, $commentId, replies, $replyId)"
                }
              }

          val testRoutes = testEndpoint(
            Routes(
              broadUsers,
              broadUsersId,
              boardUsersPosts,
              boardUsersPostsId,
              boardUsersPostsComments,
              boardUsersPostsCommentsId,
              broadPosts,
              broadPostsId,
              boardPostsComments,
              boardPostsCommentsId,
              broadComments,
              broadCommentsId,
              broadUsersComments,
              broadUsersCommentsId,
              boardUsersPostsCommentsReplies,
              boardUsersPostsCommentsRepliesId,
            ),
          ) _

          testRoutes("/users", "path(users)") &&
          testRoutes(s"/users/$userId", s"path(users, $userId)") &&
          testRoutes(s"/users/$userId/posts", s"path(users, $userId, posts)") &&
          testRoutes(s"/users/$userId/posts/$postId", s"path(users, $userId, posts, $postId)") &&
          testRoutes(s"/users/$userId/posts/$postId/comments", s"path(users, $userId, posts, $postId, comments)") &&
          testRoutes(
            s"/users/$userId/posts/$postId/comments/$commentId",
            s"path(users, $userId, posts, $postId, comments, $commentId)",
          ) &&
          testRoutes(s"/posts", "path(posts)") &&
          testRoutes(s"/posts/$postId", s"path(posts, $postId)") &&
          testRoutes(s"/posts/$postId/comments", s"path(posts, $postId, comments)") &&
          testRoutes(s"/posts/$postId/comments/$commentId", s"path(posts, $postId, comments, $commentId)") &&
          testRoutes("/comments", "path(comments)") &&
          testRoutes(s"/comments/$commentId", s"path(comments, $commentId)") &&
          testRoutes(s"/users/$userId/comments", s"path(users, $userId, comments)") &&
          testRoutes(s"/users/$userId/comments/$commentId", s"path(users, $userId, comments, $commentId)") &&
          testRoutes(
            s"/users/$userId/posts/$postId/comments/$commentId/replies",
            s"path(users, $userId, posts, $postId, comments, $commentId, replies)",
          ) &&
          testRoutes(
            s"/users/$userId/posts/$postId/comments/$commentId/replies/$replyId",
            s"path(users, $userId, posts, $postId, comments, $commentId, replies, $replyId)",
          )
        }
      },
      test("composite in codecs") {
        check(Gen.alphaNumericString, Gen.alphaNumericString) { (queryValue, headerValue) =>
          val headerOrQuery             = HeaderCodec.headerAs[String]("X-Header") | HttpCodec.query[String]("header")
          val endpoint                  = Endpoint(GET / "test").out[String].inCodec(headerOrQuery)
          val routes                    = endpoint.implementHandler(Handler.identity).toRoutes
          val request                   = Request.get(
            URL
              .decode(s"/test?header=$queryValue")
              .toOption
              .get,
          )
          val requestWithHeaderAndQuery = request.addHeader("X-Header", headerValue)
          val requestWithHeader         = Request
            .get(
              URL
                .decode("/test")
                .toOption
                .get,
            )
            .addHeader("X-Header", headerValue)

          for {
            response       <- routes.runZIO(request)
            onlyQuery      <- response.body.asString.orDie
            response       <- routes.runZIO(requestWithHeader)
            onlyHeader     <- response.body.asString.orDie
            response       <- routes.runZIO(requestWithHeaderAndQuery)
            headerAndQuery <- response.body.asString.orDie
          } yield assertTrue(
            onlyQuery == s""""$queryValue"""",
            onlyHeader == s""""$headerValue"""",
            headerAndQuery == s""""$headerValue"""",
          )
        }
      },
      test("composite out codecs") {
        val headerOrQuery  = HeaderCodec.headerAs[String]("X-Header") | StatusCodec.status(Status.Created)
        val endpoint       = Endpoint(GET / "test").query(HttpCodec.query[Boolean]("Created")).outCodec(headerOrQuery)
        val routes         =
          endpoint.implementHandler {
            Handler.fromFunction { created =>
              if (created) Right(()) else Left("not created")
            }
          }.toRoutes
        val requestCreated = Request.get(
          URL
            .decode("/test?Created=true")
            .toOption
            .get,
        )
        val requestNotCreated = Request.get(
          URL
            .decode("/test?Created=false")
            .toOption
            .get,
        )

        for {
          notCreated <- routes.runZIO(requestNotCreated)
          header = notCreated.rawHeader("X-Header").get
          response <- routes.runZIO(requestCreated)
          value = header == "not created" &&
            extractStatus(notCreated) == Status.Ok &&
            extractStatus(response) == Status.Created
        } yield assertTrue(value)

      },
      suite("request bodies")(
        test("simple input") {
          check(Gen.string, Gen.int) { (postValue, postId) =>
            implicit val newPostSchema: Schema[NewPost]         = DeriveSchema.gen[NewPost]
            implicit val postCreatedSchema: Schema[PostCreated] = DeriveSchema.gen[PostCreated]

            val endpoint =
              Endpoint(POST / "posts")
                .in[NewPost](Doc.p("New post"))
                .out[PostCreated](Status.Created, MediaType.application.`json`)
            val routes   =
              endpoint.implementHandler(Handler.succeed(PostCreated(postId))).toRoutes
            val request  =
              Request
                .post(
                  URL.decode("/posts").toOption.get,
                  Body.fromString(s"""{"value": "$postValue"}"""),
                )

            for {
              response <- routes.runZIO(request)
              code        = extractStatus(response)
              contentType = response.header(Header.ContentType)
              body <- response.body.asString.orDie
            } yield assertTrue(extractStatus(response).isSuccess) &&
              assertTrue(code == Status.Created) &&
              assertTrue(contentType == Some(ContentType(MediaType.application.`json`))) &&
              assertTrue(body == s"""{"id":$postId}""")
          }
        },
        test("bad request for failed codec") {
          check(Gen.string, Gen.int) { (postValue, postId) =>
            implicit val newPostSchema: Schema[NewPost] = DeriveSchema.gen[NewPost]

            val endpoint =
              Endpoint(POST / "posts")
                .in[NewPost]
                .out[Int]
            val routes   =
              endpoint.implementHandler(Handler.succeed(postId)).toRoutes

            for {
              response <- routes.runZIO(
                Request
                  .post(
                    URL.decode("/posts").toOption.get,
                    Body.fromString(s"""{"vale": "$postValue"}"""),
                  ),
              )
            } yield assertTrue(extractStatus(response).code == 400)
          }
        },
      ),
      suite("byte stream input/output")(
        test("responding with a byte stream") {
          check(Gen.chunkOfBounded(1, 1024)(Gen.byte)) { bytes =>
            val route = Endpoint(GET / "test-byte-stream")
              .outStream[Byte](Doc.p("Test data"))
              .implementHandler(Handler.succeed(ZStream.fromChunk(bytes).rechunk(16)))
              .toRoutes

            for {
              result   <- route.runZIO(Request.get(URL.decode("/test-byte-stream").toOption.get)).exit
              response <- result match {
                case Exit.Success(value) => ZIO.succeed(value)
                case Exit.Failure(cause) =>
                  cause.failureOrCause match {
                    case Left(response) => ZIO.succeed(response)
                    case Right(cause)   => ZIO.failCause(cause)
                  }
              }
              body     <- response.body.asChunk.orDie
            } yield assertTrue(
              response.header(ContentType) == Some(ContentType(MediaType.application.`octet-stream`)),
              body == bytes,
            )
          }
        },
        test("responding with a byte stream, custom media type") {
          check(Gen.chunkOfBounded(1, 1024)(Gen.byte)) { bytes =>
            val route = Endpoint(GET / "test-byte-stream")
              .outStream[Byte](Status.Ok, MediaType.image.png)
              .implementHandler(Handler.succeed(ZStream.fromChunk(bytes).rechunk(16)))
              .toRoutes

            for {
              result   <- route.runZIO(Request.get(URL.decode("/test-byte-stream").toOption.get)).exit
              response <- result match {
                case Exit.Success(value) => ZIO.succeed(value)
                case Exit.Failure(cause) =>
                  cause.failureOrCause match {
                    case Left(response) => ZIO.succeed(response)
                    case Right(cause)   => ZIO.failCause(cause)
                  }
              }
              body     <- response.body.asChunk.orDie
            } yield assertTrue(
              response.header(ContentType) == Some(ContentType(MediaType.image.png)),
              body == bytes,
            )
          }
        },
        test("request body as a byte stream") {
          check(Gen.chunkOfBounded(1, 1024)(Gen.byte)) { bytes =>
            val route = Endpoint(POST / "test-byte-stream")
              .inStream[Byte]
              .out[Long]
              .implementHandler {
                Handler.fromFunctionZIO { byteStream =>
                  byteStream.runCount
                }
              }
              .toRoutes

            for {
              result   <- route
                .runZIO(Request.post(URL.decode("/test-byte-stream").toOption.get, Body.fromChunk(bytes)))
                .exit
              response <- result match {
                case Exit.Success(value) => ZIO.succeed(value)
                case Exit.Failure(cause) =>
                  cause.failureOrCause match {
                    case Left(response) => ZIO.succeed(response)
                    case Right(cause)   => ZIO.failCause(cause)
                  }
              }
              body     <- response.body.asString.orDie
            } yield assertTrue(body == bytes.size.toString)
          }
        },
      ),
    ),
  )

  case class NewPost(value: String)
  case class PostCreated(id: Int)
}
