use texform_core::{
    ast::{Argument, ArgumentKind, ArgumentValue, Ast, ContentMode, GroupKind, Node},
    parse::ParseContext,
    serialize::{
        AdjacentCharSpacing, CommandSpacing, EnvironmentNameSpacing, InfixGrouping,
        MathGroupInnerSpacing, MathScriptOptions, ScriptOrder, ScriptSpacing, SerializeOptions,
        SyntaxSerializeOptions, serialize, serialize_with,
    },
};

fn parse_to_ast(src: &str) -> texform_core::ast::Ast {
    let document = ParseContext::shared()
        .parse(src, &texform_core::parse::ParseConfig::STRICT)
        .try_into_document()
        .unwrap();
    Ast::from_syntax_root(&document.0.to_syntax())
}

#[test]
fn try_into_document_returns_diagnostics_present_when_partial_tree_has_errors() {
    let error = ParseContext::shared()
        .parse(
            r"\text{\frac{a}{b}}",
            &texform_core::parse::ParseConfig::default(),
        )
        .try_into_document()
        .expect_err("partial parses with diagnostics should not produce a document");

    assert!(error.document().is_some(), "expected partial document");
    assert!(
        !error.diagnostics().is_empty(),
        "expected parse diagnostics"
    );
}

#[test]
fn try_into_document_returns_no_document_when_strict_parse_fails() {
    let error = ParseContext::shared()
        .parse(r"\unknowncmd", &texform_core::parse::ParseConfig::STRICT)
        .try_into_document()
        .expect_err("strict parse failures should not produce a document");

    assert!(error.document().is_none(), "expected no document");
    assert!(
        !error.diagnostics().is_empty(),
        "expected parse diagnostics"
    );
}

#[test]
fn test_serialize_simple_math_chars() {
    let ast = parse_to_ast("ab");
    assert_eq!(serialize(&ast), "a b");
}

#[test]
fn test_serialize_root_does_not_emit_extra_braces() {
    let mut ast = Ast::new();
    let root = ast.root();
    let left = ast.new_node(Node::Char('a'));
    let plus = ast.new_node(Node::Char('+'));
    let right = ast.new_node(Node::Char('b'));

    ast.append_child(root, left);
    ast.append_child(root, plus);
    ast.append_child(root, right);

    assert_eq!(serialize(&ast), "a + b");
}

#[test]
fn test_serialize_with_minimal_command_spacing() {
    let ast = parse_to_ast(r"\sqrt{a}");
    let mut options = SerializeOptions::default();
    options.math.spacing.commands = CommandSpacing::Minimal;

    assert_eq!(serialize_with(&ast, &options), r"\sqrt{ a }");
}

#[test]
fn test_compact_math_group_inner_spacing_affects_command_wrapper_braces() {
    let ast = parse_to_ast(r"\sqrt{a}");
    let mut options = SerializeOptions::default();
    options.math.spacing.group_inner_spacing = MathGroupInnerSpacing::Compact;

    assert_eq!(serialize_with(&ast, &options), r"\sqrt {a}");
}

#[test]
fn test_serialize_with_compact_adjacent_char_spacing() {
    let ast = parse_to_ast("a+b");
    let mut options = SerializeOptions::default();
    options.math.spacing.adjacent_chars = AdjacentCharSpacing::Compact;

    assert_eq!(serialize_with(&ast, &options), "a+b");
}

#[test]
fn test_serialize_manual_nodes_for_groups_and_literals() {
    let mut ast = Ast::new();
    let root = ast.root();

    let explicit = ast.new_node(Node::Group {
        children: Vec::new(),
        kind: GroupKind::Explicit,
        mode: ContentMode::Math,
    });
    let implicit = ast.new_node(Node::Group {
        children: Vec::new(),
        kind: GroupKind::Implicit,
        mode: ContentMode::Math,
    });
    let x = ast.new_node(Node::Char('x'));
    let y = ast.new_node(Node::Char('y'));
    let unknown = ast.new_node(Node::Command {
        name: "mystery".to_string(),
        args: vec![],
        known: false,
    });
    let active_space = ast.new_node(Node::ActiveSpace);
    let text = ast.new_node(Node::Text("abc".to_string()));

    ast.append_child(explicit, x);
    ast.append_child(implicit, y);
    ast.append_child(root, explicit);
    ast.append_child(root, implicit);
    ast.append_child(root, unknown);
    ast.append_child(root, active_space);
    ast.append_child(root, text);

    assert_eq!(serialize(&ast), r"{ x } { y } \mystery ~ abc");
}

#[test]
fn test_serialize_command_argument_does_not_double_wrap_group_content() {
    let mut ast = Ast::new();
    let root = ast.root();

    let group = ast.new_node(Node::Group {
        children: Vec::new(),
        kind: GroupKind::Implicit,
        mode: ContentMode::Math,
    });
    let x = ast.new_node(Node::Char('x'));
    ast.append_child(group, x);

    let command = ast.new_node(Node::Command {
        name: "sqrt".to_string(),
        args: vec![Some(Argument {
            kind: ArgumentKind::Mandatory,
            value: ArgumentValue::MathContent(group),
        })],
        known: true,
    });
    ast.append_child(root, command);

    assert_eq!(serialize(&ast), r"\sqrt { x }");
}

#[test]
fn test_serialize_delimited_group_with_none_delimiter() {
    let ast = parse_to_ast(r"\left. x\right|");

    assert_eq!(serialize(&ast), r"\left . x \right |");
}

#[test]
fn test_serialize_delimited_group_with_square_brackets() {
    let ast = parse_to_ast(r"\left[a+b\right]");

    assert_eq!(serialize(&ast), r"\left [ a + b \right ]");
}

#[test]
fn test_serialize_scripted_nodes_use_sub_first_and_explicit_grouping() {
    let ast = parse_to_ast("x^2_i");

    assert_eq!(serialize(&ast), "x _ { i } ^ { 2 }");
}

#[test]
fn test_serialize_prime_superscript_uses_shorthand() {
    assert_eq!(serialize(&parse_to_ast("f'")), "f'");
    assert_eq!(serialize(&parse_to_ast("f''")), "f''");
}

#[test]
fn test_serialize_prime_superscript_respects_script_order() {
    assert_eq!(serialize(&parse_to_ast("f_n'")), "f _ { n }'");

    let ast = parse_to_ast("f_n'");
    let mut options = SerializeOptions::default();
    options.math.scripts.order = ScriptOrder::SupFirst;

    assert_eq!(serialize_with(&ast, &options), "f' _ { n }");
}

#[test]
fn test_serialize_mixed_prime_superscript_keeps_script_group() {
    assert_eq!(serialize(&parse_to_ast("f'^2")), "f ^ { ' 2 }");
}

#[test]
fn test_serializer_option_surfaces_only_expose_live_fields() {
    let MathScriptOptions {
        spacing: _,
        order: _,
    } = MathScriptOptions::default();
    let SyntaxSerializeOptions { environments: _ } = SyntaxSerializeOptions::default();
}

#[test]
fn test_compact_math_group_inner_spacing_affects_script_wrapper_braces() {
    let ast = parse_to_ast("x^2_i");
    let mut options = SerializeOptions::default();
    options.math.spacing.group_inner_spacing = MathGroupInnerSpacing::Compact;

    assert_eq!(serialize_with(&ast, &options), "x _ {i} ^ {2}");
}

#[test]
fn test_serialize_with_sup_first_order() {
    let ast = parse_to_ast("x_i^2");
    let mut options = SerializeOptions::default();
    options.math.scripts.order = ScriptOrder::SupFirst;

    assert_eq!(serialize_with(&ast, &options), "x ^ { 2 } _ { i }");
}

#[test]
fn test_implicit_and_explicit_groups_share_text_form() {
    let mut ast = Ast::new();
    let root = ast.root();
    let implicit = ast.new_node(Node::Group {
        children: Vec::new(),
        kind: GroupKind::Implicit,
        mode: ContentMode::Math,
    });
    let explicit = ast.new_node(Node::Group {
        children: Vec::new(),
        kind: GroupKind::Explicit,
        mode: ContentMode::Math,
    });
    let x = ast.new_node(Node::Char('x'));
    let y = ast.new_node(Node::Char('y'));
    ast.append_child(implicit, x);
    ast.append_child(explicit, y);
    ast.append_child(root, implicit);
    ast.append_child(root, explicit);

    assert_eq!(serialize(&ast), "{ x } { y }");
}

#[test]
fn test_empty_group_uses_single_inner_padding_space() {
    let mut ast = Ast::new();
    let root = ast.root();
    let group = ast.new_node(Node::Group {
        children: Vec::new(),
        kind: GroupKind::Implicit,
        mode: ContentMode::Math,
    });
    ast.append_child(root, group);

    assert_eq!(serialize(&ast), "{ }");
}

#[test]
fn test_compact_math_group_inner_spacing_removes_brace_padding() {
    let ast = parse_to_ast("{} {a}");
    let mut options = SerializeOptions::default();
    options.math.spacing.group_inner_spacing = MathGroupInnerSpacing::Compact;

    assert_eq!(serialize_with(&ast, &options), "{} {a}");
}

#[test]
fn test_serialize_inline_math_keeps_text_mode_compact() {
    let ast = parse_to_ast(r"\text{ab$x$cd}");

    assert_eq!(serialize(&ast), r"\text {ab$x$cd}");
}

#[test]
fn test_serialize_text_argument_single_text_chunk_stays_compact() {
    let ast = parse_to_ast(r"\text{x}");

    assert_eq!(serialize(&ast), r"\text {x}");
}

#[test]
fn test_serialize_frac_and_text_argument() {
    let ast = parse_to_ast(r"\frac{a}{\text{abc}}");

    assert_eq!(serialize(&ast), r"\frac { a } { \text {abc} }");
}

#[test]
fn test_serialize_text_mode_single_char_argument_uses_text_content_variant() {
    let mut ast = Ast::new();
    let root = ast.root();
    let ch = ast.new_node(Node::Char('x'));
    let command = ast.new_node(Node::Command {
        name: "text".to_string(),
        args: vec![Some(Argument {
            kind: ArgumentKind::Mandatory,
            value: ArgumentValue::TextContent(ch),
        })],
        known: true,
    });
    ast.append_child(root, command);

    assert_eq!(serialize(&ast), r"\text {x}");
}

#[test]
fn test_serialize_scalar_arguments_stay_opaque() {
    let mut ast = Ast::new();
    let root = ast.root();
    let file = ast.new_node(Node::Text("file".to_string()));
    let command = ast.new_node(Node::Command {
        name: "includegraphics".to_string(),
        args: vec![
            Some(Argument {
                kind: ArgumentKind::Optional,
                value: ArgumentValue::KeyVal("width=1em".to_string()),
            }),
            Some(Argument {
                kind: ArgumentKind::Mandatory,
                value: ArgumentValue::TextContent(file),
            }),
        ],
        known: true,
    });
    ast.append_child(root, command);

    assert_eq!(serialize(&ast), r"\includegraphics [width=1em] {file}");
}

#[test]
fn test_serialize_other_scalar_argument_variants() {
    let mut ast = Ast::new();
    let root = ast.root();

    let label = ast.new_node(Node::Command {
        name: "label".to_string(),
        args: vec![Some(Argument {
            kind: ArgumentKind::Mandatory,
            value: ArgumentValue::CSName("sec:intro".to_string()),
        })],
        known: true,
    });
    let numeral = ast.new_node(Node::Command {
        name: "romannumeral".to_string(),
        args: vec![Some(Argument {
            kind: ArgumentKind::Mandatory,
            value: ArgumentValue::Integer("12".to_string()),
        })],
        known: true,
    });
    let columns = ast.new_node(Node::Command {
        name: "arraycols".to_string(),
        args: vec![Some(Argument {
            kind: ArgumentKind::Mandatory,
            value: ArgumentValue::Column("lcr".to_string()),
        })],
        known: true,
    });
    let delim = ast.new_node(Node::Command {
        name: "delim".to_string(),
        args: vec![Some(Argument {
            kind: ArgumentKind::Mandatory,
            value: ArgumentValue::Delimiter(texform_core::ast::Delimiter::Control(
                "langle".to_string(),
            )),
        })],
        known: true,
    });

    ast.append_child(root, label);
    ast.append_child(root, numeral);
    ast.append_child(root, columns);
    ast.append_child(root, delim);

    assert_eq!(
        serialize(&ast),
        r"\label {sec:intro} \romannumeral {12} \arraycols {lcr} \delim {\langle}"
    );
}

#[test]
fn test_serialize_paired_argument_replays_recorded_delimiters_and_skips_missing_slots() {
    let mut ast = Ast::new();
    let root = ast.root();
    let x = ast.new_node(Node::Char('x'));
    let command = ast.new_node(Node::Command {
        name: "qty".to_string(),
        args: vec![
            None,
            Some(Argument {
                kind: ArgumentKind::Paired {
                    open: texform_core::ast::Delimiter::Char('|'),
                    close: texform_core::ast::Delimiter::Char('|'),
                },
                value: ArgumentValue::MathContent(x),
            }),
        ],
        known: true,
    });
    ast.append_child(root, command);

    assert_eq!(serialize(&ast), r"\qty | x |");
}

#[test]
fn test_serialize_star_slot_sticks_to_command_name() {
    let mut ast = Ast::new();
    let root = ast.root();
    let body = ast.new_node(Node::Char('x'));
    let command = ast.new_node(Node::Command {
        name: "operatorname".to_string(),
        args: vec![
            Some(Argument {
                kind: ArgumentKind::Star,
                value: ArgumentValue::Boolean(true),
            }),
            Some(Argument {
                kind: ArgumentKind::Mandatory,
                value: ArgumentValue::MathContent(body),
            }),
        ],
        known: true,
    });
    ast.append_child(root, command);

    assert_eq!(serialize(&ast), r"\operatorname* { x }");
}

#[test]
fn test_serialize_environment_uses_spaced_header_by_default() {
    let ast = parse_to_ast(r"\begin {matrix}a&b\\c&d\end {matrix}");

    assert_eq!(
        serialize(&ast),
        r"\begin {matrix} a & b \\ c & d \end {matrix}"
    );
}

#[test]
fn test_serialize_with_compact_environment_header() {
    let ast = parse_to_ast(r"\begin {matrix}ab\end {matrix}");
    let mut options = SerializeOptions::default();
    options.syntax.environments.name_spacing = EnvironmentNameSpacing::Compact;

    assert_eq!(
        serialize_with(&ast, &options),
        r"\begin{matrix} a b \end{matrix}"
    );
}

#[test]
fn test_environment_name_spacing_is_independent_from_command_spacing() {
    let ast = parse_to_ast(r"\begin {matrix}ab\end {matrix}");
    let mut options = SerializeOptions::default();
    options.math.spacing.commands = CommandSpacing::Minimal;
    options.syntax.environments.name_spacing = EnvironmentNameSpacing::Spaced;

    assert_eq!(
        serialize_with(&ast, &options),
        r"\begin {matrix} a b \end {matrix}"
    );
}

#[test]
fn test_serialize_infix_node_without_transform() {
    let ast = parse_to_ast(r"a\over b");

    assert_eq!(serialize(&ast), r"a \over b");
}

#[test]
fn test_serialize_control_sequence_keeps_boundary_before_math_char() {
    let ast = parse_to_ast(r"\\x");

    assert_eq!(serialize(&ast), r"\\ x");
}

#[test]
fn test_serialize_flat_declarative_without_scope_wrapper() {
    let mut ast = Ast::new();
    let root = ast.root();
    let decl = ast.new_node(Node::Declarative {
        name: "bfseries".to_string(),
        args: Vec::new(),
    });
    let x = ast.new_node(Node::Char('x'));
    ast.append_child(root, decl);
    ast.append_child(root, x);

    assert_eq!(serialize(&ast), r"\bfseries x");
}

#[test]
fn test_serialize_infix_always_explicit_groups_operands() {
    let ast = parse_to_ast(r"a \over b");
    let mut options = SerializeOptions::default();
    options.math.infix.grouping = InfixGrouping::AlwaysExplicit;

    assert_eq!(serialize_with(&ast, &options), r"{ a } \over { b }");
}

#[test]
fn test_serialize_infix_when_required_keeps_nested_braces() {
    let ast = parse_to_ast(r"{a \over b} \over c");
    let mut options = SerializeOptions::default();
    options.math.infix.grouping = InfixGrouping::WhenRequired;

    assert_eq!(serialize_with(&ast, &options), r"{ a \over b } \over c");
}

#[test]
fn test_serialize_infix_when_required_keeps_flat_declarative_unbraced() {
    let ast = parse_to_ast(r"a \displaystyle b \over c");
    let mut options = SerializeOptions::default();
    options.math.infix.grouping = InfixGrouping::WhenRequired;

    assert_eq!(serialize_with(&ast, &options), r"a \displaystyle b \over c");
}

#[test]
fn test_serialize_infix_empty_left_operand_stays_unbraced() {
    let ast = parse_to_ast(r"\over x");
    let mut explicit = SerializeOptions::default();
    explicit.math.infix.grouping = InfixGrouping::AlwaysExplicit;

    assert_eq!(serialize(&ast), r"\over x");
    assert_eq!(serialize_with(&ast, &explicit), r"\over { x }");
}

#[test]
fn test_serialize_infix_empty_right_operand_stays_unbraced() {
    let ast = parse_to_ast(r"x \over");
    let mut explicit = SerializeOptions::default();
    explicit.math.infix.grouping = InfixGrouping::AlwaysExplicit;

    assert_eq!(serialize(&ast), r"x \over");
    assert_eq!(serialize_with(&ast, &explicit), r"{ x } \over");
}

#[test]
fn test_serialize_environment_preserves_explicit_body_group() {
    let mut ast = Ast::new();
    let root = ast.root();
    let body = ast.new_node(Node::Group {
        children: Vec::new(),
        kind: GroupKind::Explicit,
        mode: ContentMode::Math,
    });
    let x = ast.new_node(Node::Char('x'));
    ast.append_child(body, x);
    let env = ast.new_node(Node::Environment {
        name: "matrix".to_string(),
        args: Vec::new(),
        known: true,
        body,
    });
    ast.append_child(root, env);

    assert_eq!(serialize(&ast), r"\begin {matrix} { x } \end {matrix}");
}

#[test]
fn test_serialize_environment_inside_text_mode_stays_compact() {
    let mut ast = Ast::new();
    let root = ast.root();
    let body = ast.new_node(Node::Group {
        children: Vec::new(),
        kind: GroupKind::Implicit,
        mode: ContentMode::Text,
    });
    let body_text = ast.new_node(Node::Text("x".to_string()));
    ast.append_child(body, body_text);

    let env = ast.new_node(Node::Environment {
        name: "quote".to_string(),
        args: Vec::new(),
        known: true,
        body,
    });

    let text_group = ast.new_node(Node::Group {
        children: Vec::new(),
        kind: GroupKind::Implicit,
        mode: ContentMode::Text,
    });
    let left = ast.new_node(Node::Text("a".to_string()));
    let right = ast.new_node(Node::Text("b".to_string()));
    ast.append_child(text_group, left);
    ast.append_child(text_group, env);
    ast.append_child(text_group, right);

    let command = ast.new_node(Node::Command {
        name: "text".to_string(),
        args: vec![Some(Argument {
            kind: ArgumentKind::Mandatory,
            value: ArgumentValue::TextContent(text_group),
        })],
        known: true,
    });
    ast.append_child(root, command);

    assert_eq!(serialize(&ast), r"\text {a\begin {quote}x\end {quote}b}");
}

#[test]
fn test_serialize_scalar_paired_argument_keeps_math_spacing() {
    let mut ast = Ast::new();
    let root = ast.root();
    let command = ast.new_node(Node::Command {
        name: "qty".to_string(),
        args: vec![Some(Argument {
            kind: ArgumentKind::Paired {
                open: texform_core::ast::Delimiter::Char('|'),
                close: texform_core::ast::Delimiter::Char('|'),
            },
            value: ArgumentValue::Integer("12".to_string()),
        })],
        known: true,
    });
    ast.append_child(root, command);

    assert_eq!(serialize(&ast), r"\qty | 12 |");
}

#[test]
fn test_serialize_text_mode_control_word_keeps_text_boundary() {
    let mut ast = Ast::new();
    let root = ast.root();

    let text_group = ast.new_node(Node::Group {
        children: Vec::new(),
        kind: GroupKind::Implicit,
        mode: ContentMode::Text,
    });
    let alpha = ast.new_node(Node::Command {
        name: "alpha".to_string(),
        args: Vec::new(),
        known: true,
    });
    let suffix = ast.new_node(Node::Text("x".to_string()));
    ast.append_child(text_group, alpha);
    ast.append_child(text_group, suffix);

    let command = ast.new_node(Node::Command {
        name: "text".to_string(),
        args: vec![Some(Argument {
            kind: ArgumentKind::Mandatory,
            value: ArgumentValue::TextContent(text_group),
        })],
        known: true,
    });
    ast.append_child(root, command);

    assert_eq!(serialize(&ast), r"\text {\alpha x}");
}

#[test]
fn test_serialize_paired_argument_unwraps_multi_item_content_group() {
    let mut ast = Ast::new();
    let root = ast.root();
    let content = ast.new_node(Node::Group {
        children: Vec::new(),
        kind: GroupKind::Implicit,
        mode: ContentMode::Math,
    });
    let x = ast.new_node(Node::Char('x'));
    let plus = ast.new_node(Node::Char('+'));
    let y = ast.new_node(Node::Char('y'));
    ast.append_child(content, x);
    ast.append_child(content, plus);
    ast.append_child(content, y);

    let command = ast.new_node(Node::Command {
        name: "qty".to_string(),
        args: vec![Some(Argument {
            kind: ArgumentKind::Paired {
                open: texform_core::ast::Delimiter::Char('('),
                close: texform_core::ast::Delimiter::Char(')'),
            },
            value: ArgumentValue::MathContent(content),
        })],
        known: true,
    });
    ast.append_child(root, command);

    assert_eq!(serialize(&ast), r"\qty ( x + y )");
}

#[test]
fn test_serialize_text_mode_paired_scalar_stays_compact() {
    let mut ast = Ast::new();
    let root = ast.root();

    let text_group = ast.new_node(Node::Group {
        children: Vec::new(),
        kind: GroupKind::Implicit,
        mode: ContentMode::Text,
    });
    let left = ast.new_node(Node::Text("a".to_string()));
    let right = ast.new_node(Node::Text("b".to_string()));
    let command = ast.new_node(Node::Command {
        name: "mark".to_string(),
        args: vec![Some(Argument {
            kind: ArgumentKind::Paired {
                open: texform_core::ast::Delimiter::Char('|'),
                close: texform_core::ast::Delimiter::Char('|'),
            },
            value: ArgumentValue::Integer("12".to_string()),
        })],
        known: true,
    });
    ast.append_child(text_group, left);
    ast.append_child(text_group, command);
    ast.append_child(text_group, right);

    let wrapper = ast.new_node(Node::Command {
        name: "text".to_string(),
        args: vec![Some(Argument {
            kind: ArgumentKind::Mandatory,
            value: ArgumentValue::TextContent(text_group),
        })],
        known: true,
    });
    ast.append_child(root, wrapper);

    assert_eq!(serialize(&ast), r"\text {a\mark|12|b}");
}

#[test]
fn test_serialize_with_compact_script_spacing() {
    let ast = parse_to_ast("x^2_i");
    let mut options = SerializeOptions::default();
    options.math.scripts.spacing = ScriptSpacing::Compact;

    assert_eq!(serialize_with(&ast, &options), "x_{ i }^{ 2 }");
}

#[test]
fn test_serialize_minimal_command_spacing_compacts_left_right_delimiter() {
    let ast = parse_to_ast(r"\left (a+b\right )");
    let mut options = SerializeOptions::default();
    options.math.spacing.commands = CommandSpacing::Minimal;

    assert_eq!(serialize_with(&ast, &options), r"\left( a + b \right)");
}

#[test]
fn test_compact_math_group_inner_spacing_affects_optional_argument_brackets() {
    let ast = parse_to_ast(r"\sqrt[3]{x}");
    let mut options = SerializeOptions::default();
    options.math.spacing.group_inner_spacing = MathGroupInnerSpacing::Compact;

    assert_eq!(serialize_with(&ast, &options), r"\sqrt [3] {x}");
}

#[test]
fn test_serialize_escaped_syntax_chars_round_trips_as_visible_chars() {
    let first = serialize(&parse_to_ast(r"\%\$\#\_\{\}"));
    let second = serialize(&parse_to_ast(&first));

    assert_eq!(first, r"\% \$ \# \_ \{ \}");
    assert_eq!(second, first);
}

#[test]
fn test_serialize_text_escaped_braces_round_trips_as_visible_chars() {
    let first = serialize(&parse_to_ast(r"\text{\{a\}}"));
    let second = serialize(&parse_to_ast(&first));

    assert_eq!(first, r"\text {\{a\}}");
    assert_eq!(second, first);
}

#[test]
fn test_serialize_is_text_idempotent_for_canonical_samples() {
    let samples = [
        (r"\frac{a}{b}", r"\frac { a } { b }"),
        ("x^2_i", "x _ { i } ^ { 2 }"),
        (r"\left (a+b\right )", r"\left ( a + b \right )"),
        (
            r"\begin {matrix}ab\end {matrix}",
            r"\begin {matrix} a b \end {matrix}",
        ),
    ];

    for (src, expected) in samples {
        let first = serialize(&parse_to_ast(src));
        let second = serialize(&parse_to_ast(&first));
        assert_eq!(first, expected);
        assert_eq!(second, first);
    }
}
