///|
/// Coverage report for one behavior tree execution trace.
pub(all) struct TraceCoverage {
  total_nodes : Int
  visited_nodes : Array[String]
  missed_nodes : Array[String]
  success_nodes : Array[String]
  failure_nodes : Array[String]
  running_nodes : Array[String]
} derive(Debug, Eq)

///|
pub fn TraceCoverage::visited_count(self : TraceCoverage) -> Int {
  self.visited_nodes.length()
}

///|
pub fn TraceCoverage::missed_count(self : TraceCoverage) -> Int {
  self.missed_nodes.length()
}

///|
pub fn TraceCoverage::is_full(self : TraceCoverage) -> Bool {
  self.missed_nodes.length() == 0
}

///|
pub fn TraceCoverage::percent(self : TraceCoverage) -> Int {
  if self.total_nodes == 0 {
    100
  } else {
    self.visited_nodes.length() * 100 / self.total_nodes
  }
}

///|
pub fn TraceCoverage::summary(self : TraceCoverage) -> String {
  "coverage=" +
  self.percent().to_string() +
  "%, visited=" +
  self.visited_nodes.length().to_string() +
  "/" +
  self.total_nodes.to_string() +
  ", missed=" +
  self.missed_nodes.length().to_string()
}

///|
pub fn TraceCoverage::lines(self : TraceCoverage) -> Array[String] {
  let out = Array::new()
  out.push(self.summary())
  out.push("visited: " + join_strings(self.visited_nodes, ", "))
  if self.missed_nodes.length() > 0 {
    out.push("missed: " + join_strings(self.missed_nodes, ", "))
  }
  if self.success_nodes.length() > 0 {
    out.push("success: " + join_strings(self.success_nodes, ", "))
  }
  if self.failure_nodes.length() > 0 {
    out.push("failure: " + join_strings(self.failure_nodes, ", "))
  }
  if self.running_nodes.length() > 0 {
    out.push("running: " + join_strings(self.running_nodes, ", "))
  }
  out
}

///|
pub fn coverage_from_trace(
  tree : BehaviorTree,
  events : Array[TickEvent],
) -> TraceCoverage {
  let visited = Array::new()
  let success = Array::new()
  let failure = Array::new()
  let running = Array::new()
  let mut i = 0
  while i < events.length() {
    let id = events[i].node_id
    if tree.has_node(id) && !string_array_contains(visited, id) {
      visited.push(id)
    }
    if tree.has_node(id) {
      match events[i].status {
        Success =>
          if !string_array_contains(success, id) {
            success.push(id)
          }
        Failure =>
          if !string_array_contains(failure, id) {
            failure.push(id)
          }
        Running =>
          if !string_array_contains(running, id) {
            running.push(id)
          }
      }
    }
    i = i + 1
  }
  let missed = Array::new()
  let mut n = 0
  while n < tree.nodes.length() {
    if !string_array_contains(visited, tree.nodes[n].id) {
      missed.push(tree.nodes[n].id)
    }
    n = n + 1
  }
  {
    total_nodes: tree.node_count(),
    visited_nodes: visited,
    missed_nodes: missed,
    success_nodes: success,
    failure_nodes: failure,
    running_nodes: running,
  }
}

///|
pub fn fixture_coverage(name : String) -> Result[TraceCoverage, BtError] {
  match load_fixture(name) {
    Ok(doc) => {
      let engine = new_engine(doc.tree, blackboard=doc.blackboard)
      match engine.run_until_done(max_ticks=32) {
        Ok(_) => Ok(coverage_from_trace(doc.tree, engine.trace))
        Err(err) => Err(InvalidTree(err.message()))
      }
    }
    Err(err) => Err(err)
  }
}

///|
pub(all) struct CatalogCoverage {
  fixtures : Int
  full_coverage : Int
  partial_coverage : Int
  lines : Array[String]
} derive(Debug, Eq)

///|
pub fn CatalogCoverage::summary(self : CatalogCoverage) -> String {
  "fixtures=" +
  self.fixtures.to_string() +
  ", full=" +
  self.full_coverage.to_string() +
  ", partial=" +
  self.partial_coverage.to_string()
}

///|
pub fn fixture_catalog_coverage() -> CatalogCoverage {
  let specs = fixture_catalog()
  let lines = Array::new()
  let mut full = 0
  let mut partial = 0
  let mut i = 0
  while i < specs.length() {
    match fixture_coverage(specs[i].name) {
      Ok(report) => {
        if report.is_full() {
          full = full + 1
        } else {
          partial = partial + 1
        }
        lines.push(specs[i].name + ": " + report.summary())
      }
      Err(err) => {
        partial = partial + 1
        lines.push(specs[i].name + ": error " + err.message())
      }
    }
    i = i + 1
  }
  {
    fixtures: specs.length(),
    full_coverage: full,
    partial_coverage: partial,
    lines,
  }
}

///|
pub fn coverage_markdown(name : String, coverage : TraceCoverage) -> String {
  let lines = Array::new()
  lines.push("## Coverage: " + name)
  lines.push("")
  let raw = coverage.lines()
  let mut i = 0
  while i < raw.length() {
    lines.push("- " + raw[i])
    i = i + 1
  }
  join_strings(lines, "\n")
}