///|
/// Errors in a suite's identity or recorded values.
pub(all) suberror CaseError {
  Invalid(String)
}

///|
/// A named collection of observations, keyed by a stable case identifier.
pub struct Suite {
  name : String
  priv values : Map[String, Json]
}

///|
/// Create a suite. Use the same name on each backend.
pub fn Suite::new(name : String) -> Suite raise CaseError {
  if name.trim().is_empty() {
    raise Invalid("suite name must not be empty")
  }
  { name, values: {}, }
}

///|
fn validate_value(value : Json, depth : Int) -> Unit raise CaseError {
  if depth > 64 {
    raise Invalid("observation exceeds 64 levels of nesting")
  }
  match value {
    Number(n, ..) =>
      if n.is_nan() || n.is_inf() {
        raise Invalid("non-finite number: record an explicit string instead")
      }
    Array(xs) =>
      for x in xs {
        validate_value(x, depth + 1)
      }
    Object(xs) =>
      for _, x in xs {
        validate_value(x, depth + 1)
      }
    _ => ()
  }
}

///|
/// Record a value. Case IDs must be unique within a suite.
pub fn Suite::observe(
  self : Suite,
  id : String,
  value : Json,
) -> Unit raise CaseError {
  if id.trim().is_empty() {
    raise Invalid("case ID must not be empty")
  }
  if self.values.contains(id) {
    raise Invalid("duplicate case ID: \{id}")
  }
  let json = value
  validate_value(json, 0)
  self.values[id] = copy_value(json)
}

///|
/// Number of recorded cases.
pub fn Suite::size(self : Suite) -> Int {
  self.values.length()
}

///|
/// Encode a versioned observation document. No diagnostic text is included.
pub fn Suite::encode(self : Suite) -> String raise CaseError {
  if self.values.is_empty() {
    raise Invalid("cannot encode an empty suite")
  }
  let ids = self.values.keys().to_array()
  ids.sort_by((a, b) => a.lexical_compare(b))
  let cases : Array[Json] = ids.map(id => {
    "id": id.to_json(),
    "value": self.values[id],
  })
  let doc : Json = {
    "casekit": 1,
    "suite": self.name.to_json(),
    "cases": cases.to_json(),
  }
  canonical(doc)
}

///|
/// Record a floating-point value without silently converting NaN or infinity to null.
pub fn Suite::observe_number(
  self : Suite,
  id : String,
  value : Double,
) -> Unit raise CaseError {
  if value.is_nan() || value.is_inf() {
    raise Invalid("non-finite number: record an explicit string instead")
  }
  self.observe(id, value.to_json())
}