///|
/// Ordered XML content. Editing returns new values; no shared mutable children.
pub(all) enum JUnitNode {
  Element(JUnitElement)
  Text(String)
  CData(String)
  Comment(String)
  Instruction(String, String)
}

///|
/// A report element, including producer-specific attributes and children.
/// XML validity is checked when a document is serialized.
pub struct JUnitElement {
  name : String
  attributes : ReadOnlyArray[(String, String)]
  content : ReadOnlyArray[JUnitNode]
}

///|
pub fn JUnitElement::new(
  name : String,
  attributes~ : Array[(String, String)] = [],
  content~ : Array[JUnitNode] = [],
) -> JUnitElement {
  {
    name,
    attributes: ReadOnlyArray::from_array(attributes.iter().to_array()),
    content: ReadOnlyArray::from_array(content.iter().to_array()),
  }
}

///|
pub fn JUnitElement::attribute(self : JUnitElement, name : String) -> String? {
  for pair in self.attributes {
    if pair.0 == name {
      return Some(pair.1)
    }
  }
  None
}

///|
/// Set one attribute, replacing all earlier occurrences of that name.
pub fn JUnitElement::with_attribute(
  self : JUnitElement,
  name : String,
  value : String,
) -> JUnitElement {
  let attrs = self.attributes.iter().filter(pair => pair.0 != name).to_array()
  attrs.push((name, value))
  { ..self, attributes: ReadOnlyArray::from_array(attrs), }
}

///|
pub fn JUnitElement::without_attribute(
  self : JUnitElement,
  name : String,
) -> JUnitElement {
  {
    ..self,
    attributes: ReadOnlyArray::from_array(
      self.attributes.iter().filter(pair => pair.0 != name).to_array(),
    ),
  }
}

///|
pub fn JUnitElement::with_content(
  self : JUnitElement,
  content : Array[JUnitNode],
) -> JUnitElement {
  { ..self, content: ReadOnlyArray::from_array(content.iter().to_array()), }
}

///|
pub fn JUnitElement::append(
  self : JUnitElement,
  child : JUnitNode,
) -> JUnitElement {
  let content = self.content.iter().to_array()
  content.push(child)
  { ..self, content: ReadOnlyArray::from_array(content), }
}

///|
/// Direct child elements, in source order. An omitted name selects all.
pub fn JUnitElement::elements(
  self : JUnitElement,
  name~ : String? = None,
) -> Array[JUnitElement] {
  let result = []
  for node in self.content {
    if node is Element(child) && (name == None || name == Some(child.name)) {
      result.push(child)
    }
  }
  result
}

///|
pub fn JUnitElement::suite(name : String) -> JUnitElement {
  JUnitElement::new("testsuite", attributes=[("name", name)])
}

///|
pub fn JUnitElement::testcase(
  name : String,
  classname~ : String? = None,
  time_us~ : Int64? = None,
) -> JUnitElement raise InputError {
  if name == "" {
    raise InvalidInput("testcase name must not be empty")
  }
  let mut element = JUnitElement::new("testcase", attributes=[("name", name)])
  if classname is Some(value) {
    element = element.with_attribute("classname", value)
  }
  if time_us is Some(value) {
    if value < 0L {
      raise InvalidInput("testcase time must be nonnegative microseconds")
    }
    element = element.with_attribute("time", document_seconds(value))
  }
  element
}

///|
/// Final or Surefire retry result. Multiple result elements remain separate.
pub fn JUnitElement::result(
  kind : String,
  message~ : String = "",
  type_~ : String = "",
  body~ : String = "",
) -> JUnitElement raise InputError {
  if !document_result_tag(kind) {
    raise InvalidInput("unsupported JUnit result tag: \{kind}")
  }
  JUnitElement::new(kind, attributes=[("message", message), ("type", type_)], content=[
    Text(body),
  ])
}

///|
/// Append a property without discarding existing or duplicate property entries.
pub fn JUnitElement::add_property(
  self : JUnitElement,
  name : String,
  value : String,
) -> JUnitElement {
  let property = Element(
    JUnitElement::new("property", attributes=[("name", name), ("value", value)]),
  )
  let content = self.content.iter().to_array()
  for i = 0; i < content.length(); i = i + 1 {
    if content[i] is Element(child) && child.name == "properties" {
      content[i] = Element(child.append(property))
      return self.with_content(content)
    }
  }
  content.push(Element(JUnitElement::new("properties", content=[property])))
  self.with_content(content)
}

///|
/// Replace direct system-out or system-err children with one text element.
pub fn JUnitElement::with_output(
  self : JUnitElement,
  kind : String,
  text : String,
) -> JUnitElement raise InputError {
  if kind != "system-out" && kind != "system-err" {
    raise InvalidInput("output kind must be system-out or system-err")
  }
  let content = self.content
    .iter()
    .filter(node => {
      match node {
        Element(child) => child.name != kind
        _ => true
      }
    })
    .to_array()
  content.push(Element(JUnitElement::new(kind, content=[Text(text)])))
  self.with_content(content)
}

///|
/// Replace final result elements, preserving retry markers and other metadata.
pub fn JUnitElement::with_results(
  self : JUnitElement,
  results : Array[JUnitElement],
) -> JUnitElement raise InputError {
  if self.name != "testcase" {
    raise InvalidInput("results belong to testcase")
  }
  for result in results {
    if result.name != "failure" &&
      result.name != "error" &&
      result.name != "skipped" {
      raise InvalidInput("final result must be failure, error or skipped")
    }
  }
  let content = self.content
    .iter()
    .filter(node => {
      match node {
        Element(child) =>
          child.name != "failure" &&
          child.name != "error" &&
          child.name != "skipped"
        _ => true
      }
    })
    .to_array()
  for result in results {
    content.push(Element(result))
  }
  self.with_content(content)
}

///|
fn document_result_tag(name : String) -> Bool {
  match name {
    "failure"
    | "error"
    | "skipped"
    | "flakyFailure"
    | "flakyError"
    | "rerunFailure"
    | "rerunError" => true
    _ => false
  }
}

///|
fn document_seconds(value : Int64) -> String {
  let fraction = (value % 1000000L).to_string()
  "\{value / 1000000L}.\{String::make(6 - fraction.length(), '0')}\{fraction}"
}