///|
/// Options for repairing an incomplete suite. The planner uses the same
/// interaction model as `audit`, then greedily proposes additional feasible
/// cases that cover the largest number of currently missing interactions.
pub(all) struct RepairOptions {
  strength : Int
  max_candidates : Int
  max_suggestions : Int
}

///|
pub fn RepairOptions::default() -> RepairOptions {
  { strength: 2, max_candidates: 100000, max_suggestions: 1000 }
}

///|
pub fn RepairOptions::pairwise(
  max_candidates? : Int = 100000,
  max_suggestions? : Int = 1000,
) -> RepairOptions {
  { strength: 2, max_candidates, max_suggestions }
}

///|
pub fn RepairOptions::from_generation(
  options : GenerationOptions,
  max_suggestions? : Int = 1000,
) -> RepairOptions {
  {
    strength: options.strength,
    max_candidates: options.max_candidates,
    max_suggestions,
  }
}

///|
pub(all) struct RepairStep {
  index : Int
  test_case : TestCase
  newly_covered : Array[MissingInteraction]
  remaining_missing : Int
  risk_score : Int
  reasons : Array[String]
} derive(Debug, Eq)

///|
pub(all) struct RepairPlan {
  model : Model
  original_cases : Array[TestCase]
  additions : Array[TestCase]
  merged_cases : Array[TestCase]
  original_report : CoverageReport
  final_report : CoverageReport
  steps : Array[RepairStep]
  complete : Bool
  initial_missing : Int
  final_missing : Int
}

///|
pub(all) struct RepairGate {
  require_complete : Bool
  max_additions : Int
  max_final_missing : Int
  min_largest_gain : Int
}

///|
pub(all) enum RepairGateFailureKind {
  RepairIncomplete(Int)
  TooManyRepairAdditions(Int, Int)
  TooManyRemainingInteractions(Int, Int)
  LargestGainTooLow(Int, Int)
} derive(Debug, Eq)

///|
pub(all) struct RepairGateFailure {
  kind : RepairGateFailureKind
  message : String
} derive(Debug, Eq)

///|
pub(all) struct RepairGateReport {
  passed : Bool
  failures : Array[RepairGateFailure]
  additions : Int
  initial_missing : Int
  final_missing : Int
  largest_gain : Int
  complete : Bool
}

///|
priv struct RepairRisk {
  score : Int
  reasons : Array[String]
}

///|
priv struct RepairChoice {
  row_index : Int
  gain : Int
  risk : RepairRisk
}

///|
pub fn plan_repair(
  model : Model,
  cases : Array[TestCase],
  constraints? : Array[Constraint] = [],
  options? : RepairOptions = RepairOptions::default(),
) -> Result[RepairPlan, CaseWeaveError] {
  plan_repair_internal(model, cases, constraints, options, None)
}

///|
pub fn ScenarioSpec::plan_repair(
  self : ScenarioSpec,
  cases : Array[TestCase],
  options? : RepairOptions = RepairOptions::from_generation(self.options),
) -> Result[RepairPlan, CaseWeaveError] {
  plan_repair_internal(self.model, cases, self.constraints, options, Some(self))
}

///|
fn plan_repair_internal(
  model : Model,
  cases : Array[TestCase],
  constraints : Array[Constraint],
  options : RepairOptions,
  spec : ScenarioSpec?,
) -> Result[RepairPlan, CaseWeaveError] {
  match validate_repair_options(model, options) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  match validate_constraints(constraints, model) {
    Err(error) => return Err(error)
    Ok(_) => ()
  }
  let enumeration = match
    enumerate_valid_rows(model, constraints, options.max_candidates) {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  if enumeration.rows.length() == 0 {
    return Err(NoValidCases)
  }
  let interactions = collect_interactions(enumeration.rows, options.strength)
  let covered = Array::make(interactions.length(), false)
  let existing_rows : Array[Array[Int]] = []
  for case_index = 0; case_index < cases.length(); case_index = case_index + 1 {
    let row = match
      encode_repair_case(model, cases[case_index], constraints, case_index) {
      Err(error) => return Err(error)
      Ok(value) => value
    }
    existing_rows.push(row)
    mark_row_coverage(row, interactions, covered)
  }
  let original_report = match
    audit(
      model,
      cases,
      strength=options.strength,
      constraints~,
      max_candidates=options.max_candidates,
    ) {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  let initial_missing = count_uncovered(covered)
  let addition_rows : Array[Array[Int]] = []
  let additions : Array[TestCase] = []
  let steps : Array[RepairStep] = []
  let mut remaining = initial_missing
  while remaining > 0 && steps.length() < options.max_suggestions {
    let choice = match
      choose_repair_choice(
        model,
        enumeration.rows,
        interactions,
        covered,
        existing_rows,
        addition_rows,
        spec,
      ) {
      None => return Err(UncoverableInteraction)
      Some(value) => value
    }
    let row = enumeration.rows[choice.row_index]
    let newly = newly_covered_interactions(model, row, interactions, covered)
    if newly.length() == 0 {
      return Err(UncoverableInteraction)
    }
    mark_row_coverage(row, interactions, covered)
    remaining = count_uncovered(covered)
    let test_case = model.decode(row)
    addition_rows.push(row.copy())
    additions.push(test_case)
    steps.push({
      index: steps.length() + 1,
      test_case,
      newly_covered: newly,
      remaining_missing: remaining,
      risk_score: choice.risk.score,
      reasons: repair_reasons(choice.gain, choice.risk.reasons),
    })
  }
  let merged_cases = merge_repair_cases(cases, additions)
  let final_report = match
    audit(
      model,
      merged_cases,
      strength=options.strength,
      constraints~,
      max_candidates=options.max_candidates,
    ) {
    Err(error) => return Err(error)
    Ok(value) => value
  }
  Ok({
    model,
    original_cases: cases.copy(),
    additions,
    merged_cases,
    original_report,
    final_report,
    steps,
    complete: final_report.is_complete(),
    initial_missing,
    final_missing: final_report.missing.length(),
  })
}

///|
fn validate_repair_options(
  model : Model,
  options : RepairOptions,
) -> Result[Unit, CaseWeaveError] {
  if options.strength < 1 || options.strength > model.parameter_count() {
    return Err(InvalidStrength(options.strength, model.parameter_count()))
  }
  if options.max_candidates < 1 {
    return Err(InvalidCandidateLimit(options.max_candidates))
  }
  if options.max_suggestions < 0 {
    return Err(InvalidSuggestionLimit(options.max_suggestions))
  }
  Ok(())
}

///|
fn encode_repair_case(
  model : Model,
  test_case : TestCase,
  constraints : Array[Constraint],
  case_index : Int,
) -> Result[Array[Int], CaseWeaveError] {
  let row = match model.encode(test_case) {
    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))
    }
  }
  Ok(row)
}

///|
fn choose_repair_choice(
  model : Model,
  valid_rows : Array[Array[Int]],
  interactions : Array[Interaction],
  covered : Array[Bool],
  existing_rows : Array[Array[Int]],
  addition_rows : Array[Array[Int]],
  spec : ScenarioSpec?,
) -> RepairChoice? {
  let mut best : RepairChoice? = None
  for row_index = 0; row_index < valid_rows.length(); row_index = row_index + 1 {
    let row = valid_rows[row_index]
    if row_exists(existing_rows, row) || row_exists(addition_rows, row) {
      continue
    }
    let gain = repair_gain(row, interactions, covered)
    if gain <= 0 {
      continue
    }
    let risk = score_repair_row(model, row, spec)
    let candidate : RepairChoice = { row_index, gain, risk }
    match best {
      None => best = Some(candidate)
      Some(current) =>
        if repair_choice_is_better(candidate, current) {
          best = Some(candidate)
        }
    }
  }
  best
}

///|
fn repair_choice_is_better(
  candidate : RepairChoice,
  current : RepairChoice,
) -> Bool {
  if candidate.gain > current.gain {
    return true
  }
  if candidate.gain < current.gain {
    return false
  }
  if candidate.risk.score > current.risk.score {
    return true
  }
  if candidate.risk.score < current.risk.score {
    return false
  }
  candidate.row_index < current.row_index
}

///|
fn row_exists(rows : Array[Array[Int]], target : Array[Int]) -> Bool {
  for row in rows {
    if row == target {
      return true
    }
  }
  false
}

///|
fn repair_gain(
  row : Array[Int],
  interactions : Array[Interaction],
  covered : Array[Bool],
) -> Int {
  let mut gain = 0
  for index = 0; index < interactions.length(); index = index + 1 {
    if !covered[index] && row_covers(row, interactions[index]) {
      gain = gain + 1
    }
  }
  gain
}

///|
fn mark_row_coverage(
  row : Array[Int],
  interactions : Array[Interaction],
  covered : Array[Bool],
) -> Unit {
  for index = 0; index < interactions.length(); index = index + 1 {
    if row_covers(row, interactions[index]) {
      covered[index] = true
    }
  }
}

///|
fn newly_covered_interactions(
  model : Model,
  row : Array[Int],
  interactions : Array[Interaction],
  covered : Array[Bool],
) -> Array[MissingInteraction] {
  let newly : Array[MissingInteraction] = []
  for index = 0; index < interactions.length(); index = index + 1 {
    if !covered[index] && row_covers(row, interactions[index]) {
      newly.push(named_interaction(model, interactions[index]))
    }
  }
  newly
}

///|
fn count_uncovered(covered : Array[Bool]) -> Int {
  let mut count = 0
  for value in covered {
    if !value {
      count = count + 1
    }
  }
  count
}

///|
fn score_repair_row(
  model : Model,
  row : Array[Int],
  spec : ScenarioSpec?,
) -> RepairRisk {
  match spec {
    None => { score: 0, reasons: [] }
    Some(value) => {
      let test_case = model.decode(row)
      match value.score_case(test_case) {
        Err(_) => { score: 0, reasons: [] }
        Ok(risk) => { score: risk.score, reasons: risk.reasons }
      }
    }
  }
}

///|
fn repair_reasons(gain : Int, risk_reasons : Array[String]) -> Array[String] {
  let reasons : Array[String] = []
  reasons.push("covers \{gain} missing interaction(s)")
  for reason in risk_reasons {
    reasons.push(reason)
  }
  reasons
}

///|
fn merge_repair_cases(
  existing : Array[TestCase],
  additions : Array[TestCase],
) -> Array[TestCase] {
  let merged : Array[TestCase] = []
  for test_case in existing {
    merged.push(test_case)
  }
  for test_case in additions {
    if !contains_test_case(merged, test_case) {
      merged.push(test_case)
    }
  }
  merged
}

///|
pub fn RepairPlan::is_complete(self : RepairPlan) -> Bool {
  self.complete
}

///|
pub fn RepairPlan::addition_count(self : RepairPlan) -> Int {
  self.additions.length()
}

///|
pub fn RepairPlan::additions(self : RepairPlan) -> Array[TestCase] {
  self.additions.copy()
}

///|
pub fn RepairPlan::merged_cases(self : RepairPlan) -> Array[TestCase] {
  self.merged_cases.copy()
}

///|
pub fn RepairPlan::steps(self : RepairPlan) -> Array[RepairStep] {
  self.steps.copy()
}

///|
pub fn RepairPlan::summary(self : RepairPlan) -> String {
  let status = if self.complete { "complete" } else { "partial" }
  "repair=\{status}, original=\{self.original_cases.length()}, additions=\{self.additions.length()}, initial-missing=\{self.initial_missing}, final-missing=\{self.final_missing}, coverage=\{self.final_report.coverage_percent}%"
}

///|
pub fn RepairPlan::additions_markdown(self : RepairPlan) -> String {
  cases_to_markdown(self.model, self.additions)
}

///|
pub fn RepairPlan::merged_markdown(self : RepairPlan) -> String {
  cases_to_markdown(self.model, self.merged_cases)
}

///|
pub fn RepairPlan::steps_markdown(self : RepairPlan, top? : Int = -1) -> String {
  let builder = StringBuilder::new()
  builder.write_string(
    "| step | case | newly covered | remaining | risk | reasons |\n",
  )
  builder.write_string("| --- | --- | --- | --- | --- | --- |\n")
  let limit = if top < 0 || top > self.steps.length() {
    self.steps.length()
  } else {
    top
  }
  for index = 0; index < limit; index = index + 1 {
    let step = self.steps[index]
    builder.write_string("| ")
    builder.write_string(step.index.to_string())
    builder.write_string(" | ")
    builder.write_string(
      repair_markdown_inline(step.test_case.assignment_text(self.model)),
    )
    builder.write_string(" | ")
    builder.write_string(step.newly_covered.length().to_string())
    builder.write_string(" | ")
    builder.write_string(step.remaining_missing.to_string())
    builder.write_string(" | ")
    builder.write_string(step.risk_score.to_string())
    builder.write_string(" | ")
    builder.write_string(
      repair_markdown_inline(join_repair_reasons(step.reasons)),
    )
    builder.write_string(" |\n")
  }
  builder.to_string()
}

///|
pub fn RepairPlan::missing_after_text(self : RepairPlan) -> String {
  self.final_report.missing_text()
}

///|
pub fn RepairPlan::to_markdown(self : RepairPlan) -> String {
  let builder = StringBuilder::new()
  builder.write_string("## CaseWeave Repair Plan\n\n")
  builder.write_string("- status: ")
  builder.write_string(if self.complete { "complete" } else { "partial" })
  builder.write_char('\n')
  builder.write_string("- original cases: ")
  builder.write_string(self.original_cases.length().to_string())
  builder.write_char('\n')
  builder.write_string("- suggested additions: ")
  builder.write_string(self.additions.length().to_string())
  builder.write_char('\n')
  builder.write_string("- initial missing interactions: ")
  builder.write_string(self.initial_missing.to_string())
  builder.write_char('\n')
  builder.write_string("- final missing interactions: ")
  builder.write_string(self.final_missing.to_string())
  builder.write_string("\n\n")
  builder.write_string("### Suggested steps\n\n")
  builder.write_string(self.steps_markdown())
  if self.final_missing > 0 {
    builder.write_string("\n### Remaining gaps\n\n")
    builder.write_string(self.missing_after_text())
  }
  builder.to_string()
}

///|
fn join_repair_reasons(reasons : Array[String]) -> String {
  if reasons.length() == 0 {
    return "none"
  }
  let builder = StringBuilder::new()
  for index = 0; index < reasons.length(); index = index + 1 {
    if index > 0 {
      builder.write_string("; ")
    }
    builder.write_string(reasons[index])
  }
  builder.to_string()
}

///|
fn repair_markdown_inline(value : String) -> String {
  let builder = StringBuilder::new()
  for char in value.to_array() {
    match char {
      '|' => builder.write_string("\\|")
      '\n' => builder.write_string("
") '\r' => () _ => builder.write_char(char) } } builder.to_string() } ///| pub fn RepairPlan::highest_risk_step(self : RepairPlan) -> RepairStep? { if self.steps.length() == 0 { return None } let mut best = self.steps[0] for index = 1; index < self.steps.length(); index = index + 1 { let step = self.steps[index] if step.risk_score > best.risk_score { best = step } } Some(best) } ///| pub fn RepairPlan::largest_gain_step(self : RepairPlan) -> RepairStep? { if self.steps.length() == 0 { return None } let mut best = self.steps[0] for index = 1; index < self.steps.length(); index = index + 1 { let step = self.steps[index] if step.newly_covered.length() > best.newly_covered.length() { best = step } } Some(best) } ///| pub fn RepairPlan::risk_summary(self : RepairPlan) -> String { let risk_step = self.highest_risk_step() let gain_step = self.largest_gain_step() let builder = StringBuilder::new() match risk_step { None => builder.write_string("highest-risk: none\n") Some(step) => { builder.write_string("highest-risk: step ") builder.write_string(step.index.to_string()) builder.write_string(", score=") builder.write_string(step.risk_score.to_string()) builder.write_string(", case=") builder.write_string(step.test_case.assignment_text(self.model)) builder.write_char('\n') } } match gain_step { None => builder.write_string("largest-gain: none\n") Some(step) => { builder.write_string("largest-gain: step ") builder.write_string(step.index.to_string()) builder.write_string(", newly-covered=") builder.write_string(step.newly_covered.length().to_string()) builder.write_string(", case=") builder.write_string(step.test_case.assignment_text(self.model)) builder.write_char('\n') } } builder.to_string() } ///| pub fn RepairGate::default() -> RepairGate { { require_complete: true, max_additions: 100000, max_final_missing: 0, min_largest_gain: 1, } } ///| pub fn RepairGate::budget(max_additions : Int) -> RepairGate { { require_complete: true, max_additions, max_final_missing: 0, min_largest_gain: 1, } } ///| pub fn RepairGate::best_effort( max_additions : Int, max_final_missing : Int, ) -> RepairGate { { require_complete: false, max_additions, max_final_missing, min_largest_gain: 0, } } ///| pub fn RepairGate::normalize(self : RepairGate) -> RepairGate { { require_complete: self.require_complete, max_additions: if self.max_additions < 0 { 0 } else { self.max_additions }, max_final_missing: if self.max_final_missing < 0 { 0 } else { self.max_final_missing }, min_largest_gain: if self.min_largest_gain < 0 { 0 } else { self.min_largest_gain }, } } ///| pub fn RepairPlan::evaluate_repair_gate( self : RepairPlan, gate? : RepairGate = RepairGate::default(), ) -> RepairGateReport { let normalized = gate.normalize() let failures : Array[RepairGateFailure] = [] let largest_gain = self.largest_repair_gain() if normalized.require_complete && !self.complete { failures.push({ kind: RepairIncomplete(self.final_missing), message: "Repair plan is incomplete; \{self.final_missing} interaction(s) remain missing.", }) } if self.additions.length() > normalized.max_additions { failures.push({ kind: TooManyRepairAdditions( self.additions.length(), normalized.max_additions, ), message: "Repair suggestions exceed the configured budget: \{self.additions.length()} > \{normalized.max_additions}.", }) } if self.final_missing > normalized.max_final_missing { failures.push({ kind: TooManyRemainingInteractions( self.final_missing, normalized.max_final_missing, ), message: "Remaining missing interactions exceed the gate: \{self.final_missing} > \{normalized.max_final_missing}.", }) } if largest_gain < normalized.min_largest_gain { failures.push({ kind: LargestGainTooLow(largest_gain, normalized.min_largest_gain), message: "The largest repair step covers too few interactions: \{largest_gain} < \{normalized.min_largest_gain}.", }) } { passed: failures.length() == 0, failures, additions: self.additions.length(), initial_missing: self.initial_missing, final_missing: self.final_missing, largest_gain, complete: self.complete, } } ///| pub fn RepairPlan::largest_repair_gain(self : RepairPlan) -> Int { let mut gain = 0 for step in self.steps { if step.newly_covered.length() > gain { gain = step.newly_covered.length() } } gain } ///| pub fn RepairGateReport::is_passed(self : RepairGateReport) -> Bool { self.passed } ///| pub fn RepairGateReport::summary(self : RepairGateReport) -> String { let status = if self.passed { "pass" } else { "fail" } "repair-gate=\{status}, failures=\{self.failures.length()}, additions=\{self.additions}, initial-missing=\{self.initial_missing}, final-missing=\{self.final_missing}, largest-gain=\{self.largest_gain}" } ///| pub fn RepairGateReport::failure_text(self : RepairGateReport) -> String { if self.failures.length() == 0 { return "no repair gate failures\n" } let builder = StringBuilder::new() for failure in self.failures { builder.write_string("- ") builder.write_string(failure.message) builder.write_char('\n') } builder.to_string() } ///| pub fn RepairGateReport::to_markdown(self : RepairGateReport) -> String { let builder = StringBuilder::new() builder.write_string("## CaseWeave Repair Gate\n\n") builder.write_string("- status: ") builder.write_string(if self.passed { "pass" } else { "fail" }) builder.write_char('\n') builder.write_string("- additions: ") builder.write_string(self.additions.to_string()) builder.write_char('\n') builder.write_string("- initial missing: ") builder.write_string(self.initial_missing.to_string()) builder.write_char('\n') builder.write_string("- final missing: ") builder.write_string(self.final_missing.to_string()) builder.write_char('\n') builder.write_string("- largest gain: ") builder.write_string(self.largest_gain.to_string()) builder.write_string("\n\n") if self.failures.length() == 0 { builder.write_string("No repair gate failures.\n") } else { for failure in self.failures { builder.write_string("- ") builder.write_string(failure.message) builder.write_char('\n') } } builder.to_string() } ///| pub fn RepairOptions::normalize(self : RepairOptions) -> RepairOptions { { strength: if self.strength < 1 { 1 } else { self.strength }, max_candidates: if self.max_candidates < 1 { 1 } else { self.max_candidates }, max_suggestions: if self.max_suggestions < 0 { 0 } else { self.max_suggestions }, } }