///|
pub(all) struct RuntimeRequest {
  meta : RequestMeta
  body : Bytes
} derive(Eq, Debug)

///|
pub fn RuntimeRequest::new(meta : RequestMeta, body : Bytes) -> RuntimeRequest {
  RuntimeRequest::{ meta, body }
}

///|
pub fn RuntimeRequest::get(uri : String) -> RuntimeRequest {
  RuntimeRequest::{ meta: RequestMeta::simple("GET", uri), body: b"" }
}

///|
pub fn RuntimeRequest::copy(self : RuntimeRequest) -> RuntimeRequest {
  RuntimeRequest::{
    meta: RequestMeta::{ ..self.meta, headers: self.meta.headers.copy() },
    body: self.body,
  }
}

///|
pub(all) struct TransportResponse {
  meta : ResponseMeta
  body : Bytes
} derive(Eq, Debug)

///|
pub fn TransportResponse::new(
  meta : ResponseMeta,
  body : Bytes,
) -> TransportResponse {
  TransportResponse::{ meta, body }
}

///|
pub fn TransportResponse::copy(self : TransportResponse) -> TransportResponse {
  TransportResponse::{
    meta: ResponseMeta::{ ..self.meta, headers: self.meta.headers.copy() },
    body: self.body,
  }
}

///|
pub suberror TransportError {
  ScriptExhausted
  ScriptedTransportFailure(String)
} derive(Eq, Debug)

///|
pub fn TransportError::message(self : TransportError) -> String {
  match self {
    ScriptExhausted => "transport script is exhausted"
    ScriptedTransportFailure(message) => message
  }
}

///|
pub(open) trait Transport {
  fn execute(Self, RuntimeRequest) -> TransportResponse raise TransportError
}

///|
pub(all) enum TransportOutcome {
  TransportSuccess(TransportResponse)
  TransportFailure(String)
} derive(Eq, Debug)

///|
pub struct FakeTransport {
  priv outcomes : Array[TransportOutcome]
  priv mut cursor : Int
}

///|
pub fn FakeTransport::new(
  responses : Array[TransportResponse],
) -> FakeTransport {
  FakeTransport::{
    outcomes: responses.map(fn(response) { TransportSuccess(response) }),
    cursor: 0,
  }
}

///|
pub fn FakeTransport::with_outcomes(
  outcomes : Array[TransportOutcome],
) -> FakeTransport {
  FakeTransport::{ outcomes, cursor: 0 }
}

///|
pub fn FakeTransport::call_count(self : FakeTransport) -> Int {
  self.cursor
}

///|
pub fn FakeTransport::execute(
  self : FakeTransport,
  _request : RuntimeRequest,
) -> TransportResponse raise TransportError {
  if self.cursor >= self.outcomes.length() {
    raise ScriptExhausted
  }
  let outcome = self.outcomes[self.cursor]
  self.cursor = self.cursor + 1
  match outcome {
    TransportSuccess(response) => response.copy()
    TransportFailure(message) => raise ScriptedTransportFailure(message)
  }
}

///|
pub impl Transport for FakeTransport with fn execute(self, request) {
  FakeTransport::execute(self, request)
}

///|
pub struct RecordingTransport {
  priv outcomes : Array[TransportOutcome]
  priv mut cursor : Int
  priv requests : Array[RuntimeRequest]
}

///|
pub fn RecordingTransport::new(
  responses : Array[TransportResponse],
) -> RecordingTransport {
  RecordingTransport::{
    outcomes: responses.map(fn(response) { TransportSuccess(response) }),
    cursor: 0,
    requests: [],
  }
}

///|
pub fn RecordingTransport::with_outcomes(
  outcomes : Array[TransportOutcome],
) -> RecordingTransport {
  RecordingTransport::{ outcomes, cursor: 0, requests: [] }
}

///|
pub fn RecordingTransport::calls(
  self : RecordingTransport,
) -> Array[RuntimeRequest] {
  self.requests.map(fn(request) { request.copy() })
}

///|
pub fn RecordingTransport::call_count(self : RecordingTransport) -> Int {
  self.cursor
}

///|
pub fn RecordingTransport::execute(
  self : RecordingTransport,
  request : RuntimeRequest,
) -> TransportResponse raise TransportError {
  self.requests.push(request.copy())
  if self.cursor >= self.outcomes.length() {
    raise ScriptExhausted
  }
  let outcome = self.outcomes[self.cursor]
  self.cursor = self.cursor + 1
  match outcome {
    TransportSuccess(response) => response.copy()
    TransportFailure(message) => raise ScriptedTransportFailure(message)
  }
}

///|
pub impl Transport for RecordingTransport with fn execute(self, request) {
  RecordingTransport::execute(self, request)
}