xberg 1.1.2

High-performance document intelligence library for Rust. Extract text, metadata, and structured data from PDFs, Office documents, images, and 107 formats and 371 programming languages via tree-sitter code intelligence with async/sync APIs.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
//! Comprehensive TDD test suite for RTF extraction.
//!
//! This test suite validates RTF extraction capabilities.
//! Tests cover:
//! - Accent and Unicode handling
//! - Bookmarks and internal links
//! - Footnotes and references
//! - Text formatting (bold, italic, underline, strikeout, superscript, subscript, small caps)
//! - Headings and structure
//! - Image extraction
//! - External hyperlinks
//! - List extraction (simple and complex nested lists)
//! - Table extraction (simple and complex with special formatting)
//! - Unicode characters and special symbols
//!
//! Test Organization:
//! - Basic Content Extraction (unicode, accent)
//! - Structure Preservation (heading, list_simple, list_complex)
//! - Table Extraction (table_simple, table_error_codes)
//! - Formatting Detection (formatting)
//! - Special Features (footnote, bookmark, link)
//! - Integration Tests (deterministic extraction, no content loss)
//!
//! Success Criteria:
//! - All tests passing (100%)
//! - No content loss (should extract meaningful text from all files)
//! - Deterministic extraction (same input = same output)
//!
//! Note: These tests require the `office` feature to be enabled.

#![allow(clippy::print_stdout, clippy::print_stderr, clippy::dbg_macro)] // ~keep: test/bench binaries print by design; org logging policy exempts tests
#![cfg(feature = "office")]
#![allow(clippy::doc_suspicious_footnotes)]

use std::path::PathBuf;
use xberg::core::config::ExtractionConfig;

mod helpers;
use helpers::extract_uri_document;

/// Helper function to get path to RTF test document
fn get_rtf_path(filename: &str) -> PathBuf {
    let manifest_dir = env!("CARGO_MANIFEST_DIR");
    PathBuf::from(manifest_dir)
        .parent()
        .expect("xberg crate should have a parent")
        .parent()
        .expect("parent should have a parent")
        .join("test_documents")
        .join("rtf")
        .join(filename)
}

/// Helper for reaching the workspace root from the xberg crate
fn workspace_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .parent()
        .expect("xberg crate should have a parent")
        .parent()
        .expect("workspace root exists")
        .to_path_buf()
}

/// Test extraction of RTF file with accent characters (accented vowels).
///
/// File: accent.rtf
/// Content: "le café où on ne fume pas"
/// Expected: Correctly extracts French text with accented characters (é, ù)
/// Pandoc baseline: le café où on ne fume pas
#[tokio::test]
async fn test_rtf_accent_extraction() {
    let config = ExtractionConfig::default();
    let path = get_rtf_path("accent.rtf");

    let result = extract_uri_document(&path, Some("application/rtf"), &config).await;

    assert!(result.is_ok(), "RTF extraction should succeed for accent.rtf");
    let extraction = result.expect("Operation failed");

    assert_eq!(extraction.mime_type, "application/rtf");

    assert!(!extraction.content.is_empty(), "Content should not be empty");

    let content = extraction.content.to_lowercase();

    assert!(
        extraction.content.contains("café") || content.contains("cafe"),
        "Should extract French word 'café' or 'cafe'"
    );

    assert!(
        extraction.content.contains("") || content.contains("ou"),
        "Should extract French word 'où' or 'ou'"
    );

    assert!(
        content.contains("fume") || content.contains("smoking"),
        "Should extract content about smoking"
    );
}

/// Test extraction of RTF file with CP1251 hex byte escapes.
///
/// File: ansicpg1251.rtf
/// Content: Cyrillic text encoded as `\'hh` bytes with `\ansicpg1251`
/// Expected: Decodes byte escapes with the declared ANSI codepage
#[tokio::test]
async fn test_rtf_ansicpg1251_extraction() {
    let config = ExtractionConfig::default();
    let path = get_rtf_path("ansicpg1251.rtf");

    let result = extract_uri_document(&path, Some("application/rtf"), &config).await;

    assert!(result.is_ok(), "RTF extraction should succeed for ansicpg1251.rtf");
    let extraction = result.expect("Operation failed");

    assert_eq!(extraction.mime_type, "application/rtf");
    assert!(
        extraction.content.contains("Привет, мир!"),
        "Should decode CP1251 hex escapes as Cyrillic text (found: {})",
        extraction.content
    );
    assert!(
        !extraction.content.contains("Ïðèâåò"),
        "Should not decode CP1251 bytes as Windows-1252 mojibake"
    );
}

/// Test extraction of RTF file with bookmarks (internal anchors/references).
///
/// File: bookmark.rtf
/// Content: Bookmark anchor labeled "Bookmark_1" and link text "click me"
/// Expected: Extracts bookmark definition and link text
/// Pandoc baseline: [Bookmark_1]{#bookmark_1} and [click me](#bookmark_1)
#[tokio::test]
async fn test_rtf_bookmark_extraction() {
    let config = ExtractionConfig::default();
    let path = get_rtf_path("bookmark.rtf");

    let result = extract_uri_document(&path, Some("application/rtf"), &config).await;

    assert!(result.is_ok(), "RTF extraction should succeed for bookmark.rtf");
    let extraction = result.expect("Operation failed");

    let content = extraction.content.to_lowercase();

    assert!(
        content.contains("bookmark") || content.contains("click") || content.contains("me"),
        "Should extract bookmark or link text (found: {})",
        extraction.content
    );
}

/// Test extraction of RTF file with footnotes.
///
/// File: footnote.rtf
/// Content: Academic text with footnote references and their content
/// Expected: Extracts both main text and footnote content
/// Pandoc baseline: Uses [^1] and [^2] syntax for footnotes
#[tokio::test]
async fn test_rtf_footnote_extraction() {
    let config = ExtractionConfig::default();
    let path = get_rtf_path("footnote.rtf");

    let result = extract_uri_document(&path, Some("application/rtf"), &config).await;

    assert!(result.is_ok(), "RTF extraction should succeed for footnote.rtf");
    let extraction = result.expect("Operation failed");

    assert!(!extraction.content.is_empty(), "Content should not be empty");

    let content = extraction.content.to_lowercase();

    assert!(
        content.contains("mead") || content.contains("landmark"),
        "Should extract main text about Mead's study"
    );

    assert!(
        content.contains("note")
            || content.contains("annotated")
            || content.contains("bibliography")
            || content.contains("sahlins"),
        "Should extract footnote content or references"
    );

    assert!(
        content.contains("footnote") || extraction.content.contains("[^") || content.contains("annotated"),
        "Should contain footnote indicators"
    );
}

/// Test extraction of RTF file with various text formatting.
///
/// File: formatting.rtf
/// Content: Text with bold, italic, underline, strikeout, superscript, subscript, small caps
/// Expected: Preserves or indicates all formatting types
/// Pandoc baseline: Detailed formatting in markdown syntax
#[tokio::test]
async fn test_rtf_formatting_extraction() {
    let config = ExtractionConfig::default();
    let path = get_rtf_path("formatting.rtf");

    let result = extract_uri_document(&path, Some("application/rtf"), &config).await;

    assert!(result.is_ok(), "RTF extraction should succeed for formatting.rtf");
    let extraction = result.expect("Operation failed");

    assert!(!extraction.content.is_empty(), "Content should not be empty");

    let content = extraction.content.to_lowercase();

    assert!(
        content.contains("formatting") || content.contains("test") || content.contains("bold"),
        "Should extract formatting-related content"
    );

    assert!(
        extraction.content.contains("**bold**") || content.contains("bold"),
        "Should preserve or indicate bold text"
    );

    assert!(
        extraction.content.contains("*italic") || content.contains("italic"),
        "Should preserve or indicate italic text"
    );

    let has_formatting = extraction.content.contains("**")
        || extraction.content.contains("*")
        || extraction.content.contains("__")
        || extraction.content.contains("_")
        || extraction.content.contains("~~")
        || extraction.content.contains("^")
        || extraction.content.contains("~")
        || content.contains("bold");

    assert!(has_formatting, "Should preserve or indicate text formatting");
}

/// Test extraction of RTF file with heading hierarchy.
///
/// File: heading.rtf
/// Content: Three levels of headings (H1, H2, H3) followed by paragraph
/// Expected: Extracts all headings and paragraph text
/// Pandoc baseline: Markdown heading syntax (# ## ###)
#[tokio::test]
async fn test_rtf_heading_extraction() {
    let config = ExtractionConfig::default();
    let path = get_rtf_path("heading.rtf");

    let result = extract_uri_document(&path, Some("application/rtf"), &config).await;

    assert!(result.is_ok(), "RTF extraction should succeed for heading.rtf");
    let extraction = result.expect("Operation failed");

    assert!(!extraction.content.is_empty(), "Content should not be empty");

    assert!(
        extraction.content.contains("Heading 1"),
        "Should extract Heading 1 text"
    );

    assert!(
        extraction.content.contains("Heading 2"),
        "Should extract Heading 2 text"
    );

    assert!(
        extraction.content.contains("Heading 3"),
        "Should extract Heading 3 text"
    );

    assert!(
        extraction.content.contains("Paragraph"),
        "Should extract paragraph text"
    );

    let content_lower = extraction.content.to_lowercase();
    assert!(
        extraction.content.contains("#")
            || (content_lower.contains("heading 1") && content_lower.contains("heading 2")),
        "Should preserve heading hierarchy"
    );
}

/// Test extraction of RTF file with embedded or referenced image.
///
/// File: image.rtf
/// Content: Image reference with dimensions (2.0in x 2.0in)
/// Expected: Extracts image reference and/or dimensions
/// Pandoc baseline: Markdown image syntax with dimensions
#[tokio::test]
async fn test_rtf_image_extraction() {
    let config = ExtractionConfig::default();
    let path = get_rtf_path("image.rtf");

    let result = extract_uri_document(&path, Some("application/rtf"), &config).await;

    assert!(result.is_ok(), "RTF extraction should succeed for image.rtf");
    let extraction = result.expect("Operation failed");

    assert!(!extraction.content.is_empty(), "Content should not be empty");

    let content = extraction.content.to_lowercase();

    assert!(
        extraction.content.contains("!")
            || content.contains("image")
            || extraction.content.contains(".jpg")
            || content.contains("2.0")
            || content.contains("width")
            || content.contains("height"),
        "Should contain image reference or dimension information (found: {})",
        extraction.content
    );
}

/// Test extraction of RTF file with external hyperlink.
///
/// File: link.rtf
/// Content: Link to pandoc.org website
/// Expected: Extracts link text and/or URL
/// Pandoc baseline: Markdown link syntax [pandoc](http://pandoc.org)
#[tokio::test]
async fn test_rtf_link_extraction() {
    let config = ExtractionConfig::default();
    let path = get_rtf_path("link.rtf");

    let result = extract_uri_document(&path, Some("application/rtf"), &config).await;

    assert!(result.is_ok(), "RTF extraction should succeed for link.rtf");
    let extraction = result.expect("Operation failed");

    assert!(!extraction.content.is_empty(), "Content should not be empty");

    let content = extraction.content.to_lowercase();

    assert!(
        content.contains("pandoc") || content.contains("http"),
        "Should extract link-related content (found: {})",
        extraction.content
    );
}

/// Test extraction of RTF file with complex nested list structure.
///
/// File: list_complex.rtf
/// Content: Multi-level nested list with various numbering (numeric, alphabetic, roman)
/// Expected: Extracts all list items preserving or indicating hierarchy
/// Pandoc baseline: Markdown nested list with mixed numbering schemes
#[tokio::test]
async fn test_rtf_list_complex_extraction() {
    let config = ExtractionConfig::default();
    let path = get_rtf_path("list_complex.rtf");

    let result = extract_uri_document(&path, Some("application/rtf"), &config).await;

    assert!(result.is_ok(), "RTF extraction should succeed for list_complex.rtf");
    let extraction = result.expect("Operation failed");

    assert!(!extraction.content.is_empty(), "Content should not be empty");

    let content = extraction.content.to_lowercase();

    assert!(content.contains("one"), "Should extract list item 'One'");

    assert!(content.contains("two"), "Should extract list item 'Two'");

    assert!(
        content.contains("three") || content.contains("three"),
        "Should extract nested list item 'Three'"
    );

    assert!(
        content.contains("five") || content.contains("six"),
        "Should extract deeply nested list items"
    );

    assert!(
        extraction.content.contains("1")
            || extraction.content.contains("-")
            || extraction.content.contains("")
            || content.contains("one"),
        "Should preserve list structure indicators"
    );

    assert!(
        content.contains("out of list") || content.contains("out"),
        "Should extract separator text 'Out of list'"
    );

    assert!(
        content.contains("seven") || content.contains("eight") || content.contains("7") || content.contains("8"),
        "Should extract restarted list numbering (7, 8)"
    );
}

/// Test extraction of RTF file with simple bulleted list.
///
/// File: list_simple.rtf
/// Content: Simple bullet list with one nested item and list break
/// Expected: Extracts all list items and indicates nesting
/// Pandoc baseline: Simple markdown bullet list with nesting
#[tokio::test]
async fn test_rtf_list_simple_extraction() {
    let config = ExtractionConfig::default();
    let path = get_rtf_path("list_simple.rtf");

    let result = extract_uri_document(&path, Some("application/rtf"), &config).await;

    assert!(result.is_ok(), "RTF extraction should succeed for list_simple.rtf");
    let extraction = result.expect("Operation failed");

    assert!(!extraction.content.is_empty(), "Content should not be empty");

    let content = extraction.content.to_lowercase();

    assert!(content.contains("one"), "Should extract list item 'one'");

    assert!(content.contains("two"), "Should extract list item 'two'");

    assert!(content.contains("sub"), "Should extract nested list item 'sub'");

    assert!(content.contains("new"), "Should extract 'new list' text");

    assert!(
        content.contains("one") && content.contains("two") && content.contains("sub"),
        "Should extract all list items"
    );
}

/// Test extraction of RTF file with table containing error codes.
///
/// File: table_error_codes.rtf
/// Content: Table with Code and Error columns, 23 rows of Pandoc error codes
/// Expected: Extracts table structure and all data cells
/// Pandoc baseline: Markdown table format with 2 columns and 23 rows
///
/// Note: RTF table extraction via Pandoc markdown output may result in empty content
/// due to limitations in Pandoc's markdown table rendering. Tables are present
/// in Pandoc's internal JSON representation but may not render in text format.
#[tokio::test]
async fn test_rtf_table_error_codes_extraction() {
    let config = ExtractionConfig::default();
    let path = get_rtf_path("table_error_codes.rtf");

    let result = extract_uri_document(&path, Some("application/rtf"), &config).await;

    assert!(
        result.is_ok(),
        "RTF extraction should succeed for table_error_codes.rtf"
    );
    let extraction = result.expect("Operation failed");

    assert!(
        extraction.mime_type == "application/rtf",
        "MIME type should be preserved"
    );
}

/// Test extraction of RTF file with simple 4-column, 2-row table.
///
/// File: table_simple.rtf
/// Content: Table with headers A, B, C, D and data row E, F, G, H
/// Expected: Extracts all cells in correct table structure
/// Pandoc baseline: Markdown table format
///
/// Note: RTF table extraction via Pandoc markdown output may result in empty content
/// due to limitations in Pandoc's markdown table rendering. Tables are present
/// in Pandoc's internal JSON representation but may not render in text format.
#[tokio::test]
async fn test_rtf_table_simple_extraction() {
    let config = ExtractionConfig::default();
    let path = get_rtf_path("table_simple.rtf");

    let result = extract_uri_document(&path, Some("application/rtf"), &config).await;

    assert!(result.is_ok(), "RTF extraction should succeed for table_simple.rtf");
    let extraction = result.expect("Operation failed");

    assert!(
        extraction.mime_type == "application/rtf",
        "MIME type should be preserved"
    );
}

/// Test extraction of RTF file with various Unicode characters.
///
/// File: unicode.rtf
/// Content: Smart quotes, Greek letters (α, ä)
/// Expected: Correctly extracts and preserves Unicode characters
/// Pandoc baseline: "hi"'hi'αä
#[tokio::test]
async fn test_rtf_unicode_extraction() {
    let config = ExtractionConfig::default();
    let path = get_rtf_path("unicode.rtf");

    let result = extract_uri_document(&path, Some("application/rtf"), &config).await;

    assert!(result.is_ok(), "RTF extraction should succeed for unicode.rtf");
    let extraction = result.expect("Operation failed");

    assert!(!extraction.content.is_empty(), "Content should not be empty");

    assert!(
        extraction.content.contains("hi") || extraction.content.contains("α") || extraction.content.contains("ä"),
        "Should extract unicode content (found: {})",
        extraction.content
    );
}

/// Test that RTF extraction is deterministic
/// Same input should produce identical output
#[tokio::test]
async fn test_rtf_extraction_deterministic_unicode() {
    let config = ExtractionConfig::default();
    let path = get_rtf_path("unicode.rtf");

    let result1 = extract_uri_document(&path, Some("application/rtf"), &config).await;
    let result2 = extract_uri_document(&path, Some("application/rtf"), &config).await;

    assert!(result1.is_ok() && result2.is_ok(), "Both extractions should succeed");

    let extraction1 = result1.expect("Operation failed");
    let extraction2 = result2.expect("Operation failed");

    assert_eq!(
        extraction1.content, extraction2.content,
        "FAIL: Extraction is not deterministic. Same input produced different outputs."
    );
}

/// Test that RTF extraction is deterministic for complex files
/// Same input should produce identical output
#[tokio::test]
async fn test_rtf_extraction_deterministic_list_complex() {
    let config = ExtractionConfig::default();
    let path = get_rtf_path("list_complex.rtf");

    let result1 = extract_uri_document(&path, Some("application/rtf"), &config).await;
    let result2 = extract_uri_document(&path, Some("application/rtf"), &config).await;

    assert!(result1.is_ok() && result2.is_ok(), "Both extractions should succeed");

    let extraction1 = result1.expect("Operation failed");
    let extraction2 = result2.expect("Operation failed");

    assert_eq!(
        extraction1.content, extraction2.content,
        "FAIL: Extraction is not deterministic. Same input produced different outputs."
    );
}

/// Test no critical content loss
/// All RTF files should extract non-empty content (except possibly image-only files)
#[tokio::test]
async fn test_rtf_no_critical_content_loss() {
    let config = ExtractionConfig::default();

    let must_extract = vec![
        "unicode.rtf",
        "ansicpg1251.rtf",
        "accent.rtf",
        "heading.rtf",
        "list_simple.rtf",
        "list_complex.rtf",
        "formatting.rtf",
        "footnote.rtf",
        "bookmark.rtf",
        "link.rtf",
    ];

    for filename in must_extract {
        let path = get_rtf_path(filename);
        let result = extract_uri_document(&path, Some("application/rtf"), &config).await;

        assert!(
            result.is_ok(),
            "FAIL: Extraction failed for {} (critical file)",
            filename
        );

        let extraction = result.expect("Operation failed");
        assert!(
            !extraction.content.is_empty(),
            "FAIL: CRITICAL - Extracted 0 bytes from {}. RTF extractor lost all content.",
            filename
        );

        assert!(
            extraction.content.len() >= 5,
            "FAIL: Extracted only {} bytes from {} (expected at least 5 characters). Content: '{}'",
            extraction.content.len(),
            filename,
            extraction.content
        );
    }
}

/// Test MIME type preservation
/// All RTF extractions should preserve the application/rtf MIME type
#[tokio::test]
async fn test_rtf_mime_type_preservation() {
    let config = ExtractionConfig::default();

    let test_files = vec![
        "unicode.rtf",
        "ansicpg1251.rtf",
        "accent.rtf",
        "heading.rtf",
        "list_simple.rtf",
    ];

    for filename in test_files {
        let path = get_rtf_path(filename);
        let result = extract_uri_document(&path, Some("application/rtf"), &config).await;

        assert!(result.is_ok(), "Extraction should succeed for {}", filename);

        let extraction = result.expect("Operation failed");
        assert_eq!(
            extraction.mime_type, "application/rtf",
            "FAIL: MIME type not preserved for {}",
            filename
        );
    }
}

/// Parity check: RTF extracted from the DOCX `word_sample.docx` should
/// carry the same content signals and metadata as the DOCX extractor.
#[tokio::test]
async fn test_rtf_word_sample_matches_docx_metadata_and_content() {
    let root = workspace_root();
    let rtf_path = root.join("test_documents/rtf/word_sample.rtf");
    let docx_path = root.join("test_documents/docx/word_sample.docx");

    if !rtf_path.exists() || !docx_path.exists() {
        println!("Skipping word_sample parity test: fixtures missing");
        return;
    }

    let config = ExtractionConfig::default();
    let rtf_result = extract_uri_document(&rtf_path, Some("application/rtf"), &config)
        .await
        .expect("RTF extraction should succeed for word_sample");
    let docx_result = extract_uri_document(&docx_path, None, &config)
        .await
        .expect("DOCX extraction should succeed for word_sample");

    let rtf_content_lower = rtf_result.content.to_lowercase();
    assert!(
        rtf_content_lower.contains("swim"),
        "RTF content should include the same body text as DOCX"
    );

    assert_eq!(
        rtf_result.metadata.created_by.as_deref(),
        docx_result.metadata.created_by.as_deref(),
        "Metadata field created_by should align with DOCX"
    );
    assert_eq!(
        rtf_result.metadata.modified_by.as_deref(),
        docx_result.metadata.modified_by.as_deref(),
        "Metadata field modified_by should align with DOCX"
    );
    assert_eq!(
        rtf_result.metadata.created_at.as_deref(),
        docx_result.metadata.created_at.as_deref(),
        "Metadata field created_at should align with DOCX"
    );
    assert_eq!(
        rtf_result.metadata.additional.get("revision").and_then(|v| v.as_str()),
        docx_result.metadata.additional.get("revision").and_then(|v| v.as_str()),
        "Metadata field revision should align with DOCX"
    );

    for (key, expected) in [
        ("page_count", 2),
        ("word_count", 108),
        ("character_count", 620),
        ("line_count", 5),
        ("paragraph_count", 1),
    ] {
        assert_eq!(
            rtf_result.metadata.additional.get(key).and_then(|v| v.as_i64()),
            Some(expected),
            "Metadata field {} should match DOCX values",
            key
        );
    }
}

/// RTF generated from lorem_ipsum.docx should expose the same document statistics
/// we validate for the DOCX extractor.
#[tokio::test]
async fn test_rtf_lorem_ipsum_metadata_alignment() {
    let root = workspace_root();
    let rtf_path = root.join("test_documents/rtf/lorem_ipsum.rtf");

    if !rtf_path.exists() {
        println!("Skipping lorem_ipsum metadata test: fixture missing");
        return;
    }

    let config = ExtractionConfig::default();
    let result = extract_uri_document(&rtf_path, Some("application/rtf"), &config)
        .await
        .expect("RTF extraction should succeed for lorem_ipsum");

    assert!(
        result.content.to_lowercase().contains("lorem ipsum"),
        "Content should contain lorem ipsum text"
    );

    for (key, expected) in [
        ("page_count", 1),
        ("word_count", 520),
        ("character_count", 2967),
        ("line_count", 24),
        ("paragraph_count", 6),
    ] {
        assert_eq!(
            result.metadata.additional.get(key).and_then(|v| v.as_i64()),
            Some(expected),
            "Metadata field {} should match DOCX values",
            key
        );
    }
}

/// The comprehensive extraction fixture should mirror the coverage of the ODT/DOCX variants:
/// headings, section text, table content, and metadata fields should all be present.
#[tokio::test]
async fn test_rtf_comprehensive_extraction_alignment() {
    let root = workspace_root();
    let rtf_path = root.join("test_documents/rtf/extraction_test.rtf");
    let docx_path = root.join("test_documents/docx/extraction_test.docx");
    let odt_path = root.join("test_documents/odt/extraction_test.odt");

    if !rtf_path.exists() {
        println!("⚠️  Test document not found at {:?}, skipping", rtf_path);
        return;
    }
    if !docx_path.exists() || !odt_path.exists() {
        println!(
            "⚠️  Companion DOCX/ODT documents missing (docx: {}, odt: {}), skipping",
            docx_path.exists(),
            odt_path.exists()
        );
        return;
    }

    let config = ExtractionConfig::default();
    let rtf_result = extract_uri_document(&rtf_path, Some("application/rtf"), &config)
        .await
        .expect("RTF extraction should succeed for extraction_test.rtf");
    let docx_result = extract_uri_document(&docx_path, None, &config)
        .await
        .expect("DOCX extraction should succeed for extraction_test.docx");
    let odt_result = extract_uri_document(&odt_path, None, &config)
        .await
        .expect("ODT extraction should succeed for extraction_test.odt");

    assert!(
        rtf_result.content.contains("Comprehensive Extraction Test Document"),
        "Should include document heading"
    );
    assert!(
        rtf_result.content.contains("First Section"),
        "Should include first section heading"
    );
    assert!(
        rtf_result.content.contains("Second Section"),
        "Should include second section heading"
    );
    assert!(
        rtf_result.content.contains("Third Section"),
        "Should include third section heading"
    );

    for expected in ["Header 1", "Cell 1A", "Product", "Apple"] {
        assert!(
            rtf_result.content.contains(expected),
            "Should include table content '{}'",
            expected
        );
    }
    assert!(
        !rtf_result.tables.is_empty(),
        "Should extract structured tables from RTF"
    );
    assert!(
        rtf_result
            .tables
            .iter()
            .any(|t| t.markdown.contains("Header 1") || t.markdown.contains("Cell 1A")),
        "Table markdown should include header/data cells"
    );
    assert!(
        rtf_result.tables.len() >= docx_result.tables.len() && rtf_result.tables.len() >= odt_result.tables.len(),
        "RTF should capture at least as many tables as DOCX/ODT"
    );

    for (key, expected) in [
        ("page_count", 1),
        ("word_count", 83),
        ("character_count", 475),
        ("line_count", 12),
        ("paragraph_count", 8),
    ] {
        assert_eq!(
            rtf_result.metadata.additional.get(key).and_then(|v| v.as_i64()),
            Some(expected),
            "Metadata field {} should be populated",
            key
        );
    }
}