///|
/// One step of a FakeTransport script: the client command to expect (verbatim,
/// or `""` for a spontaneous server reply such as the greeting, or `"*"` to
/// accept any line) and the reply lines to send back.
pub struct FakeStep {
  expect : String
  send : Array[String]
} derive(Eq, Debug)

///|
pub fn FakeStep::new(expect : String, send : Array[String]) -> FakeStep {
  { expect, send }
}

///|
pub fn FakeStep::greeting(send : Array[String]) -> FakeStep {
  { expect: "", send }
}

///|
pub fn FakeStep::reply(expect : String, send : Array[String]) -> FakeStep {
  { expect, send }
}

///|
/// A deterministic, scripted SMTP server used for offline testing. It
/// implements `SmtpTransport` and records every client command so tests can
/// assert the exact command sequence and the message body transmitted.
pub struct FakeTransport {
  script : Array[FakeStep]
  mut step_index : Int
  mut reply_queue : Array[String]
  mut expecting : String
  mut data_mode : Bool
  received : Array[String]
  data_lines : Array[String]
  mut connected : Bool
  mut failure : String?
}

///|
pub fn FakeTransport::new(script : Array[FakeStep]) -> FakeTransport {
  {
    script,
    step_index: 0,
    reply_queue: [],
    expecting: "",
    data_mode: false,
    received: [],
    data_lines: [],
    connected: false,
    failure: None,
  }
}

///|
/// Every client command line received (verbatim).
pub fn FakeTransport::received(self : FakeTransport) -> Array[String] {
  self.received
}

///|
/// The message body lines received during the DATA phase.
pub fn FakeTransport::data_lines(self : FakeTransport) -> Array[String] {
  self.data_lines
}

///|
/// The first script mismatch description, if any.
pub fn FakeTransport::failure(self : FakeTransport) -> String? {
  self.failure
}

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

///|
/// True when every script step was consumed (the conversation completed).
pub fn FakeTransport::is_complete(self : FakeTransport) -> Bool {
  self.step_index >= self.script.length()
}

///|
/// Queue spontaneous (greeting) steps and set the next expected command.
fn FakeTransport::advance(self : FakeTransport) -> Unit {
  while self.step_index < self.script.length() {
    let step = self.script[self.step_index]
    if step.expect == "" {
      self.reply_queue = self.reply_queue + step.send
      self.step_index += 1
    } else {
      self.expecting = step.expect
      return
    }
  }
  self.expecting = ""
}

///|
/// Match a client command against the current script step and queue the reply.
fn FakeTransport::match_command(
  self : FakeTransport,
  line : String,
) -> Unit raise MailFailure {
  if self.expecting == "" {
    self.failure = Some("unexpected command '\{line}' (script exhausted)")
    raise MailFailure::transport("FakeTransport: unexpected command '\{line}'")
  }
  if self.expecting != "*" && self.expecting != line {
    self.failure = Some("expected '\{self.expecting}' but got '\{line}'")
    raise MailFailure::transport(
      "FakeTransport: expected '\{self.expecting}' but got '\{line}'",
    )
  }
  let step = self.script[self.step_index]
  self.reply_queue = self.reply_queue + step.send
  if step.expect == "DATA" &&
    step.send.length() > 0 &&
    step.send[0].has_prefix("354") {
    self.data_mode = true
  }
  self.step_index += 1
  self.advance()
}

///|
pub impl SmtpTransport for FakeTransport with fn connect(self, _config) {
  @async.pause() catch {
    _ => ()
  }
  if self.script.is_empty() {
    raise MailFailure::transport("FakeTransport: empty script")
  }
  self.connected = true
  self.step_index = 0
  self.reply_queue = []
  self.data_mode = false
  self.expecting = ""
  self.advance()
}

///|
pub impl SmtpTransport for FakeTransport with fn write_line(self, line) {
  @async.pause() catch {
    _ => ()
  }
  self.received.push(line)
  if self.data_mode {
    if line == "." {
      self.data_mode = false
      self.match_command(line)
    } else {
      self.data_lines.push(line)
    }
    return
  }
  self.match_command(line)
}

///|
pub impl SmtpTransport for FakeTransport with fn read_line(self) {
  @async.pause() catch {
    _ => ()
  }
  if self.reply_queue.is_empty() {
    self.failure = Some("read on empty reply queue")
    raise MailFailure::transport("FakeTransport: reply queue exhausted")
  }
  let line = self.reply_queue[0]
  let _ = self.reply_queue.remove(0)
  line
}

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