rumdl 0.1.51

A fast Markdown linter written in Rust (Ru(st) MarkDown Linter)
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
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
use rumdl_lib::lint_context::LintContext;
use rumdl_lib::rule::Rule;
use rumdl_lib::rules::MD034NoBareUrls;
use std::fs::write;

#[test]
fn test_valid_urls() {
    let rule = MD034NoBareUrls;
    let content = "[Link](https://example.com)\n<https://example.com>";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert!(result.is_empty());
}

#[test]
fn test_bare_urls() {
    let rule = MD034NoBareUrls;
    let content = "This is a bare URL: https://example.com/foobar";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert_eq!(result.len(), 1, "Bare URLs should be flagged");
    assert_eq!(result[0].line, 1);
    assert_eq!(result[0].rule_name.as_deref(), Some("MD034"));
    let fixed = rule.fix(&ctx).unwrap();
    assert_eq!(fixed, "This is a bare URL: <https://example.com/foobar>");
}

#[test]
fn test_multiple_urls() {
    let rule = MD034NoBareUrls;
    let content = "Visit https://example.com and http://another.com";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert_eq!(result.len(), 2, "Bare URLs should be flagged");
    let fixed = rule.fix(&ctx).unwrap();
    assert_eq!(fixed, "Visit <https://example.com> and <http://another.com>");
}

#[test]
fn test_urls_in_code_block() {
    let rule = MD034NoBareUrls;
    let content = "```
https://example.com
```
https://outside.com";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    // Only https://outside.com should be flagged (URL in code block is ignored)
    assert_eq!(result.len(), 1, "Bare URL outside code block should be flagged");
    let fixed = rule.fix(&ctx).unwrap();
    assert_eq!(fixed, "```\nhttps://example.com\n```\n<https://outside.com>");
}

#[test]
fn test_urls_in_inline_code() {
    let rule = MD034NoBareUrls;
    let content = "`https://example.com`\nhttps://outside.com";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    // https://outside.com should be flagged (URL in inline code is ignored)
    assert_eq!(result.len(), 1, "Bare URL outside inline code should be flagged");
    let fixed = rule.fix(&ctx).unwrap();
    assert_eq!(fixed, "`https://example.com`\n<https://outside.com>");
}

#[test]
fn test_urls_in_markdown_links() {
    let rule = MD034NoBareUrls;
    let content = "[Example](https://example.com)\nhttps://bare.com";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    // https://bare.com should be flagged (URL in markdown link is ignored)
    assert_eq!(result.len(), 1, "Bare URL should be flagged");
    let fixed = rule.fix(&ctx).unwrap();
    assert_eq!(fixed, "[Example](https://example.com)\n<https://bare.com>");
}

#[test]
fn test_ftp_urls() {
    let rule = MD034NoBareUrls;
    let content = "Download from ftp://example.com/file";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert_eq!(result.len(), 1);
    let fixed = rule.fix(&ctx).unwrap();
    assert_eq!(fixed, "Download from <ftp://example.com/file>");
}

#[test]
fn test_complex_urls() {
    let rule = MD034NoBareUrls;
    let content = "Visit https://example.com/path?param=value#fragment";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert_eq!(result.len(), 1, "Bare URL should be flagged");
    let fixed = rule.fix(&ctx).unwrap();
    assert_eq!(fixed, "Visit <https://example.com/path?param=value#fragment>");
}

#[test]
fn test_multiple_protocols() {
    let rule = MD034NoBareUrls;
    let content = "http://example.com\nhttps://secure.com\nftp://files.com";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let debug_str = format!("test_multiple_protocols\nMD034 test content: {content}\n");
    let _ = write("/tmp/md034_ast_debug.txt", debug_str);
    let result = rule.check(&ctx).unwrap();
    assert_eq!(result.len(), 3, "All bare URLs should be flagged");
    let fixed = rule.fix(&ctx).unwrap();
    assert_eq!(fixed, "<http://example.com>\n<https://secure.com>\n<ftp://files.com>");
}

#[test]
fn test_mixed_content() {
    let rule = MD034NoBareUrls;
    let content = "# Heading\nVisit https://example.com\n> Quote with https://another.com";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let debug_str = format!("test_mixed_content\nMD034 test content: {content}\n");
    let _ = write("/tmp/md034_ast_debug.txt", debug_str);
    let result = rule.check(&ctx).unwrap();
    assert_eq!(result.len(), 2, "Bare URLs should be flagged");
    let fixed = rule.fix(&ctx).unwrap();
    assert_eq!(
        fixed,
        "# Heading\nVisit <https://example.com>\n> Quote with <https://another.com>"
    );
}

#[test]
fn test_not_urls() {
    let rule = MD034NoBareUrls;
    let content = "Text with example.com and just://something";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert!(result.is_empty());
}

#[test]
fn test_badge_links_not_flagged() {
    let rule = MD034NoBareUrls;
    let content =
        "[![npm version](https://img.shields.io/npm/v/react.svg?style=flat)](https://www.npmjs.com/package/react)";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert!(result.is_empty(), "Badge links should not be flagged as bare URLs");
}

#[test]
fn test_multiple_badges_and_links_on_one_line() {
    let rule = MD034NoBareUrls;
    let content = "# [React](https://react.dev/) \
&middot; [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/facebook/react/blob/main/LICENSE) \
[![npm version](https://img.shields.io/npm/v/react.svg?style=flat)](https://www.npmjs.com/package/react) \
[![(Runtime) Build and Test](https://github.com/facebook/react/actions/workflows/runtime_build_and_test.yml/badge.svg)](https://github.com/facebook/react/actions/workflows/runtime_build_and_test.yml) \
[![(Compiler) TypeScript](https://github.com/facebook/react/actions/workflows/compiler_typescript.yml/badge.svg?branch=main)](https://github.com/facebook/react/actions/workflows/compiler_typescript.yml) \
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://legacy.reactjs.org/docs/how-to-contribute.html#your-first-pull-request)";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert!(
        result.is_empty(),
        "Multiple badges and links on one line should not be flagged as bare URLs"
    );
}

#[test]
fn debug_ast_multiple_urls() {
    let content = "Visit https://example.com and http://another.com";
    let _ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let debug_str = format!("MD034 test content: {content}\n");
    match write("/tmp/md034_ast_debug.txt", debug_str) {
        Ok(_) => (),
        Err(e) => panic!("Failed to write AST debug file: {e}"),
    }
    // No assertion: this is for manual inspection
}

#[test]
fn test_md034_edge_cases() {
    let rule = MD034NoBareUrls;
    let cases = [
        // URL inside inline code - should not be flagged
        ("`https://example.com`", 0),
        // URL inside code block - should not be flagged
        ("```\nhttps://example.com\n```", 0),
        // Malformed URL - should not be flagged
        ("This is not a URL: htp://example.com", 0),
        // Custom scheme - should not be flagged (not http/https/ftp)
        ("custom://example.com", 0),
        // URL with trailing period - should be flagged (period should not be part of URL)
        ("See https://example.com.", 1),
        // URL with space in the middle - the valid part before space should be flagged
        ("https://example .com", 1),
        // URL in blockquote - should be flagged
        ("> https://example.com", 1),
        // URL in list item - should be flagged
        ("- https://example.com", 1),
        // URL with non-ASCII character - should be flagged
        ("https://exämple.com", 1),
        // Valid http URL with non-standard port - should be flagged
        ("http://example.com:8080", 1),
        // Valid URL with query string and fragment - should be flagged
        ("https://example.com/path?query=1#frag", 1),
        // URL with missing scheme - should be flagged (markdownlint flags www.example.com)
        ("www.example.com", 1),
        // URL in table cell - should be flagged
        ("| https://example.com |", 1),
        // URL in heading - should be flagged
        ("# https://example.com", 1),
        // URL in reference definition - should not be flagged
        ("[ref]: https://example.com", 0),
        // URL in markdown image - should not be flagged
        ("![alt](https://example.com/image.png)", 0),
        // URL in markdown link - should not be flagged
        ("[link](https://example.com)", 0),
        // True bare URL with non-standard scheme - should not be flagged (not http/https/ftp)
        ("foo://example.com", 0),
        // True bare URL with typo in scheme - should not be flagged (invalid scheme)
        ("htps://example.com", 0),
        // True bare URL with valid scheme but inside code span - should not be flagged
        ("`http://example.com`", 0),
        // True bare URL with valid scheme - should be flagged
        ("http://example.com", 1),
    ];
    for (content, expected) in cases.iter() {
        let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
        let result = rule.check(&ctx).unwrap();
        assert_eq!(result.len(), *expected, "Failed for content: {content}");
        let fixed = rule.fix(&ctx).unwrap();

        // If we expect warnings, the fix should change the content
        if *expected > 0 {
            assert_ne!(fixed, *content, "Fix should change content with warnings: {content}");
            // The fixed version should have no warnings
            let ctx_fixed = LintContext::new(&fixed, rumdl_lib::config::MarkdownFlavor::Standard, None);
            let result_fixed = rule.check(&ctx_fixed).unwrap();
            assert_eq!(result_fixed.len(), 0, "Fixed content should have no warnings: {fixed}");
        } else {
            assert_eq!(
                fixed, *content,
                "Fix should not change content without warnings: {content}"
            );
        }
    }
}

// #[test]
// fn test_performance_md034() {
//     use std::time::Instant;
//     let rule = MD034NoBareUrls;

//     // Generate a large document with a mix of bare URLs, proper links, and code blocks
//     let mut content = String::with_capacity(500_000);

//     // Add a mix of content with URLs in various contexts
//     for i in 0..1000 {
//         // Regular text with bare URLs
//         if i % 5 == 0 {
//             content.push_str(&format!(
//                 "Paragraph {} with a bare URL https://example.com/page{} and some text.\n\n",
//                 i, i
//             ));
//         }
//         // Proper markdown links
//         else if i % 5 == 1 {
//             content.push_str(&format!(
//                 "Paragraph {} with a [proper link](https://example.com/page{}) and some text.\n\n",
//                 i, i
//             ));
//         }
//         // Auto-linked URLs
//         else if i % 5 == 2 {
//             content.push_str(&format!(
//                 "Paragraph {} with an auto-linked <https://example.com/page{}> and some text.\n\n",
//                 i, i
//             ));
//         }
//         // Code blocks with URLs
//         else if i % 5 == 3 {
//             content.push_str(&format!(
//                 "```\ncode block {} with https://example.com/page{} url\n```\n\n",
//                 i, i
//             ));
//         }
//         // Inline code with URLs
//         else {
//             content.push_str(&format!(
//                 "Paragraph {} with `https://example.com/page{}` in code and some text.\n\n",
//                 i, i
//             ));
//         }
//     }

//     // Add a section with multiple URLs on the same line
//     content.push_str("\n## Multiple URLs on same line\n\n");
//     for i in 0..200 {
//         content.push_str(&format!(
//             "Line with multiple bare URLs: https://example1.com/page{} and https://example2.com/page{} and https://example3.com/page{}\n",
//             i, i+1, i+2
//         ));
//     }

//     // Add some content without URLs to test the fast path
//     content.push_str("\n## Content without URLs\n\n");
//     for i in 0..100 {
//         content.push_str(&format!(
//             "This is paragraph {} without any URLs or links.\n\n",
//             i
//         ));
//     }

//     println!("Generated test content of {} bytes", content.len());

//     // Measure performance of check method
//     let start = Instant::now();
//     let ctx = LintContext::new(&content);
//     let result = rule.check(&ctx).unwrap();
//     let check_duration = start.elapsed();

//     // Measure performance of fix method
//     let start = Instant::now();
//     let fixed = rule.fix(&ctx).unwrap();
//     let fix_duration = start.elapsed();

//     println!(
//         "MD034 check duration: {:?} for content length {}",
//         check_duration,
//         content.len()
//     );
//     println!("MD034 fix duration: {:?}", fix_duration);
//     println!("Found {} warnings", result.len());

//     // Verify results
//     assert!(!result.is_empty(), "Should have found bare URLs");
//     assert!(
//         fixed.contains("<https://example.com/page0>"),
//         "Should have fixed bare URLs"
//     );
//     assert!(
//         !fixed.contains("code block 3 with <https://"),
//         "Should not fix URLs in code blocks"
//     );
//     assert!(
//         !fixed.contains("with `<https://"),
//         "Should not fix URLs in inline code"
//     );

//     // Performance assertion - should complete in a reasonable time
//     assert!(
//         check_duration.as_millis() < 150,
//         "Check should complete in under 100ms ({}ms)",
//         check_duration.as_millis()
//     );
//     assert!(
//         fix_duration.as_millis() < 100,
//         "Fix should complete in under 100ms ({}ms)",
//         fix_duration.as_millis()
//     );
// }

// CRITICAL PARITY TESTS: Email Detection Enhancement
// These tests cover the major MD034 improvement that added email detection
// which increased parity by +5 warnings

#[test]
fn test_bare_email_addresses() {
    let rule = MD034NoBareUrls;
    let content = "Contact us at support@example.com or admin@test.org";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert_eq!(result.len(), 2, "Bare email addresses should be flagged as bare URLs");
    assert_eq!(result[0].line, 1);
    assert_eq!(result[1].line, 1);

    assert!(
        result[0]
            .message
            .contains("Email address without angle brackets or link formatting")
    );
    assert!(
        result[1]
            .message
            .contains("Email address without angle brackets or link formatting")
    );

    let fixed = rule.fix(&ctx).unwrap();
    assert_eq!(fixed, "Contact us at <support@example.com> or <admin@test.org>");
}

#[test]
fn test_email_addresses_various_formats() {
    let rule = MD034NoBareUrls;
    let test_cases = [
        ("Email: user@domain.com", 1, "Email: <user@domain.com>"),
        (
            "Complex email: user.name+tag@sub.domain.co.uk",
            1,
            "Complex email: <user.name+tag@sub.domain.co.uk>",
        ),
        (
            "Email with numbers: user123@example123.com",
            1,
            "Email with numbers: <user123@example123.com>",
        ),
        (
            "Email with hyphens: user-name@sub-domain.example-site.org",
            1,
            "Email with hyphens: <user-name@sub-domain.example-site.org>",
        ),
        ("Short TLD: user@example.co", 1, "Short TLD: <user@example.co>"),
        ("Long TLD: user@example.museum", 1, "Long TLD: <user@example.museum>"),
    ];

    for (content, expected_count, expected_fix) in test_cases.iter() {
        let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
        let result = rule.check(&ctx).unwrap();
        assert_eq!(result.len(), *expected_count, "Failed for content: {content}");

        if *expected_count > 0 {
            assert!(
                result.iter().any(|w| w
                    .message
                    .contains("Email address without angle brackets or link formatting")),
                "Email detection failed for: {content}"
            );

            let fixed = rule.fix(&ctx).unwrap();
            assert_eq!(fixed, *expected_fix, "Fix failed for: {content}");
        }
    }
}

#[test]
fn test_email_exclusions() {
    let rule = MD034NoBareUrls;
    let test_cases = [
        // Emails in markdown links should not be flagged
        ("[Contact](mailto:user@example.com)", 0),
        // Emails in angle brackets (already auto-linked) should not be flagged
        ("<user@example.com>", 0),
        // Emails in code spans should not be flagged
        ("`user@example.com`", 0),
        // Emails in code blocks should not be flagged
        ("```\nuser@example.com\n```", 0),
        // Emails in HTML attributes should not be flagged
        ("<a href=\"mailto:user@example.com\">Contact</a>", 0),
    ];

    for (content, expected_count) in test_cases.iter() {
        let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
        let result = rule.check(&ctx).unwrap();
        assert_eq!(result.len(), *expected_count, "Failed for content: {content}");
    }
}

// CRITICAL PARITY TESTS: Localhost URL Support Enhancement
// These tests cover the major MD034 improvement that added localhost URL detection
// which increased parity by +5 warnings (combined with email detection = +10 total)

#[test]
fn test_localhost_urls() {
    let rule = MD034NoBareUrls;
    let content = "Visit http://localhost:3000 and https://localhost:8080/api";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert_eq!(result.len(), 2, "Localhost URLs should be flagged as bare URLs");
    assert!(
        result
            .iter()
            .any(|w| w.message.contains("URL without angle brackets or link formatting")),
        "Localhost URL detection failed"
    );

    let fixed = rule.fix(&ctx).unwrap();
    assert_eq!(fixed, "Visit <http://localhost:3000> and <https://localhost:8080/api>");
}

#[test]
fn test_localhost_variations() {
    let rule = MD034NoBareUrls;
    let test_cases = [
        ("http://localhost", 1, "<http://localhost>"),
        ("https://localhost", 1, "<https://localhost>"),
        ("http://localhost:8080", 1, "<http://localhost:8080>"),
        ("https://localhost:3000", 1, "<https://localhost:3000>"),
        ("http://localhost/path", 1, "<http://localhost/path>"),
        ("https://localhost:9090/api/v1", 1, "<https://localhost:9090/api/v1>"),
        ("ftp://localhost", 1, "<ftp://localhost>"), // FTP is also supported
    ];

    for (content, expected_count, expected_fix) in test_cases.iter() {
        let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
        let result = rule.check(&ctx).unwrap();
        assert_eq!(result.len(), *expected_count, "Failed for content: {content}");

        if *expected_count > 0 {
            assert!(
                result
                    .iter()
                    .any(|w| w.message.contains("URL without angle brackets or link formatting")),
                "Localhost/protocol detection failed for: {content}"
            );

            let fixed = rule.fix(&ctx).unwrap();
            assert_eq!(fixed, *expected_fix, "Fix failed for: {content}");
        }
    }
}

#[test]
fn test_ip_address_urls() {
    let rule = MD034NoBareUrls;
    let content = "Connect to http://127.0.0.1:8080 or https://192.168.1.100";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert_eq!(result.len(), 2, "IP address URLs should be flagged as bare URLs");
    let fixed = rule.fix(&ctx).unwrap();
    assert_eq!(fixed, "Connect to <http://127.0.0.1:8080> or <https://192.168.1.100>");
}

#[test]
fn test_combined_emails_and_localhost() {
    let rule = MD034NoBareUrls;
    let content = "Contact admin@localhost.com or visit http://localhost:9090\nAlso try user@example.org and https://192.168.1.1:3000";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert_eq!(result.len(), 4, "Should detect both emails and localhost URLs");

    let fixed = rule.fix(&ctx).unwrap();
    let expected = "Contact <admin@localhost.com> or visit <http://localhost:9090>\nAlso try <user@example.org> and <https://192.168.1.1:3000>";
    assert_eq!(fixed, expected);
}

// REGRESSION TESTS: Prevent false positives that were previously fixed

#[test]
fn test_multiline_markdown_links_not_flagged() {
    let rule = MD034NoBareUrls;
    // This is the exact pattern that was causing false positives before the fix
    let content = "Details about each issue type and the issue lifecycle are discussed in the [MLflow Issue\nPolicy](https://github.com/mlflow/mlflow/blob/master/ISSUE_POLICY.md).\n\nAfter you have agreed upon an implementation strategy for your feature\nor patch with an MLflow committer, the next step is to introduce your\nchanges (see [developing\nchanges](https://github.com/mlflow/mlflow/blob/master/CONTRIBUTING.md#developing-and-testing-mlflow))\nas a pull request against the MLflow Repository.";

    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();

    // Should not flag any URLs since they are all properly formatted as markdown links
    assert!(
        result.is_empty(),
        "Multi-line markdown links should not be flagged as bare URLs. Found {} warnings: {:#?}",
        result.len(),
        result
    );

    // Fix should not change anything since there are no bare URLs
    let fixed = rule.fix(&ctx).unwrap();
    assert_eq!(
        fixed, content,
        "Fix should not change content with properly formatted multi-line markdown links"
    );
}

#[test]
fn test_issue_48_url_in_link_text() {
    // Issue #48: URL within link text should not be flagged as a bare URL
    let rule = MD034NoBareUrls;
    let content = "Also don't forget that the next time you need to figure out which `datetime` format you need, **[use the strptime tool at https://pym.dev/strptime](https://www.pythonmorsels.com/strptime/)**!";

    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();

    // The URL https://pym.dev/strptime is part of the link text and should NOT be flagged
    assert!(
        result.is_empty() || result.iter().all(|w| !w.message.contains("URL")),
        "URL within link text should not be flagged as bare URL. Found {} warnings: {:#?}",
        result.len(),
        result
    );
}

#[test]
fn test_issue_47_urls_emails_in_html_attributes() {
    // Issue #47: Email addresses and URLs in HTML attributes should not be flagged
    let rule = MD034NoBareUrls;
    let content = r#"# Example

This is **some text**.

<input type="email" name="fields[email]" id="drip-email" placeholder="email@domain.com">
<input name="fields[url]" value="https://www.example.com">"#;

    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();

    // Neither the email in placeholder nor the URL in value should be flagged
    assert!(
        result.is_empty(),
        "Emails and URLs within HTML attributes should not be flagged. Found {} warnings: {:#?}",
        result.len(),
        result
    );
}

#[test]
fn test_mixed_multiline_links_and_bare_urls() {
    let rule = MD034NoBareUrls;
    // Test content with both multi-line markdown links (should not be flagged) and bare URLs (should be flagged)
    let content = "This has a [multi-line\nlink](https://github.com/example/repo) which should not be flagged.\n\nBut this bare URL should be flagged: https://bare-url.com\n\nAnd this [another multi-line\nlink with long URL](https://github.com/very/long/repository/path/that/spans/multiple/lines) should also not be flagged.";

    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();

    // Should only flag the one bare URL
    assert_eq!(
        result.len(),
        1,
        "Should only flag the bare URL, not the multi-line markdown links. Found {} warnings: {:#?}",
        result.len(),
        result
    );

    // Verify the flagged URL is the correct one
    assert!(
        result[0]
            .message
            .contains("URL without angle brackets or link formatting"),
        "Should flag bare URL with correct message"
    );

    // Check that the fix only wraps the bare URL
    let fixed = rule.fix(&ctx).unwrap();
    assert!(
        fixed.contains("<https://bare-url.com>"),
        "Should wrap the bare URL in angle brackets"
    );
    assert!(
        fixed.contains("[multi-line\nlink](https://github.com/example/repo)"),
        "Should not modify the multi-line markdown link"
    );
    assert!(
        fixed.contains("[another multi-line\nlink with long URL](https://github.com/very/long/repository/path/that/spans/multiple/lines)"),
        "Should not modify the second multi-line markdown link"
    );
}

#[test]
fn test_issue_104_url_in_empty_link() {
    // Issue #104: URL in link text with empty URL part [url]()
    // This is the pattern from issue #104: [https://github.com/pfeif/hx-complete-generator]()
    // The URL is in the link text with empty URL part
    let rule = MD034NoBareUrls;
    let content = "check it out in its new repository at [https://github.com/pfeif/hx-complete-generator]().";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();

    // The URL in [url]() should NOT be flagged because it's part of a markdown link construct
    // (even though the link is empty/invalid, it's still a link construct that should be handled by MD042)
    assert_eq!(
        result.len(),
        0,
        "URL in [url]() link text should not be flagged as bare URL. This is MD042 territory. Found {} warnings: {:#?}",
        result.len(),
        result
    );
}

#[test]
fn test_issue_104_url_in_empty_bracket_link() {
    // Issue #104: Similar pattern with [url][]
    let rule = MD034NoBareUrls;
    let content = "Visit [https://www.google.com][] for more info.";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();

    // Should not be flagged - it's part of a markdown link reference construct
    assert_eq!(
        result.len(),
        0,
        "URL in [url][] should not be flagged as bare URL. Found {} warnings: {:#?}",
        result.len(),
        result
    );
}

#[test]
fn test_issue_104_full_paragraph_not_corrupted() {
    // Issue #104: Full regression test with the actual paragraph from the bug report
    // This tests that after MD042 fixes the empty link, MD034 doesn't corrupt the text
    let rule = MD034NoBareUrls;

    // This is what the content looks like AFTER MD042 has fixed the empty link
    // MD042 now intelligently uses the URL from the text as the destination
    let content_after_md042 = "I've never been one to implement hacky solutions because life is just easier\nwhen everything gets done \"by the book.\" So, if you're reading this and want to\nsee the code that creates this extension and prevents me from pouring needless\nhours into meticulously maintaining the files by hand, I welcome you to check it\nout in its new repository at [https://github.com/pfeif/hx-complete-generator](https://github.com/pfeif/hx-complete-generator).";

    let ctx = LintContext::new(content_after_md042, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();

    // MD034 should NOT flag the URL because it's properly in a markdown link now
    assert_eq!(
        result.len(),
        0,
        "After MD042 fixes empty link, MD034 should not flag the URL. Found {} warnings: {:#?}",
        result.len(),
        result
    );

    // Verify MD034 fix produces exactly the expected output (no modifications)
    let fixed = rule.fix(&ctx).unwrap();
    assert_eq!(
        fixed, content_after_md042,
        "MD034 should not modify content that has properly formatted links"
    );
}

// Issue #116: URLs in front matter should not be flagged
#[test]
fn test_urls_in_yaml_front_matter() {
    let rule = MD034NoBareUrls;
    let content = "---\nurl: http://example.com\ntitle: Test\n---\n\n# Content";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert!(result.is_empty(), "URLs in YAML front matter should not be flagged");
}

#[test]
fn test_urls_in_toml_front_matter() {
    let rule = MD034NoBareUrls;
    let content = "+++\nurl = \"http://example.com\"\ntitle = \"Test\"\n+++\n\n# Content";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert!(result.is_empty(), "URLs in TOML front matter should not be flagged");
}

#[test]
fn test_urls_in_json_front_matter() {
    let rule = MD034NoBareUrls;
    let content = "{\n\"url\": \"http://example.com\",\n\"title\": \"Test\"\n}\n\n# Content";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert!(result.is_empty(), "URLs in JSON front matter should not be flagged");
}

#[test]
fn test_bare_url_after_front_matter() {
    let rule = MD034NoBareUrls;
    let content = "---\nurl: http://example.com\n---\n\nVisit http://bare-url.com";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert_eq!(result.len(), 1, "Bare URL after front matter should be flagged");
    assert!(result[0].message.contains("http://bare-url.com"));

    let fixed = rule.fix(&ctx).unwrap();
    assert!(
        fixed.contains("<http://bare-url.com>"),
        "Bare URL should be wrapped in angle brackets"
    );
    assert!(
        fixed.contains("url: http://example.com"),
        "URL in front matter should remain unchanged"
    );
}

#[test]
fn test_email_in_front_matter() {
    let rule = MD034NoBareUrls;
    let content = "---\nauthor_email: user@example.com\ncontact: admin@test.org\n---\n\n# Content";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert!(result.is_empty(), "Emails in front matter should not be flagged");
}

#[test]
fn test_multiple_urls_in_front_matter() {
    let rule = MD034NoBareUrls;
    let content = "---\nurl: http://example.com\nrepository: https://github.com/user/repo\nwebsite: ftp://files.example.org\n---\n\n# Content";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert!(result.is_empty(), "Multiple URLs in front matter should not be flagged");
}

#[test]
fn test_issue_116_exact_reproduction() {
    // This is the exact test case from issue #116
    let rule = MD034NoBareUrls;
    let content = "---\nurl: http://example.com\n---\n\n# Repro";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert!(
        result.is_empty(),
        "Issue #116: URL in front matter should not be flagged"
    );
}

#[test]
fn test_issue_151_urls_in_html_block_attributes() {
    // This is the exact test case from issue #151
    // URLs in HTML tag attributes should not be flagged
    let rule = MD034NoBareUrls;
    let content = r#"<figure>
  <img
    src="https://example.com/test.html"
  />
</figure>"#;
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert!(
        result.is_empty(),
        "Issue #151: URL in HTML block attribute should not be flagged"
    );
}

#[test]
fn test_issue_151_single_line_html_tag_with_url() {
    let rule = MD034NoBareUrls;
    let content = r#"<img src="https://example.com/image.png" alt="test" />"#;
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert!(
        result.is_empty(),
        "Single-line HTML tag with URL in attribute should not be flagged"
    );
}

#[test]
fn test_issue_151_multiple_urls_in_html_block() {
    let rule = MD034NoBareUrls;
    let content = r#"<div>
  <img src="https://example.com/image1.png" />
  <img src="https://example.com/image2.png" />
  <a href="https://example.com/page.html">Link</a>
</div>"#;
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert!(
        result.is_empty(),
        "Multiple URLs in HTML block attributes should not be flagged"
    );
}

#[test]
fn test_issue_151_various_html_tag_types() {
    let rule = MD034NoBareUrls;
    let content = r#"<section>
  <div data-url="https://example.com/api">
    <iframe src="https://example.com/embed.html"></iframe>
  </div>
</section>"#;
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert!(
        result.is_empty(),
        "URLs in various HTML tag types should not be flagged"
    );
}

#[test]
fn test_issue_151_nested_html_blocks_with_urls() {
    let rule = MD034NoBareUrls;
    let content = r#"<article>
  <header>
    <img src="https://example.com/logo.png" />
  </header>
  <div class="content">
    <a href="https://example.com/link1.html">Link 1</a>
    <figure>
      <img src="https://example.com/image.png" />
    </figure>
  </div>
</article>"#;
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert!(result.is_empty(), "Nested HTML blocks with URLs should not be flagged");
}

#[test]
fn test_issue_151_html_block_with_mixed_content() {
    let rule = MD034NoBareUrls;
    let content = r#"<div>
  Some text content
  <img src="https://example.com/image.png" />
  More text
</div>

Outside HTML: https://example.com/should-flag.html"#;
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert_eq!(result.len(), 1, "Only bare URL outside HTML block should be flagged");
    assert_eq!(result[0].line, 7);
}

/// Regression test for issue #178: Multi-byte Unicode characters before code spans
/// caused byte-vs-character position mismatch, leading to false positives
#[test]
fn test_issue_178_unicode_before_inline_code_url() {
    let rule = MD034NoBareUrls;

    // Curly apostrophe (U+2019) is 3 bytes in UTF-8, causing byte offset mismatch
    let content = "- Some code\u{2019}s example `https://example.com` containing a URL";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert!(
        result.is_empty(),
        "URL in inline code after curly apostrophe should NOT be flagged, got {result:?}"
    );

    // Multiple lines with curly apostrophe
    let content2 = "- [Some normal URL](https://example.com)\n- Some code\u{2019}s example `https://example.com` containing an URL\n- Some code\u{2019}s repro example `https://example.com`";
    let ctx2 = LintContext::new(content2, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result2 = rule.check(&ctx2).unwrap();
    assert!(
        result2.is_empty(),
        "URLs in inline code should NOT be flagged, got {result2:?}"
    );
}

/// Test various multi-byte Unicode characters before inline code with URLs
#[test]
fn test_unicode_multibyte_chars_before_inline_code_url() {
    let rule = MD034NoBareUrls;

    // Various multi-byte characters
    let test_cases = [
        ("Left curly quote", "Text \u{2018}quoted\u{2019} `https://example.com`"),
        ("Em dash", "Text\u{2014}dash `https://example.com`"),
        ("Euro sign", "Price 100\u{20AC} `https://example.com`"),
        ("Japanese", "\u{3042}\u{3044}\u{3046} `https://example.com`"),
        ("Emoji", "\u{1F600} happy `https://example.com`"),
        ("Chinese", "\u{4E2D}\u{6587} `https://example.com`"),
    ];

    for (name, content) in test_cases {
        let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
        let result = rule.check(&ctx).unwrap();
        assert!(
            result.is_empty(),
            "{name}: URL in inline code after multi-byte chars should NOT be flagged, got {result:?}"
        );
    }
}

#[test]
fn test_reference_definitions_with_titles_not_flagged() {
    let rule = MD034NoBareUrls;

    // Reference definitions should NOT be flagged - they are valid markdown link syntax
    let test_cases = [
        // Basic reference definition without title
        "[example]: https://example.com",
        // Reference definition with double-quoted title
        "[example]: https://example.com \"Title here\"",
        // Reference definition with single-quoted title
        "[example]: https://example.com 'Title here'",
        // Reference definition with parenthesized title
        "[example]: https://example.com (Title here)",
        // Reference with backticks in label
        "[`maturin`]: https://github.com/PyO3/maturin \"Build and publish crates\"",
        // Reference with angle brackets
        "[example]: <https://example.com> \"Title\"",
        // Real-world example from pyo3
        "[feature flags]: https://doc.rust-lang.org/cargo/reference/features.html \"Features - The Cargo Book\"",
        // Multiple reference definitions
        "[ref1]: https://example.com\n[ref2]: https://test.com \"Test title\"",
        // Indented reference definition
        "  [example]: https://example.com \"Indented\"",
    ];

    for content in test_cases {
        let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
        let result = rule.check(&ctx).unwrap();
        assert!(
            result.is_empty(),
            "Reference definition should NOT be flagged as bare URL:\n{content}\nGot: {result:?}"
        );
    }
}

#[test]
fn test_bare_urls_still_flagged_with_reference_definitions() {
    let rule = MD034NoBareUrls;

    // Mix of reference definitions (ok) and bare URLs (should be flagged)
    let content = r#"# Test Document

This has a bare URL: https://bare.example.com

[example]: https://example.com "This is fine"

Another bare URL: https://another.bare.url
"#;

    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();

    // Should flag exactly 2 bare URLs, not the reference definition
    assert_eq!(
        result.len(),
        2,
        "Expected 2 bare URLs flagged, got {}:\n{:?}",
        result.len(),
        result
    );

    // Verify the flagged URLs
    assert!(result[0].message.contains("https://bare.example.com"));
    assert!(result[1].message.contains("https://another.bare.url"));
}

#[test]
fn test_www_urls_without_protocol() {
    let rule = MD034NoBareUrls;

    // www URLs should be detected as bare URLs (matching markdownlint behavior)
    let content = "# Test\n\nVisit www.example.com for info.";

    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();

    assert_eq!(
        result.len(),
        1,
        "www URL should be flagged as bare URL. Got: {result:?}"
    );
    assert!(
        result[0].message.contains("www.example.com"),
        "Message should contain the www URL"
    );
}

// =============================================================================
// URL boundary detection tests
// =============================================================================

/// Test that URLs inside markdown links are not flagged (basic case)
#[test]
fn test_url_inside_markdown_link_not_flagged() {
    let rule = MD034NoBareUrls;

    let content = "[Link text](https://example.com)";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();

    assert!(
        result.is_empty(),
        "URL inside markdown link should NOT be flagged: {result:?}"
    );
}

/// Test URL inside markdown link followed by text
#[test]
fn test_url_inside_markdown_link_with_trailing_text() {
    let rule = MD034NoBareUrls;

    let content = "See [here](https://example.com) for details.";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();

    assert!(
        result.is_empty(),
        "URL inside markdown link should NOT be flagged even with trailing text: {result:?}"
    );
}

/// Test multiple markdown links on the same line
#[test]
fn test_multiple_markdown_links_same_line() {
    let rule = MD034NoBareUrls;

    let content = "[Link1](https://example.com) and [Link2](https://test.com) are both valid.";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();

    assert!(
        result.is_empty(),
        "Multiple URLs inside markdown links should NOT be flagged: {result:?}"
    );
}

/// Test URL inside image syntax
#[test]
fn test_url_inside_image_not_flagged() {
    let rule = MD034NoBareUrls;

    let content = "![Alt text](https://example.com/image.png)";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();

    assert!(
        result.is_empty(),
        "URL inside image syntax should NOT be flagged: {result:?}"
    );
}

/// Test URL inside nested parentheses (complex boundary)
#[test]
fn test_url_with_nested_parentheses_in_link() {
    let rule = MD034NoBareUrls;

    // Wikipedia-style URL inside a markdown link
    let content = "[Rust](https://en.wikipedia.org/wiki/Rust_(programming_language))";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();

    assert!(
        result.is_empty(),
        "URL with nested parens inside markdown link should NOT be flagged: {result:?}"
    );
}

/// Test that bare URLs outside links ARE still flagged
#[test]
fn test_bare_url_outside_link_still_flagged() {
    let rule = MD034NoBareUrls;

    let content = "Visit https://example.com for more info.";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();

    assert_eq!(result.len(), 1, "Bare URL outside markdown link SHOULD be flagged");
    assert!(result[0].message.contains("https://example.com"));
}

/// Test mixed: markdown link and bare URL on same line
#[test]
fn test_markdown_link_and_bare_url_same_line() {
    let rule = MD034NoBareUrls;

    let content = "[Good link](https://example.com) but also https://bare.url here";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();

    // Should only flag the bare URL, not the one in the markdown link
    assert_eq!(result.len(), 1, "Should flag only the bare URL, got: {result:?}");
    assert!(
        result[0].message.contains("https://bare.url"),
        "Should flag the bare URL, not the markdown link URL"
    );
}

/// Test URL starting inside link construct (boundary edge case)
#[test]
fn test_url_starting_inside_link_boundary() {
    let rule = MD034NoBareUrls;

    // URL detection might find a URL that extends beyond the link boundary
    // if the link has complex structure. The fix ensures we check if the URL
    // *starts* inside the construct, not if it's entirely contained.
    let content = "[Link](https://example.com/path)";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();

    assert!(
        result.is_empty(),
        "URL starting inside link should NOT be flagged: {result:?}"
    );
}

/// Test URL in angle brackets (autolink) not flagged
#[test]
fn test_url_in_angle_brackets_not_flagged() {
    let rule = MD034NoBareUrls;

    let content = "Contact us at <https://example.com>";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();

    assert!(
        result.is_empty(),
        "URL in angle brackets (autolink) should NOT be flagged: {result:?}"
    );
}

/// Test URL in reference definition not flagged
#[test]
fn test_url_in_reference_definition_boundary() {
    let rule = MD034NoBareUrls;

    let content = "[ref]: https://example.com\n\nSee [ref] for details.";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();

    assert!(
        result.is_empty(),
        "URL in reference definition should NOT be flagged: {result:?}"
    );
}

// =============================================================================
// XMPP URI tests (GFM extended autolinks)
// =============================================================================

/// Test bare XMPP URIs are flagged
#[test]
fn test_bare_xmpp_uri() {
    let rule = MD034NoBareUrls;

    let content = "Contact me at xmpp:user@example.com";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();

    assert_eq!(result.len(), 1, "Bare XMPP URI should be flagged");
    assert!(
        result[0].message.contains("xmpp:user@example.com"),
        "Message should contain the XMPP URI"
    );

    let fixed = rule.fix(&ctx).unwrap();
    assert_eq!(fixed, "Contact me at <xmpp:user@example.com>");
}

/// Test XMPP URI with resource path
#[test]
fn test_xmpp_uri_with_resource() {
    let rule = MD034NoBareUrls;

    let content = "My chat address: xmpp:foo@bar.baz/txt";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();

    assert_eq!(result.len(), 1, "Bare XMPP URI with resource should be flagged");
    assert!(
        result[0].message.contains("xmpp:foo@bar.baz/txt"),
        "Message should contain the XMPP URI with resource"
    );

    let fixed = rule.fix(&ctx).unwrap();
    assert_eq!(fixed, "My chat address: <xmpp:foo@bar.baz/txt>");
}

/// Test XMPP URI in angle brackets (properly formatted) is not flagged
#[test]
fn test_xmpp_uri_in_angle_brackets() {
    let rule = MD034NoBareUrls;

    let content = "Contact me at <xmpp:user@example.com>";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();

    assert!(
        result.is_empty(),
        "XMPP URI in angle brackets should NOT be flagged: {result:?}"
    );
}

/// Test XMPP URI in markdown link is not flagged
#[test]
fn test_xmpp_uri_in_markdown_link() {
    let rule = MD034NoBareUrls;

    let content = "[Chat with me](xmpp:user@example.com)";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();

    assert!(
        result.is_empty(),
        "XMPP URI in markdown link should NOT be flagged: {result:?}"
    );
}

/// Test multiple XMPP URIs
#[test]
fn test_multiple_xmpp_uris() {
    let rule = MD034NoBareUrls;

    let content = "Contact xmpp:alice@example.com or xmpp:bob@example.org/work";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();

    assert_eq!(result.len(), 2, "Both bare XMPP URIs should be flagged");

    let fixed = rule.fix(&ctx).unwrap();
    assert_eq!(fixed, "Contact <xmpp:alice@example.com> or <xmpp:bob@example.org/work>");
}

/// Test XMPP URI mixed with regular URLs and emails
#[test]
fn test_xmpp_uri_mixed_with_urls_and_emails() {
    let rule = MD034NoBareUrls;

    let content = "Website: https://example.com\nEmail: user@example.com\nXMPP: xmpp:chat@example.com";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();

    assert_eq!(result.len(), 3, "URL, email, and XMPP URI should all be flagged");

    let fixed = rule.fix(&ctx).unwrap();
    assert_eq!(
        fixed,
        "Website: <https://example.com>\nEmail: <user@example.com>\nXMPP: <xmpp:chat@example.com>"
    );
}

/// Test XMPP URI in code block is not flagged
#[test]
fn test_xmpp_uri_in_code_block() {
    let rule = MD034NoBareUrls;

    let content = "```\nxmpp:user@example.com\n```";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();

    assert!(
        result.is_empty(),
        "XMPP URI in code block should NOT be flagged: {result:?}"
    );
}

/// Test XMPP URI in inline code is not flagged
#[test]
fn test_xmpp_uri_in_inline_code() {
    let rule = MD034NoBareUrls;

    let content = "Use `xmpp:user@example.com` for chat.";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();

    assert!(
        result.is_empty(),
        "XMPP URI in inline code should NOT be flagged: {result:?}"
    );
}

/// Test XMPP URI variations per GFM spec
#[test]
fn test_xmpp_uri_variations() {
    let rule = MD034NoBareUrls;

    let test_cases = [
        // Basic XMPP URI
        ("xmpp:user@domain.com", 1, "<xmpp:user@domain.com>"),
        // With subdomain
        ("xmpp:chat@chat.example.org", 1, "<xmpp:chat@chat.example.org>"),
        // With resource
        ("xmpp:user@domain.net/mobile", 1, "<xmpp:user@domain.net/mobile>"),
        // With complex resource
        (
            "xmpp:user@domain.com/resource/path",
            1,
            "<xmpp:user@domain.com/resource/path>",
        ),
        // With numbers
        ("xmpp:user123@domain456.com", 1, "<xmpp:user123@domain456.com>"),
        // With dots in username
        ("xmpp:first.last@domain.com", 1, "<xmpp:first.last@domain.com>"),
    ];

    for (content, expected_count, expected_fix) in test_cases.iter() {
        let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
        let result = rule.check(&ctx).unwrap();
        assert_eq!(result.len(), *expected_count, "Failed for XMPP URI: {content}");

        if *expected_count > 0 {
            let fixed = rule.fix(&ctx).unwrap();
            assert_eq!(fixed, *expected_fix, "Fix failed for XMPP URI: {content}");
        }
    }
}

/// Test www URLs with query strings (GFM autolink extension)
#[test]
fn test_www_urls_with_query_string() {
    let rule = MD034NoBareUrls;

    let test_cases = [
        (
            "www.example.com?param=value",
            1,
            "<https://www.example.com?param=value>",
        ),
        ("www.example.com?a=1&b=2", 1, "<https://www.example.com?a=1&b=2>"),
        (
            "www.example.com/path?query=test",
            1,
            "<https://www.example.com/path?query=test>",
        ),
    ];

    for (content, expected_count, expected_fix) in test_cases.iter() {
        let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
        let result = rule.check(&ctx).unwrap();
        assert_eq!(result.len(), *expected_count, "Failed for www URL: {content}");

        if *expected_count > 0 {
            let fixed = rule.fix(&ctx).unwrap();
            assert_eq!(fixed, *expected_fix, "Fix failed for www URL: {content}");
        }
    }
}

/// Test www URLs with fragment identifiers
#[test]
fn test_www_urls_with_fragment() {
    let rule = MD034NoBareUrls;

    let test_cases = [
        ("www.example.com#section", 1, "<https://www.example.com#section>"),
        (
            "www.example.com/page#anchor",
            1,
            "<https://www.example.com/page#anchor>",
        ),
        ("www.example.com?q=1#frag", 1, "<https://www.example.com?q=1#frag>"),
    ];

    for (content, expected_count, expected_fix) in test_cases.iter() {
        let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
        let result = rule.check(&ctx).unwrap();
        assert_eq!(result.len(), *expected_count, "Failed for www URL: {content}");

        if *expected_count > 0 {
            let fixed = rule.fix(&ctx).unwrap();
            assert_eq!(fixed, *expected_fix, "Fix failed for www URL: {content}");
        }
    }
}

/// Test www URLs with port numbers
#[test]
fn test_www_urls_with_port() {
    let rule = MD034NoBareUrls;

    let test_cases = [
        ("www.example.com:8080", 1, "<https://www.example.com:8080>"),
        ("www.example.com:3000/path", 1, "<https://www.example.com:3000/path>"),
        ("www.example.com:443?q=1", 1, "<https://www.example.com:443?q=1>"),
    ];

    for (content, expected_count, expected_fix) in test_cases.iter() {
        let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
        let result = rule.check(&ctx).unwrap();
        assert_eq!(result.len(), *expected_count, "Failed for www URL: {content}");

        if *expected_count > 0 {
            let fixed = rule.fix(&ctx).unwrap();
            assert_eq!(fixed, *expected_fix, "Fix failed for www URL: {content}");
        }
    }
}

/// Test www URLs in context (embedded in sentences)
#[test]
fn test_www_urls_in_context() {
    let rule = MD034NoBareUrls;

    let test_cases = [
        ("Visit www.example.com for more info.", 1),
        ("Check out www.example.com/docs#getting-started today!", 1),
        ("Server at www.internal.example.com:8080/api is ready.", 1),
    ];

    for (content, expected_count) in test_cases.iter() {
        let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
        let result = rule.check(&ctx).unwrap();
        assert_eq!(result.len(), *expected_count, "Failed for: {content}");
    }
}

/// Test www URLs properly formatted (should NOT be flagged)
#[test]
fn test_www_urls_not_flagged_when_formatted() {
    let rule = MD034NoBareUrls;

    let test_cases = [
        "<https://www.example.com>",
        "[link](https://www.example.com)",
        "[www.example.com](https://www.example.com)",
        "`www.example.com`",
    ];

    for content in test_cases.iter() {
        let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
        let result = rule.check(&ctx).unwrap();
        assert!(result.is_empty(), "Formatted www URL should NOT be flagged: {content}");
    }
}

/// Test mixed www and protocol URLs
#[test]
fn test_www_and_protocol_urls_mixed() {
    let rule = MD034NoBareUrls;

    let content = "Visit www.example.com and https://other.com for info.";
    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();
    assert_eq!(result.len(), 2, "Both www and https URLs should be flagged");

    let fixed = rule.fix(&ctx).unwrap();
    assert_eq!(
        fixed,
        "Visit <https://www.example.com> and <https://other.com> for info."
    );
}

/// Test that multi-byte UTF-8 characters before emails don't cause panics
/// Regression test for kubernetes/website Bengali text issue
#[test]
fn test_email_detection_with_multibyte_utf8() {
    let rule = MD034NoBareUrls;

    // Bengali text followed by email - the email address starts at a byte offset
    // that could land inside a multi-byte character if we subtract 5 naively
    let content = "কুবারনেটিস কমিউনিটির মধ্যে ঘটে যাওয়া ঘটনাগুলির জন্য, conduct@kubernetes.io মাধ্যমে";

    let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
    let result = rule.check(&ctx).unwrap();

    // The email should be detected
    assert_eq!(result.len(), 1, "Email should be detected in Bengali text");
    assert!(
        result[0].message.contains("Email address without angle brackets"),
        "Should flag bare email"
    );
}

/// Test various multi-byte UTF-8 edge cases with emails
#[test]
fn test_email_detection_various_scripts() {
    let rule = MD034NoBareUrls;

    let test_cases = [
        // Japanese
        ("日本語テキスト user@example.com 日本語", 1),
        // Chinese
        ("中文文本 user@example.com 更多中文", 1),
        // Arabic
        ("نص عربي user@example.com نص آخر", 1),
        // Emoji
        ("🎉 email@test.com 🎉", 1),
        // Mixed scripts
        ("日本語 中文 العربية user@example.com more", 1),
    ];

    for (content, expected_count) in test_cases.iter() {
        let ctx = LintContext::new(content, rumdl_lib::config::MarkdownFlavor::Standard, None);
        let result = rule.check(&ctx).unwrap();
        assert_eq!(
            result.len(),
            *expected_count,
            "Failed for multi-byte content: {content}"
        );
    }
}