use serde_json::{json, Value};

/// Maximum recursion depth for ADF node rendering. Guards against
/// stack overflow from maliciously deep or cyclic documents.
const MAX_RENDER_DEPTH: usize = 128;

/// Convert an ADF (Atlassian Document Format) JSON value to plain text.
pub fn adf_to_text(value: &Value) -> String {
    let mut output = String::new();
    render_node(value, &mut output, 0);
    output.trim_end().to_string()
}

fn render_node(node: &Value, out: &mut String, depth: usize) {
    if depth > MAX_RENDER_DEPTH {
        return;
    }
    let node_type = node.get("type").and_then(|v| v.as_str()).unwrap_or("");

    match node_type {
        "doc" => render_children(node, out, depth),
        "paragraph" => {
            render_children(node, out, depth);
            out.push('\n');
        }
        "text" => {
            if let Some(text) = node.get("text").and_then(|v| v.as_str()) {
                out.push_str(text);
            }
        }
        "mention" => {
            let mention_text = node
                .get("attrs")
                .and_then(|a| a.get("text"))
                .and_then(|v| v.as_str())
                .or_else(|| {
                    node.get("attrs")
                        .and_then(|a| a.get("id"))
                        .and_then(|v| v.as_str())
                })
                .unwrap_or("@unknown");
            out.push_str(mention_text);
        }
        "hardBreak" => {
            out.push('\n');
        }
        "heading" => {
            let level = node
                .get("attrs")
                .and_then(|a| a.get("level"))
                .and_then(|v| v.as_u64())
                .unwrap_or(1);
            let hashes = "#".repeat(level as usize);
            out.push_str(&hashes);
            out.push(' ');
            render_children(node, out, depth);
            out.push('\n');
        }
        "bulletList" => {
            render_list(node, out, depth, false);
        }
        "orderedList" => {
            render_list(node, out, depth, true);
        }
        "listItem" => {
            render_children(node, out, depth);
        }
        "codeBlock" => {
            let language = node
                .get("attrs")
                .and_then(|a| a.get("language"))
                .and_then(|v| v.as_str())
                .unwrap_or("");
            out.push_str("```");
            out.push_str(language);
            out.push('\n');
            render_children(node, out, depth);
            out.push_str("```\n");
        }
        "blockquote" => {
            let mut inner = String::new();
            render_children(node, &mut inner, depth);
            for line in inner.lines() {
                out.push_str("> ");
                out.push_str(line);
                out.push('\n');
            }
        }
        "rule" => {
            out.push_str("---\n");
        }
        "table" => {
            render_table(node, out, depth);
        }
        _ => {
            // Unknown node — try to render children
            render_children(node, out, depth);
        }
    }
}

fn render_children(node: &Value, out: &mut String, depth: usize) {
    if let Some(children) = node.get("content").and_then(|v| v.as_array()) {
        for child in children {
            render_node(child, out, depth);
        }
    }
}

fn render_list(node: &Value, out: &mut String, depth: usize, ordered: bool) {
    let indent = "  ".repeat(depth);
    if let Some(items) = node.get("content").and_then(|v| v.as_array()) {
        for (i, item) in items.iter().enumerate() {
            let bullet = if ordered {
                format!("{}{}. ", indent, i + 1)
            } else {
                format!("{}- ", indent)
            };
            out.push_str(&bullet);
            let mut item_text = String::new();
            render_node(item, &mut item_text, depth + 1);
            out.push_str(item_text.trim());
            out.push('\n');
        }
    }
}

fn render_table(node: &Value, out: &mut String, depth: usize) {
    let indent = "  ".repeat(depth);
    let mut rows: Vec<Vec<String>> = Vec::new();

    if let Some(row_nodes) = node.get("content").and_then(|v| v.as_array()) {
        for row in row_nodes {
            let mut cells: Vec<String> = Vec::new();
            if let Some(cell_nodes) = row.get("content").and_then(|v| v.as_array()) {
                for cell in cell_nodes {
                    let mut cell_text = String::new();
                    render_children(cell, &mut cell_text, depth + 1);
                    let normalized = cell_text
                        .lines()
                        .map(str::trim)
                        .filter(|line| !line.is_empty())
                        .collect::<Vec<_>>()
                        .join(" <br> ");
                    cells.push(normalized);
                }
            }
            if !cells.is_empty() {
                rows.push(cells);
            }
        }
    }

    if rows.is_empty() {
        return;
    }

    let col_count = rows.iter().map(|r| r.len()).max().unwrap_or(0);
    for row in &mut rows {
        while row.len() < col_count {
            row.push(String::new());
        }
    }

    let mut widths = vec![3usize; col_count];
    for row in &rows {
        for (i, cell) in row.iter().enumerate() {
            widths[i] = widths[i].max(cell.len());
        }
    }

    for (idx, row) in rows.iter().enumerate() {
        out.push_str(&indent);
        out.push('|');
        for (i, cell) in row.iter().enumerate() {
            out.push(' ');
            out.push_str(cell);
            let pad = widths[i].saturating_sub(cell.len());
            out.push_str(&" ".repeat(pad));
            out.push(' ');
            out.push('|');
        }
        out.push('\n');

        if idx == 0 {
            out.push_str(&indent);
            out.push('|');
            for width in &widths {
                out.push(' ');
                out.push_str(&"-".repeat(*width));
                out.push(' ');
                out.push('|');
            }
            out.push('\n');
        }
    }
}

/// Inject ADF mention nodes into a document for each `@DisplayName` in `mention_map`.
///
/// Scans every text node and replaces `@DisplayName` occurrences with proper ADF
/// mention nodes that Jira resolves to user accounts.
pub fn inject_mentions(node: &mut Value, mention_map: &[(String, String)]) {
    if mention_map.is_empty() {
        return;
    }
    inject_mentions_node(node, mention_map, 0);
}

fn inject_mentions_node(node: &mut Value, mention_map: &[(String, String)], depth: usize) {
    if depth > MAX_RENDER_DEPTH {
        return;
    }
    if let Some(Value::Array(content)) = node.get_mut("content") {
        let old = std::mem::take(content);
        let mut new_content: Vec<Value> = Vec::new();
        for child in old {
            let child_type = child
                .get("type")
                .and_then(|t| t.as_str())
                .map(str::to_owned);
            let child_text = child
                .get("text")
                .and_then(|t| t.as_str())
                .map(str::to_owned);
            if child_type.as_deref() == Some("text") {
                if let Some(text) = child_text {
                    new_content.extend(expand_text_mentions(&text, mention_map));
                    continue;
                }
            }
            new_content.push(child);
        }
        *content = new_content;
    }
    if let Some(Value::Array(content)) = node.get_mut("content") {
        for child in content.iter_mut() {
            inject_mentions_node(child, mention_map, depth + 1);
        }
    }
}

fn expand_text_mentions(text: &str, mention_map: &[(String, String)]) -> Vec<Value> {
    let mut nodes: Vec<Value> = vec![json!({ "type": "text", "text": text })];
    for (display_name, account_id) in mention_map {
        let pattern = format!("@{display_name}");
        let mut next: Vec<Value> = Vec::new();
        for node in nodes {
            let node_type = node.get("type").and_then(|t| t.as_str()).map(str::to_owned);
            let node_text = node.get("text").and_then(|t| t.as_str()).map(str::to_owned);
            if node_type.as_deref() == Some("text") {
                if let Some(t) = node_text {
                    if t.contains(pattern.as_str()) {
                        let parts: Vec<&str> = t.split(pattern.as_str()).collect();
                        for (i, part) in parts.iter().enumerate() {
                            if !part.is_empty() {
                                next.push(json!({ "type": "text", "text": *part }));
                            }
                            if i < parts.len() - 1 {
                                next.push(json!({
                                    "type": "mention",
                                    "attrs": { "id": account_id, "text": pattern }
                                }));
                            }
                        }
                        continue;
                    }
                }
            }
            next.push(node);
        }
        nodes = next;
    }
    nodes
}

/// Collect all mentioned Jira account IDs found in an ADF document.
pub fn mentioned_account_ids(value: &Value) -> Vec<String> {
    let mut out = Vec::new();
    collect_mentioned_account_ids(value, &mut out, 0);
    out
}

fn collect_mentioned_account_ids(node: &Value, out: &mut Vec<String>, depth: usize) {
    if depth > MAX_RENDER_DEPTH {
        return;
    }
    if node.get("type").and_then(|v| v.as_str()) == Some("mention") {
        if let Some(id) = node
            .get("attrs")
            .and_then(|attrs| attrs.get("id"))
            .and_then(|id| id.as_str())
        {
            if !out.iter().any(|existing| existing == id) {
                out.push(id.to_string());
            }
        }
    }

    if let Some(children) = node.get("content").and_then(|v| v.as_array()) {
        for child in children {
            collect_mentioned_account_ids(child, out, depth + 1);
        }
    }
}

/// Convert plain text to ADF JSON — each non-empty line becomes a paragraph.
pub fn plain_text_to_adf(text: &str) -> Value {
    let mut content: Vec<Value> = text
        .lines()
        .filter(|l| !l.is_empty())
        .map(|line| {
            json!({
                "type": "paragraph",
                "content": [{ "type": "text", "text": line }]
            })
        })
        .collect();

    if content.is_empty() {
        content.push(json!({ "type": "paragraph", "content": [] }));
    }

    json!({ "version": 1, "type": "doc", "content": content })
}

/// Convert Markdown text to ADF JSON.
pub fn markdown_to_adf(markdown: &str) -> Value {
    use comrak::{parse_document, Arena, Options};

    let arena = Arena::new();
    let mut options = Options::default();
    options.extension.table = true;
    let root = parse_document(&arena, markdown, &options);

    let mut content: Vec<Value> = Vec::new();
    let mut unsupported: Vec<&'static str> = Vec::new();
    convert_node(root, &mut content, &mut unsupported);

    if !unsupported.is_empty() {
        content.push(json!({
            "type": "blockquote",
            "content": [{
                "type": "paragraph",
                "content": [{
                    "type": "text",
                    "text": format!(
                        "Unsupported Markdown constructs were flattened during conversion: {}",
                        unsupported.join(", ")
                    )
                }]
            }]
        }));
    }

    json!({
        "version": 1,
        "type": "doc",
        "content": content
    })
}

fn convert_node<'a>(
    node: &'a comrak::nodes::AstNode<'a>,
    out: &mut Vec<Value>,
    unsupported: &mut Vec<&'static str>,
) {
    use comrak::nodes::{ListType, NodeValue};

    match &node.data.borrow().value {
        NodeValue::Document => {
            for child in node.children() {
                convert_node(child, out, unsupported);
            }
        }
        NodeValue::Paragraph => {
            let inline_content = collect_inline_children(node, unsupported);
            if !inline_content.is_empty() {
                out.push(json!({
                    "type": "paragraph",
                    "content": inline_content
                }));
            }
        }
        NodeValue::Heading(heading) => {
            let inline_content = collect_inline_children(node, unsupported);
            out.push(json!({
                "type": "heading",
                "attrs": { "level": heading.level },
                "content": inline_content
            }));
        }
        NodeValue::List(list) => {
            let mut items: Vec<Value> = Vec::new();
            for child in node.children() {
                let mut item_content: Vec<Value> = Vec::new();
                convert_node(child, &mut item_content, unsupported);
                items.push(json!({
                    "type": "listItem",
                    "content": item_content
                }));
            }
            let list_type = match list.list_type {
                ListType::Ordered => "orderedList",
                ListType::Bullet => "bulletList",
            };
            out.push(json!({
                "type": list_type,
                "content": items
            }));
        }
        NodeValue::Item(_) => {
            for child in node.children() {
                convert_node(child, out, unsupported);
            }
        }
        NodeValue::CodeBlock(code) => {
            let language = code.info.trim().to_string();
            let text = code.literal.trim_end_matches('\n').to_string();
            out.push(json!({
                "type": "codeBlock",
                "attrs": { "language": language },
                "content": [{ "type": "text", "text": text }]
            }));
        }
        NodeValue::BlockQuote => {
            let mut inner: Vec<Value> = Vec::new();
            for child in node.children() {
                convert_node(child, &mut inner, unsupported);
            }
            out.push(json!({
                "type": "blockquote",
                "content": inner
            }));
        }
        NodeValue::Table(_) => {
            out.push(render_markdown_table(node, unsupported));
        }
        NodeValue::ThematicBreak => {
            out.push(json!({ "type": "rule" }));
        }
        NodeValue::HtmlBlock(html) => {
            note_unsupported(unsupported, "html block");
            let text = html.literal.trim_end_matches('\n');
            if !text.is_empty() {
                out.push(json!({
                    "type": "codeBlock",
                    "attrs": { "language": "html" },
                    "content": [{ "type": "text", "text": text }]
                }));
            }
        }
        NodeValue::DescriptionList
        | NodeValue::DescriptionItem(_)
        | NodeValue::DescriptionTerm
        | NodeValue::DescriptionDetails => {
            note_unsupported(unsupported, "description list");
            for child in node.children() {
                convert_node(child, out, unsupported);
            }
        }
        NodeValue::FootnoteDefinition(_) => {
            note_unsupported(unsupported, "footnote");
            for child in node.children() {
                convert_node(child, out, unsupported);
            }
        }
        NodeValue::LineBreak | NodeValue::SoftBreak => {}
        _ => {
            for child in node.children() {
                convert_node(child, out, unsupported);
            }
        }
    }
}

fn collect_inline_children<'a>(
    node: &'a comrak::nodes::AstNode<'a>,
    unsupported: &mut Vec<&'static str>,
) -> Vec<Value> {
    let mut inline_content: Vec<Value> = Vec::new();
    for child in node.children() {
        collect_inline(child, &mut inline_content, unsupported);
    }
    inline_content
}

fn collect_inline<'a>(
    node: &'a comrak::nodes::AstNode<'a>,
    out: &mut Vec<Value>,
    unsupported: &mut Vec<&'static str>,
) {
    use comrak::nodes::NodeValue;

    match &node.data.borrow().value {
        NodeValue::Text(text) => {
            out.push(json!({ "type": "text", "text": text }));
        }
        NodeValue::SoftBreak => {
            out.push(json!({ "type": "text", "text": " " }));
        }
        NodeValue::LineBreak => {
            out.push(json!({ "type": "hardBreak" }));
        }
        NodeValue::Code(code) => {
            out.push(json!({
                "type": "text",
                "text": code.literal,
                "marks": [{ "type": "code" }]
            }));
        }
        NodeValue::Strong => {
            apply_mark(node, out, unsupported, json!({ "type": "strong" }));
        }
        NodeValue::Emph => {
            apply_mark(node, out, unsupported, json!({ "type": "em" }));
        }
        NodeValue::Strikethrough => {
            apply_mark(node, out, unsupported, json!({ "type": "strike" }));
        }
        NodeValue::Link(link) => {
            apply_mark(
                node,
                out,
                unsupported,
                json!({
                    "type": "link",
                    "attrs": { "href": link.url.clone() }
                }),
            );
        }
        NodeValue::Image(link) => {
            let mut inner: Vec<Value> = Vec::new();
            for child in node.children() {
                collect_inline(child, &mut inner, unsupported);
            }
            let label = inner
                .iter()
                .filter_map(|item| item.get("text").and_then(|value| value.as_str()))
                .collect::<Vec<_>>()
                .join("");
            let text = if label.trim().is_empty() {
                link.url.clone()
            } else {
                format!("{} ({})", label.trim(), link.url)
            };
            out.push(json!({ "type": "text", "text": text }));
        }
        NodeValue::HtmlInline(html) => {
            note_unsupported(unsupported, "inline html");
            out.push(json!({ "type": "text", "text": html }));
        }
        _ => {
            for child in node.children() {
                collect_inline(child, out, unsupported);
            }
        }
    }
}

fn apply_mark<'a>(
    node: &'a comrak::nodes::AstNode<'a>,
    out: &mut Vec<Value>,
    unsupported: &mut Vec<&'static str>,
    mark: Value,
) {
    let mut inner: Vec<Value> = Vec::new();
    for child in node.children() {
        collect_inline(child, &mut inner, unsupported);
    }
    for mut item in inner {
        let marks = item.get("marks").cloned().unwrap_or_else(|| json!([]));
        let mut marks_arr = marks.as_array().cloned().unwrap_or_default();
        marks_arr.push(mark.clone());
        item["marks"] = json!(marks_arr);
        out.push(item);
    }
}

fn render_markdown_table<'a>(
    table: &'a comrak::nodes::AstNode<'a>,
    unsupported: &mut Vec<&'static str>,
) -> Value {
    let mut rows: Vec<Value> = Vec::new();

    for (row_idx, row) in table.children().enumerate() {
        let mut cells: Vec<Value> = Vec::new();
        for cell in row.children() {
            let mut block_content: Vec<Value> = Vec::new();
            let inline_content = collect_inline_children(cell, unsupported);
            if !inline_content.is_empty() {
                block_content.push(json!({
                    "type": "paragraph",
                    "content": inline_content
                }));
            }
            for child in cell.children() {
                convert_node(child, &mut block_content, unsupported);
            }
            if block_content.is_empty() {
                block_content.push(json!({ "type": "paragraph", "content": [] }));
            }
            cells.push(json!({
                "type": if row_idx == 0 { "tableHeader" } else { "tableCell" },
                "content": block_content
            }));
        }
        if !cells.is_empty() {
            rows.push(json!({ "type": "tableRow", "content": cells }));
        }
    }

    json!({ "type": "table", "content": rows })
}

fn note_unsupported(unsupported: &mut Vec<&'static str>, value: &'static str) {
    if !unsupported.contains(&value) {
        unsupported.push(value);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_adf_to_text_paragraph() {
        let adf = json!({
            "type": "doc",
            "content": [{
                "type": "paragraph",
                "content": [{ "type": "text", "text": "Hello world" }]
            }]
        });
        assert_eq!(adf_to_text(&adf), "Hello world");
    }

    #[test]
    fn test_adf_to_text_heading() {
        let adf = json!({
            "type": "doc",
            "content": [{
                "type": "heading",
                "attrs": { "level": 2 },
                "content": [{ "type": "text", "text": "Title" }]
            }]
        });
        assert!(adf_to_text(&adf).starts_with("## Title"));
    }

    #[test]
    fn test_markdown_to_adf_paragraph() {
        let adf = markdown_to_adf("Hello world");
        assert_eq!(adf["type"], "doc");
        assert_eq!(adf["content"][0]["type"], "paragraph");
    }

    #[test]
    fn test_markdown_to_adf_heading() {
        let adf = markdown_to_adf("# My Heading");
        assert_eq!(adf["content"][0]["type"], "heading");
        assert_eq!(adf["content"][0]["attrs"]["level"], 1);
    }

    #[test]
    fn test_markdown_to_adf_table_support() {
        let adf = markdown_to_adf("| Name | Status |\n| --- | --- |\n| API | Done |");
        assert_eq!(adf["content"][0]["type"], "table");
        assert_eq!(
            adf["content"][0]["content"][0]["content"][0]["type"],
            "tableHeader"
        );
        assert_eq!(
            adf["content"][0]["content"][1]["content"][1]["type"],
            "tableCell"
        );
        assert_eq!(
            adf["content"][0]["content"][0]["content"][0]["content"][0]["content"][0]["text"],
            "Name"
        );
        assert_eq!(
            adf["content"][0]["content"][1]["content"][1]["content"][0]["content"][0]["text"],
            "Done"
        );
    }

    #[test]
    fn test_markdown_to_adf_list_link_and_code_block() {
        let adf = markdown_to_adf("- [docs](https://example.com)\n\n```rust\nfn main() {}\n```");
        assert_eq!(adf["content"][0]["type"], "bulletList");
        assert_eq!(adf["content"][1]["type"], "codeBlock");
    }

    #[test]
    fn test_markdown_to_adf_unsupported_note() {
        let adf = markdown_to_adf("<div>raw html</div>");
        assert_eq!(adf["content"][0]["type"], "codeBlock");
        assert_eq!(adf["content"][1]["type"], "blockquote");
    }

    #[test]
    fn test_adf_table_to_markdown_table() {
        let adf = json!({
            "type": "table",
            "content": [
                {
                    "type": "tableRow",
                    "content": [
                        {"type": "tableHeader", "content": [{"type": "paragraph", "content": [{"type": "text", "text": "Name"}]}]},
                        {"type": "tableHeader", "content": [{"type": "paragraph", "content": [{"type": "text", "text": "Status"}]}]}
                    ]
                },
                {
                    "type": "tableRow",
                    "content": [
                        {"type": "tableCell", "content": [{"type": "paragraph", "content": [{"type": "text", "text": "API"}]}]},
                        {"type": "tableCell", "content": [{"type": "paragraph", "content": [{"type": "text", "text": "Done"}]}]}
                    ]
                }
            ]
        });
        let text = adf_to_text(&adf);
        assert!(text.contains("| Name "));
        assert!(text.contains("| API"));
    }

    #[test]
    fn test_mentioned_account_ids_collects_nested_mentions() {
        let adf = json!({
            "type": "doc",
            "content": [{
                "type": "paragraph",
                "content": [
                    { "type": "text", "text": "Hi " },
                    { "type": "mention", "attrs": { "id": "acct-1", "text": "@Mulham" } },
                    { "type": "text", "text": " and " },
                    { "type": "mention", "attrs": { "id": "acct-2", "text": "@Team" } },
                    { "type": "mention", "attrs": { "id": "acct-1", "text": "@Mulham" } }
                ]
            }]
        });

        assert_eq!(mentioned_account_ids(&adf), vec!["acct-1", "acct-2"]);
    }

    #[test]
    fn test_expand_text_mentions_single_mention_mid_sentence() {
        let nodes = expand_text_mentions(
            "fix @Alice done",
            &[("Alice".to_string(), "acct-1".to_string())],
        );

        assert_eq!(
            nodes,
            vec![
                json!({ "type": "text", "text": "fix " }),
                json!({ "type": "mention", "attrs": { "id": "acct-1", "text": "@Alice" } }),
                json!({ "type": "text", "text": " done" }),
            ]
        );
    }

    #[test]
    fn test_expand_text_mentions_handles_start_and_end_mentions() {
        let start = expand_text_mentions(
            "@Alice done",
            &[("Alice".to_string(), "acct-1".to_string())],
        );
        let end = expand_text_mentions(
            "done @Alice",
            &[("Alice".to_string(), "acct-1".to_string())],
        );

        assert_eq!(
            start,
            vec![
                json!({ "type": "mention", "attrs": { "id": "acct-1", "text": "@Alice" } }),
                json!({ "type": "text", "text": " done" }),
            ]
        );
        assert_eq!(
            end,
            vec![
                json!({ "type": "text", "text": "done " }),
                json!({ "type": "mention", "attrs": { "id": "acct-1", "text": "@Alice" } }),
            ]
        );
    }

    #[test]
    fn test_expand_text_mentions_handles_multiple_mentions_and_spaces() {
        let nodes = expand_text_mentions(
            "Ping @Alice and @Bob Marley today",
            &[
                ("Alice".to_string(), "acct-1".to_string()),
                ("Bob Marley".to_string(), "acct-2".to_string()),
            ],
        );

        assert_eq!(
            nodes,
            vec![
                json!({ "type": "text", "text": "Ping " }),
                json!({ "type": "mention", "attrs": { "id": "acct-1", "text": "@Alice" } }),
                json!({ "type": "text", "text": " and " }),
                json!({ "type": "mention", "attrs": { "id": "acct-2", "text": "@Bob Marley" } }),
                json!({ "type": "text", "text": " today" }),
            ]
        );
    }

    #[test]
    fn test_expand_text_mentions_no_match_leaves_text_node_intact() {
        let nodes = expand_text_mentions(
            "hello world",
            &[("Alice".to_string(), "acct-1".to_string())],
        );

        assert_eq!(
            nodes,
            vec![json!({ "type": "text", "text": "hello world" })]
        );
    }

    #[test]
    fn test_inject_mentions_empty_map_returns_early_without_changes() {
        let mut adf = json!({
            "type": "doc",
            "content": [{
                "type": "paragraph",
                "content": [{ "type": "text", "text": "hello @Alice" }]
            }]
        });
        let original = adf.clone();

        inject_mentions(&mut adf, &[]);

        assert_eq!(adf, original);
    }

    #[test]
    fn test_inject_mentions_traverses_nested_adf_nodes() {
        let mut adf = json!({
            "type": "doc",
            "content": [
                {
                    "type": "paragraph",
                    "content": [{ "type": "text", "text": "hello @Alice" }]
                },
                {
                    "type": "bulletList",
                    "content": [{
                        "type": "listItem",
                        "content": [{
                            "type": "paragraph",
                            "content": [{ "type": "text", "text": "cc @Bob Marley" }]
                        }]
                    }]
                }
            ]
        });

        inject_mentions(
            &mut adf,
            &[
                ("Alice".to_string(), "acct-1".to_string()),
                ("Bob Marley".to_string(), "acct-2".to_string()),
            ],
        );

        assert_eq!(
            adf["content"][0]["content"][0],
            json!({ "type": "text", "text": "hello " })
        );
        assert_eq!(
            adf["content"][0]["content"][1],
            json!({ "type": "mention", "attrs": { "id": "acct-1", "text": "@Alice" } })
        );
        assert_eq!(
            adf["content"][1]["content"][0]["content"][0]["content"][0],
            json!({ "type": "text", "text": "cc " })
        );
        assert_eq!(
            adf["content"][1]["content"][0]["content"][0]["content"][1],
            json!({ "type": "mention", "attrs": { "id": "acct-2", "text": "@Bob Marley" } })
        );
    }
}
