///|
/// Facts proven by validating one structured conversion result.
pub struct ConversionIntegrity {
  source_scalar_value : Int
  covered_scalar_value : Int
  dropped_scalar_value : Int
  token_value : Int
  candidate_value : Int
  diagnostic_value : Int
} derive(Eq, Debug)

///|
fn validate_token_decision(token : ConvertedToken) -> Result[Unit, PinyinError] {
  match (token.kind(), token.decision_source()) {
    (PinyinToken, PhraseLexicon) | (PinyinToken, UnicodeReading) => Ok(())
    (LiteralToken, UnknownPreserved) => Ok(())
    _ => Err(InvalidConversionResult("token_decision_mismatch"))
  }
}

///|
fn validate_token_candidates(
  token : ConvertedToken,
) -> Result[Int, PinyinError] {
  let candidates = token.candidates()
  if token.kind() == PinyinToken && candidates.length() == 0 {
    return Err(InvalidConversionResult("pinyin_token_without_candidates"))
  }
  if token.kind() == LiteralToken && candidates.length() > 0 {
    return Err(InvalidConversionResult("literal_token_with_candidates"))
  }
  for candidate in candidates {
    match parse_syllable(candidate) {
      Err(_) =>
        return Err(
          InvalidConversionResult("unparseable_candidate:" + candidate),
        )
      Ok(_) => ()
    }
  }
  Ok(candidates.length())
}

///|
/// Validates spans, token decisions, and candidate syllables without rerunning conversion.
pub fn validate_conversion_result(
  result : ConversionResult,
) -> Result[ConversionIntegrity, PinyinError] {
  let source_count = text_code_points(result.source()).length()
  let tokens = result.tokens()
  let mut previous_end = 0
  let mut covered = 0
  let mut candidates = 0
  for token in tokens {
    let span = token.span()
    if span.length() <= 0 {
      return Err(InvalidConversionResult("empty_token_span"))
    }
    if span.start() < previous_end {
      return Err(InvalidConversionResult("overlapping_or_unsorted_spans"))
    }
    if span.end() > source_count {
      return Err(InvalidConversionResult("span_outside_source"))
    }
    match validate_token_decision(token) {
      Err(error) => return Err(error)
      Ok(_) => ()
    }
    match validate_token_candidates(token) {
      Err(error) => return Err(error)
      Ok(count) => candidates = candidates + count
    }
    covered = covered + span.length()
    previous_end = span.end()
  }
  if covered > source_count {
    return Err(InvalidConversionResult("covered_scalars_exceed_source"))
  }
  Ok({
    source_scalar_value: source_count,
    covered_scalar_value: covered,
    dropped_scalar_value: source_count - covered,
    token_value: tokens.length(),
    candidate_value: candidates,
    diagnostic_value: result.diagnostics().length(),
  })
}

///|
pub fn ConversionIntegrity::source_scalars(self : ConversionIntegrity) -> Int {
  self.source_scalar_value
}

///|
pub fn ConversionIntegrity::covered_scalars(self : ConversionIntegrity) -> Int {
  self.covered_scalar_value
}

///|
pub fn ConversionIntegrity::dropped_scalars(self : ConversionIntegrity) -> Int {
  self.dropped_scalar_value
}

///|
pub fn ConversionIntegrity::tokens(self : ConversionIntegrity) -> Int {
  self.token_value
}

///|
pub fn ConversionIntegrity::validated_candidates(
  self : ConversionIntegrity,
) -> Int {
  self.candidate_value
}

///|
pub fn ConversionIntegrity::diagnostics(self : ConversionIntegrity) -> Int {
  self.diagnostic_value
}

///|
pub fn ConversionIntegrity::has_full_coverage(
  self : ConversionIntegrity,
) -> Bool {
  self.dropped_scalar_value == 0
}