///|
/// Deterministic expectations captured from one behavior tree asset run.
pub(all) struct RegressionBaseline {
  name : String
  expected_status : BtStatus
  expected_digest : String
  expected_ticks : Int
  minimum_coverage : Int
  max_ticks : Int
} derive(Debug, Eq)

///|
pub fn regression_baseline(
  name : String,
  expected_status : BtStatus,
  expected_digest : String,
  expected_ticks : Int,
  minimum_coverage : Int,
  max_ticks? : Int,
) -> RegressionBaseline {
  {
    name,
    expected_status,
    expected_digest,
    expected_ticks,
    minimum_coverage,
    max_ticks: max_ticks.unwrap_or(32),
  }
}

///|
pub fn RegressionBaseline::summary(self : RegressionBaseline) -> String {
  self.name +
  ": status=" +
  self.expected_status.to_text() +
  ", ticks=" +
  self.expected_ticks.to_string() +
  ", coverage>=" +
  self.minimum_coverage.to_string() +
  "%, digest=" +
  self.expected_digest
}

///|
/// Result of checking an asset against a deterministic baseline.
pub(all) struct BaselineReport {
  name : String
  expected_status : BtStatus
  actual_status : BtStatus
  expected_digest : String
  actual_digest : String
  expected_ticks : Int
  actual_ticks : Int
  minimum_coverage : Int
  actual_coverage : Int
  status_matches : Bool
  digest_matches : Bool
  ticks_match : Bool
  coverage_meets : Bool
} derive(Debug, Eq)

///|
pub fn BaselineReport::ok(self : BaselineReport) -> Bool {
  self.status_matches &&
  self.digest_matches &&
  self.ticks_match &&
  self.coverage_meets
}

///|
pub fn BaselineReport::failed_checks(self : BaselineReport) -> Int {
  let mut failed = 0
  if !self.status_matches {
    failed = failed + 1
  }
  if !self.digest_matches {
    failed = failed + 1
  }
  if !self.ticks_match {
    failed = failed + 1
  }
  if !self.coverage_meets {
    failed = failed + 1
  }
  failed
}

///|
pub fn BaselineReport::summary(self : BaselineReport) -> String {
  self.name +
  ": ok=" +
  (if self.ok() { "true" } else { "false" }) +
  ", failed_checks=" +
  self.failed_checks().to_string() +
  ", status=" +
  self.actual_status.to_text() +
  ", ticks=" +
  self.actual_ticks.to_string() +
  ", coverage=" +
  self.actual_coverage.to_string() +
  "%, digest=" +
  self.actual_digest
}

///|
pub fn BaselineReport::lines(self : BaselineReport) -> Array[String] {
  [
    self.summary(),
    "status: expected=" +
    self.expected_status.to_text() +
    ", actual=" +
    self.actual_status.to_text() +
    ", match=" +
    bool_text(self.status_matches),
    "digest: expected=" +
    self.expected_digest +
    ", actual=" +
    self.actual_digest +
    ", match=" +
    bool_text(self.digest_matches),
    "ticks: expected=" +
    self.expected_ticks.to_string() +
    ", actual=" +
    self.actual_ticks.to_string() +
    ", match=" +
    bool_text(self.ticks_match),
    "coverage: minimum=" +
    self.minimum_coverage.to_string() +
    "%, actual=" +
    self.actual_coverage.to_string() +
    "%, pass=" +
    bool_text(self.coverage_meets),
  ]
}

///|
pub fn BaselineReport::markdown(self : BaselineReport) -> String {
  let lines = Array::new()
  lines.push("# MoonBTKit Regression: " + self.name)
  lines.push("")
  let details = self.lines()
  let mut i = 0
  while i < details.length() {
    lines.push("- " + details[i])
    i = i + 1
  }
  join_strings(lines, "\n")
}

///|
/// Capture a baseline from a successful deterministic DSL execution.
pub fn capture_baseline(
  name : String,
  source : String,
  max_ticks? : Int,
) -> Result[RegressionBaseline, BtError] {
  let limit = max_ticks.unwrap_or(32)
  match run_baseline_source(source, limit) {
    Ok(run) =>
      Ok({
        name,
        expected_status: run.status,
        expected_digest: run.digest,
        expected_ticks: run.ticks,
        minimum_coverage: run.coverage,
        max_ticks: limit,
      })
    Err(err) => Err(err)
  }
}

///|
/// Verify a DSL asset against status, digest, tick, and coverage expectations.
pub fn verify_baseline(
  source : String,
  baseline : RegressionBaseline,
) -> Result[BaselineReport, BtError] {
  match run_baseline_source(source, baseline.max_ticks) {
    Ok(run) =>
      Ok({
        name: baseline.name,
        expected_status: baseline.expected_status,
        actual_status: run.status,
        expected_digest: baseline.expected_digest,
        actual_digest: run.digest,
        expected_ticks: baseline.expected_ticks,
        actual_ticks: run.ticks,
        minimum_coverage: baseline.minimum_coverage,
        actual_coverage: run.coverage,
        status_matches: baseline.expected_status == run.status,
        digest_matches: baseline.expected_digest == run.digest,
        ticks_match: baseline.expected_ticks == run.ticks,
        coverage_meets: run.coverage >= baseline.minimum_coverage,
      })
    Err(err) => Err(err)
  }
}

///|
priv struct BaselineRun {
  status : BtStatus
  digest : String
  ticks : Int
  coverage : Int
}

///|
fn run_baseline_source(
  source : String,
  max_ticks : Int,
) -> Result[BaselineRun, BtError] {
  match parse_dsl(source) {
    Ok(doc) => {
      let engine = new_engine(doc.tree, blackboard=doc.blackboard)
      match engine.run_until_done(max_ticks~) {
        Ok(result) => {
          let coverage = coverage_from_trace(doc.tree, engine.trace)
          Ok({
            status: result.status,
            digest: engine.trace_digest(),
            ticks: result.tick,
            coverage: coverage.percent(),
          })
        }
        Err(err) => Err(err)
      }
    }
    Err(err) => Err(err)
  }
}

///|
fn bool_text(value : Bool) -> String {
  if value {
    "true"
  } else {
    "false"
  }
}