///|
/// Selects whether a session records through a transport or serves a cassette.
pub(all) enum SessionMode {
  Record
  Replay
  StrictOffline
} derive(Eq, @debug.Debug)

///|
/// A transport adapter supplied by the host application.
pub(all) enum TransportError {
  Failed(String)
} derive(Eq, @debug.Debug)

///|
/// Errors returned by the record/replay session.
pub(all) enum SessionError {
  ReplayMiss(Array[MatchCandidate])
  TransportFailed(TransportError)
  RedactionFailed(RedactionError)
} derive(Eq, @debug.Debug)

///|
/// Stateful record/replay session. Replay never invokes its transport.
pub(all) struct Session {
  mut cassette : Cassette
  config : MatchConfig
  redaction : RedactionConfig
  mode : SessionMode
  mut consumed : Array[Bool]
} derive(@debug.Debug)

///|
/// Create a session with an explicit matching policy.
pub fn Session::new(
  cassette : Cassette,
  mode : SessionMode,
  config : MatchConfig,
) -> Session {
  Session::new_with_redaction(
    cassette,
    mode,
    config,
    RedactionConfig::default(),
  )
}

///|
/// Create a session with explicit matching and redaction policies.
pub fn Session::new_with_redaction(
  cassette : Cassette,
  mode : SessionMode,
  config : MatchConfig,
  redaction : RedactionConfig,
) -> Session {
  let count = cassette.interactions.length()
  { cassette, config, redaction, mode, consumed: Array::make(count, false), }
}

///|
/// Create a session using the conservative default matching policy.
pub fn Session::with_defaults(
  cassette : Cassette,
  mode : SessionMode,
) -> Session {
  Session::new(cassette, mode, MatchConfig::default())
}

///|
/// Send one request through the selected mode.
pub fn Session::send(
  self : Session,
  request : Request,
  transport : (Request) -> Result[Response, TransportError],
) -> Result[Response, SessionError] {
  match self.mode {
    Record =>
      match transport(request) {
        Ok(response) => {
          let interaction = { request, response, }
          match redact_interaction(interaction, self.redaction) {
            Ok(safe_interaction) => {
              self.cassette.interactions.push(safe_interaction)
              Ok(response)
            }
            Err(error) => Err(RedactionFailed(error))
          }
        }
        Err(error) => Err(TransportFailed(error))
      }
    Replay | StrictOffline =>
      match redact_request(request, self.redaction) {
        Err(error) => Err(RedactionFailed(error))
        Ok(safe_request) =>
          match
            match_request(
              safe_request,
              self.cassette,
              self.config,
              self.consumed,
            ) {
            Matched(index) => {
              self.consumed[index] = true
              Ok(self.cassette.interactions[index].response)
            }
            NoMatch(candidates) => Err(ReplayMiss(candidates))
          }
      }
  }
}

///|
/// Explain why a request would miss the current cassette.
pub fn Session::diagnose(self : Session, request : Request) -> MismatchReport {
  diagnose_mismatch_with_redaction(
    request,
    self.cassette,
    self.config,
    self.consumed,
    self.redaction,
  )
}

///|
/// Return the cassette, including interactions recorded so far.
pub fn Session::cassette(self : Session) -> Cassette {
  self.cassette
}