///|
/// A named reusable model scenario for demos and acceptance smoke tests.
pub struct Scenario {
  name : String
  solver : Solver
  expected_solutions : Int
}

///|
/// Construct a scenario record.
pub fn scenario(
  name : String,
  solver : Solver,
  expected_solutions : Int,
) -> Scenario {
  { name, solver, expected_solutions }
}

///|
/// Read a scenario name.
pub fn Scenario::name(self : Scenario) -> String {
  self.name
}

///|
/// Solve a scenario with a cap.
pub fn Scenario::solve(self : Scenario, limit : Int) -> Array[Solution] {
  self.solver.limit(limit)
  self.solver.solve_all()
}

///|
/// Read expected solution count.
pub fn Scenario::expected(self : Scenario) -> Int {
  self.expected_solutions
}

///|
/// Read scenario statistics.
pub fn Scenario::stats(self : Scenario) -> SearchStats {
  self.solver.stats()
}

///|
/// Return whether the observed count matches the expected count.
pub fn Scenario::matches(self : Scenario, solutions : Array[Solution]) -> Bool {
  solutions.length() == self.expected_solutions
}

///|
/// Return a tiny arithmetic scenario.
pub fn arithmetic_scenario() -> Scenario {
  let solver = new_solver()
  let a = solver.add_variable(variable("a", 0, 9))
  let b = solver.add_variable(variable("b", 0, 9))
  solver.add_constraint(sum([a, b], 9))
  solver.add_constraint(less_than(a, b))
  scenario("arithmetic", solver, 4)
}

///|
/// Return a finite table scenario.
pub fn table_scenario() -> Scenario {
  let solver = new_solver()
  let left = solver.add_variable(variable("left", 0, 2))
  let right = solver.add_variable(variable("right", 0, 2))
  let result = solver.add_variable(variable("result", 0, 4))
  solver.add_constraint(
    table([left, right, result], [
      [0, 0, 0],
      [0, 1, 1],
      [1, 0, 1],
      [1, 1, 2],
      [2, 2, 4],
    ]),
  )
  scenario("table", solver, 5)
}

///|
/// Return a Boolean circuit scenario.
pub fn boolean_scenario() -> Scenario {
  let circuit = boolean_circuit()
  let left = circuit.input("left")
  let right = circuit.input("right")
  let output = circuit.and_gate(left, right)
  circuit.solver.add_constraint(equal(output, 1))
  scenario("boolean", circuit.solver, 1)
}

///|
/// Return a graph-coloring scenario.
pub fn coloring_scenario() -> Scenario {
  match cycle_coloring(5, 3) {
    Some(problem) => scenario("cycle_coloring", problem.solver, 1)
    None => arithmetic_scenario()
  }
}

///|
/// Return a schedule scenario with rotation rules.
pub fn schedule_scenario() -> Scenario {
  match balanced_schedule(3, 3, 2) {
    Some(problem) => {
      ignore(problem.avoid_same_shift(0))
      scenario("balanced_schedule", problem.solver, 1)
    }
    None => arithmetic_scenario()
  }
}

///|
/// Return a small resource scheduling scenario.
pub fn resource_scenario() -> Scenario {
  match resource_schedule(6, 1) {
    None => arithmetic_scenario()
    Some(problem) => {
      ignore(problem.add_flexible_task("compile", 2, 1))
      ignore(problem.add_flexible_task("test", 2, 1))
      scenario("resource_schedule", problem.solver, 1)
    }
  }
}

///|
/// Return a canonical model scenario collection.
pub fn scenario_suite() -> Array[Scenario] {
  [
    arithmetic_scenario(),
    table_scenario(),
    boolean_scenario(),
    coloring_scenario(),
    schedule_scenario(),
    resource_scenario(),
  ]
}

///|
/// Run every smoke scenario and return a concise report.
pub fn scenario_report(limit : Int) -> String {
  let builder = StringBuilder()
  for index, scenario in scenario_suite() {
    if index > 0 {
      builder.write_char('\n')
    }
    let solutions = scenario.solve(limit)
    builder.write_string(
      "\{scenario.name}: observed=\{solutions.length()}, expected=\{scenario.expected()}, ok=\{scenario.matches(solutions)}",
    )
  }
  builder.to_string()
}

///|
/// Return whether every smoke scenario meets its expected count.
pub fn scenarios_pass(limit : Int) -> Bool {
  for scenario in scenario_suite() {
    if !scenario.matches(scenario.solve(limit)) {
      return false
    }
  }
  true
}

///|
/// Build a precedence-chain solver for workflow examples.
pub fn workflow_chain(task_count : Int) -> Solver? {
  if task_count < 1 {
    return None
  }
  let solver = new_solver()
  let tasks : Array[Int] = []
  for task in 0.. Solver? {
  if feature_count < 1 || required < 0 || required > feature_count {
    return None
  }
  let solver = new_solver()
  let features : Array[Int] = []
  for feature in 0..