///|
/// Validate a temperature interval before a design sweep.
pub fn validate_temperature_interval(
  minimum : Double,
  maximum : Double,
) -> ValidationReport {
  let report = ValidationReport::new()
  if minimum <= 0.0 {
    report.push(Error, "minimum_temperature", "temperature must be positive")
  } else if minimum > maximum {
    report.push(Error, "temperature_interval", "minimum exceeds maximum")
  } else {
    report
  }
}

///|
/// Validate a volume grid.
pub fn validate_volume_grid(
  minimum : Double,
  maximum : Double,
  points : Int,
) -> ValidationReport {
  let report = ValidationReport::new()
  let report = if minimum < 0.0 {
    report.push(Error, "minimum_volume", "volume must be non-negative")
  } else {
    report
  }
  let report = if maximum < minimum {
    report.push(Error, "maximum_volume", "maximum must not be below minimum")
  } else {
    report
  }
  if points < 1 {
    report.push(Error, "points", "at least one point is required")
  } else {
    report
  }
}

///|
/// Validate controller sampling settings.
pub fn validate_controller_settings(
  config : ControllerConfig,
  dt : Double,
  steps : Int,
) -> ValidationReport {
  let report = ValidationReport::new()
  let report = if !config.validate() {
    report.push(Error, "controller", "invalid gains or limits")
  } else {
    report
  }
  let report = if dt <= 0.0 {
    report.push(Error, "dt", "time step must be positive")
  } else {
    report
  }
  if steps < 1 {
    report.push(Error, "steps", "at least one step is required")
  } else {
    report
  }
}

///|
/// Validate that a numerical tolerance is useful and finite.
pub fn validate_tolerance(
  tolerance : Double,
  maximum_depth : Int,
) -> ValidationReport {
  let report = ValidationReport::new()
  let report = if tolerance <= 0.0 {
    report.push(Error, "tolerance", "tolerance must be positive")
  } else {
    report
  }
  if maximum_depth < 1 {
    report.push(Error, "maximum_depth", "depth must be positive")
  } else {
    report
  }
}

///|
/// Validate a feed against a named operating window.
pub fn validate_feed_window(
  feed : Feed,
  minimum_temperature : Double,
  maximum_temperature : Double,
) -> ValidationReport {
  let report = validate_feed(feed)
  let report = if !thermal_window_ok(
      feed.temperature,
      minimum_temperature,
      maximum_temperature,
    ) {
    report.push(
      Warning,
      "temperature",
      "feed temperature is outside the operating window",
    )
  } else {
    report
  }
  if feed.heat_capacity_flow <= 0.0 {
    report.push(
      Error,
      "heat_capacity_flow",
      "heat capacity flow must be positive",
    )
  } else {
    report
  }
}

///|
/// Validate a reaction for a stated design temperature.
pub fn validate_reaction_temperature(
  reaction : Reaction,
  temperature : Double,
) -> ValidationReport {
  let report = validate_reaction(reaction)
  let report = if temperature <= 0.0 {
    report.push(Error, "temperature", "temperature must be positive")
  } else {
    report
  }
  if reaction.reference_temperature <= 0.0 {
    report.push(
      Error,
      "reference_temperature",
      "reference temperature must be positive",
    )
  } else {
    report
  }
}

///|
/// Validate a candidate design point.
pub fn validate_design_point(
  point : DesignPoint,
  feed : Feed,
) -> ValidationReport {
  let report = ValidationReport::new()
  let report = if point.volume < 0.0 {
    report.push(Error, "volume", "volume must be non-negative")
  } else {
    report
  }
  let report = if !design_point_is_physical(point, feed) {
    report.push(Error, "physical", "design point violates physical bounds")
  } else {
    report
  }
  if point.rate_at_outlet < -1.0e-9 {
    report.push(Error, "rate", "outlet rate must be non-negative")
  } else {
    report
  }
}

///|
/// Validate every point in a reactor sweep.
pub fn validate_sweep(
  points : ArrayView[ReactorSweepPoint],
  feed : Feed,
) -> ValidationReport {
  let report = ValidationReport::new()
  let report = if feed.concentration < 0.0 {
    report.push(Error, "feed", "feed concentration must be non-negative")
  } else {
    report
  }
  let report = if !sweep_is_sorted(points) {
    report.push(Error, "volume", "sweep is not sorted")
  } else {
    report
  }
  for point in points {
    let report = if point.conversion < 0.0 || point.conversion >= 1.0 {
      report.push(Error, "conversion", "sweep conversion is outside bounds")
    } else {
      report
    }
    if point.outlet_temperature <= 0.0 || point.conversion < 0.0 {
      return report.push(Error, "state", "sweep state is non-physical")
    }
    ignore(report)
  }
  report
}

///|
/// Check whether a benchmark result meets its declared tolerance.
pub fn benchmark_result_is_reproducible(result : BenchmarkResult) -> Bool {
  result.passed && result.absolute_error <= result.tolerance
}

///|
/// Count benchmark failures.
pub fn benchmark_failure_count(results : ArrayView[BenchmarkResult]) -> Int {
  results.fold(init=0, fn(acc, result) {
    if result.passed {
      acc
    } else {
      acc + 1
    }
  })
}

///|
/// Find the largest benchmark error.
pub fn maximum_benchmark_error(results : ArrayView[BenchmarkResult]) -> Double {
  results.fold(init=0.0, fn(acc, result) { acc.max(result.absolute_error) })
}

///|
/// Check that benchmark metadata is complete.
pub fn benchmark_metadata_complete(case : BenchmarkCase) -> Bool {
  case.id.length() > 0 &&
  case.title.length() > 0 &&
  case.source.length() > 0 &&
  case.source_url.has_prefix("http") &&
  case.units.length() > 0 &&
  case.assumptions.length() > 0
}

///|
/// Count metadata-complete benchmark cases.
pub fn complete_benchmark_count(cases : ArrayView[BenchmarkCase]) -> Int {
  cases.fold(init=0, fn(acc, case) {
    if benchmark_metadata_complete(case) {
      acc + 1
    } else {
      acc
    }
  })
}

///|
/// Validate all declared benchmark tolerances.
pub fn benchmark_tolerances_valid(cases : ArrayView[BenchmarkCase]) -> Bool {
  for case in cases {
    if case.tolerance < 0.0 || !benchmark_metadata_complete(case) {
      return false
    }
  }
  true
}

///|
/// Return the first invalid benchmark id.
pub fn first_invalid_benchmark(cases : ArrayView[BenchmarkCase]) -> String? {
  for case in cases {
    if !benchmark_metadata_complete(case) {
      return Some(case.id)
    }
  }
  None
}

///|
/// Check whether a report contains the standard CSV header.
pub fn has_standard_csv_header(report : String, header : String) -> Bool {
  report.has_prefix(header)
}

///|
/// Validate a stable report row count.
pub fn report_row_count(report : String) -> Int {
  report
  .split("\n")
  .fold(init=0, fn(acc, row) { if row.length() == 0 { acc } else { acc + 1 } })
}

///|
/// Check a text report for required labels.
pub fn report_contains_all(report : String, labels : ArrayView[String]) -> Bool {
  for label in labels {
    if !report.contains(label) {
      return false
    }
  }
  true
}

///|
/// Check that a conversion interval encloses its nominal point.
pub fn interval_encloses_nominal(interval : ConversionInterval) -> Bool {
  interval.minimum_conversion <= interval.nominal_conversion &&
  interval.nominal_conversion <= interval.maximum_conversion
}

///|
/// Check that an uncertainty summary is ordered and non-negative.
pub fn uncertainty_summary_is_ordered(summary : UncertaintySummary) -> Bool {
  summary.samples >= 0 &&
  summary.minimum <= summary.p05 &&
  summary.p05 <= summary.p50 &&
  summary.p50 <= summary.p95 &&
  summary.p95 <= summary.maximum &&
  summary.standard_deviation >= 0.0
}

///|
/// Validate a train's aggregate volume and conversion.
pub fn validate_train(result : TrainResult) -> ValidationReport {
  let report = ValidationReport::new()
  let report = if result.total_volume < 0.0 {
    report.push(Error, "total_volume", "total volume must be non-negative")
  } else {
    report
  }
  let report = if result.final_conversion < 0.0 ||
    result.final_conversion >= 1.0 {
    report.push(Error, "final_conversion", "final conversion is outside bounds")
  } else {
    report
  }
  if result.total_residence_time < 0.0 {
    report.push(Error, "residence_time", "residence time must be non-negative")
  } else {
    report
  }
}