///|
/// One step of a scripted transport. Constructed by callers (or tests) to
/// script a `FakeTransport`.
#warnings("-unused_constructor")
pub(all) enum FakeStep {
  /// The next write must produce exactly these bytes.
  Expect(Bytes)
  /// The next read returns these bytes.
  Recv(Bytes)
  /// The next write is accepted without assertion and this response is
  /// returned by the following read.
  Respond(Bytes)
  /// The next transport operation fails with this error.
  Fail(LdapError)
  /// The next read reports a closed connection.
  Close
} derive(@debug.Debug)

///|
/// A fully scripted transport for deterministic, offline tests. It verifies
/// the request bytes written by the client and feeds back scripted response
/// bytes.
pub struct FakeTransport {
  steps : Array[FakeStep]
  mut index : Int
  log : Array[Bytes]
  mut connected : Bool
  priv mut pending : Bytes?
}

///|
pub fn FakeTransport::new() -> FakeTransport {
  { steps: [], index: 0, log: [], connected: false, pending: None }
}

///|
pub fn FakeTransport::with_steps(steps : Array[FakeStep]) -> FakeTransport {
  { steps, index: 0, log: [], connected: false, pending: None }
}

///|
/// The request bytes received so far.
pub fn FakeTransport::log(self : FakeTransport) -> Array[Bytes] {
  self.log
}

///|
pub fn FakeTransport::is_connected(self : FakeTransport) -> Bool {
  self.connected
}

///|
pub fn FakeTransport::has_unconsumed_steps(self : FakeTransport) -> Bool {
  self.index < self.steps.length() || self.pending is Some(_)
}

///|
fn FakeTransport::next_step(self : FakeTransport) -> FakeStep? {
  if self.index >= self.steps.length() {
    return None
  }
  let step = self.steps[self.index]
  self.index = self.index + 1
  Some(step)
}

///|
pub impl LdapTransport for FakeTransport with fn connect(self, _config) {
  self.connected = true
  Ok(())
}

///|
pub impl LdapTransport for FakeTransport with fn write(self, data) {
  match self.next_step() {
    None => Err(LdapError::ScriptMismatch("no step expected a write"))
    Some(Respond(response)) => {
      self.pending = Some(response)
      self.log.push(data)
      Ok(())
    }
    Some(Expect(expected)) =>
      if expected == data {
        self.log.push(data)
        Ok(())
      } else {
        Err(
          LdapError::ScriptMismatch(
            "request bytes differ: got \{to_hex_upper(data[:])} want \{to_hex_upper(expected[:])}",
          ),
        )
      }
    Some(Fail(err)) => Err(err)
    Some(_) =>
      Err(LdapError::ScriptMismatch("expected a read step, got a write"))
  }
}

///|
pub impl LdapTransport for FakeTransport with fn read(self) {
  match self.pending {
    Some(data) => {
      self.pending = None
      Ok(data)
    }
    None =>
      match self.next_step() {
        None => Err(LdapError::PrematureClose)
        Some(Recv(data)) => Ok(data)
        Some(Respond(data)) => Ok(data)
        Some(Close) => Err(LdapError::PrematureClose)
        Some(Fail(err)) => Err(err)
        Some(Expect(_)) =>
          Err(LdapError::ScriptMismatch("expected a write step, got a read"))
      }
  }
}

///|
pub impl LdapTransport for FakeTransport with fn close(self) {
  self.connected = false
}