///|
/// Reusable boundary cases for callers that want a predictable preflight suite.
pub struct BoundaryCase {
  name : String
  input : String
  expected_valid : Bool
  focus : String
} derive(Debug, Eq)

///|
pub fn BoundaryCase::new(
  name : String,
  input : String,
  expected_valid : Bool,
  focus : String,
) -> BoundaryCase {
  { name, input, expected_valid, focus }
}

///|
pub fn BoundaryCase::is_empty(self : BoundaryCase) -> Bool {
  self.input == ""
}

///|
pub fn BoundaryCase::to_markdown(self : BoundaryCase) -> String {
  "| " +
  self.name +
  " | " +
  self.focus +
  " | " +
  self.expected_valid.to_string() +
  " |"
}

///|
pub struct BoundaryResult {
  case_name : String
  expected_valid : Bool
  actual_valid : Bool
  diagnostics : String
} derive(Debug, Eq)

///|
pub fn BoundaryResult::passed(self : BoundaryResult) -> Bool {
  self.expected_valid == self.actual_valid
}

///|
pub fn BoundaryResult::to_markdown(self : BoundaryResult) -> String {
  let status = if self.passed() { "pass" } else { "fail" }
  "| " +
  self.case_name +
  " | " +
  self.expected_valid.to_string() +
  " | " +
  self.actual_valid.to_string() +
  " | " +
  status +
  " | " +
  self.diagnostics +
  " |"
}

///|
pub struct BoundarySuite {
  name : String
  cases : Array[BoundaryCase]
} derive(Debug, Eq)

///|
pub fn BoundarySuite::new(
  name : String,
  cases : Array[BoundaryCase],
) -> BoundarySuite {
  { name, cases }
}

///|
pub fn BoundarySuite::parser_edges() -> BoundarySuite {
  BoundarySuite::new("parser boundary suite", [
    BoundaryCase::new("empty", "", false, "empty input"),
    BoundaryCase::new("whitespace", "   \n\t", false, "blank input"),
    BoundaryCase::new("small-json", "{\"name\":\"x\"}", false, "missing schema"),
    BoundaryCase::new(
      "trailing-json", "{\"name\":\"x\"} trailing", false, "trailing content",
    ),
    BoundaryCase::new("bad-json", "{\"name\":", false, "unterminated object"),
    BoundaryCase::new("bad-yaml", "name:\n  -", false, "incomplete sequence"),
    BoundaryCase::new(
      "duplicate-key", "name: x\nname: y", false, "duplicate mapping key",
    ),
    BoundaryCase::new(
      "deep-json", "{\"a\":{\"b\":{\"c\":1}}}", false, "incomplete schema",
    ),
  ])
}

///|
pub fn BoundarySuite::document_edges() -> BoundarySuite {
  BoundarySuite::new("document boundary suite", [
    BoundaryCase::new(
      "minimal-name", "{\"name\":\"x\"}", false, "required sections",
    ),
    BoundaryCase::new(
      "missing-palette", "{\"name\":\"x\",\"tagline\":\"y\"}", false, "palette",
    ),
    BoundaryCase::new("wrong-type", "{\"name\":12}", false, "scalar type"),
    BoundaryCase::new(
      "empty-array", "{\"name\":\"x\",\"palette\":{\"colors\":[]}}", false, "empty palette",
    ),
    BoundaryCase::new(
      "unknown-fields", "{\"name\":\"x\",\"unexpected\":true}", false, "forward compatibility",
    ),
  ])
}

///|
pub fn BoundarySuite::len(self : BoundarySuite) -> Int {
  self.cases.length()
}

///|
pub fn BoundarySuite::case_names(self : BoundarySuite) -> Array[String] {
  self.cases.map(fn(item) { item.name })
}

///|
pub fn BoundarySuite::run(self : BoundarySuite) -> Array[BoundaryResult] {
  self.cases.map(fn(item) {
    let parsed = parse_brand_document(item.input)
    BoundaryResult::{
      case_name: item.name,
      expected_valid: item.expected_valid,
      actual_valid: parsed.value is Some(_) && !parsed.diagnostics.has_errors(),
      diagnostics: parsed.diagnostics.summary(),
    }
  })
}

///|
pub fn BoundarySuite::passed(self : BoundarySuite) -> Bool {
  self.run().all(fn(result) { result.passed() })
}

///|
pub fn BoundarySuite::failures(self : BoundarySuite) -> Array[BoundaryResult] {
  self.run().filter(fn(result) { !result.passed() })
}

///|
pub fn BoundarySuite::to_markdown(self : BoundarySuite) -> String {
  let lines : Array[String] = [
    "## " + self.name,
    "",
    "| Case | Expected | Actual | Status | Diagnostics |",
    "| --- | --- | --- | --- | --- |",
  ]
  for result in self.run() {
    lines.push(result.to_markdown())
  }
  lines.join("\n")
}

///|
pub fn BoundarySuite::to_json(self : BoundarySuite) -> String {
  "{\"name\":\"" +
  json_escape(self.name) +
  "\",\"passed\":" +
  self.passed().to_string() +
  ",\"results\":[" +
  self
  .run()
  .map(fn(result) {
    "{\"case\":\"" +
    json_escape(result.case_name) +
    "\",\"passed\":" +
    result.passed().to_string() +
    ",\"diagnostics\":\"" +
    json_escape(result.diagnostics) +
    "\"}"
  })
  .join(",") +
  "]}"
}

///|
pub fn standard_boundary_suites() -> Array[BoundarySuite] {
  [BoundarySuite::parser_edges(), BoundarySuite::document_edges()]
}

///|
pub fn run_boundary_suites() -> Bool {
  standard_boundary_suites().all(fn(suite) { suite.passed() })
}

///|
pub fn boundary_report_markdown() -> String {
  let sections : Array[String] = ["# Boundary report", ""]
  for suite in standard_boundary_suites() {
    sections.push(suite.to_markdown())
    sections.push("")
  }
  sections.join("\n")
}

///|
pub fn boundary_report_json() -> String {
  "[" +
  standard_boundary_suites().map(fn(suite) { suite.to_json() }).join(",") +
  "]"
}