alef-e2e 0.16.65

Fixture-driven e2e test generator for alef
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
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
//! Swift e2e test generator using XCTest.
//!
//! Generates a standalone Swift package at `e2e/swift_e2e/` that depends on the
//! binding at `packages/swift/` via `.package(path:)`.
//!
//! IMPORTANT: SwiftPM 6.0 derives the identity of path-based dependencies from
//! the path's *basename* and ignores any explicit `name:` override. If the
//! consumer (`e2e/swift/`) and the dep (`packages/swift/`) share the same path
//! basename `swift`, SwiftPM treats them as the same package and fails
//! resolution with: `product '<X>' required by package 'swift' target '...' not
//! found in package 'swift'`. The e2e package is therefore emitted under
//! `swift_e2e/` to guarantee a distinct identity from any sibling
//! `packages/swift/` dep.

use crate::config::E2eConfig;
use crate::escape::{escape_java as escape_swift_str, expand_fixture_templates, sanitize_filename, sanitize_ident};
use crate::field_access::{FieldResolver, SwiftFirstClassMap};
use crate::fixture::{Assertion, Fixture, FixtureGroup, ValidationErrorExpectation};
use alef_core::backend::GeneratedFile;
use alef_core::config::ResolvedCrateConfig;
use alef_core::hash::{self, CommentStyle};
use alef_core::template_versions::toolchain;
use anyhow::Result;
use heck::{ToLowerCamelCase, ToSnakeCase, ToUpperCamelCase};
use std::collections::HashMap;
use std::collections::HashSet;
use std::fmt::Write as FmtWrite;
use std::path::PathBuf;

use super::E2eCodegen;
use super::client;

/// Swift e2e code generator.
pub struct SwiftE2eCodegen;

impl E2eCodegen for SwiftE2eCodegen {
    fn generate(
        &self,
        groups: &[FixtureGroup],
        e2e_config: &E2eConfig,
        config: &ResolvedCrateConfig,
        type_defs: &[alef_core::ir::TypeDef],
        _enums: &[alef_core::ir::EnumDef],
    ) -> Result<Vec<GeneratedFile>> {
        let lang = self.language_name();
        // Emit under `<output>/swift_e2e/` so the consumer's SwiftPM identity
        // (derived from path basename) does not collide with the dep at
        // `packages/swift/` (also basename `swift`). SwiftPM 6.0 deprecated the
        // `name:` parameter on `.package(path:)` and uses the path basename as
        // the package's identity unconditionally, so disambiguation must happen
        // at the filesystem level. Consumers of the alef-emitted e2e must
        // `cd e2e/swift_e2e/` to run `swift test`.
        let output_base = PathBuf::from(e2e_config.effective_output()).join("swift_e2e");

        let mut files = Vec::new();

        // Resolve call config with overrides.
        let call = &e2e_config.call;
        let overrides = call.overrides.get(lang);
        let function_name = overrides
            .and_then(|o| o.function.as_ref())
            .cloned()
            .unwrap_or_else(|| call.function.clone());
        let result_var = &call.result_var;
        let result_is_simple = overrides.is_some_and(|o| o.result_is_simple);

        // Resolve package config.
        let swift_pkg = e2e_config.resolve_package("swift");
        let pkg_name = swift_pkg
            .as_ref()
            .and_then(|p| p.name.as_ref())
            .cloned()
            .unwrap_or_else(|| config.name.to_upper_camel_case());
        let pkg_path = swift_pkg
            .as_ref()
            .and_then(|p| p.path.as_ref())
            .cloned()
            .unwrap_or_else(|| "../../packages/swift".to_string());
        let pkg_version = swift_pkg
            .as_ref()
            .and_then(|p| p.version.as_ref())
            .cloned()
            .or_else(|| config.resolved_version())
            .unwrap_or_else(|| "0.1.0".to_string());

        // The Swift module name: UpperCamelCase of the package name.
        let module_name = pkg_name.as_str();

        // Resolve the registry URL: derive from the configured repository when
        // available (with a `.git` suffix per SwiftPM convention). Falls back
        // to a vendor-neutral placeholder when no repo is configured.
        let registry_url = config
            .try_github_repo()
            .map(|repo| {
                let base = repo.trim_end_matches('/').trim_end_matches(".git");
                format!("{base}.git")
            })
            .unwrap_or_else(|_| format!("https://example.invalid/{module_name}.git"));

        // Generate Package.swift for the standalone e2e consumer at
        // `<output>/swift_e2e/`. `swift test` is run from that directory.
        files.push(GeneratedFile {
            path: output_base.join("Package.swift"),
            content: render_package_swift(module_name, &registry_url, &pkg_path, &pkg_version, e2e_config.dep_mode),
            generated_header: false,
        });

        // Tests are placed alongside Package.swift under `<output>/swift_e2e/Tests/...`.
        let tests_base = output_base.clone();

        // Build the Swift first-class/opaque classification map for per-segment
        // dispatch in `render_swift_with_first_class_map`. A TypeDef is treated
        // as first-class (Codable struct → property access) when it's not opaque,
        // has serde derives, and every binding field is primitive/optional. This
        // mirrors `can_emit_first_class_struct` in alef-backend-swift.
        let swift_first_class_map = build_swift_first_class_map(type_defs, e2e_config);

        let field_resolver = FieldResolver::new_with_swift_first_class(
            &e2e_config.fields,
            &e2e_config.fields_optional,
            &e2e_config.result_fields,
            &e2e_config.fields_array,
            &e2e_config.fields_method_calls,
            &HashMap::new(),
            swift_first_class_map,
        );

        // Resolve client_factory override for swift (enables client-instance dispatch).
        let client_factory: Option<&str> = overrides.and_then(|o| o.client_factory.as_deref());

        // Emit a shared TestHelpers.swift that gives `RustString` a
        // `CustomStringConvertible` conformance. swift-bridge generates the
        // `RustString` opaque class but does NOT make it print readably — so
        // any error thrown from a bridge function (the `throw RustString(...)`
        // branches) surfaces in XCTest's failure output as the bare type name
        // `"RustBridge.RustString"`, with the actual Rust error message
        // hidden inside the unprinted instance. The retroactive extension
        // here pulls `.toString()` into `.description` so failures print
        // something diagnostic. Single file per test target; idempotent
        // across regens.
        files.push(GeneratedFile {
            path: tests_base
                .join("Tests")
                .join(format!("{module_name}E2ETests"))
                .join("TestHelpers.swift"),
            content: render_test_helpers_swift(),
            generated_header: true,
        });

        // One test file per fixture group.
        for group in groups {
            let active: Vec<&Fixture> = group
                .fixtures
                .iter()
                .filter(|f| super::should_include_fixture(f, lang, e2e_config))
                .collect();

            if active.is_empty() {
                continue;
            }

            let class_name = format!("{}Tests", sanitize_filename(&group.category).to_upper_camel_case());
            let filename = format!("{class_name}.swift");
            let content = render_test_file(
                &group.category,
                &active,
                e2e_config,
                module_name,
                &class_name,
                &function_name,
                result_var,
                &e2e_config.call.args,
                &field_resolver,
                result_is_simple,
                &e2e_config.fields_enum,
                client_factory,
            );
            files.push(GeneratedFile {
                path: tests_base
                    .join("Tests")
                    .join(format!("{module_name}E2ETests"))
                    .join(filename),
                content,
                generated_header: true,
            });
        }

        Ok(files)
    }

    fn language_name(&self) -> &'static str {
        "swift"
    }
}

// ---------------------------------------------------------------------------
// Rendering
// ---------------------------------------------------------------------------

/// Render the shared `TestHelpers.swift` file emitted into each Swift e2e
/// test target. Adds a `CustomStringConvertible` conformance to swift-bridge's
/// `RustString` so error messages from bridge throws print their actual Rust
/// content instead of the bare class name.
fn render_test_helpers_swift() -> String {
    let header = hash::header(CommentStyle::DoubleSlash);
    format!(
        r#"{header}import Foundation
import RustBridge

// Make `RustString` print its content in XCTest failure output. Without this,
// every error thrown from the swift-bridge layer surfaces as
// `caught error: "RustBridge.RustString"` with the actual message hidden
// inside the opaque class instance. The `@retroactive` keyword acknowledges
// that the conformed-to protocol (`CustomStringConvertible`) and the
// conforming type (`RustString`) both live outside this module — required by
// Swift 6 to silence the retroactive-conformance warning. swift-bridge does
// not give `RustString` a `description` of its own, so there is no conflict.
extension RustString: @retroactive CustomStringConvertible {{
    public var description: String {{ self.toString() }}
}}
"#
    )
}

fn render_package_swift(
    module_name: &str,
    registry_url: &str,
    pkg_path: &str,
    pkg_version: &str,
    dep_mode: crate::config::DependencyMode,
) -> String {
    let min_macos = toolchain::SWIFT_MIN_MACOS;

    // For local deps SwiftPM identity = last path component (e.g. "../../packages/swift" → "swift").
    // For registry deps identity is inferred from the URL.
    // Use explicit .product(name:package:) to avoid ambiguity under tools-version 6.0.
    let (dep_block, product_dep) = match dep_mode {
        crate::config::DependencyMode::Registry => {
            let dep = format!(r#"        .package(url: "{registry_url}", from: "{pkg_version}")"#);
            let pkg_id = registry_url
                .trim_end_matches('/')
                .trim_end_matches(".git")
                .split('/')
                .next_back()
                .unwrap_or(module_name);
            let prod = format!(r#".product(name: "{module_name}", package: "{pkg_id}")"#);
            (dep, prod)
        }
        crate::config::DependencyMode::Local => {
            // SwiftPM 6.0 deprecated the `name:` parameter on `.package(path:)`:
            // package identity is derived from the path's last component, ignoring
            // any explicit `name:`. The `.product(package:)` reference must therefore
            // match that identity (the path basename), not the dep's declared
            // `Package(name:)`. The product `name:` still matches the library
            // declared in the dep's manifest (e.g. `.library(name: "Kreuzberg")`).
            let pkg_id = pkg_path.trim_end_matches('/').rsplit('/').next().unwrap_or(module_name);
            let dep = format!(r#"        .package(path: "{pkg_path}")"#);
            let prod = format!(r#".product(name: "{module_name}", package: "{pkg_id}")"#);
            (dep, prod)
        }
    };
    // SwiftPM platform enums use the major version only (.v13, .v14, ...);
    // strip patch components to match the scaffold's `Package.swift`.
    let min_macos_major = min_macos.split('.').next().unwrap_or(min_macos);
    let min_ios = toolchain::SWIFT_MIN_IOS;
    let min_ios_major = min_ios.split('.').next().unwrap_or(min_ios);
    // The consumer's minimum iOS must be >= the dep's minimum iOS or SwiftPM hides
    // the product as platform-incompatible. Use the same constant the swift backend
    // emits into the dep's Package.swift.
    format!(
        r#"// swift-tools-version: 6.0
import PackageDescription

let package = Package(
    name: "E2eSwift",
    platforms: [
        .macOS(.v{min_macos_major}),
        .iOS(.v{min_ios_major}),
    ],
    dependencies: [
{dep_block},
    ],
    targets: [
        .testTarget(
            name: "{module_name}E2ETests",
            dependencies: [{product_dep}]
        ),
    ]
)
"#
    )
}

#[allow(clippy::too_many_arguments)]
fn render_test_file(
    category: &str,
    fixtures: &[&Fixture],
    e2e_config: &E2eConfig,
    module_name: &str,
    class_name: &str,
    function_name: &str,
    result_var: &str,
    args: &[crate::config::ArgMapping],
    field_resolver: &FieldResolver,
    result_is_simple: bool,
    enum_fields: &HashSet<String>,
    client_factory: Option<&str>,
) -> String {
    // Detect whether any fixture in this group uses a file_path or bytes arg — if so
    // the test class chdir's to <repo>/test_documents at setUp time so the
    // fixture-relative paths in test bodies (e.g. "docx/fake.docx") resolve correctly.
    // The Swift binding's `extractBytes`/`extractFile` e2e wrappers consult
    // `FIXTURES_DIR` first, otherwise resolve against the current directory.
    // Mirrors the Ruby/Python conftest pattern that chdirs to test_documents.
    let needs_chdir = fixtures.iter().any(|f| {
        let call_config = e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.input);
        call_config
            .args
            .iter()
            .any(|a| a.arg_type == "file_path" || a.arg_type == "bytes")
    });

    let mut out = String::new();
    out.push_str(&hash::header(CommentStyle::DoubleSlash));
    let _ = writeln!(out, "import XCTest");
    let _ = writeln!(out, "import Foundation");
    let _ = writeln!(out, "import {module_name}");
    let _ = writeln!(out, "import RustBridge");
    let _ = writeln!(out);
    let _ = writeln!(out, "/// E2e tests for category: {category}.");
    let _ = writeln!(out, "final class {class_name}: XCTestCase {{");

    if needs_chdir {
        // Chdir once at class setUp so all fixture file_path arguments resolve relative
        // to the repository's test_documents directory.
        //
        // #filePath = <repo>/e2e/swift_e2e/Tests/<Module>E2ETests/<Class>.swift
        // 5 deletingLastPathComponent() calls climb to the repo root before appending
        // "test_documents". Mirrors the Ruby/Python conftest pattern that chdirs to
        // test_documents.
        let _ = writeln!(out, "    override class func setUp() {{");
        let _ = writeln!(out, "        super.setUp()");
        let _ = writeln!(out, "        let _testDocs = URL(fileURLWithPath: #filePath)");
        let _ = writeln!(out, "            .deletingLastPathComponent() // <Module>Tests/");
        let _ = writeln!(out, "            .deletingLastPathComponent() // Tests/");
        let _ = writeln!(out, "            .deletingLastPathComponent() // swift/");
        let _ = writeln!(out, "            .deletingLastPathComponent() // packages/");
        let _ = writeln!(out, "            .deletingLastPathComponent() // <repo root>");
        let _ = writeln!(
            out,
            "            .appendingPathComponent(\"{}\")",
            e2e_config.test_documents_dir
        );
        let _ = writeln!(
            out,
            "        if FileManager.default.fileExists(atPath: _testDocs.path) {{"
        );
        let _ = writeln!(
            out,
            "            FileManager.default.changeCurrentDirectoryPath(_testDocs.path)"
        );
        let _ = writeln!(out, "        }}");
        let _ = writeln!(out, "    }}");
        let _ = writeln!(out);
    }

    for fixture in fixtures {
        if fixture.is_http_test() {
            render_http_test_method(&mut out, fixture);
        } else {
            render_test_method(
                &mut out,
                fixture,
                e2e_config,
                function_name,
                result_var,
                args,
                field_resolver,
                result_is_simple,
                enum_fields,
                client_factory,
            );
        }
        let _ = writeln!(out);
    }

    let _ = writeln!(out, "}}");
    out
}

// ---------------------------------------------------------------------------
// HTTP test rendering — TestClientRenderer impl + thin driver wrapper
// ---------------------------------------------------------------------------

/// Renderer that emits XCTest `func test...() throws` methods using `URLSession`
/// against the mock server (`ProcessInfo.processInfo.environment["MOCK_SERVER_URL"]`).
struct SwiftTestClientRenderer;

impl client::TestClientRenderer for SwiftTestClientRenderer {
    fn language_name(&self) -> &'static str {
        "swift"
    }

    fn sanitize_test_name(&self, id: &str) -> String {
        // Swift test methods are `func testFoo()` — upper-camel-case after "test".
        sanitize_ident(id).to_upper_camel_case()
    }

    /// Emit `func test{FnName}() throws {` (or a skip stub when the fixture is skipped).
    ///
    /// XCTest has no first-class skip annotation prior to Swift Testing (`@Test`).
    /// For skipped fixtures we emit `try XCTSkipIf(true, reason)` inside the
    /// function body so XCTest records them as skipped rather than omitting them.
    fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
        let _ = writeln!(out, "    /// {description}");
        let _ = writeln!(out, "    func test{fn_name}() throws {{");
        if let Some(reason) = skip_reason {
            let escaped = escape_swift(reason);
            let _ = writeln!(out, "        try XCTSkipIf(true, \"{escaped}\")");
        }
    }

    fn render_test_close(&self, out: &mut String) {
        let _ = writeln!(out, "    }}");
    }

    /// Emit a synchronous `URLSession` round-trip to the mock server.
    ///
    /// `ProcessInfo.processInfo.environment["MOCK_SERVER_URL"]!` provides the base
    /// URL; the fixture path is appended directly.  The call uses a semaphore so the
    /// generated test body stays synchronous (compatible with `throws` functions —
    /// no `async` XCTest support needed).
    fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
        let method = ctx.method.to_uppercase();
        let fixture_path = escape_swift(ctx.path);

        let _ = writeln!(
            out,
            "        let _baseURL = ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]!"
        );
        let _ = writeln!(
            out,
            "        var _req = URLRequest(url: URL(string: _baseURL + \"{fixture_path}\")!)"
        );
        let _ = writeln!(out, "        _req.httpMethod = \"{method}\"");

        // Headers
        let mut header_pairs: Vec<(&String, &String)> = ctx.headers.iter().collect();
        header_pairs.sort_by_key(|(k, _)| k.as_str());
        for (k, v) in &header_pairs {
            let expanded_v = expand_fixture_templates(v);
            let ek = escape_swift(k);
            let ev = escape_swift(&expanded_v);
            let _ = writeln!(out, "        _req.setValue(\"{ev}\", forHTTPHeaderField: \"{ek}\")");
        }

        // Body
        if let Some(body) = ctx.body {
            let json_str = serde_json::to_string(body).unwrap_or_default();
            let escaped_body = escape_swift(&json_str);
            let _ = writeln!(out, "        _req.httpBody = \"{escaped_body}\".data(using: .utf8)");
            let _ = writeln!(
                out,
                "        _req.setValue(\"application/json\", forHTTPHeaderField: \"Content-Type\")"
            );
        }

        let _ = writeln!(out, "        var {}: HTTPURLResponse?", ctx.response_var);
        let _ = writeln!(out, "        var _responseData: Data?");
        let _ = writeln!(out, "        let _sema = DispatchSemaphore(value: 0)");
        let _ = writeln!(
            out,
            "        URLSession.shared.dataTask(with: _req) {{ data, resp, _ in"
        );
        let _ = writeln!(out, "            {} = resp as? HTTPURLResponse", ctx.response_var);
        let _ = writeln!(out, "            _responseData = data");
        let _ = writeln!(out, "            _sema.signal()");
        let _ = writeln!(out, "        }}.resume()");
        let _ = writeln!(out, "        _sema.wait()");
        let _ = writeln!(out, "        let _resp = try XCTUnwrap({})", ctx.response_var);
    }

    fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
        let _ = writeln!(out, "        XCTAssertEqual(_resp.statusCode, {status})");
    }

    fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
        let lower_name = name.to_lowercase();
        let header_expr = format!("_resp.value(forHTTPHeaderField: \"{}\")", escape_swift(&lower_name));
        match expected {
            "<<present>>" => {
                let _ = writeln!(out, "        XCTAssertNotNil({header_expr})");
            }
            "<<absent>>" => {
                let _ = writeln!(out, "        XCTAssertNil({header_expr})");
            }
            "<<uuid>>" => {
                let _ = writeln!(out, "        let _hdrVal_{lower_name} = try XCTUnwrap({header_expr})");
                let _ = writeln!(
                    out,
                    "        XCTAssertNotNil(_hdrVal_{lower_name}.range(of: #\"^[0-9a-f]{{8}}-[0-9a-f]{{4}}-[0-9a-f]{{4}}-[0-9a-f]{{4}}-[0-9a-f]{{12}}$\"#, options: .regularExpression))"
                );
            }
            exact => {
                let escaped = escape_swift(exact);
                let _ = writeln!(out, "        XCTAssertEqual({header_expr}, \"{escaped}\")");
            }
        }
    }

    fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
        if let serde_json::Value::String(s) = expected {
            let escaped = escape_swift(s);
            let _ = writeln!(
                out,
                "        let _bodyStr = String(data: try XCTUnwrap(_responseData), encoding: .utf8) ?? \"\""
            );
            let _ = writeln!(
                out,
                "        XCTAssertEqual(_bodyStr.trimmingCharacters(in: .whitespacesAndNewlines), \"{escaped}\")"
            );
        } else {
            let json_str = serde_json::to_string(expected).unwrap_or_default();
            let escaped = escape_swift(&json_str);
            let _ = writeln!(out, "        let _bodyData = try XCTUnwrap(_responseData)");
            let _ = writeln!(
                out,
                "        let _expected = try JSONSerialization.jsonObject(with: \"{escaped}\".data(using: .utf8)!)"
            );
            let _ = writeln!(
                out,
                "        let _actual = try JSONSerialization.jsonObject(with: _bodyData)"
            );
            let _ = writeln!(
                out,
                "        XCTAssertEqual(NSDictionary(dictionary: _expected as? [String: AnyHashable] ?? [:]), NSDictionary(dictionary: _actual as? [String: AnyHashable] ?? [:]))"
            );
        }
    }

    fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
        if let Some(obj) = expected.as_object() {
            let _ = writeln!(out, "        let _bodyData = try XCTUnwrap(_responseData)");
            let _ = writeln!(
                out,
                "        let _bodyObj = try XCTUnwrap(try JSONSerialization.jsonObject(with: _bodyData) as? [String: Any])"
            );
            for (key, val) in obj {
                let escaped_key = escape_swift(key);
                let swift_val = json_to_swift(val);
                let _ = writeln!(
                    out,
                    "        XCTAssertEqual(_bodyObj[\"{escaped_key}\"] as? AnyHashable, ({swift_val}) as AnyHashable)"
                );
            }
        }
    }

    fn render_assert_validation_errors(
        &self,
        out: &mut String,
        _response_var: &str,
        errors: &[ValidationErrorExpectation],
    ) {
        let _ = writeln!(out, "        let _bodyData = try XCTUnwrap(_responseData)");
        let _ = writeln!(
            out,
            "        let _bodyObj = try XCTUnwrap(try JSONSerialization.jsonObject(with: _bodyData) as? [String: Any])"
        );
        let _ = writeln!(
            out,
            "        let _errors = _bodyObj[\"errors\"] as? [[String: Any]] ?? []"
        );
        for ve in errors {
            let escaped_msg = escape_swift(&ve.msg);
            let _ = writeln!(
                out,
                "        XCTAssertTrue(_errors.contains(where: {{ ($0[\"msg\"] as? String)?.contains(\"{escaped_msg}\") == true }}), \"expected validation error: {escaped_msg}\")"
            );
        }
    }
}

/// Render an XCTest method for an HTTP server fixture via the shared driver.
///
/// HTTP 101 (WebSocket upgrade) is emitted as a skip stub because `URLSession`
/// cannot handle Upgrade responses.
fn render_http_test_method(out: &mut String, fixture: &Fixture) {
    let Some(http) = &fixture.http else {
        return;
    };

    // HTTP 101 (WebSocket upgrade) — URLSession cannot handle upgrade responses.
    if http.expected_response.status_code == 101 {
        let method_name = sanitize_ident(&fixture.id).to_upper_camel_case();
        let description = fixture.description.replace('"', "\\\"");
        let _ = writeln!(out, "    /// {description}");
        let _ = writeln!(out, "    func test{method_name}() throws {{");
        let _ = writeln!(
            out,
            "        try XCTSkipIf(true, \"HTTP 101 WebSocket upgrade cannot be tested via URLSession\")"
        );
        let _ = writeln!(out, "    }}");
        return;
    }

    client::http_call::render_http_test(out, &SwiftTestClientRenderer, fixture);
}

// ---------------------------------------------------------------------------
// Function-call test rendering
// ---------------------------------------------------------------------------

#[allow(clippy::too_many_arguments)]
fn render_test_method(
    out: &mut String,
    fixture: &Fixture,
    e2e_config: &E2eConfig,
    _function_name: &str,
    _result_var: &str,
    _args: &[crate::config::ArgMapping],
    field_resolver: &FieldResolver,
    result_is_simple: bool,
    enum_fields: &HashSet<String>,
    global_client_factory: Option<&str>,
) {
    // Resolve per-fixture call config.
    let call_config = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
    let lang = "swift";
    let call_overrides = call_config.overrides.get(lang);
    let function_name = call_overrides
        .and_then(|o| o.function.as_ref())
        .cloned()
        .unwrap_or_else(|| call_config.function.to_lower_camel_case());
    // Per-call client_factory takes precedence over the global one.
    let client_factory: Option<&str> = call_overrides
        .and_then(|o| o.client_factory.as_deref())
        .or(global_client_factory);
    let result_var = &call_config.result_var;
    let args = &call_config.args;
    // Per-call flags: base call flag OR per-language override OR global flag.
    // Also treat the call as simple when *any* language override marks it as bytes.
    // Calls like `speech()` have `result_is_bytes = true` on C/C#/Java overrides but
    // no explicit `result_is_simple` on the Swift override — yet the Swift binding
    // returns `Data` directly (not a struct), so assertions must use `result.isEmpty`
    // rather than `result.audio().toString().isEmpty`.
    let result_is_bytes_any_lang =
        call_config.result_is_bytes || call_config.overrides.values().any(|o| o.result_is_bytes);
    let result_is_simple = call_config.result_is_simple
        || call_overrides.is_some_and(|o| o.result_is_simple)
        || result_is_simple
        || result_is_bytes_any_lang;
    let result_is_array = call_config.result_is_array;
    // When the call returns `Option<T>` the Swift binding exposes the result as
    // `Optional<…>` (e.g. `getEmbeddingPreset(...) -> EmbeddingPreset?`). Bare-result
    // `is_empty`/`not_empty` assertions must use `XCTAssertNil` / `XCTAssertNotNil`
    // rather than `.toString().isEmpty`, which is undefined on opaque optionals.
    let result_is_option = call_config.result_is_option || call_overrides.is_some_and(|o| o.result_is_option);

    let method_name = fixture.id.to_upper_camel_case();
    let description = &fixture.description;
    let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
    let is_async = call_config.r#async;

    // Streaming detection (call-level `streaming` opt-out is honored).
    let is_streaming = crate::codegen::streaming_assertions::resolve_is_streaming(fixture, call_config.streaming);
    let collect_snippet_opt = if is_streaming && !expects_error {
        crate::codegen::streaming_assertions::StreamingFieldResolver::collect_snippet(lang, result_var, "chunks")
    } else {
        None
    };
    // When swift has streaming-virtual-field assertions but no collect snippet
    // is available (the swift-bridge surface does not yet expose a typed
    // `chatStream` async sequence we can drain into a typed
    // `[ChatCompletionChunk]`), emit a skip stub rather than reference an
    // undefined `chunks` local in the assertion expressions. This keeps the
    // swift test target compiling while the binding catches up.
    if is_streaming && !expects_error && collect_snippet_opt.is_none() {
        if is_async {
            let _ = writeln!(out, "    func test{method_name}() async throws {{");
        } else {
            let _ = writeln!(out, "    func test{method_name}() throws {{");
        }
        let _ = writeln!(out, "        // {description}");
        let _ = writeln!(
            out,
            "        try XCTSkipIf(true, \"swift: streaming chunk collection is not yet supported via the swift-bridge surface (fixture: {})\")",
            fixture.id
        );
        let _ = writeln!(out, "    }}");
        return;
    }
    let collect_snippet = collect_snippet_opt.unwrap_or_default();

    // Detect whether this call has any json_object args that cannot be constructed
    // in Swift — swift-bridge opaque types do not provide a fromJson initialiser.
    // When such args exist and no `options_via` is configured for swift, emit a
    // skip stub so the test compiles but is recorded as skipped rather than
    // generating invalid code that passes `nil` or a string literal where a
    // strongly-typed request object is required.
    let has_unresolvable_json_object_arg = {
        let options_via = call_overrides.and_then(|o| o.options_via.as_deref());
        options_via.is_none() && args.iter().any(|a| a.arg_type == "json_object" && a.name != "config")
    };

    if has_unresolvable_json_object_arg {
        if is_async {
            let _ = writeln!(out, "    func test{method_name}() async throws {{");
        } else {
            let _ = writeln!(out, "    func test{method_name}() throws {{");
        }
        let _ = writeln!(out, "        // {description}");
        let _ = writeln!(
            out,
            "        try XCTSkipIf(true, \"swift: json_object request construction requires options_via configuration (fixture: {})\");",
            fixture.id
        );
        let _ = writeln!(out, "    }}");
        return;
    }

    // Visitor-driven fixtures: emit a class that conforms to `HtmlVisitorProtocol`
    // and wrap it via `makeHtmlVisitorHandle(...)`. The handle is then threaded
    // into the options via `conversionOptionsFromJsonWithVisitor(json, handle)`.
    let mut visitor_setup_lines: Vec<String> = Vec::new();
    let visitor_handle_expr: Option<String> = fixture
        .visitor
        .as_ref()
        .map(|spec| super::swift_visitors::build_swift_visitor(&mut visitor_setup_lines, spec, &fixture.id));

    // Resolve extra_args from per-call swift overrides (e.g. `nil` for optional
    // query-param arguments on list_files/list_batches that have no fixture-level
    // input field).
    let extra_args: Vec<String> = call_overrides.map(|o| o.extra_args.clone()).unwrap_or_default();

    // Merge per-call enum_fields keys into the effective enum set so that
    // fields like "status" (BatchStatus, BatchObject) are treated as enum-typed
    // even when they are not globally listed in fields_enum (they are context-
    // dependent — BatchStatus on BatchObject but plain String on ResponseObject).
    let effective_enum_fields: std::borrow::Cow<HashSet<String>> = {
        let per_call = call_overrides.map(|o| &o.enum_fields);
        if let Some(pc) = per_call {
            if !pc.is_empty() {
                let mut merged = enum_fields.clone();
                merged.extend(pc.keys().cloned());
                std::borrow::Cow::Owned(merged)
            } else {
                std::borrow::Cow::Borrowed(enum_fields)
            }
        } else {
            std::borrow::Cow::Borrowed(enum_fields)
        }
    };

    let options_via_str: Option<&str> = call_overrides.and_then(|o| o.options_via.as_deref());
    let options_type_str: Option<&str> = call_overrides.and_then(|o| o.options_type.as_deref());
    // Derive the Swift handle-config parsing function from the C override's
    // `c_engine_factory` field. E.g. `"CrawlConfig"` → snake → `"crawl_config_from_json"`
    // → camelCase → `"crawlConfigFromJson"`.
    let handle_config_fn_owned: Option<String> = call_config
        .overrides
        .get("c")
        .and_then(|c| c.c_engine_factory.as_deref())
        .map(|ty| format!("{}_from_json", ty.to_snake_case()).to_lower_camel_case());
    let (mut setup_lines, args_str) = build_args_and_setup(
        &fixture.input,
        args,
        &fixture.id,
        fixture.has_host_root_route(),
        &function_name,
        options_via_str,
        options_type_str,
        handle_config_fn_owned.as_deref(),
        visitor_handle_expr.as_deref(),
    );
    // Prepend visitor class declarations (before any setup lines that reference the handle).
    if !visitor_setup_lines.is_empty() {
        visitor_setup_lines.extend(setup_lines);
        setup_lines = visitor_setup_lines;
    }

    // Append extra_args to the argument list.
    let args_str = if extra_args.is_empty() {
        args_str
    } else if args_str.is_empty() {
        extra_args.join(", ")
    } else {
        format!("{args_str}, {}", extra_args.join(", "))
    };

    // When a client_factory is set, dispatch via a client instance:
    //   let client = try <FactoryType>(apiKey: "test-key", baseUrl: <mock_url>)
    //   try await client.<method>(args)
    // Otherwise fall back to free-function call (Kreuzberg / non-client-factory libraries).
    let has_mock = fixture.mock_response.is_some();
    let (call_setup, call_expr) = if let Some(_factory) = client_factory {
        let env_key = format!("MOCK_SERVER_{}", fixture.id.to_ascii_uppercase().replace('-', "_"));
        let mock_url = if fixture.has_host_root_route() {
            format!(
                "ProcessInfo.processInfo.environment[\"{env_key}\"] ?? (ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{}\")",
                fixture.id
            )
        } else {
            format!(
                "ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{}\"",
                fixture.id
            )
        };
        let client_constructor = if has_mock {
            format!("let _client = try DefaultClient(apiKey: \"test-key\", baseUrl: {mock_url})")
        } else {
            // Live API: check for api_key_var; if not present use mock URL anyway.
            if let Some(env_var) = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref()) {
                format!(
                    "let _apiKey = ProcessInfo.processInfo.environment[\"{env_var}\"]\n        \
                     let _baseUrl: String? = _apiKey != nil ? nil : {mock_url}\n        \
                     let _client = try DefaultClient(apiKey: _apiKey ?? \"test-key\", baseUrl: _baseUrl)"
                )
            } else {
                format!("let _client = try DefaultClient(apiKey: \"test-key\", baseUrl: {mock_url})")
            }
        };
        let expr = if is_async {
            format!("try await _client.{function_name}({args_str})")
        } else {
            format!("try _client.{function_name}({args_str})")
        };
        (Some(client_constructor), expr)
    } else {
        // Free-function call (no client_factory).
        let expr = if is_async {
            format!("try await {function_name}({args_str})")
        } else {
            format!("try {function_name}({args_str})")
        };
        (None, expr)
    };
    // For backwards compatibility: qualified_function_name unused when client_factory is set.
    let _ = function_name;

    if is_async {
        let _ = writeln!(out, "    func test{method_name}() async throws {{");
    } else {
        let _ = writeln!(out, "    func test{method_name}() throws {{");
    }
    let _ = writeln!(out, "        // {description}");

    if expects_error {
        // For error fixtures, setup may itself throw (e.g. config validation
        // happens at engine construction). Wrap the whole pipeline — setup
        // and the call — in a single do/catch so any throw counts as success.
        if is_async {
            // XCTAssertThrowsError is a synchronous macro; for async-throwing
            // functions use a do/catch with explicit XCTFail to enforce that
            // the throw actually happens. `await XCTAssertThrowsError(...)` is
            // not valid Swift — it evaluates `await` against a non-async expr.
            let _ = writeln!(out, "        do {{");
            for line in &setup_lines {
                let _ = writeln!(out, "            {line}");
            }
            if let Some(setup) = &call_setup {
                let _ = writeln!(out, "            {setup}");
            }
            let _ = writeln!(out, "            _ = {call_expr}");
            let _ = writeln!(out, "            XCTFail(\"expected to throw\")");
            let _ = writeln!(out, "        }} catch {{");
            let _ = writeln!(out, "            // success");
            let _ = writeln!(out, "        }}");
        } else {
            // Synchronous: emit setup outside (it's expected to succeed) and
            // wrap only the throwing call in XCTAssertThrowsError. If setup
            // itself throws, that propagates as the test's own failure — but
            // sync tests use `throws` so the test method itself rethrows,
            // which XCTest still records as caught. Keep this simple: use a
            // do/catch so setup-time throws also count as expected failures.
            let _ = writeln!(out, "        do {{");
            for line in &setup_lines {
                let _ = writeln!(out, "            {line}");
            }
            if let Some(setup) = &call_setup {
                let _ = writeln!(out, "            {setup}");
            }
            let _ = writeln!(out, "            _ = {call_expr}");
            let _ = writeln!(out, "            XCTFail(\"expected to throw\")");
            let _ = writeln!(out, "        }} catch {{");
            let _ = writeln!(out, "            // success");
            let _ = writeln!(out, "        }}");
        }
        let _ = writeln!(out, "    }}");
        return;
    }

    for line in &setup_lines {
        let _ = writeln!(out, "        {line}");
    }

    // Emit client construction if a client_factory is configured.
    if let Some(setup) = &call_setup {
        let _ = writeln!(out, "        {setup}");
    }

    let _ = writeln!(out, "        let {result_var} = {call_expr}");

    // Emit the collect snippet for streaming fixtures (drains the async sequence into
    // a local `chunks: [ChatCompletionChunk]` array used by streaming-virtual assertions).
    if !collect_snippet.is_empty() {
        for line in collect_snippet.lines() {
            let _ = writeln!(out, "        {line}");
        }
    }

    // Each fixture's call returns a different IR type. Override the resolver's
    // Swift first-class-map `root_type` with the call's `result_type` (looked up
    // across c/csharp/java/kotlin/go/php overrides — these are language-agnostic
    // IR type names that any backend can use to anchor field-access dispatch).
    let fixture_root_type: Option<String> = swift_call_result_type(call_config);
    let fixture_resolver = field_resolver.with_swift_root_type(fixture_root_type);

    for assertion in &fixture.assertions {
        render_assertion(
            out,
            assertion,
            result_var,
            &fixture_resolver,
            result_is_simple,
            result_is_array,
            result_is_option,
            &effective_enum_fields,
            is_streaming,
        );
    }

    let _ = writeln!(out, "    }}");
}

#[allow(clippy::too_many_arguments)]
/// Build setup lines and the argument list for the function call.
///
/// Swift-bridge wrappers require strongly-typed values that don't have implicit
/// Swift literal conversions:
///
/// - `bytes` args become `RustVec<UInt8>` — fixture supplies a relative file path
///   string which is read at test time and pushed into a `RustVec<UInt8>` setup
///   variable. A literal byte array is base64-decoded or UTF-8 encoded inline.
/// - `json_object` args become opaque `ExtractionConfig` (or sibling) instances —
///   a JSON string is decoded via `extractionConfigFromJson(...)` in a setup line.
/// - Optional args missing from the fixture must still appear at the call site
///   as `nil` whenever a later positional arg is present, otherwise Swift slots
///   subsequent values into the wrong parameter.
fn build_args_and_setup(
    input: &serde_json::Value,
    args: &[crate::config::ArgMapping],
    fixture_id: &str,
    has_host_root_route: bool,
    function_name: &str,
    options_via: Option<&str>,
    options_type: Option<&str>,
    handle_config_fn: Option<&str>,
    visitor_handle_expr: Option<&str>,
) -> (Vec<String>, String) {
    if args.is_empty() {
        return (Vec::new(), String::new());
    }

    let mut setup_lines: Vec<String> = Vec::new();
    let mut parts: Vec<String> = Vec::new();

    // Pre-compute, for each arg index, whether any later arg has a fixture-provided
    // value (or is required and will emit a default). When an optional arg is empty
    // but a later arg WILL emit, we must keep the slot with `nil` so positional
    // alignment is preserved.
    let later_emits: Vec<bool> = (0..args.len())
        .map(|i| {
            args.iter().skip(i + 1).any(|a| {
                let f = a.field.strip_prefix("input.").unwrap_or(&a.field);
                let v = input.get(f);
                let has_value = matches!(v, Some(x) if !x.is_null());
                has_value || !a.optional || (a.arg_type == "json_object" && a.name == "config")
            })
        })
        .collect();

    for (idx, arg) in args.iter().enumerate() {
        if arg.arg_type == "mock_url" {
            let env_key = format!("MOCK_SERVER_{}", fixture_id.to_ascii_uppercase().replace('-', "_"));
            let url_expr = if has_host_root_route {
                format!(
                    "ProcessInfo.processInfo.environment[\"{env_key}\"] ?? (ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{fixture_id}\")"
                )
            } else {
                format!("ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{fixture_id}\"")
            };
            setup_lines.push(format!("let {} = {url_expr}", arg.name));
            parts.push(arg.name.clone());
            continue;
        }

        if arg.arg_type == "handle" {
            let var_name = format!("{}Obj", arg.name.to_lower_camel_case());
            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
            let config_val = input.get(field);
            let has_config = config_val
                .is_some_and(|v| !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty())));
            if has_config {
                if let Some(from_json_fn) = handle_config_fn {
                    let json_str = serde_json::to_string(config_val.unwrap()).unwrap_or_default();
                    let escaped = escape_swift_str(&json_str);
                    let config_var = format!("{}Config", arg.name.to_lower_camel_case());
                    setup_lines.push(format!("let {config_var} = try {from_json_fn}(\"{escaped}\")"));
                    setup_lines.push(format!("let {var_name} = try createEngine({config_var})"));
                } else {
                    setup_lines.push(format!("let {var_name} = try createEngine(nil)"));
                }
            } else {
                setup_lines.push(format!("let {var_name} = try createEngine(nil)"));
            }
            parts.push(var_name);
            continue;
        }

        // bytes args: fixture stores a fixture-relative path string. Generate
        // setup that reads it into a Data and pushes each byte into a
        // RustVec<UInt8>. Literal byte arrays inline the bytes; missing values
        // produce an empty vec (or `nil` when optional).
        if arg.arg_type == "bytes" {
            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
            let val = input.get(field);
            match val {
                None | Some(serde_json::Value::Null) if arg.optional => {
                    if later_emits[idx] {
                        parts.push("nil".to_string());
                    }
                }
                None | Some(serde_json::Value::Null) => {
                    let var_name = format!("{}Vec", arg.name.to_lower_camel_case());
                    setup_lines.push(format!("let {var_name} = RustVec<UInt8>()"));
                    parts.push(var_name);
                }
                Some(serde_json::Value::String(s)) => {
                    let escaped = escape_swift(s);
                    let var_name = format!("{}Vec", arg.name.to_lower_camel_case());
                    let data_var = format!("{}Data", arg.name.to_lower_camel_case());
                    setup_lines.push(format!(
                        "let {data_var} = try Data(contentsOf: URL(fileURLWithPath: \"{escaped}\"))"
                    ));
                    setup_lines.push(format!("let {var_name} = RustVec<UInt8>()"));
                    setup_lines.push(format!("for _byte in {data_var} {{ {var_name}.push(value: _byte) }}"));
                    parts.push(var_name);
                }
                Some(serde_json::Value::Array(arr)) => {
                    let var_name = format!("{}Vec", arg.name.to_lower_camel_case());
                    setup_lines.push(format!("let {var_name} = RustVec<UInt8>()"));
                    for v in arr {
                        if let Some(n) = v.as_u64() {
                            setup_lines.push(format!("{var_name}.push(value: UInt8({n}))"));
                        }
                    }
                    parts.push(var_name);
                }
                Some(other) => {
                    // Fallback: encode the JSON serialisation as UTF-8 bytes.
                    let json_str = serde_json::to_string(other).unwrap_or_default();
                    let escaped = escape_swift(&json_str);
                    let var_name = format!("{}Vec", arg.name.to_lower_camel_case());
                    setup_lines.push(format!("let {var_name} = RustVec<UInt8>()"));
                    setup_lines.push(format!(
                        "for _byte in Array(\"{escaped}\".utf8) {{ {var_name}.push(value: _byte) }}"
                    ));
                    parts.push(var_name);
                }
            }
            continue;
        }

        // json_object "config" args: the swift-bridge wrapper requires an opaque
        // config instance (e.g., `ExtractionConfig`, `ProcessConfig`), not a JSON string.
        // Derive the from-json helper name from options_type if available, else default
        // to kreuzberg's `extractionConfigFromJson` for backward compatibility.
        // Batch functions (batchExtract*) hardcode config internally — skip it.
        let is_config_arg = arg.name == "config" && arg.arg_type == "json_object";
        let is_batch_fn = function_name.starts_with("batch") || function_name.starts_with("Batch");
        if is_config_arg && !is_batch_fn {
            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
            let val = input.get(field);
            let json_str = match val {
                None | Some(serde_json::Value::Null) => "{}".to_string(),
                Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
            };
            let escaped = escape_swift(&json_str);
            let var_name = format!("{}Obj", arg.name.to_lower_camel_case());
            // Derive the from-json helper name from options_type, or default to extractionConfigFromJson
            let from_json_fn = if let Some(type_name) = options_type {
                format!("{}FromJson", type_name.to_lower_camel_case())
            } else {
                "extractionConfigFromJson".to_string()
            };
            setup_lines.push(format!("let {var_name} = try {from_json_fn}(\"{escaped}\")"));
            parts.push(var_name);
            continue;
        }

        // json_object non-config args with options_via = "from_json":
        // Use the generated `{typeCamelCase}FromJson(_:)` helper so the fixture JSON is
        // deserialised into the opaque swift-bridge type rather than passed as a raw string.
        // When arg.field == "input", the entire fixture input IS the request object.
        // When a visitor handle is present, use `{typeCamelCase}FromJsonWithVisitor(json, handle)`
        // instead to attach the visitor to the options in one step.
        if arg.arg_type == "json_object" && options_via == Some("from_json") {
            if let Some(type_name) = options_type {
                let resolved_val = super::resolve_field(input, &arg.field);
                let json_str = match resolved_val {
                    serde_json::Value::Null => "{}".to_string(),
                    v => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
                };
                let escaped = escape_swift(&json_str);
                let var_name = format!("_{}", arg.name.to_lower_camel_case());
                if let Some(handle_expr) = visitor_handle_expr {
                    // Use the visitor-aware helper: `{typeCamelCase}FromJsonWithVisitor(json, handle)`.
                    // The handle expression builds a VisitorHandle from the local class instance.
                    // The function name mirrors emit_options_field_options_helper: camelCase of
                    // `{options_snake}_from_json_with_visitor`.
                    let with_visitor_fn = format!("{}FromJsonWithVisitor", type_name.to_lower_camel_case());
                    let handle_var = format!("_visitorHandle_{}", var_name.trim_start_matches('_'));
                    setup_lines.push(format!("let {handle_var} = {handle_expr}"));
                    setup_lines.push(format!(
                        "let {var_name} = try {with_visitor_fn}(\"{escaped}\", {handle_var})"
                    ));
                } else {
                    let from_json_fn = format!("{}FromJson", type_name.to_lower_camel_case());
                    setup_lines.push(format!("let {var_name} = try {from_json_fn}(\"{escaped}\")"));
                }
                parts.push(var_name);
                continue;
            }
        }

        let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
        let val = input.get(field);
        match val {
            None | Some(serde_json::Value::Null) if arg.optional => {
                // Optional arg with no fixture value: keep the slot with `nil`
                // when a later arg will emit, so positional alignment matches
                // the swift-bridge wrapper signature.
                if later_emits[idx] {
                    parts.push("nil".to_string());
                }
            }
            None | Some(serde_json::Value::Null) => {
                let default_val = match arg.arg_type.as_str() {
                    "string" => "\"\"".to_string(),
                    "int" | "integer" => "0".to_string(),
                    "float" | "number" => "0.0".to_string(),
                    "bool" | "boolean" => "false".to_string(),
                    _ => "nil".to_string(),
                };
                parts.push(default_val);
            }
            Some(v) => {
                parts.push(json_to_swift(v));
            }
        }
    }

    (setup_lines, parts.join(", "))
}

#[allow(clippy::too_many_arguments)]
fn render_assertion(
    out: &mut String,
    assertion: &Assertion,
    result_var: &str,
    field_resolver: &FieldResolver,
    result_is_simple: bool,
    result_is_array: bool,
    result_is_option: bool,
    enum_fields: &HashSet<String>,
    is_streaming: bool,
) {
    // When the bare result is `Optional<T>` (no field path) the opaque class
    // exposed by swift-bridge has no `.toString()` method, so the usual
    // `.toString().isEmpty` pattern produces compile errors. Detect the
    // "bare result" case and prefer `XCTAssertNil` / `XCTAssertNotNil`.
    let bare_result_is_option = result_is_option && assertion.field.as_deref().filter(|f| !f.is_empty()).is_none();
    // Streaming virtual fields resolve against the `chunks` collected-array variable.
    // Intercept before is_valid_for_result so they are never skipped.
    // Also intercept `usage.*` deep-paths in streaming tests: `AsyncThrowingStream` does
    // not have a `usage()` method, so we must route them through the chunks accessor.
    if let Some(f) = &assertion.field {
        let is_streaming_usage_path =
            is_streaming && (f == "usage" || (f.starts_with("usage.") || f.starts_with("usage[")));
        if !f.is_empty()
            && (crate::codegen::streaming_assertions::is_streaming_virtual_field(f) || is_streaming_usage_path)
        {
            if let Some(expr) =
                crate::codegen::streaming_assertions::StreamingFieldResolver::accessor(f, "swift", "chunks")
            {
                let line = match assertion.assertion_type.as_str() {
                    "count_min" => {
                        if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
                            format!("        XCTAssertGreaterThanOrEqual(chunks.count, {n})\n")
                        } else {
                            String::new()
                        }
                    }
                    "count_equals" => {
                        if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
                            format!("        XCTAssertEqual(chunks.count, {n})\n")
                        } else {
                            String::new()
                        }
                    }
                    "equals" => {
                        if let Some(serde_json::Value::String(s)) = &assertion.value {
                            let escaped = escape_swift(s);
                            format!("        XCTAssertEqual({expr}, \"{escaped}\")\n")
                        } else if let Some(b) = assertion.value.as_ref().and_then(|v| v.as_bool()) {
                            format!("        XCTAssertEqual({expr}, {b})\n")
                        } else {
                            String::new()
                        }
                    }
                    "not_empty" => {
                        format!("        XCTAssertFalse({expr}.isEmpty, \"expected non-empty\")\n")
                    }
                    "is_empty" => {
                        format!("        XCTAssertTrue({expr}.isEmpty, \"expected empty\")\n")
                    }
                    "is_true" => {
                        format!("        XCTAssertTrue({expr})\n")
                    }
                    "is_false" => {
                        format!("        XCTAssertFalse({expr})\n")
                    }
                    "greater_than" => {
                        if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
                            format!("        XCTAssertGreaterThan(chunks.count, {n})\n")
                        } else {
                            String::new()
                        }
                    }
                    "contains" => {
                        if let Some(serde_json::Value::String(s)) = &assertion.value {
                            let escaped = escape_swift(s);
                            format!(
                                "        XCTAssertTrue({expr}.contains(\"{escaped}\"), \"expected to contain: {escaped}\")\n"
                            )
                        } else {
                            String::new()
                        }
                    }
                    _ => format!(
                        "        // streaming field '{f}': assertion type '{}' not rendered\n",
                        assertion.assertion_type
                    ),
                };
                if !line.is_empty() {
                    out.push_str(&line);
                }
            }
            return;
        }
    }

    // Skip assertions on fields that don't exist on the result type.
    if let Some(f) = &assertion.field {
        if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
            let _ = writeln!(out, "        // skipped: field '{f}' not available on result type");
            return;
        }
    }

    // Skip assertions that traverse a tagged-union variant boundary.
    // In Swift, FormatMetadata and similar enum-backed opaque types are exposed as
    // plain classes by swift-bridge — variant accessor methods (e.g., `.excel()`)
    // are not generated, so such assertions cannot be expressed.
    if let Some(f) = &assertion.field {
        if !f.is_empty() && field_resolver.tagged_union_split(f).is_some() {
            let _ = writeln!(
                out,
                "        // skipped: field '{f}' crosses a tagged-union variant boundary (not expressible in Swift)"
            );
            return;
        }
    }

    // Determine if this field is an enum type.
    let field_is_enum = assertion
        .field
        .as_deref()
        .is_some_and(|f| enum_fields.contains(f) || enum_fields.contains(field_resolver.resolve(f)));

    let field_is_optional = assertion.field.as_deref().is_some_and(|f| {
        !f.is_empty() && (field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f)))
    });
    let field_is_array = assertion.field.as_deref().is_some_and(|f| {
        !f.is_empty()
            && (field_resolver.is_array(f)
                || field_resolver.is_array(field_resolver.resolve(f))
                || field_resolver.is_collection_root(f)
                || field_resolver.is_collection_root(field_resolver.resolve(f)))
    });

    let field_expr_raw = if result_is_simple {
        result_var.to_string()
    } else {
        match &assertion.field {
            Some(f) if !f.is_empty() => field_resolver.accessor(f, "swift", result_var),
            _ => result_var.to_string(),
        }
    };

    // swift-bridge `RustVec<T>` exposes its elements as `T.SelfRef`, which holds
    // a raw pointer into the parent Vec's storage. When the Vec is a temporary
    // (e.g. `result.json_ld()` called inline), Swift ARC may release it before
    // the ref is used, leaving the ref's pointer dangling. Materialise the
    // temporary into a local so it survives the full expression chain.
    //
    // The local name is suffixed with the assertion type plus a hash of the
    // assertion's discriminating fields so multiple assertions on the same
    // collection don't redeclare the same name.
    let local_suffix = {
        use std::hash::{Hash, Hasher};
        let mut hasher = std::collections::hash_map::DefaultHasher::new();
        assertion.field.hash(&mut hasher);
        assertion
            .value
            .as_ref()
            .map(|v| v.to_string())
            .unwrap_or_default()
            .hash(&mut hasher);
        format!(
            "{}_{:x}",
            assertion.assertion_type.replace(['-', '.'], "_"),
            hasher.finish() & 0xffff_ffff,
        )
    };
    let (vec_setup, field_expr, is_map_subscript) = materialise_vec_temporaries(&field_expr_raw, &local_suffix);
    // The `contains` / `not_contains` traversal branch builds its own
    // accessor from `field_resolver.accessor(array_part, ...)`, ignoring
    // `field_expr`. Emitting the vec_setup there would produce dead
    // `let _vec_… = …` lines, so skip it for those traversal cases.
    let field_uses_traversal = assertion.field.as_deref().is_some_and(|f| f.contains("[]."));
    let traversal_skips_field_expr = field_uses_traversal
        && matches!(
            assertion.assertion_type.as_str(),
            "contains" | "not_contains" | "not_empty" | "is_empty"
        );
    if !traversal_skips_field_expr {
        for line in &vec_setup {
            let _ = writeln!(out, "        {line}");
        }
    }

    // In Swift, optional chaining with `?.` makes the result optional even if the
    // called method's return type isn't marked optional. For example:
    // `result.markdown()?.content()` returns `Optional<RustString>` because
    // `markdown()` is optional and the `?.` operator wraps the result.
    // Detect this by checking if the accessor contains `?.`.
    let accessor_is_optional = field_expr.contains("?.");

    // For enum fields, need to handle the string representation differently in Swift.
    // Swift enums don't have `.rawValue` unless they're explicitly RawRepresentable.
    // Check if this is an enum type and handle accordingly.
    // For optional fields (Optional<RustString>), use optional chaining before toString().
    // For other fields: swift-bridge returns all Rust `String` fields as `RustString`.
    // We add .toString() here so string assertions (contains, hasPrefix, etc.) work.
    // Non-string opaque fields (DocumentStructure, etc.) should not appear in string
    // assertions — the fixture schema controls which assertions apply to which fields.
    let string_expr = if is_map_subscript {
        // The field_expr already evaluates to `String?` (from a JSON-decoded
        // `[String: String]` subscript). No `.toString()` chain needed —
        // coalesce the optional to "" and use the Swift String directly.
        format!("({field_expr} ?? \"\")")
    } else if field_is_enum && (field_is_optional || accessor_is_optional) {
        // Enum-typed fields that are also optional (e.g. `finish_reason() -> Optional<RustString>`)
        // must use optional chaining: `?.toString() ?? ""` to unwrap before converting to Swift String.
        format!("({field_expr}?.toString() ?? \"\")")
    } else if field_is_enum {
        // Enum-typed fields are now bridged as `String` (RustString in Swift) rather than
        // as opaque enum handles. The getter on the Rust side calls `to_string()` internally
        // and returns a `String` across the FFI. In Swift this arrives as `RustString`, so
        // `.toString()` converts it to a Swift `String` — one call, not two.
        format!("{field_expr}.toString()")
    } else if field_is_optional {
        // Leaf field itself is Optional<RustString> — need ?.toString() to unwrap.
        format!("({field_expr}?.toString() ?? \"\")")
    } else if accessor_is_optional {
        // Ancestor optional chain propagates; leaf is non-optional RustString within chain.
        // Use .toString() directly — the whole expr is Optional<String> due to propagation.
        format!("({field_expr}.toString() ?? \"\")")
    } else {
        format!("{field_expr}.toString()")
    };

    match assertion.assertion_type.as_str() {
        "equals" => {
            if let Some(expected) = &assertion.value {
                let swift_val = json_to_swift(expected);
                if expected.is_string() {
                    if field_is_enum {
                        // Enum fields: `to_string()` (snake_case) returns RustString;
                        // `.toString()` converts it to a Swift String.
                        // `string_expr` already incorporates this call chain.
                        let trim_expr =
                            format!("{string_expr}.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)");
                        let _ = writeln!(out, "        XCTAssertEqual({trim_expr}, {swift_val})");
                    } else {
                        // For optional strings (String?), use ?? to coalesce before trimming.
                        // `.toString()` converts RustString → Swift String before calling
                        // `.trimmingCharacters`, which requires a concrete String type.
                        // string_expr already incorporates field_is_optional via ?.toString() ?? "".
                        let trim_expr =
                            format!("{string_expr}.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)");
                        let _ = writeln!(out, "        XCTAssertEqual({trim_expr}, {swift_val})");
                    }
                } else {
                    let _ = writeln!(out, "        XCTAssertEqual({field_expr}, {swift_val})");
                }
            }
        }
        "contains" => {
            if let Some(expected) = &assertion.value {
                let swift_val = json_to_swift(expected);
                // When the root result IS the array (result_is_simple + result_is_array) and
                // there is no field path, check array membership via map+contains.
                let no_field = assertion.field.as_deref().is_none_or(|f| f.is_empty());
                if result_is_simple && result_is_array && no_field {
                    // RustVec<RustString> iteration yields RustStringRef (no `toString()`);
                    // use `.as_str().toString()` to convert each element to a Swift String.
                    let _ = writeln!(
                        out,
                        "        XCTAssertTrue({result_var}.map {{ $0.as_str().toString() }}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
                    );
                } else {
                    // []. traversal: field like "links[].url" → contains(where:) closure.
                    let traversal_handled = if let Some(f) = assertion.field.as_deref() {
                        if let Some(dot) = f.find("[].") {
                            let array_part = &f[..dot];
                            let elem_part = &f[dot + 3..];
                            let line = swift_traversal_contains_assert(
                                array_part,
                                elem_part,
                                f,
                                &swift_val,
                                result_var,
                                false,
                                &format!("expected to contain: \\({swift_val})"),
                                enum_fields,
                                field_resolver,
                            );
                            let _ = writeln!(out, "{line}");
                            true
                        } else {
                            false
                        }
                    } else {
                        false
                    };
                    if !traversal_handled {
                        // For array fields (RustVec<RustString>), check membership via map+contains.
                        let field_is_array = assertion
                            .field
                            .as_deref()
                            .is_some_and(|f| field_resolver.is_array(field_resolver.resolve(f)));
                        if field_is_array {
                            let contains_expr =
                                swift_array_contains_expr(assertion.field.as_deref(), result_var, field_resolver);
                            let _ = writeln!(
                                out,
                                "        XCTAssertTrue(({contains_expr} ?? []).contains({swift_val}), \"expected to contain: \\({swift_val})\")"
                            );
                        } else if field_is_enum {
                            // Enum fields: use `toString().toString()` (via string_expr) to get the
                            // serde variant name as a Swift String, then check substring containment.
                            let _ = writeln!(
                                out,
                                "        XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
                            );
                        } else {
                            let _ = writeln!(
                                out,
                                "        XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
                            );
                        }
                    }
                }
            }
        }
        "contains_all" => {
            if let Some(values) = &assertion.values {
                // []. traversal: field like "links[].link_type" → contains(where:) per value.
                if let Some(f) = assertion.field.as_deref() {
                    if let Some(dot) = f.find("[].") {
                        let array_part = &f[..dot];
                        let elem_part = &f[dot + 3..];
                        for val in values {
                            let swift_val = json_to_swift(val);
                            let line = swift_traversal_contains_assert(
                                array_part,
                                elem_part,
                                f,
                                &swift_val,
                                result_var,
                                false,
                                &format!("expected to contain: \\({swift_val})"),
                                enum_fields,
                                field_resolver,
                            );
                            let _ = writeln!(out, "{line}");
                        }
                        // handled — skip remaining branches
                    } else {
                        // For array fields (RustVec<RustString>), check membership via map+contains.
                        let field_is_array = field_resolver.is_array(field_resolver.resolve(f));
                        if field_is_array {
                            let contains_expr =
                                swift_array_contains_expr(assertion.field.as_deref(), result_var, field_resolver);
                            for val in values {
                                let swift_val = json_to_swift(val);
                                let _ = writeln!(
                                    out,
                                    "        XCTAssertTrue(({contains_expr} ?? []).contains({swift_val}), \"expected to contain: \\({swift_val})\")"
                                );
                            }
                        } else if field_is_enum {
                            // Enum fields: use `toString().toString()` (via string_expr) to get the
                            // serde variant name as a Swift String, then check substring containment.
                            for val in values {
                                let swift_val = json_to_swift(val);
                                let _ = writeln!(
                                    out,
                                    "        XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
                                );
                            }
                        } else {
                            for val in values {
                                let swift_val = json_to_swift(val);
                                let _ = writeln!(
                                    out,
                                    "        XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
                                );
                            }
                        }
                    }
                } else {
                    // No field — fall back to existing string_expr path.
                    for val in values {
                        let swift_val = json_to_swift(val);
                        let _ = writeln!(
                            out,
                            "        XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
                        );
                    }
                }
            }
        }
        "not_contains" => {
            if let Some(expected) = &assertion.value {
                let swift_val = json_to_swift(expected);
                // []. traversal: "links[].url" → XCTAssertFalse(array.contains(where:))
                let traversal_handled = if let Some(f) = assertion.field.as_deref() {
                    if let Some(dot) = f.find("[].") {
                        let array_part = &f[..dot];
                        let elem_part = &f[dot + 3..];
                        let line = swift_traversal_contains_assert(
                            array_part,
                            elem_part,
                            f,
                            &swift_val,
                            result_var,
                            true,
                            &format!("expected NOT to contain: \\({swift_val})"),
                            enum_fields,
                            field_resolver,
                        );
                        let _ = writeln!(out, "{line}");
                        true
                    } else {
                        false
                    }
                } else {
                    false
                };
                if !traversal_handled {
                    let _ = writeln!(
                        out,
                        "        XCTAssertFalse({string_expr}.contains({swift_val}), \"expected NOT to contain: \\({swift_val})\")"
                    );
                }
            }
        }
        "not_empty" => {
            // For optional fields (Optional<T>), check that the value is non-nil.
            // For array fields (RustVec<T>), check .isEmpty on the vec directly.
            // For result_is_simple (e.g. Data, String), use .isEmpty directly on
            // the result — avoids calling .toString() on non-RustString types.
            // For string fields, convert to Swift String and check .isEmpty.
            // []. traversal: "links[].url" → contains(where: { !elem.isEmpty })
            let traversal_not_empty_handled = if let Some(f) = assertion.field.as_deref() {
                if let Some(dot) = f.find("[].") {
                    let array_part = &f[..dot];
                    let elem_part = &f[dot + 3..];
                    let array_accessor = field_resolver.accessor(array_part, "swift", result_var);
                    let resolved_full = field_resolver.resolve(f);
                    let resolved_elem_part = resolved_full
                        .find("[].")
                        .map(|d| &resolved_full[d + 3..])
                        .unwrap_or(elem_part);
                    let elem_accessor = field_resolver.accessor(resolved_elem_part, "swift", "$0");
                    let elem_is_enum = enum_fields.contains(f) || enum_fields.contains(resolved_full);
                    let elem_is_optional = field_resolver.is_optional(resolved_elem_part)
                        || field_resolver.is_optional(field_resolver.resolve(resolved_elem_part));
                    let elem_str = if elem_is_enum {
                        format!("{elem_accessor}.to_string().toString()")
                    } else if elem_is_optional {
                        format!("({elem_accessor}?.toString() ?? \"\")")
                    } else {
                        format!("{elem_accessor}.toString()")
                    };
                    let _ = writeln!(
                        out,
                        "        XCTAssertTrue({array_accessor}.contains(where: {{ !{elem_str}.isEmpty }}), \"expected non-empty value\")"
                    );
                    true
                } else {
                    false
                }
            } else {
                false
            };
            if !traversal_not_empty_handled {
                if bare_result_is_option {
                    let _ = writeln!(out, "        XCTAssertNotNil({result_var}, \"expected non-nil value\")");
                } else if field_is_optional {
                    let _ = writeln!(out, "        XCTAssertNotNil({field_expr}, \"expected non-nil value\")");
                } else if field_is_array {
                    let _ = writeln!(
                        out,
                        "        XCTAssertFalse({field_expr}.isEmpty, \"expected non-empty value\")"
                    );
                } else if result_is_simple {
                    // result_is_simple: result is a primitive (Data, String, etc.) — use .isEmpty directly.
                    let _ = writeln!(
                        out,
                        "        XCTAssertFalse({result_var}.isEmpty, \"expected non-empty value\")"
                    );
                } else {
                    // First-class Swift struct fields are properties typed as native Swift
                    // `String` / `[T]` / `Data` etc — all of which expose `.count` (and
                    // `String`/`Array` also expose `.isEmpty`). Use `.count > 0` so the same
                    // path works whether the field is a String or an Array.
                    //
                    // When the accessor contains a `?.` optional chain, `.count` returns an
                    // Optional which Swift cannot compare directly to `0`; coalesce via `?? 0`
                    // so the assertion typechecks.
                    //
                    // For opaque method-call accessors (`result.id()`), the returned type is
                    // `RustString`, which lacks `.count`. Convert to Swift `String` first via
                    // `.toString()`. Array fields short-circuit above via `field_is_array`, so
                    // method-call accessors landing here are guaranteed to be the scalar /
                    // string flavour; vec accessors return `RustVec` (whose `.count` is fine).
                    let count_target = swift_count_target(&field_expr, field_resolver, assertion.field.as_deref());
                    let len_expr = if accessor_is_optional {
                        format!("({count_target}.count ?? 0)")
                    } else {
                        format!("{count_target}.count")
                    };
                    let _ = writeln!(
                        out,
                        "        XCTAssertGreaterThan({len_expr}, 0, \"expected non-empty value\")"
                    );
                }
            }
        }
        "is_empty" => {
            if bare_result_is_option {
                let _ = writeln!(out, "        XCTAssertNil({result_var}, \"expected nil value\")");
            } else if field_is_optional {
                let _ = writeln!(out, "        XCTAssertNil({field_expr}, \"expected nil value\")");
            } else if field_is_array {
                let _ = writeln!(
                    out,
                    "        XCTAssertTrue({field_expr}.isEmpty, \"expected empty value\")"
                );
            } else {
                // Symmetric with not_empty: use .count == 0 on first-class Swift types.
                // Wrap opaque method-call accessors (`result.id()`) with `.toString()` so
                // `.count` lands on Swift `String`, not `RustString` (which lacks `.count`).
                let count_target = swift_count_target(&field_expr, field_resolver, assertion.field.as_deref());
                let len_expr = if accessor_is_optional {
                    format!("({count_target}.count ?? 0)")
                } else {
                    format!("{count_target}.count")
                };
                let _ = writeln!(out, "        XCTAssertEqual({len_expr}, 0, \"expected empty value\")");
            }
        }
        "contains_any" => {
            if let Some(values) = &assertion.values {
                let checks: Vec<String> = values
                    .iter()
                    .map(|v| {
                        let swift_val = json_to_swift(v);
                        format!("{string_expr}.contains({swift_val})")
                    })
                    .collect();
                let joined = checks.join(" || ");
                let _ = writeln!(
                    out,
                    "        XCTAssertTrue({joined}, \"expected to contain at least one of the specified values\")"
                );
            }
        }
        "greater_than" => {
            if let Some(val) = &assertion.value {
                let swift_val = json_to_swift(val);
                // For optional numeric fields (or when the accessor chain is optional),
                // coalesce to 0 before comparing so the expression is non-optional.
                let field_is_optional = accessor_is_optional
                    || assertion.field.as_deref().is_some_and(|f| {
                        field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f))
                    });
                let compare_expr = if field_is_optional {
                    format!("({field_expr} ?? 0)")
                } else {
                    field_expr.clone()
                };
                let _ = writeln!(out, "        XCTAssertGreaterThan({compare_expr}, {swift_val})");
            }
        }
        "less_than" => {
            if let Some(val) = &assertion.value {
                let swift_val = json_to_swift(val);
                let field_is_optional = accessor_is_optional
                    || assertion.field.as_deref().is_some_and(|f| {
                        field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f))
                    });
                let compare_expr = if field_is_optional {
                    format!("({field_expr} ?? 0)")
                } else {
                    field_expr.clone()
                };
                let _ = writeln!(out, "        XCTAssertLessThan({compare_expr}, {swift_val})");
            }
        }
        "greater_than_or_equal" => {
            if let Some(val) = &assertion.value {
                let swift_val = json_to_swift(val);
                // For optional numeric fields (or when the accessor chain is optional),
                // coalesce to 0 before comparing so the expression is non-optional.
                let field_is_optional = accessor_is_optional
                    || assertion.field.as_deref().is_some_and(|f| {
                        field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f))
                    });
                let compare_expr = if field_is_optional {
                    format!("({field_expr} ?? 0)")
                } else {
                    field_expr.clone()
                };
                let _ = writeln!(out, "        XCTAssertGreaterThanOrEqual({compare_expr}, {swift_val})");
            }
        }
        "less_than_or_equal" => {
            if let Some(val) = &assertion.value {
                let swift_val = json_to_swift(val);
                let field_is_optional = accessor_is_optional
                    || assertion.field.as_deref().is_some_and(|f| {
                        field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f))
                    });
                let compare_expr = if field_is_optional {
                    format!("({field_expr} ?? 0)")
                } else {
                    field_expr.clone()
                };
                let _ = writeln!(out, "        XCTAssertLessThanOrEqual({compare_expr}, {swift_val})");
            }
        }
        "starts_with" => {
            if let Some(expected) = &assertion.value {
                let swift_val = json_to_swift(expected);
                let _ = writeln!(
                    out,
                    "        XCTAssertTrue({string_expr}.hasPrefix({swift_val}), \"expected to start with: \\({swift_val})\")"
                );
            }
        }
        "ends_with" => {
            if let Some(expected) = &assertion.value {
                let swift_val = json_to_swift(expected);
                let _ = writeln!(
                    out,
                    "        XCTAssertTrue({string_expr}.hasSuffix({swift_val}), \"expected to end with: \\({swift_val})\")"
                );
            }
        }
        "min_length" => {
            if let Some(val) = &assertion.value {
                if let Some(n) = val.as_u64() {
                    // Use string_expr.count: for RustString fields string_expr already has
                    // .toString() appended, giving a Swift String whose .count is character count.
                    let _ = writeln!(out, "        XCTAssertGreaterThanOrEqual({string_expr}.count, {n})");
                }
            }
        }
        "max_length" => {
            if let Some(val) = &assertion.value {
                if let Some(n) = val.as_u64() {
                    let _ = writeln!(out, "        XCTAssertLessThanOrEqual({string_expr}.count, {n})");
                }
            }
        }
        "count_min" => {
            if let Some(val) = &assertion.value {
                if let Some(n) = val.as_u64() {
                    // For fields nested inside an optional parent (e.g. document.nodes where
                    // document is Optional), the accessor generates `result.document().nodes()`
                    // which doesn't compile in Swift without optional chaining.
                    let count_expr = swift_array_count_expr(assertion.field.as_deref(), result_var, field_resolver);
                    let _ = writeln!(out, "        XCTAssertGreaterThanOrEqual({count_expr}, {n})");
                }
            }
        }
        "count_equals" => {
            if let Some(val) = &assertion.value {
                if let Some(n) = val.as_u64() {
                    let count_expr = swift_array_count_expr(assertion.field.as_deref(), result_var, field_resolver);
                    let _ = writeln!(out, "        XCTAssertEqual({count_expr}, {n})");
                }
            }
        }
        "is_true" => {
            let _ = writeln!(out, "        XCTAssertTrue({field_expr})");
        }
        "is_false" => {
            let _ = writeln!(out, "        XCTAssertFalse({field_expr})");
        }
        "matches_regex" => {
            if let Some(expected) = &assertion.value {
                let swift_val = json_to_swift(expected);
                let _ = writeln!(
                    out,
                    "        XCTAssertNotNil({string_expr}.range(of: {swift_val}, options: .regularExpression), \"expected value to match regex: \\({swift_val})\")"
                );
            }
        }
        "not_error" => {
            // Already handled by the call succeeding without exception.
        }
        "error" => {
            // Handled at the test method level.
        }
        "method_result" => {
            let _ = writeln!(out, "        // method_result assertions not yet implemented for Swift");
        }
        other => {
            panic!("Swift e2e generator: unsupported assertion type: {other}");
        }
    }
}

/// Build a Swift accessor path for the given fixture field, inserting `()` on
/// every segment and `?` after every optional non-leaf segment.
///
/// This is the core helper for count/contains helpers that need to reconstruct
/// the path with correct optional chaining from the raw fixture field name.
///
/// Rewrite a Swift accessor expression to capture any `RustVec` temporaries
/// in a local before subscripting them. Returns `(setup_lines, rewritten_expr)`.
///
/// swift-bridge's `Vec_<T>$get` returns a raw pointer into the Vec's storage
/// wrapped in a `T.SelfRef`. If the Vec was a temporary, ARC may release it
/// before the ref is dereferenced, leaving the pointer dangling and reads
/// returning empty/garbage. Hoisting the Vec into a `let` binding ties the
/// Vec's lifetime to the enclosing function scope, so the ref stays valid.
///
/// Only the first `()[...]` occurrence per expression is materialised — that
/// covers all current fixture access patterns (single-level subscripts on a
/// result field). Nested subscripts are rare and would need a more elaborate
/// pass; if they appear, this returns conservative output (just the first
/// hoist) which is still correct.
/// Returns `(setup_lines, rewritten_expr, is_map_subscript)`. `is_map_subscript` is
/// true when the subscript key was a string literal, indicating the parent
/// accessor returns a JSON-encoded Map (RustString) and the rewritten expression
/// already evaluates to `String?` so callers should NOT append `.toString()`.
fn materialise_vec_temporaries(expr: &str, name_suffix: &str) -> (Vec<String>, String, bool) {
    let Some(idx) = expr.find("()[") else {
        return (Vec::new(), expr.to_string(), false);
    };
    let after_open = idx + 3; // position after `()[`
    let Some(close_rel) = expr[after_open..].find(']') else {
        return (Vec::new(), expr.to_string(), false);
    };
    let subscript_end = after_open + close_rel; // index of `]`
    let prefix = &expr[..idx + 2]; // includes `()`
    let subscript = &expr[idx + 2..=subscript_end]; // `[N]`
    let tail = &expr[subscript_end + 1..]; // everything after `]`
    let method_dot = expr[..idx].rfind('.').unwrap_or(0);
    let method = &expr[method_dot + 1..idx];
    let local = format!("_vec_{}_{}", method, name_suffix);

    // String-key subscript (e.g. `["title"]`) signals a Map-like access. swift-bridge
    // serialises non-leaf Maps (e.g. `HashMap<String, String>`) as JSON-encoded
    // RustString rather than exposing a Swift dictionary. Decode the RustString to
    // `[String: String]` before subscripting so `_vec_X["title"]` works.
    let inner = subscript.trim_start_matches('[').trim_end_matches(']');
    let is_string_key = inner.starts_with('"') && inner.ends_with('"');
    let setup = if is_string_key {
        format!(
            "let {local} = (try? JSONSerialization.jsonObject(with: ({prefix}.toString() ?? \"{{}}\").data(using: .utf8)!) as? [String: String]) ?? [:]"
        )
    } else {
        format!("let {local} = {prefix}")
    };

    let rewritten = format!("{local}{subscript}{tail}");
    (vec![setup], rewritten, is_string_key)
}

/// Returns `(accessor_expr, has_optional)` where `has_optional` is true when
/// at least one `?.` was inserted.
fn swift_build_accessor(field: &str, result_var: &str, field_resolver: &FieldResolver) -> (String, bool) {
    let resolved = field_resolver.resolve(field);
    let parts: Vec<&str> = resolved.split('.').collect();

    // Build a set of optional prefix paths for O(1) lookup during the walk.
    // We track path_so_far incrementally.
    let mut out = result_var.to_string();
    let mut has_optional = false;
    let mut path_so_far = String::new();
    let total = parts.len();
    for (i, part) in parts.iter().enumerate() {
        let is_leaf = i == total - 1;
        // Handle array index subscripts within a segment, e.g. `data[0]`.
        // `data[0]` must become `.data()[0]` not `.data[0]()`.
        // Split at the first `[` if present.
        let (field_name, subscript): (&str, Option<&str>) = if let Some(bracket_pos) = part.find('[') {
            (&part[..bracket_pos], Some(&part[bracket_pos..]))
        } else {
            (part, None)
        };

        if !path_so_far.is_empty() {
            path_so_far.push('.');
        }
        // Build the base path (without subscript) for the optional check. When the
        // segment is e.g. `tool_calls[0]`, we want to check `is_optional` against
        // "choices[0].message.tool_calls" not "choices[0].message.tool_calls[0]".
        let base_path = {
            let mut p = path_so_far.clone();
            p.push_str(field_name);
            p
        };
        // Now push the full part (with subscript if any) so path_so_far is correct
        // for subsequent segment checks.
        path_so_far.push_str(part);

        out.push('.');
        out.push_str(field_name);
        if let Some(sub) = subscript {
            // When the getter for this subscripted field is itself optional
            // (e.g. tool_calls returns Optional<RustVec<T>>), insert `?` before
            // the subscript so Swift unwraps the Optional before indexing.
            let field_is_optional = field_resolver.is_optional(&base_path);
            if field_is_optional {
                out.push_str("()?");
                has_optional = true;
            } else {
                out.push_str("()");
            }
            out.push_str(sub);
            // Do NOT append a trailing `?` after the subscript index: in Swift,
            // `optionalVec?[N]` via `Collection.subscript` returns the element
            // type `T` directly (the subscript is non-optional and the force-unwrap
            // inside RustVec's subscript is unconditional).  Optional chaining
            // already consumed the `?` in `?[N]`, so the result is `T` (non-optional
            // in the compiler's view), and a subsequent `?.member()` would be flagged
            // as "optional chaining on non-optional value".  The parent `has_optional`
            // flag is still set when `field_is_optional` is true, which causes the
            // enclosing expression to be wrapped in `(... ?? fallback)` correctly.
        } else {
            out.push_str("()");
            // Insert `?` after `()` for non-leaf optional fields so the next
            // member access becomes `?.`.
            if !is_leaf && field_resolver.is_optional(&base_path) {
                out.push('?');
                has_optional = true;
            }
        }
    }
    (out, has_optional)
}

/// Generate a `[String]?` expression for a `RustVec<RustString>` (or optional variant) field
/// so that `contains` membership checks work against plain Swift Strings.
///
/// The result is `Optional<[String]>` — callers should coalesce with `?? []`.
///
/// We use `?.map { $0.as_str().toString() }` because:
/// 1. Iterating a `RustVec<RustString>` yields `RustStringRef` (not `RustString`), which
///    only has `as_str()` but not `toString()` directly.
/// 2. The accessor may end with an `Optional<RustVec<RustString>>` (e.g. `sheet_names()` is
///    `Option<Vec<String>>` in Rust, which becomes `Optional<RustVec<RustString>>` in Swift).
/// 3. Optional chaining from parent `?.` already produces `Optional<RustVec<T>>`.
///
/// `?.map { $0.as_str().toString() }` converts each `RustStringRef` to a Swift `String`,
/// giving `[String]` wrapped in `Optional`. The `?? []` in callers coalesces nil to an empty
/// array.
/// Generate a `XCTAssert{True|False}(array.contains(where: { elem_str.contains(val) }), msg)` line
/// for field paths that traverse a collection with `[].` notation (e.g. `links[].url`).
///
/// `array_part` — left side of `[].` (e.g. `"links"`)
/// `element_part` — right side (e.g. `"url"` or `"link_type"`)
/// `full_field` — original assertion.field (used for enum lookup against the full path)
#[allow(clippy::too_many_arguments)]
fn swift_traversal_contains_assert(
    array_part: &str,
    element_part: &str,
    full_field: &str,
    val_expr: &str,
    result_var: &str,
    negate: bool,
    msg: &str,
    enum_fields: &std::collections::HashSet<String>,
    field_resolver: &FieldResolver,
) -> String {
    let array_accessor = field_resolver.accessor(array_part, "swift", result_var);
    let resolved_full = field_resolver.resolve(full_field);
    let resolved_elem_part = resolved_full
        .find("[].")
        .map(|d| &resolved_full[d + 3..])
        .unwrap_or(element_part);
    let elem_accessor = field_resolver.accessor(resolved_elem_part, "swift", "$0");
    let elem_is_enum = enum_fields.contains(full_field) || enum_fields.contains(resolved_full);
    let elem_is_optional = field_resolver.is_optional(resolved_elem_part)
        || field_resolver.is_optional(field_resolver.resolve(resolved_elem_part));
    let elem_str = if elem_is_enum {
        // Enum-typed fields are bridged as `String` (RustString in Swift).
        // A single `.toString()` converts RustString → Swift String.
        format!("{elem_accessor}.toString()")
    } else if elem_is_optional {
        format!("({elem_accessor}?.toString() ?? \"\")")
    } else {
        format!("{elem_accessor}.toString()")
    };
    let assert_fn = if negate { "XCTAssertFalse" } else { "XCTAssertTrue" };
    format!("        {assert_fn}({array_accessor}.contains(where: {{ {elem_str}.contains({val_expr}) }}), \"{msg}\")")
}

fn swift_array_contains_expr(field: Option<&str>, result_var: &str, field_resolver: &FieldResolver) -> String {
    let Some(f) = field else {
        return format!("{result_var}.map {{ $0.as_str().toString() }}");
    };
    let (accessor, _has_optional) = swift_build_accessor(f, result_var, field_resolver);
    // Always use `?.map` — the array field (sheet_names, etc.) may itself return
    // Optional<RustVec<T>> even if not listed in fields_optional.
    format!("{accessor}?.map {{ $0.as_str().toString() }}")
}

/// Generate a `.count` expression for an array field that may be nested inside optional parents.
///
/// Swift-bridge exposes all Rust fields as methods with `()`. When ancestor segments are
/// optional, we use `?.` chaining. The final count is coalesced with `?? 0` when there
/// are optional ancestors so the XCTAssert macro receives a non-optional `Int`.
///
/// Also check if the field itself (the leaf) is optional, which happens when the field
/// returns Optional<RustVec<T>> (e.g., `links()` may return Optional).
fn swift_array_count_expr(field: Option<&str>, result_var: &str, field_resolver: &FieldResolver) -> String {
    let Some(f) = field else {
        return format!("{result_var}.count");
    };
    let (accessor, mut has_optional) = swift_build_accessor(f, result_var, field_resolver);
    // Also check if the leaf field itself is optional.
    if field_resolver.is_optional(f) {
        has_optional = true;
    }
    if has_optional {
        // In Swift, accessing .count on an optional with ?. returns Optional<Int>,
        // so we coalesce with ?? 0 to get a concrete Int for XCTAssert.
        if accessor.contains("?.") {
            format!("{accessor}.count ?? 0")
        } else {
            // If no ?. but field is optional, the field_expr itself is Optional<RustVec<T>>
            // so we need ?. to call count.
            format!("({accessor}?.count ?? 0)")
        }
    } else {
        format!("{accessor}.count")
    }
}

/// Convert a `serde_json::Value` to a Swift literal string.
fn json_to_swift(value: &serde_json::Value) -> String {
    match value {
        serde_json::Value::String(s) => format!("\"{}\"", escape_swift(s)),
        serde_json::Value::Bool(b) => b.to_string(),
        serde_json::Value::Number(n) => n.to_string(),
        serde_json::Value::Null => "nil".to_string(),
        serde_json::Value::Array(arr) => {
            let items: Vec<String> = arr.iter().map(json_to_swift).collect();
            format!("[{}]", items.join(", "))
        }
        serde_json::Value::Object(_) => {
            let json_str = serde_json::to_string(value).unwrap_or_default();
            format!("\"{}\"", escape_swift(&json_str))
        }
    }
}

/// Escape a string for embedding in a Swift double-quoted string literal.
fn escape_swift(s: &str) -> String {
    escape_swift_str(s)
}

/// Return the count-able target expression for `field_expr`.
///
/// For opaque method-call accessors (ending in `()` or `()?`), the returned
/// value depends on the field's IR kind:
///
/// - `Vec<T>` ⇒ `RustVec<T>`, which exposes `.count` directly. No wrap.
/// - `String` ⇒ `RustString`, which does NOT expose `.count`. Wrap with
///   `.toString()` so `.count` lands on Swift `String`.
///
/// First-class property accessors (no trailing parens) return Swift values
/// that already support `.count` directly.
///
/// The discriminator is the field's resolved leaf type, looked up against the
/// `SwiftFirstClassMap`'s vec field set when available. If the field is
/// unknown (None), fall back to the conservative wrap — RustString is the
/// dominant scalar-leaf case for top-level assertions.
fn swift_count_target(field_expr: &str, field_resolver: &FieldResolver, field: Option<&str>) -> String {
    let is_method_call = field_expr.trim_end().ends_with(')');
    if !is_method_call {
        return field_expr.to_string();
    }
    if let Some(f) = field
        && field_resolver.leaf_is_vec_via_swift_map(field_resolver.resolve(f))
    {
        return field_expr.to_string();
    }
    format!("{field_expr}.toString()")
}

/// Resolve the IR type name backing this call's result.
///
/// Lookup order mirrors PHP's `derive_root_type` for `[crates.e2e.calls.*]`
/// configs: any of `c, csharp, java, kotlin, go, php` overrides may carry a
/// `result_type = "ChatCompletionResponse"` field. The first non-empty value
/// wins. These overrides are language-agnostic IR type names — they were
/// originally added for the C/C# backends and other backends piggy-back on them
/// because the IR names are shared across every binding.
///
/// Returns `None` when no override sets `result_type`; the renderer then falls
/// back to the workspace-default heuristic in `SwiftFirstClassMap` (which
/// defaults to property access — the right call for first-class result types
/// like `FileObject` but wrong for opaque types like `ChatCompletionResponse`).
fn swift_call_result_type(call_config: &alef_core::config::e2e::CallConfig) -> Option<String> {
    const LOOKUP_LANGS: &[&str] = &["c", "csharp", "java", "kotlin", "go", "php"];
    for lang in LOOKUP_LANGS {
        if let Some(o) = call_config.overrides.get(*lang)
            && let Some(rt) = o.result_type.as_deref()
            && !rt.is_empty()
        {
            return Some(rt.to_string());
        }
    }
    None
}

/// Returns true when the field type would be emitted as a Swift primitive value
/// (`String`, `Bool`, `Int`-family, `Double`, etc.) — these can appear on
/// first-class Codable structs without forcing the host type into a typealias.
/// Mirrors `first_class_field_supported` in alef-backend-swift.
fn swift_first_class_field_supported(ty: &alef_core::ir::TypeRef) -> bool {
    use alef_core::ir::TypeRef;
    match ty {
        TypeRef::Primitive(_) | TypeRef::String => true,
        TypeRef::Optional(inner) => swift_first_class_field_supported(inner),
        _ => false,
    }
}

/// Build the per-type Swift first-class/opaque classification map used by
/// `render_swift_with_first_class_map`.
///
/// A TypeDef is treated as first-class (Codable Swift struct → property access)
/// when it is not opaque, has serde derives, has at least one field, and every
/// binding field is a Swift primitive (or `Optional<primitive>`). All other
/// public types end up as typealiases to opaque `RustBridge.X` classes whose
/// fields are swift-bridge methods (`.id()`, `.status()`).
///
/// `field_types` records the next-type that each Named field traverses into,
/// so the renderer can advance its current-type cursor through nested
/// `data[0].id` style paths.
fn build_swift_first_class_map(
    type_defs: &[alef_core::ir::TypeDef],
    e2e_config: &crate::config::E2eConfig,
) -> SwiftFirstClassMap {
    use alef_core::ir::TypeRef;
    let mut first_class_types: HashSet<String> = HashSet::new();
    let mut field_types: HashMap<String, HashMap<String, String>> = HashMap::new();
    let mut vec_field_names: HashSet<String> = HashSet::new();
    fn inner_named(ty: &TypeRef) -> Option<String> {
        match ty {
            TypeRef::Named(n) => Some(n.clone()),
            TypeRef::Optional(inner) | TypeRef::Vec(inner) => inner_named(inner),
            _ => None,
        }
    }
    fn is_vec_ty(ty: &TypeRef) -> bool {
        match ty {
            TypeRef::Vec(_) => true,
            TypeRef::Optional(inner) => is_vec_ty(inner),
            _ => false,
        }
    }
    for td in type_defs {
        let is_first_class = !td.is_opaque
            && td.has_serde
            && !td.fields.is_empty()
            && td.fields.iter().all(|f| swift_first_class_field_supported(&f.ty));
        if is_first_class {
            first_class_types.insert(td.name.clone());
        }
        let mut td_field_types: HashMap<String, String> = HashMap::new();
        for f in &td.fields {
            if let Some(named) = inner_named(&f.ty) {
                td_field_types.insert(f.name.clone(), named);
            }
            if is_vec_ty(&f.ty) {
                vec_field_names.insert(f.name.clone());
            }
        }
        if !td_field_types.is_empty() {
            field_types.insert(td.name.clone(), td_field_types);
        }
    }
    // Best-effort root-type detection: pick a unique TypeDef that contains all
    // `result_fields`. Falls back to `None` (renderer defaults to first-class
    // property syntax for unknown roots).
    let root_type = if e2e_config.result_fields.is_empty() {
        None
    } else {
        let matches: Vec<&alef_core::ir::TypeDef> = type_defs
            .iter()
            .filter(|td| {
                let names: HashSet<&str> = td.fields.iter().map(|f| f.name.as_str()).collect();
                e2e_config.result_fields.iter().all(|rf| names.contains(rf.as_str()))
            })
            .collect();
        if matches.len() == 1 {
            Some(matches[0].name.clone())
        } else {
            None
        }
    };
    SwiftFirstClassMap {
        first_class_types,
        field_types,
        vec_field_names,
        root_type,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::field_access::FieldResolver;
    use std::collections::{HashMap, HashSet};

    fn make_resolver_tool_calls() -> FieldResolver {
        // Resolver for `choices[0].message.tool_calls[0].function.name`:
        //   - `choices` is a registered array field
        //   - `choices.message.tool_calls` is optional (Optional<RustVec<ToolCall>>)
        let mut optional = HashSet::new();
        optional.insert("choices.message.tool_calls".to_string());
        let mut arrays = HashSet::new();
        arrays.insert("choices".to_string());
        FieldResolver::new(&HashMap::new(), &optional, &HashSet::new(), &arrays, &HashSet::new())
    }

    /// Regression: after `tool_calls()?[0]` the codegen must NOT append a trailing `?`
    /// before the next segment.  The Swift compiler sees `?[0]` as consuming the optional
    /// chain, yielding `ToolCallRef` (non-optional from the subscript's perspective), so
    /// `?.function()` triggers "cannot use optional chaining on non-optional value".
    ///
    /// The fix: do not emit `?` after the subscript index for non-leaf segments.
    #[test]
    fn optional_vec_subscript_does_not_emit_trailing_question_mark_before_next_segment() {
        let resolver = make_resolver_tool_calls();
        // Access `choices[0].message.tool_calls[0].function.name`:
        //   `tool_calls` is optional, `function` and `name` are non-optional.
        let (accessor, has_optional) =
            swift_build_accessor("choices[0].message.tool_calls[0].function.name", "result", &resolver);
        // `?` before `[0]` is correct (tool_calls is optional).
        // swift_build_accessor uses the raw field name without camelCase conversion.
        assert!(
            accessor.contains("tool_calls()?[0]"),
            "expected `tool_calls()?[0]` for optional tool_calls, got: {accessor}"
        );
        // There must NOT be `?[0]?` (trailing `?` after the index).
        assert!(
            !accessor.contains("?[0]?"),
            "must not emit trailing `?` after subscript index: {accessor}"
        );
        // The expression IS optional overall (tool_calls may be nil).
        assert!(has_optional, "expected has_optional=true for optional field chain");
        // Subsequent member access uses `.` (non-optional chain) not `?.`.
        assert!(
            accessor.contains("[0].function()"),
            "expected `.function()` (non-optional) after subscript: {accessor}"
        );
    }
}