///|
pub(all) struct NamedChoice {
  parameter : String
  value : String
} derive(Debug, Eq)

///|
pub(all) struct MissingInteraction {
  choices : Array[NamedChoice]
} derive(Debug, Eq)

///|
/// Coverage audit for an arbitrary suite. Missing interactions are named so
/// they can be printed directly in CI diagnostics.
pub(all) struct CoverageReport {
  strength : Int
  case_count : Int
  total_interactions : Int
  covered_interactions : Int
  coverage_percent : Double
  missing : Array[MissingInteraction]
} derive(Debug)

///|
pub fn CoverageReport::is_complete(self : CoverageReport) -> Bool {
  self.covered_interactions == self.total_interactions
}

///|
pub fn audit(
  model : Model,
  cases : Array[TestCase],
  strength? : Int = 2,
  constraints? : Array[Constraint] = [],
  max_candidates? : Int = 100000,
) -> Result[CoverageReport, CaseWeaveError] {
  if strength < 1 || strength > model.parameter_count() {
    return Err(InvalidStrength(strength, model.parameter_count()))
  }
  if max_candidates < 1 {
    return Err(InvalidCandidateLimit(max_candidates))
  }
  match validate_constraints(constraints, model) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  let enumeration = match
    enumerate_valid_rows(model, constraints, max_candidates) {
    Err(error) => return Err(error)
    Ok(result) => result
  }
  if enumeration.rows.length() == 0 {
    return Err(NoValidCases)
  }
  let interactions = collect_interactions(enumeration.rows, strength)
  let covered = Array::make(interactions.length(), false)
  for case_index = 0; case_index < cases.length(); case_index = case_index + 1 {
    let row = match model.encode(cases[case_index]) {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    for constraint in constraints {
      if evaluate_expression(constraint.expression, model, row) != Yes {
        return Err(ConstraintViolation(case_index, constraint.label))
      }
    }
    for interaction_index = 0
        interaction_index < interactions.length()
        interaction_index = interaction_index + 1 {
      if row_covers(row, interactions[interaction_index]) {
        covered[interaction_index] = true
      }
    }
  }
  let missing : Array[MissingInteraction] = []
  let mut covered_count = 0
  for interaction_index = 0
      interaction_index < interactions.length()
      interaction_index = interaction_index + 1 {
    if covered[interaction_index] {
      covered_count = covered_count + 1
    } else {
      missing.push(named_interaction(model, interactions[interaction_index]))
    }
  }
  let percent = if interactions.length() == 0 {
    100.0
  } else {
    covered_count.to_double() * 100.0 / interactions.length().to_double()
  }
  Ok({
    strength,
    case_count: cases.length(),
    total_interactions: interactions.length(),
    covered_interactions: covered_count,
    coverage_percent: percent,
    missing,
  })
}

///|
fn named_interaction(
  model : Model,
  interaction : Interaction,
) -> MissingInteraction {
  let choices : Array[NamedChoice] = []
  for choice in interaction.choices {
    choices.push({
      parameter: model.parameters[choice.parameter].name,
      value: model.parameters[choice.parameter].values[choice.value],
    })
  }
  { choices, }
}