///|
/// One named behavior tree asset and its acceptance expectation.
pub(all) struct AssetSpec {
  name : String
  source : String
  expected_status : BtStatus
  max_ticks : Int
} derive(Debug, Eq)

///|
pub fn asset_spec(
  name : String,
  source : String,
  expected_status? : BtStatus,
  max_ticks? : Int,
) -> AssetSpec {
  {
    name,
    source,
    expected_status: expected_status.unwrap_or(Success),
    max_ticks: max_ticks.unwrap_or(32),
  }
}

///|
/// Compile and execution evidence for one catalog asset.
pub(all) struct AssetReport {
  name : String
  parsed : Bool
  lint_ok : Bool
  run_ok : Bool
  expected_status : BtStatus
  actual_status : BtStatus
  node_count : Int
  blackboard_entries : Int
  digest : String
  coverage : Int
  canonical_dsl : String
  issues : Array[String]
} derive(Debug, Eq)

///|
pub fn AssetReport::ok(self : AssetReport) -> Bool {
  self.parsed && self.lint_ok && self.run_ok && self.issues.length() == 0
}

///|
pub fn AssetReport::summary(self : AssetReport) -> String {
  self.name +
  ": ok=" +
  (if self.ok() { "true" } else { "false" }) +
  ", parsed=" +
  bool_text(self.parsed) +
  ", lint=" +
  bool_text(self.lint_ok) +
  ", run=" +
  bool_text(self.run_ok) +
  ", nodes=" +
  self.node_count.to_string() +
  ", coverage=" +
  self.coverage.to_string() +
  "%, digest=" +
  self.digest
}

///|
pub fn AssetReport::lines(self : AssetReport) -> Array[String] {
  let lines = Array::new()
  lines.push(self.summary())
  let mut i = 0
  while i < self.issues.length() {
    lines.push("issue: " + self.issues[i])
    i = i + 1
  }
  lines
}

///|
/// Batch report that keeps valid and invalid assets in one deterministic order.
pub(all) struct AssetCatalogReport {
  name : String
  assets : Array[AssetReport]
  duplicate_names : Array[String]
} derive(Debug, Eq)

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

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

///|
pub fn AssetCatalogReport::ok(self : AssetCatalogReport) -> Bool {
  self.failed() == 0 && self.duplicate_names.length() == 0
}

///|
pub fn AssetCatalogReport::summary(self : AssetCatalogReport) -> String {
  self.name +
  ": ok=" +
  bool_text(self.ok()) +
  ", assets=" +
  self.assets.length().to_string() +
  ", passed=" +
  self.passed().to_string() +
  ", failed=" +
  self.failed().to_string() +
  ", duplicate_names=" +
  self.duplicate_names.length().to_string()
}

///|
pub fn AssetCatalogReport::lines(self : AssetCatalogReport) -> Array[String] {
  let lines = Array::new()
  lines.push(self.summary())
  if self.duplicate_names.length() > 0 {
    lines.push("duplicates: " + join_strings(self.duplicate_names, ", "))
  }
  let mut i = 0
  while i < self.assets.length() {
    let details = self.assets[i].lines()
    let mut j = 0
    while j < details.length() {
      lines.push(details[j])
      j = j + 1
    }
    i = i + 1
  }
  lines
}

///|
pub fn AssetCatalogReport::markdown(self : AssetCatalogReport) -> String {
  let lines = Array::new()
  lines.push("# MoonBTKit Asset Catalog: " + self.name)
  lines.push("")
  lines.push("- " + self.summary())
  if self.duplicate_names.length() > 0 {
    lines.push("- duplicate names: " + join_strings(self.duplicate_names, ", "))
  }
  lines.push("")
  lines.push("## Assets")
  lines.push("")
  let mut i = 0
  while i < self.assets.length() {
    lines.push("### " + self.assets[i].name)
    lines.push("")
    let details = self.assets[i].lines()
    let mut j = 0
    while j < details.length() {
      lines.push("- " + details[j])
      j = j + 1
    }
    i = i + 1
  }
  join_strings(lines, "\n")
}

///|
/// Compile, lint, execute, and measure one behavior tree asset.
pub fn audit_asset(spec : AssetSpec) -> AssetReport {
  match parse_dsl(spec.source) {
    Ok(doc) => {
      let lint = lint_tree(doc.tree)
      let engine = new_engine(doc.tree, blackboard=doc.blackboard)
      match engine.run_until_done(max_ticks=spec.max_ticks) {
        Ok(result) => {
          let issues = Array::new()
          if result.status != spec.expected_status {
            issues.push(
              "expected status " +
              spec.expected_status.to_text() +
              ", got " +
              result.status.to_text(),
            )
          }
          if !lint.ok {
            let lint_lines = lint.lines()
            let mut i = 0
            while i < lint_lines.length() {
              issues.push("lint: " + lint_lines[i])
              i = i + 1
            }
          }
          let coverage = coverage_from_trace(doc.tree, engine.trace)
          {
            name: spec.name,
            parsed: true,
            lint_ok: lint.ok,
            run_ok: result.status == spec.expected_status,
            expected_status: spec.expected_status,
            actual_status: result.status,
            node_count: doc.tree.node_count(),
            blackboard_entries: doc.blackboard.len(),
            digest: engine.trace_digest(),
            coverage: coverage.percent(),
            canonical_dsl: canonicalize_document(doc),
            issues,
          }
        }
        Err(err) => failed_asset_report(spec, true, lint.ok, err.message())
      }
    }
    Err(err) => failed_asset_report(spec, false, false, err.message())
  }
}

///|
/// Audit every catalog entry without stopping after an invalid asset.
pub fn audit_asset_catalog(
  name : String,
  specs : Array[AssetSpec],
) -> AssetCatalogReport {
  let reports = Array::new()
  let mut i = 0
  while i < specs.length() {
    reports.push(audit_asset(specs[i]))
    i = i + 1
  }
  { name, assets: reports, duplicate_names: duplicate_asset_names(specs) }
}

///|
pub fn fixture_asset_specs() -> Array[AssetSpec] {
  let fixtures = fixture_catalog()
  let specs = Array::new()
  let mut i = 0
  while i < fixtures.length() {
    specs.push(
      asset_spec(
        fixtures[i].name,
        fixtures[i].dsl,
        expected_status=fixtures[i].expected,
        max_ticks=fixtures[i].max_ticks,
      ),
    )
    i = i + 1
  }
  specs
}

///|
pub fn audit_fixture_assets() -> AssetCatalogReport {
  audit_asset_catalog("built-in-fixtures", fixture_asset_specs())
}

///|
fn failed_asset_report(
  spec : AssetSpec,
  parsed : Bool,
  lint_ok : Bool,
  issue : String,
) -> AssetReport {
  {
    name: spec.name,
    parsed,
    lint_ok,
    run_ok: false,
    expected_status: spec.expected_status,
    actual_status: Failure,
    node_count: 0,
    blackboard_entries: 0,
    digest: if parsed {
      "run-error"
    } else {
      "parse-error"
    },
    coverage: 0,
    canonical_dsl: "",
    issues: [issue],
  }
}

///|
fn duplicate_asset_names(specs : Array[AssetSpec]) -> Array[String] {
  let duplicates = Array::new()
  let mut i = 0
  while i < specs.length() {
    let mut j = 0
    let mut seen_before = false
    while j < i {
      if specs[j].name == specs[i].name {
        seen_before = true
      }
      j = j + 1
    }
    if seen_before && !contains_string(duplicates, specs[i].name) {
      duplicates.push(specs[i].name)
    }
    i = i + 1
  }
  duplicates
}