///|
/// HTTP verb used by the Chroma v2 API.
pub(all) enum Method {
  Get
  Post
  Put
  Delete
  Patch
} derive(Eq, Debug)

///|
impl Show for Method with fn output(self, logger) {
  logger.write_string(
    match self {
      Get => "GET"
      Post => "POST"
      Put => "PUT"
      Delete => "DELETE"
      Patch => "PATCH"
    },
  )
}

///|
/// A fully-built HTTP request, ready to hand to a transport.
pub(all) struct Request {
  verb : Method
  url : String
  headers : Map[String, String]
  body : String?
} derive(Eq, Debug)

///|
/// The response a transport produces.
pub(all) struct Response {
  status : Int32
  headers : Map[String, String]
  body : String
} derive(Eq, Debug)

///|
/// Minimal transport abstraction.
///
/// `chroma-client` is deliberately transport-agnostic: the library builds
/// requests and decodes responses while the host application supplies the
/// actual I/O. `MemoryTransport` covers tests.
pub(open) trait Transport {
  fn send(Self, Request) -> Response raise
}

///|
/// A scripted transport for tests: records every request and replays the
/// queued responses in order.
pub(all) struct MemoryTransport {
  responses : Array[Response]
  observed : Array[Request]
  cursor : Ref[Int32]
} derive(Debug)

///|
/// Creates a `MemoryTransport` that returns the given responses in order.
pub fn MemoryTransport::new(
  responses~ : Array[Response] = [],
) -> MemoryTransport {
  { responses, observed: [], cursor: { val: 0, }, }
}

///|
pub impl Transport for MemoryTransport with fn send(self, request) {
  self.observed.push(request)
  let index = self.cursor.val
  self.cursor.val = index + 1
  if index < self.responses.length() {
    self.responses[index]
  } else {
    { status: 200, headers: {}, body: "{}", }
  }
}

///|
/// Returns the requests this transport has observed, in send order.
pub fn MemoryTransport::observed(self : MemoryTransport) -> Array[Request] {
  self.observed
}

///|
/// Returns the last observed request, if any.
pub fn MemoryTransport::last_request(self : MemoryTransport) -> Request? {
  if self.observed.length() > 0 {
    Some(self.observed[self.observed.length() - 1])
  } else {
    None
  }
}

///|
/// Convenience constructor for a scripted JSON response.
pub fn Response::json(
  status : Int32,
  body : String,
  headers~ : Map[String, String] = { "content-type": "application/json" },
) -> Response {
  { status, headers, body, }
}

///|
pub extend Method with Eq::{not_equal, equal}

///|
pub extend Method with @debug.Debug::{to_repr}

///|
pub extend Request with Eq::{not_equal, equal}

///|
pub extend Request with @debug.Debug::{to_repr}

///|
pub extend Response with Eq::{not_equal, equal}

///|
pub extend Response with @debug.Debug::{to_repr}

///|
pub extend MemoryTransport with @debug.Debug::{to_repr}

///|
pub extend MemoryTransport with Transport::{send}

///|
pub extend Method with Show::{to_string, output}

///|
/// The asynchronous counterpart of `Transport`.
///
/// `Transport` is synchronous, which is enough for `MemoryTransport` but not
/// for the real HTTP stack: `moonbitlang/async/http` only exposes `async`
/// functions, and an `async` call cannot be made from a synchronous one. A
/// native or wasm build therefore implements this trait instead, and the
/// client methods gain an `async` suffix.
///
/// The two traits carry the same request and response types, so switching
/// between a scripted test and a live server changes only which `send` is
/// implemented.
pub(open) trait AsyncTransport {
  async fn send_async(Self, Request) -> Response
}