///|
fn check(label : String, success : Bool) -> Unit {
  if !success {
    abort(label)
  }
}

///|
fn json_number(value : Json) -> Double {
  match value {
    Number(number, ..) => number
    _ => 0.0
  }
}

///|
fn main raise {
  let base = @lib.Delta::new().insert_plain("abcd")
  check("plain text", base.plain_text() == Some("abcd"))
  check("document", base.is_document())
  check("length", base.length() == 4)
  check("slice", base.slice(1, 3).plain_text() == Some("bc"))
  let parsed = @lib.Delta::parse_json("{\"ops\":[{\"insert\":\"abc\"}]}")
  check("JSON parse", parsed.plain_text() == Some("abc"))
  check("JSON round trip", @lib.Delta::from_json(parsed.to_json()) == parsed)
  let insert = @lib.Delta::new().retain_plain(2).insert_plain("X")
  check("apply insert", base.apply(insert).plain_text() == Some("abXcd"))
  let deletion = @lib.Delta::new().retain_plain(1).delete(2)
  check("apply deletion", base.apply(deletion).plain_text() == Some("ad"))
  let replace = @lib.Delta::new().retain_plain(1).delete(2).insert_plain("XY")
  check("invert", base.apply(replace).apply(replace.invert(base)) == base)
  let attrs = Json::object({ "bold": Json::boolean(true) })
  let formatted = @lib.Delta::new().insert_text("a", attrs)
  check(
    "attribute JSON",
    @lib.Delta::from_json(formatted.to_json()) == formatted,
  )
  check(
    "attribute diff",
    @lib.attributes_compose(
      @lib.empty_attributes(),
      @lib.attributes_diff(@lib.empty_attributes(), attrs),
      false,
    ) ==
    attrs,
  )
  let a = @lib.Delta::new().retain_plain(1).insert_plain("X")
  let b = @lib.Delta::new().retain_plain(1).insert_plain("Y")
  check(
    "OT converge",
    base.apply(a).apply(a.transform(b, true)) ==
    base.apply(b).apply(b.transform(a, false)),
  )
  check("position insert", a.transform_position(1, false) == 2)
  check("position priority", a.transform_position(1, true) == 1)
  check("position delete", deletion.transform_position(2, false) == 1)
  let texts = [
    "", "a", "b", "ab", "ba", "abc", "ac", "cab", "cat", "cut", "hello",
  ]
  let mut cases = 0
  for old in texts {
    for next in texts {
      let before = @lib.Delta::new().insert_plain(old)
      let after = @lib.Delta::new().insert_plain(next)
      let patch = before.diff(after)
      check("diff " + old + " => " + next, before.apply(patch) == after)
      cases += 1
    }
  }
  let embed = Json::object({ "image": Json::string("picture") })
  let with_embed = @lib.Delta::new()
    .insert_plain("a")
    .insert_embed(embed, @lib.empty_attributes())
    .insert_plain("b")
  let without_embed = @lib.Delta::new().insert_plain("ab")
  check(
    "embed insertion",
    without_embed.apply(without_embed.diff(with_embed)) == with_embed,
  )
  check(
    "embed deletion",
    with_embed.apply(with_embed.diff(without_embed)) == without_embed,
  )
  let emoji_before = @lib.Delta::new().insert_plain("a😀b")
  let emoji_after = @lib.Delta::new().insert_plain("a🚀b")
  check("UTF-16 length", emoji_before.length() == 4)
  let emoji_patch = emoji_before.diff(emoji_after)
  check("emoji diff", emoji_before.apply(emoji_patch) == emoji_after)
  let long_prefix = "A".repeat(2000)
  let long_before = @lib.Delta::new().insert_plain(long_prefix + "old")
  let long_after = @lib.Delta::new().insert_plain(long_prefix + "new")
  check(
    "large shared prefix diff",
    long_before.apply(long_before.diff(long_after)) == long_after,
  )
  let heading = Json::object({ "header": Json::number(2) })
  let line_doc = @lib.Delta::new()
    .insert_plain("Title")
    .insert_text("\n", heading)
    .insert_plain("body\n")
  let lines = line_doc.lines()
  check("line count", lines.length() == 2)
  check("line content", lines[0].content.plain_text() == Some("Title"))
  check("block formatting", lines[0].attributes == heading)
  let history = @lib.History::new(base, 2)
  history.apply(replace)
  check("history edit", history.document().plain_text() == Some("aXYd"))
  check("history undo", history.undo() && history.document() == base)
  check(
    "history redo",
    history.redo() && history.document().plain_text() == Some("aXYd"),
  )
  check("history revision", history.revision() == 3)
  let handlers = @lib.EmbedHandlers::new()
  handlers.register(
    "counter",
    @lib.EmbedHandler::new(
      (base_value, update, _) => {
        Json::number(json_number(base_value) + json_number(update))
      },
      (update, _) => Json::number(-json_number(update)),
      (_, other, _) => other,
    ),
  )
  let counter_base = @lib.Delta::new().insert_embed(
    Json::object({ "counter": Json::number(5) }),
    @lib.empty_attributes(),
  )
  let counter_update = @lib.Delta::new().retain_embed(
    Json::object({ "counter": Json::number(2) }),
    @lib.empty_attributes(),
  )
  let counter_changed = counter_base.apply_with_handlers(
    counter_update, handlers,
  )
  check(
    "embed handler compose",
    counter_changed ==
    @lib.Delta::new().insert_embed(
      Json::object({ "counter": Json::number(7) }),
      @lib.empty_attributes(),
    ),
  )
  check(
    "embed handler invert",
    counter_changed.apply_with_handlers(
      counter_update.invert_with_handlers(counter_base, handlers),
      handlers,
    ) ==
    counter_base,
  )
  let seen_lines : Array[String] = []
  let seen_line_attrs : Array[Json] = []
  line_doc.each_line((line, attrs, index) => {
    let text = match line.plain_text() {
      Some(value) => value
      None => ""
    }
    seen_lines.push(index.to_string() + ":" + text)
    seen_line_attrs.push(attrs)
    true
  })
  check("each_line count", seen_lines == ["0:Title", "1:body"])
  check("each_line format", seen_line_attrs[0] == heading)
  let custom_lines : Array[String] = []
  @lib.Delta::new()
  .insert_plain("one|two|three")
  .each_line(
    (line, _, _) => {
      match line.plain_text() {
        Some(value) => custom_lines.push(value)
        None => ()
      }
      true
    },
    newline="|",
  )
  check("custom newline", custom_lines == ["one", "two", "three"])
  let interior = "m".repeat(1500)
  let separated_before = @lib.Delta::new().insert_plain("A" + interior + "B")
  let separated_after = @lib.Delta::new().insert_plain("X" + interior + "Y")
  let separated_patch = separated_before.diff(separated_after)
  check(
    "large separated diff",
    separated_before.apply(separated_patch) == separated_after,
  )
  let mut max_delete = 0
  for op in separated_patch.operations() {
    if op.kind() == "delete" && op.length() > max_delete {
      max_delete = op.length()
    }
  }
  check("large diff retains interior", max_delete == 1)
  let repeated = @lib.Delta::new().insert_plain("aaa")
  let repeated_longer = @lib.Delta::new().insert_plain("aaaa")
  check(
    "cursor-aware insert",
    repeated.diff_at(repeated_longer, 1) ==
    @lib.Delta::new().retain_plain(1).insert_plain("a"),
  )
  check(
    "cursor-aware delete",
    repeated_longer.diff_at(repeated, 1) ==
    @lib.Delta::new().delete(1),
  )
  let non_document_lines : Array[@lib.Delta] = []
  @lib.Delta::new()
  .retain_plain(2)
  .each_line((line, _, _) => {
    non_document_lines.push(line)
    true
  })
  check("non-document has no lines", non_document_lines.is_empty())
  let iterator = @lib.Delta::new().insert_plain("abc").iterator()
  check("iterator peek", iterator.peek_length() == 3)
  check("iterator split", iterator.next(1).length() == 1)
  check("iterator rest", iterator.rest().length() == 1 && iterator.has_next())
  ignore(iterator.next_full())
  check("iterator exhausted", !iterator.has_next())
  let mut ot_cases = 0
  for text in ["", "a", "ab", "abc", "abcd"] {
    let document = @lib.Delta::new().insert_plain(text)
    let changes : Array[@lib.Delta] = []
    for position in 0..<=text.length() {
      changes.push(@lib.Delta::new().retain_plain(position).insert_plain("X"))
      changes.push(@lib.Delta::new().retain_plain(position).insert_plain("Y"))
      if position < text.length() {
        changes.push(@lib.Delta::new().retain_plain(position).delete(1))
      }
    }
    for first in changes {
      for second in changes {
        let left = document.apply(first).apply(first.transform(second, true))
        let right = document.apply(second).apply(second.transform(first, false))
        check("OT matrix on " + text, left == right)
        ot_cases += 1
      }
    }
  }
  println(
    "MoonDelta selftest passed: " +
    cases.to_string() +
    " diff pairs, " +
    ot_cases.to_string() +
    " OT pairs, plus core checks",
  )
}