///|
/// Render a deterministic tree dump intended for conformance tests.
pub fn to_test_format(node : @dom.Node) -> String {
  let lines : Array[String] = []
  match node.kind {
    Document | Fragment =>
      for child in node.children {
        write_test_format(child, lines, 0)
      }
    _ => write_test_format(node, lines, 0)
  }
  lines.join("\n")
}

///|
fn test_format_line(indent : Int, content : StringView) -> String {
  "| " + " ".repeat(indent) + content.to_owned()
}

///|
fn test_format_qualified_name(node : @dom.Node) -> String {
  match node.ns {
    Some(ns) if ns != "html" => ns + " " + node.name
    _ => node.name
  }
}

///|
fn is_foreign_attribute_for_test_format(name : StringView) -> Bool {
  match @syn.lower_ascii(name) {
    "xlink:actuate"
    | "xlink:arcrole"
    | "xlink:href"
    | "xlink:role"
    | "xlink:show"
    | "xlink:title"
    | "xlink:type"
    | "xml:lang"
    | "xml:space"
    | "xmlns"
    | "xmlns:xlink" => true
    _ => false
  }
}

///|
fn replace_colons_for_test_format(name : StringView) -> String {
  let out = StringBuilder::new(size_hint=name.length())
  for ch in name {
    if ch == ':' {
      out.write_char(' ')
    } else {
      out.write_char(ch)
    }
  }
  out.to_string()
}

///|
fn test_format_attribute_name(node : @dom.Node, name : StringView) -> String {
  match node.ns {
    Some(ns) if ns != "html" && is_foreign_attribute_for_test_format(name) =>
      replace_colons_for_test_format(name)
    _ => name.to_owned()
  }
}

///|
fn test_format_string_less(left : StringView, right : StringView) -> Bool {
  let mut left_pos = 0
  let mut right_pos = 0
  while left_pos < left.length() && right_pos < right.length() {
    let left_ch = left.get_char(left_pos).unwrap()
    let right_ch = right.get_char(right_pos).unwrap()
    if left_ch < right_ch {
      return true
    }
    if left_ch > right_ch {
      return false
    }
    left_pos += left_ch.utf16_len()
    right_pos += right_ch.utf16_len()
  }
  left.length() < right.length()
}

///|
fn test_format_attr_less(
  left : (String, String),
  right : (String, String),
) -> Bool {
  let (left_name, left_value) = left
  let (right_name, right_value) = right
  if left_name != right_name {
    return test_format_string_less(left_name, right_name)
  }
  test_format_string_less(left_value, right_value)
}

///|
fn sort_test_format_attrs(attrs : Array[(String, String)]) -> Unit {
  for index in 1.. 0 && test_format_attr_less(current, attrs[cursor - 1]) {
      attrs[cursor] = attrs[cursor - 1]
      cursor -= 1
    }
    attrs[cursor] = current
  }
}

///|
fn write_test_format_attributes(
  node : @dom.Node,
  out : Array[String],
  indent : Int,
) -> Unit {
  let attrs : Array[(String, String)] = []
  for name, value in node.attrs {
    let display_name = test_format_attribute_name(node, name)
    let value = match value {
      Some(value) => value
      None => ""
    }
    attrs.push((display_name, value))
  }
  sort_test_format_attrs(attrs)
  for attr in attrs {
    let (name, value) = attr
    out.push(test_format_line(indent + 2, name + "=\"" + value + "\""))
  }
}

///|
fn doctype_test_format(node : @dom.Node) -> String {
  let out = StringBuilder::new()
  out.write_string("|  value
      None => ""
    }
    let system_id = match node.system_id {
      Some(value) => value
      None => ""
    }
    out.write_string(" \"")
    out.write_string(public_id)
    out.write_string("\" \"")
    out.write_string(system_id)
    out.write_char('"')
  }
  out.write_char('>')
  out.to_string()
}

///|
fn write_test_format(
  node : @dom.Node,
  out : Array[String],
  indent : Int,
) -> Unit {
  match node.kind {
    Document | Fragment =>
      out.push(test_format_line(indent, "<" + node.name + ">"))
    Element => {
      out.push(
        test_format_line(indent, "<" + test_format_qualified_name(node) + ">"),
      )
      write_test_format_attributes(node, out, indent)
    }
    Text =>
      if !node.sanitize_escape_only {
        out.push(test_format_line(indent, "\"" + node.data + "\""))
      }
    Comment => out.push(test_format_line(indent, ""))
    Doctype => out.push(doctype_test_format(node))
  }
  for child in node.children {
    write_test_format(child, out, indent + 2)
  }
}