///|
pub(all) struct VariantSpec {
  name : String
  overrides : Array[(String, BtValue)]
  expected : BtStatus
  max_ticks : Int
} derive(Debug, Eq)

///|
pub fn variant(
  name : String,
  overrides : Array[(String, BtValue)],
  expected? : BtStatus,
  max_ticks? : Int,
) -> VariantSpec {
  {
    name,
    overrides,
    expected: expected.unwrap_or(Success),
    max_ticks: max_ticks.unwrap_or(16),
  }
}

///|
pub(all) struct VariantResult {
  name : String
  status : BtStatus
  expected : BtStatus
  ticks : Int
  digest : String
  ok : Bool
  detail : String
} derive(Debug, Eq)

///|
pub fn VariantResult::to_line(self : VariantResult) -> String {
  self.name +
  ": status=" +
  self.status.to_text() +
  ", expected=" +
  self.expected.to_text() +
  ", ticks=" +
  self.ticks.to_string() +
  ", ok=" +
  (if self.ok { "true" } else { "false" }) +
  ", digest=" +
  self.digest +
  ", detail=" +
  self.detail
}

///|
pub(all) struct MatrixReport {
  name : String
  results : Array[VariantResult]
} derive(Debug, Eq)

///|
pub fn MatrixReport::passed(self : MatrixReport) -> Int {
  let mut count = 0
  let mut i = 0
  while i < self.results.length() {
    if self.results[i].ok {
      count = count + 1
    }
    i = i + 1
  }
  count
}

///|
pub fn MatrixReport::failed(self : MatrixReport) -> Int {
  self.results.length() - self.passed()
}

///|
pub fn MatrixReport::ok(self : MatrixReport) -> Bool {
  self.failed() == 0
}

///|
pub fn MatrixReport::summary(self : MatrixReport) -> String {
  self.name +
  ": variants=" +
  self.results.length().to_string() +
  ", passed=" +
  self.passed().to_string() +
  ", failed=" +
  self.failed().to_string()
}

///|
pub fn MatrixReport::lines(self : MatrixReport) -> Array[String] {
  let out = Array::new()
  out.push(self.summary())
  let mut i = 0
  while i < self.results.length() {
    out.push(self.results[i].to_line())
    i = i + 1
  }
  out
}

///|
pub fn run_variant_dsl(base_dsl : String, spec : VariantSpec) -> VariantResult {
  let source = apply_overrides(base_dsl, spec.overrides)
  match parse_dsl(source) {
    Ok(doc) => {
      let engine = new_engine(doc.tree, blackboard=doc.blackboard)
      match engine.run_until_done(max_ticks=spec.max_ticks) {
        Ok(result) => {
          let ok = result.status == spec.expected
          {
            name: spec.name,
            status: result.status,
            expected: spec.expected,
            ticks: result.tick,
            digest: engine.trace_digest(),
            ok,
            detail: if ok { "matched expected status" } else { "unexpected status" },
          }
        }
        Err(err) =>
          {
            name: spec.name,
            status: Failure,
            expected: spec.expected,
            ticks: engine.tick_count,
            digest: engine.trace_digest(),
            ok: false,
            detail: err.message(),
          }
      }
    }
    Err(err) =>
      {
        name: spec.name,
        status: Failure,
        expected: spec.expected,
        ticks: 0,
        digest: "parse-error",
        ok: false,
        detail: err.message(),
      }
  }
}

///|
pub fn run_matrix(
  name : String,
  base_dsl : String,
  variants : Array[VariantSpec],
) -> MatrixReport {
  let results = Array::new()
  let mut i = 0
  while i < variants.length() {
    results.push(run_variant_dsl(base_dsl, variants[i]))
    i = i + 1
  }
  { name, results }
}

///|
pub fn boolean_matrix(
  name : String,
  source : String,
  key : String,
  true_expected : BtStatus,
  false_expected : BtStatus,
  max_ticks? : Int,
) -> MatrixReport {
  run_matrix(
    name,
    source,
    [
      variant("true", [(key, BoolValue(true))], expected=true_expected, max_ticks=max_ticks.unwrap_or(16)),
      variant("false", [(key, BoolValue(false))], expected=false_expected, max_ticks=max_ticks.unwrap_or(16)),
    ],
  )
}

///|
pub fn int_sweep_matrix(
  name : String,
  source : String,
  key : String,
  values : Array[Int],
  expected : BtStatus,
  max_ticks? : Int,
) -> MatrixReport {
  let variants = Array::new()
  let mut i = 0
  while i < values.length() {
    variants.push(
      variant(
        key + "_" + values[i].to_string(),
        [(key, IntValue(values[i]))],
        expected~,
        max_ticks=max_ticks.unwrap_or(16),
      ),
    )
    i = i + 1
  }
  run_matrix(name, source, variants)
}

///|
pub fn fixture_matrix_report() -> MatrixReport {
  run_matrix(
    "built_in_branch_matrix",
    guarded_attack_dsl(),
    [
      variant("enemy_visible", [("enemy_visible", BoolValue(true)), ("ammo", IntValue(3))], expected=Success, max_ticks=8),
      variant("enemy_missing", [("enemy_visible", BoolValue(false)), ("ammo", IntValue(3))], expected=Success, max_ticks=8),
      variant("empty_ammo", [("enemy_visible", BoolValue(true)), ("ammo", IntValue(0))], expected=Success, max_ticks=8),
    ],
  )
}

///|
pub fn apply_overrides(source : String, overrides : Array[(String, BtValue)]) -> String {
  let mut out = source
  let mut i = 0
  while i < overrides.length() {
    out = replace_blackboard_line(out, overrides[i].0, overrides[i].1)
    i = i + 1
  }
  out
}

///|
pub fn matrix_markdown(report : MatrixReport) -> String {
  let lines = Array::new()
  lines.push("## Matrix: " + report.name)
  lines.push("")
  lines.push("- " + report.summary())
  let mut i = 0
  while i < report.results.length() {
    lines.push("- " + report.results[i].to_line())
    i = i + 1
  }
  join_strings(lines, "\n")
}