///|
pub(all) struct BtAssertion {
  name : String
  ok : Bool
  detail : String
} derive(Debug, Eq)

///|
pub fn BtAssertion::to_line(self : BtAssertion) -> String {
  (if self.ok { "ok " } else { "fail " }) + self.name + ": " + self.detail
}

///|
pub(all) struct AssertionReport {
  name : String
  assertions : Array[BtAssertion]
} derive(Debug, Eq)

///|
pub fn AssertionReport::ok(self : AssertionReport) -> Bool {
  let mut i = 0
  while i < self.assertions.length() {
    if !self.assertions[i].ok {
      return false
    }
    i = i + 1
  }
  true
}

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

///|
pub fn AssertionReport::summary(self : AssertionReport) -> String {
  self.name +
  ": assertions=" +
  self.assertions.length().to_string() +
  ", failed=" +
  self.failed_count().to_string()
}

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

///|
pub fn new_assertion_report(name : String) -> AssertionReport {
  { name, assertions: Array::new() }
}

///|
pub fn AssertionReport::add(self : AssertionReport, assertion : BtAssertion) -> Unit {
  self.assertions.push(assertion)
}

///|
pub fn assert_true(name : String, condition : Bool, detail : String) -> BtAssertion {
  { name, ok: condition, detail }
}

///|
pub fn assert_tree_valid(tree : BehaviorTree) -> BtAssertion {
  let report = tree.validate()
  { name: "tree valid", ok: report.ok, detail: report.message() }
}

///|
pub fn assert_node_count(tree : BehaviorTree, expected : Int) -> BtAssertion {
  let actual = tree.node_count()
  {
    name: "node count",
    ok: actual == expected,
    detail: "expected " + expected.to_string() + ", got " + actual.to_string(),
  }
}

///|
pub fn assert_has_node(tree : BehaviorTree, id : String) -> BtAssertion {
  {
    name: "has node " + id,
    ok: tree.has_node(id),
    detail: if tree.has_node(id) { "present" } else { "missing" },
  }
}

///|
pub fn assert_status(actual : BtStatus, expected : BtStatus) -> BtAssertion {
  {
    name: "status",
    ok: actual == expected,
    detail: "expected " + expected.to_text() + ", got " + actual.to_text(),
  }
}

///|
pub fn assert_board_value(
  board : Array[(String, BtValue)],
  key : String,
  value : BtValue,
) -> BtAssertion {
  {
    name: "board " + key,
    ok: board_contains(board, key, value),
    detail: key + "=" + value.to_text(),
  }
}

///|
pub fn assert_trace_has(
  events : Array[TickEvent],
  node_id : String,
  status : BtStatus,
) -> BtAssertion {
  {
    name: "trace " + node_id,
    ok: trace_contains(events, node_id, status),
    detail: "expected " + node_id + " -> " + status.to_text(),
  }
}

///|
pub fn assert_lint_ok(tree : BehaviorTree) -> BtAssertion {
  let lint = lint_tree(tree)
  { name: "lint", ok: lint.ok, detail: lint.summary() }
}

///|
pub fn combine_assertion_reports(
  name : String,
  reports : Array[AssertionReport],
) -> AssertionReport {
  let out = new_assertion_report(name)
  let mut r = 0
  while r < reports.length() {
    let mut i = 0
    while i < reports[r].assertions.length() {
      out.add(reports[r].assertions[i])
      i = i + 1
    }
    r = r + 1
  }
  out
}

///|
pub(all) struct CaseExpectation {
  final_status : BtStatus
  required_board : Array[(String, BtValue)]
  max_ticks : Int
} derive(Debug, Eq)

///|
pub(all) struct CaseSpec {
  name : String
  dsl : String
  expectation : CaseExpectation
} derive(Debug, Eq)

///|
pub(all) struct CaseReport {
  name : String
  parsed : Bool
  run : RunSummary
  assertions : AssertionReport
} derive(Debug, Eq)

///|
pub fn CaseReport::ok(self : CaseReport) -> Bool {
  self.parsed && self.run.ok && self.assertions.ok()
}

///|
pub fn CaseReport::to_line(self : CaseReport) -> String {
  self.name +
  ": parsed=" +
  (if self.parsed { "true" } else { "false" }) +
  ", run_ok=" +
  (if self.run.ok { "true" } else { "false" }) +
  ", assertion_failed=" +
  self.assertions.failed_count().to_string()
}

///|
pub fn case_from_fixture(spec : FixtureSpec) -> CaseSpec {
  {
    name: spec.name,
    dsl: spec.dsl,
    expectation: {
      final_status: spec.expected,
      required_board: [],
      max_ticks: spec.max_ticks,
    },
  }
}

///|
pub fn fixture_cases() -> Array[CaseSpec] {
  let specs = fixture_catalog()
  let cases = Array::new()
  let mut i = 0
  while i < specs.length() {
    cases.push(case_from_fixture(specs[i]))
    i = i + 1
  }
  cases
}

///|
pub fn run_case(spec : CaseSpec) -> CaseReport {
  match parse_dsl(spec.dsl) {
    Ok(doc) => {
      let engine = new_engine(doc.tree, blackboard=doc.blackboard)
      match engine.run_until_done(max_ticks=spec.expectation.max_ticks) {
        Ok(result) => {
          let assertions = new_assertion_report(spec.name)
          assertions.add(assert_tree_valid(doc.tree))
          assertions.add(assert_status(result.status, spec.expectation.final_status))
          let mut i = 0
          while i < spec.expectation.required_board.length() {
            assertions.add(
              assert_board_value(
                result.blackboard,
                spec.expectation.required_board[i].0,
                spec.expectation.required_board[i].1,
              ),
            )
            i = i + 1
          }
          {
            name: spec.name,
            parsed: true,
            run: {
              name: spec.name,
              final_status: result.status,
              ticks: result.tick,
              trace_events: engine.trace.length(),
              digest: engine.trace_digest(),
              ok: result.status == spec.expectation.final_status,
              detail: "case executed",
            },
            assertions,
          }
        }
        Err(err) => {
          let assertions = new_assertion_report(spec.name)
          assertions.add(assert_true("run", false, err.message()))
          {
            name: spec.name,
            parsed: true,
            run: {
              name: spec.name,
              final_status: Failure,
              ticks: engine.tick_count,
              trace_events: engine.trace.length(),
              digest: engine.trace_digest(),
              ok: false,
              detail: err.message(),
            },
            assertions,
          }
        }
      }
    }
    Err(err) => {
      let assertions = new_assertion_report(spec.name)
      assertions.add(assert_true("parse", false, err.message()))
      {
        name: spec.name,
        parsed: false,
        run: {
          name: spec.name,
          final_status: Failure,
          ticks: 0,
          trace_events: 0,
          digest: "parse-error",
          ok: false,
          detail: err.message(),
        },
        assertions,
      }
    }
  }
}

///|
pub fn run_cases(cases : Array[CaseSpec]) -> Array[CaseReport] {
  let reports = Array::new()
  let mut i = 0
  while i < cases.length() {
    reports.push(run_case(cases[i]))
    i = i + 1
  }
  reports
}

///|
pub fn all_cases_ok(reports : Array[CaseReport]) -> Bool {
  let mut i = 0
  while i < reports.length() {
    if !reports[i].ok() {
      return false
    }
    i = i + 1
  }
  true
}

///|
pub fn case_report_lines(reports : Array[CaseReport]) -> Array[String] {
  let lines = Array::new()
  let mut failed = 0
  let mut i = 0
  while i < reports.length() {
    if !reports[i].ok() {
      failed = failed + 1
    }
    lines.push(reports[i].to_line())
    i = i + 1
  }
  lines.push(
    "cases=" +
    reports.length().to_string() +
    ", failed=" +
    failed.to_string(),
  )
  lines
}

///|
pub fn replace_blackboard_line(
  source : String,
  key : String,
  value : BtValue,
) -> String {
  let lines = source.replace_all(old="\r\n", new="\n").split("\n").collect()
  let out = Array::new()
  let mut replaced = false
  let mut i = 0
  while i < lines.length() {
    let line = lines[i].to_owned()
    let tokens = split_ascii_words(trim_ascii(strip_comment(line)))
    if tokens.length() == 3 && tokens[0] == "blackboard" && tokens[1] == key {
      out.push("blackboard " + key + " " + value.to_text())
      replaced = true
    } else {
      out.push(line)
    }
    i = i + 1
  }
  if !replaced {
    out.push("blackboard " + key + " " + value.to_text())
  }
  join_strings(out, "\n")
}

///|
pub fn boolean_variants(
  base_name : String,
  source : String,
  key : String,
  expected_true : BtStatus,
  expected_false : BtStatus,
  max_ticks : Int,
) -> Array[CaseSpec] {
  [
    {
      name: base_name + "_true",
      dsl: replace_blackboard_line(source, key, BoolValue(true)),
      expectation: { final_status: expected_true, required_board: [], max_ticks },
    },
    {
      name: base_name + "_false",
      dsl: replace_blackboard_line(source, key, BoolValue(false)),
      expectation: { final_status: expected_false, required_board: [], max_ticks },
    },
  ]
}