Skip to main content

alef_e2e/codegen/
swift.rs

1//! Swift e2e test generator using XCTest.
2//!
3//! Generates test files for the swift package in `packages/swift/Tests/<Module>Tests/`.
4//!
5//! IMPORTANT: Due to SwiftPM 6.0 limitations (forbids inter-package `.package(path:)`
6//! references within a monorepo), generated test files are placed directly inside
7//! the `packages/swift` package (not in a separate `e2e/swift` package). This allows
8//! tests to depend on the library target without an explicit package dependency.
9//!
10//! The generated `Package.swift` is placed in `e2e/swift/` for documentation and CI
11//! reference but is NOT used for running tests — tests are run from the
12//! `packages/swift/` directory using `swift test`.
13
14use crate::config::E2eConfig;
15use crate::escape::{escape_java as escape_swift_str, expand_fixture_templates, sanitize_filename, sanitize_ident};
16use crate::field_access::FieldResolver;
17use crate::fixture::{Assertion, Fixture, FixtureGroup, ValidationErrorExpectation};
18use alef_core::backend::GeneratedFile;
19use alef_core::config::ResolvedCrateConfig;
20use alef_core::hash::{self, CommentStyle};
21use alef_core::template_versions::toolchain;
22use anyhow::Result;
23use heck::{ToLowerCamelCase, ToSnakeCase, ToUpperCamelCase};
24use std::collections::HashSet;
25use std::fmt::Write as FmtWrite;
26use std::path::PathBuf;
27
28use super::E2eCodegen;
29use super::client;
30
31/// Swift e2e code generator.
32pub struct SwiftE2eCodegen;
33
34impl E2eCodegen for SwiftE2eCodegen {
35    fn generate(
36        &self,
37        groups: &[FixtureGroup],
38        e2e_config: &E2eConfig,
39        config: &ResolvedCrateConfig,
40        _type_defs: &[alef_core::ir::TypeDef],
41        _enums: &[alef_core::ir::EnumDef],
42    ) -> Result<Vec<GeneratedFile>> {
43        let lang = self.language_name();
44        // Emit under `<output>/swift/` for consistency with every other language's e2e
45        // layout. SwiftPM identifies path-based deps by the path's last component,
46        // which would collide with the dep at `packages/swift/`; we disambiguate by
47        // passing an explicit `name:` to `.package(path:)` in the consumer manifest
48        // (see render_package_swift below).
49        let output_base = PathBuf::from(e2e_config.effective_output()).join("swift");
50
51        let mut files = Vec::new();
52
53        // Resolve call config with overrides.
54        let call = &e2e_config.call;
55        let overrides = call.overrides.get(lang);
56        let function_name = overrides
57            .and_then(|o| o.function.as_ref())
58            .cloned()
59            .unwrap_or_else(|| call.function.clone());
60        let result_var = &call.result_var;
61        let result_is_simple = overrides.is_some_and(|o| o.result_is_simple);
62
63        // Resolve package config.
64        let swift_pkg = e2e_config.resolve_package("swift");
65        let pkg_name = swift_pkg
66            .as_ref()
67            .and_then(|p| p.name.as_ref())
68            .cloned()
69            .unwrap_or_else(|| config.name.to_upper_camel_case());
70        let pkg_path = swift_pkg
71            .as_ref()
72            .and_then(|p| p.path.as_ref())
73            .cloned()
74            .unwrap_or_else(|| "../../packages/swift".to_string());
75        let pkg_version = swift_pkg
76            .as_ref()
77            .and_then(|p| p.version.as_ref())
78            .cloned()
79            .or_else(|| config.resolved_version())
80            .unwrap_or_else(|| "0.1.0".to_string());
81
82        // The Swift module name: UpperCamelCase of the package name.
83        let module_name = pkg_name.as_str();
84
85        // Resolve the registry URL: derive from the configured repository when
86        // available (with a `.git` suffix per SwiftPM convention). Falls back
87        // to a vendor-neutral placeholder when no repo is configured.
88        let registry_url = config
89            .try_github_repo()
90            .map(|repo| {
91                let base = repo.trim_end_matches('/').trim_end_matches(".git");
92                format!("{base}.git")
93            })
94            .unwrap_or_else(|_| format!("https://example.invalid/{module_name}.git"));
95
96        // Generate Package.swift (kept for tooling/CI reference but not used
97        // for running tests — see note below).
98        files.push(GeneratedFile {
99            path: output_base.join("Package.swift"),
100            content: render_package_swift(module_name, &registry_url, &pkg_path, &pkg_version, e2e_config.dep_mode),
101            generated_header: false,
102        });
103
104        // Swift e2e tests live in the standalone e2e/swift package (same as
105        // every other language). The generated Package.swift uses an explicit
106        // `.package(name: "{module}", path: "../../packages/swift")` dep so
107        // SwiftPM resolves the binding library by its declared identity, not
108        // the path tail (avoids collision when both this package and the dep
109        // share the path-last component `swift`).
110        let tests_base = output_base.clone();
111
112        let field_resolver = FieldResolver::new(
113            &e2e_config.fields,
114            &e2e_config.fields_optional,
115            &e2e_config.result_fields,
116            &e2e_config.fields_array,
117            &e2e_config.fields_method_calls,
118        );
119
120        // Resolve client_factory override for swift (enables client-instance dispatch).
121        let client_factory: Option<&str> = overrides.and_then(|o| o.client_factory.as_deref());
122
123        // One test file per fixture group.
124        for group in groups {
125            let active: Vec<&Fixture> = group
126                .fixtures
127                .iter()
128                .filter(|f| super::should_include_fixture(f, lang, e2e_config))
129                .collect();
130
131            if active.is_empty() {
132                continue;
133            }
134
135            let class_name = format!("{}Tests", sanitize_filename(&group.category).to_upper_camel_case());
136            let filename = format!("{class_name}.swift");
137            let content = render_test_file(
138                &group.category,
139                &active,
140                e2e_config,
141                module_name,
142                &class_name,
143                &function_name,
144                result_var,
145                &e2e_config.call.args,
146                &field_resolver,
147                result_is_simple,
148                &e2e_config.fields_enum,
149                client_factory,
150            );
151            files.push(GeneratedFile {
152                path: tests_base
153                    .join("Tests")
154                    .join(format!("{module_name}E2ETests"))
155                    .join(filename),
156                content,
157                generated_header: true,
158            });
159        }
160
161        Ok(files)
162    }
163
164    fn language_name(&self) -> &'static str {
165        "swift"
166    }
167}
168
169// ---------------------------------------------------------------------------
170// Rendering
171// ---------------------------------------------------------------------------
172
173fn render_package_swift(
174    module_name: &str,
175    registry_url: &str,
176    pkg_path: &str,
177    pkg_version: &str,
178    dep_mode: crate::config::DependencyMode,
179) -> String {
180    let min_macos = toolchain::SWIFT_MIN_MACOS;
181
182    // For local deps SwiftPM identity = last path component (e.g. "../../packages/swift" → "swift").
183    // For registry deps identity is inferred from the URL.
184    // Use explicit .product(name:package:) to avoid ambiguity under tools-version 6.0.
185    let (dep_block, product_dep) = match dep_mode {
186        crate::config::DependencyMode::Registry => {
187            let dep = format!(r#"        .package(url: "{registry_url}", from: "{pkg_version}")"#);
188            let pkg_id = registry_url
189                .trim_end_matches('/')
190                .trim_end_matches(".git")
191                .split('/')
192                .next_back()
193                .unwrap_or(module_name);
194            let prod = format!(r#".product(name: "{module_name}", package: "{pkg_id}")"#);
195            (dep, prod)
196        }
197        crate::config::DependencyMode::Local => {
198            // SwiftPM identifies path-based deps by the path's last component.
199            // Because this e2e consumer and the dep both live in directories
200            // named `swift/`, declare an explicit `name:` on `.package(path:)`
201            // to give the dep a distinct identity (matches the module name).
202            // The `.product(package:)` reference uses that identity.
203            let pkg_id = module_name;
204            let dep = format!(r#"        .package(name: "{pkg_id}", path: "{pkg_path}")"#);
205            let prod = format!(r#".product(name: "{module_name}", package: "{pkg_id}")"#);
206            (dep, prod)
207        }
208    };
209    // SwiftPM platform enums use the major version only (.v13, .v14, ...);
210    // strip patch components to match the scaffold's `Package.swift`.
211    let min_macos_major = min_macos.split('.').next().unwrap_or(min_macos);
212    let min_ios = toolchain::SWIFT_MIN_IOS;
213    let min_ios_major = min_ios.split('.').next().unwrap_or(min_ios);
214    // The consumer's minimum iOS must be >= the dep's minimum iOS or SwiftPM hides
215    // the product as platform-incompatible. Use the same constant the swift backend
216    // emits into the dep's Package.swift.
217    format!(
218        r#"// swift-tools-version: 6.0
219import PackageDescription
220
221let package = Package(
222    name: "E2eSwift",
223    platforms: [
224        .macOS(.v{min_macos_major}),
225        .iOS(.v{min_ios_major}),
226    ],
227    dependencies: [
228{dep_block},
229    ],
230    targets: [
231        .testTarget(
232            name: "{module_name}E2ETests",
233            dependencies: [{product_dep}]
234        ),
235    ]
236)
237"#
238    )
239}
240
241#[allow(clippy::too_many_arguments)]
242fn render_test_file(
243    category: &str,
244    fixtures: &[&Fixture],
245    e2e_config: &E2eConfig,
246    module_name: &str,
247    class_name: &str,
248    function_name: &str,
249    result_var: &str,
250    args: &[crate::config::ArgMapping],
251    field_resolver: &FieldResolver,
252    result_is_simple: bool,
253    enum_fields: &HashSet<String>,
254    client_factory: Option<&str>,
255) -> String {
256    // Detect whether any fixture in this group uses a file_path or bytes arg — if so
257    // the test class chdir's to <repo>/test_documents at setUp time so the
258    // fixture-relative paths in test bodies (e.g. "docx/fake.docx") resolve correctly.
259    // The Swift binding's `extractBytes`/`extractFile` e2e wrappers consult
260    // `FIXTURES_DIR` first, otherwise resolve against the current directory.
261    // Mirrors the Ruby/Python conftest pattern that chdirs to test_documents.
262    let needs_chdir = fixtures.iter().any(|f| {
263        let call_config = e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.input);
264        call_config
265            .args
266            .iter()
267            .any(|a| a.arg_type == "file_path" || a.arg_type == "bytes")
268    });
269
270    let mut out = String::new();
271    out.push_str(&hash::header(CommentStyle::DoubleSlash));
272    let _ = writeln!(out, "import XCTest");
273    let _ = writeln!(out, "import Foundation");
274    let _ = writeln!(out, "import {module_name}");
275    let _ = writeln!(out, "import RustBridge");
276    let _ = writeln!(out);
277    let _ = writeln!(out, "/// E2e tests for category: {category}.");
278    let _ = writeln!(out, "final class {class_name}: XCTestCase {{");
279
280    if needs_chdir {
281        // Chdir once at class setUp so all fixture file_path arguments resolve relative
282        // to the repository's test_documents directory.
283        //
284        // #filePath = <repo>/packages/swift/Tests/<Module>Tests/<Class>.swift
285        // 5 deletingLastPathComponent() calls climb to the repo root before appending
286        // "test_documents". Mirrors the Ruby/Python conftest pattern that chdirs to
287        // test_documents.
288        let _ = writeln!(out, "    override class func setUp() {{");
289        let _ = writeln!(out, "        super.setUp()");
290        let _ = writeln!(out, "        let _testDocs = URL(fileURLWithPath: #filePath)");
291        let _ = writeln!(out, "            .deletingLastPathComponent() // <Module>Tests/");
292        let _ = writeln!(out, "            .deletingLastPathComponent() // Tests/");
293        let _ = writeln!(out, "            .deletingLastPathComponent() // swift/");
294        let _ = writeln!(out, "            .deletingLastPathComponent() // packages/");
295        let _ = writeln!(out, "            .deletingLastPathComponent() // <repo root>");
296        let _ = writeln!(
297            out,
298            "            .appendingPathComponent(\"{}\")",
299            e2e_config.test_documents_dir
300        );
301        let _ = writeln!(
302            out,
303            "        if FileManager.default.fileExists(atPath: _testDocs.path) {{"
304        );
305        let _ = writeln!(
306            out,
307            "            FileManager.default.changeCurrentDirectoryPath(_testDocs.path)"
308        );
309        let _ = writeln!(out, "        }}");
310        let _ = writeln!(out, "    }}");
311        let _ = writeln!(out);
312    }
313
314    for fixture in fixtures {
315        if fixture.is_http_test() {
316            render_http_test_method(&mut out, fixture);
317        } else {
318            render_test_method(
319                &mut out,
320                fixture,
321                e2e_config,
322                function_name,
323                result_var,
324                args,
325                field_resolver,
326                result_is_simple,
327                enum_fields,
328                client_factory,
329            );
330        }
331        let _ = writeln!(out);
332    }
333
334    let _ = writeln!(out, "}}");
335    out
336}
337
338// ---------------------------------------------------------------------------
339// HTTP test rendering — TestClientRenderer impl + thin driver wrapper
340// ---------------------------------------------------------------------------
341
342/// Renderer that emits XCTest `func test...() throws` methods using `URLSession`
343/// against the mock server (`ProcessInfo.processInfo.environment["MOCK_SERVER_URL"]`).
344struct SwiftTestClientRenderer;
345
346impl client::TestClientRenderer for SwiftTestClientRenderer {
347    fn language_name(&self) -> &'static str {
348        "swift"
349    }
350
351    fn sanitize_test_name(&self, id: &str) -> String {
352        // Swift test methods are `func testFoo()` — upper-camel-case after "test".
353        sanitize_ident(id).to_upper_camel_case()
354    }
355
356    /// Emit `func test{FnName}() throws {` (or a skip stub when the fixture is skipped).
357    ///
358    /// XCTest has no first-class skip annotation prior to Swift Testing (`@Test`).
359    /// For skipped fixtures we emit `try XCTSkipIf(true, reason)` inside the
360    /// function body so XCTest records them as skipped rather than omitting them.
361    fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
362        let _ = writeln!(out, "    /// {description}");
363        let _ = writeln!(out, "    func test{fn_name}() throws {{");
364        if let Some(reason) = skip_reason {
365            let escaped = escape_swift(reason);
366            let _ = writeln!(out, "        try XCTSkipIf(true, \"{escaped}\")");
367        }
368    }
369
370    fn render_test_close(&self, out: &mut String) {
371        let _ = writeln!(out, "    }}");
372    }
373
374    /// Emit a synchronous `URLSession` round-trip to the mock server.
375    ///
376    /// `ProcessInfo.processInfo.environment["MOCK_SERVER_URL"]!` provides the base
377    /// URL; the fixture path is appended directly.  The call uses a semaphore so the
378    /// generated test body stays synchronous (compatible with `throws` functions —
379    /// no `async` XCTest support needed).
380    fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
381        let method = ctx.method.to_uppercase();
382        let fixture_path = escape_swift(ctx.path);
383
384        let _ = writeln!(
385            out,
386            "        let _baseURL = ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]!"
387        );
388        let _ = writeln!(
389            out,
390            "        var _req = URLRequest(url: URL(string: _baseURL + \"{fixture_path}\")!)"
391        );
392        let _ = writeln!(out, "        _req.httpMethod = \"{method}\"");
393
394        // Headers
395        let mut header_pairs: Vec<(&String, &String)> = ctx.headers.iter().collect();
396        header_pairs.sort_by_key(|(k, _)| k.as_str());
397        for (k, v) in &header_pairs {
398            let expanded_v = expand_fixture_templates(v);
399            let ek = escape_swift(k);
400            let ev = escape_swift(&expanded_v);
401            let _ = writeln!(out, "        _req.setValue(\"{ev}\", forHTTPHeaderField: \"{ek}\")");
402        }
403
404        // Body
405        if let Some(body) = ctx.body {
406            let json_str = serde_json::to_string(body).unwrap_or_default();
407            let escaped_body = escape_swift(&json_str);
408            let _ = writeln!(out, "        _req.httpBody = \"{escaped_body}\".data(using: .utf8)");
409            let _ = writeln!(
410                out,
411                "        _req.setValue(\"application/json\", forHTTPHeaderField: \"Content-Type\")"
412            );
413        }
414
415        let _ = writeln!(out, "        var {}: HTTPURLResponse?", ctx.response_var);
416        let _ = writeln!(out, "        var _responseData: Data?");
417        let _ = writeln!(out, "        let _sema = DispatchSemaphore(value: 0)");
418        let _ = writeln!(
419            out,
420            "        URLSession.shared.dataTask(with: _req) {{ data, resp, _ in"
421        );
422        let _ = writeln!(out, "            {} = resp as? HTTPURLResponse", ctx.response_var);
423        let _ = writeln!(out, "            _responseData = data");
424        let _ = writeln!(out, "            _sema.signal()");
425        let _ = writeln!(out, "        }}.resume()");
426        let _ = writeln!(out, "        _sema.wait()");
427        let _ = writeln!(out, "        let _resp = try XCTUnwrap({})", ctx.response_var);
428    }
429
430    fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
431        let _ = writeln!(out, "        XCTAssertEqual(_resp.statusCode, {status})");
432    }
433
434    fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
435        let lower_name = name.to_lowercase();
436        let header_expr = format!("_resp.value(forHTTPHeaderField: \"{}\")", escape_swift(&lower_name));
437        match expected {
438            "<<present>>" => {
439                let _ = writeln!(out, "        XCTAssertNotNil({header_expr})");
440            }
441            "<<absent>>" => {
442                let _ = writeln!(out, "        XCTAssertNil({header_expr})");
443            }
444            "<<uuid>>" => {
445                let _ = writeln!(out, "        let _hdrVal_{lower_name} = try XCTUnwrap({header_expr})");
446                let _ = writeln!(
447                    out,
448                    "        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))"
449                );
450            }
451            exact => {
452                let escaped = escape_swift(exact);
453                let _ = writeln!(out, "        XCTAssertEqual({header_expr}, \"{escaped}\")");
454            }
455        }
456    }
457
458    fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
459        if let serde_json::Value::String(s) = expected {
460            let escaped = escape_swift(s);
461            let _ = writeln!(
462                out,
463                "        let _bodyStr = String(data: try XCTUnwrap(_responseData), encoding: .utf8) ?? \"\""
464            );
465            let _ = writeln!(
466                out,
467                "        XCTAssertEqual(_bodyStr.trimmingCharacters(in: .whitespacesAndNewlines), \"{escaped}\")"
468            );
469        } else {
470            let json_str = serde_json::to_string(expected).unwrap_or_default();
471            let escaped = escape_swift(&json_str);
472            let _ = writeln!(out, "        let _bodyData = try XCTUnwrap(_responseData)");
473            let _ = writeln!(
474                out,
475                "        let _expected = try JSONSerialization.jsonObject(with: \"{escaped}\".data(using: .utf8)!)"
476            );
477            let _ = writeln!(
478                out,
479                "        let _actual = try JSONSerialization.jsonObject(with: _bodyData)"
480            );
481            let _ = writeln!(
482                out,
483                "        XCTAssertEqual(NSDictionary(dictionary: _expected as? [String: AnyHashable] ?? [:]), NSDictionary(dictionary: _actual as? [String: AnyHashable] ?? [:]))"
484            );
485        }
486    }
487
488    fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
489        if let Some(obj) = expected.as_object() {
490            let _ = writeln!(out, "        let _bodyData = try XCTUnwrap(_responseData)");
491            let _ = writeln!(
492                out,
493                "        let _bodyObj = try XCTUnwrap(try JSONSerialization.jsonObject(with: _bodyData) as? [String: Any])"
494            );
495            for (key, val) in obj {
496                let escaped_key = escape_swift(key);
497                let swift_val = json_to_swift(val);
498                let _ = writeln!(
499                    out,
500                    "        XCTAssertEqual(_bodyObj[\"{escaped_key}\"] as? AnyHashable, ({swift_val}) as AnyHashable)"
501                );
502            }
503        }
504    }
505
506    fn render_assert_validation_errors(
507        &self,
508        out: &mut String,
509        _response_var: &str,
510        errors: &[ValidationErrorExpectation],
511    ) {
512        let _ = writeln!(out, "        let _bodyData = try XCTUnwrap(_responseData)");
513        let _ = writeln!(
514            out,
515            "        let _bodyObj = try XCTUnwrap(try JSONSerialization.jsonObject(with: _bodyData) as? [String: Any])"
516        );
517        let _ = writeln!(
518            out,
519            "        let _errors = _bodyObj[\"errors\"] as? [[String: Any]] ?? []"
520        );
521        for ve in errors {
522            let escaped_msg = escape_swift(&ve.msg);
523            let _ = writeln!(
524                out,
525                "        XCTAssertTrue(_errors.contains(where: {{ ($0[\"msg\"] as? String)?.contains(\"{escaped_msg}\") == true }}), \"expected validation error: {escaped_msg}\")"
526            );
527        }
528    }
529}
530
531/// Render an XCTest method for an HTTP server fixture via the shared driver.
532///
533/// HTTP 101 (WebSocket upgrade) is emitted as a skip stub because `URLSession`
534/// cannot handle Upgrade responses.
535fn render_http_test_method(out: &mut String, fixture: &Fixture) {
536    let Some(http) = &fixture.http else {
537        return;
538    };
539
540    // HTTP 101 (WebSocket upgrade) — URLSession cannot handle upgrade responses.
541    if http.expected_response.status_code == 101 {
542        let method_name = sanitize_ident(&fixture.id).to_upper_camel_case();
543        let description = fixture.description.replace('"', "\\\"");
544        let _ = writeln!(out, "    /// {description}");
545        let _ = writeln!(out, "    func test{method_name}() throws {{");
546        let _ = writeln!(
547            out,
548            "        try XCTSkipIf(true, \"HTTP 101 WebSocket upgrade cannot be tested via URLSession\")"
549        );
550        let _ = writeln!(out, "    }}");
551        return;
552    }
553
554    client::http_call::render_http_test(out, &SwiftTestClientRenderer, fixture);
555}
556
557// ---------------------------------------------------------------------------
558// Function-call test rendering
559// ---------------------------------------------------------------------------
560
561#[allow(clippy::too_many_arguments)]
562fn render_test_method(
563    out: &mut String,
564    fixture: &Fixture,
565    e2e_config: &E2eConfig,
566    _function_name: &str,
567    _result_var: &str,
568    _args: &[crate::config::ArgMapping],
569    field_resolver: &FieldResolver,
570    result_is_simple: bool,
571    enum_fields: &HashSet<String>,
572    global_client_factory: Option<&str>,
573) {
574    // Resolve per-fixture call config.
575    let call_config = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
576    let lang = "swift";
577    let call_overrides = call_config.overrides.get(lang);
578    let function_name = call_overrides
579        .and_then(|o| o.function.as_ref())
580        .cloned()
581        .unwrap_or_else(|| call_config.function.to_lower_camel_case());
582    // Per-call client_factory takes precedence over the global one.
583    let client_factory: Option<&str> = call_overrides
584        .and_then(|o| o.client_factory.as_deref())
585        .or(global_client_factory);
586    let result_var = &call_config.result_var;
587    let args = &call_config.args;
588    // Per-call flags: base call flag OR per-language override OR global flag.
589    // Also treat the call as simple when *any* language override marks it as bytes.
590    // Calls like `speech()` have `result_is_bytes = true` on C/C#/Java overrides but
591    // no explicit `result_is_simple` on the Swift override — yet the Swift binding
592    // returns `Data` directly (not a struct), so assertions must use `result.isEmpty`
593    // rather than `result.audio().toString().isEmpty`.
594    let result_is_bytes_any_lang =
595        call_config.result_is_bytes || call_config.overrides.values().any(|o| o.result_is_bytes);
596    eprintln!(
597        "[swift debug] fixture={} call={:?} result_is_bytes={} any_override_bytes={} overrides={}",
598        fixture.id,
599        fixture.call,
600        call_config.result_is_bytes,
601        call_config.overrides.values().any(|o| o.result_is_bytes),
602        call_config.overrides.len()
603    );
604    let result_is_simple = call_config.result_is_simple
605        || call_overrides.is_some_and(|o| o.result_is_simple)
606        || result_is_simple
607        || result_is_bytes_any_lang;
608    let result_is_array = call_config.result_is_array;
609    // When the call returns `Option<T>` the Swift binding exposes the result as
610    // `Optional<…>` (e.g. `getEmbeddingPreset(...) -> EmbeddingPreset?`). Bare-result
611    // `is_empty`/`not_empty` assertions must use `XCTAssertNil` / `XCTAssertNotNil`
612    // rather than `.toString().isEmpty`, which is undefined on opaque optionals.
613    let result_is_option = call_config.result_is_option || call_overrides.is_some_and(|o| o.result_is_option);
614
615    let method_name = fixture.id.to_upper_camel_case();
616    let description = &fixture.description;
617    let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
618    let is_async = call_config.r#async;
619
620    // Streaming detection (call-level `streaming` opt-out is honored).
621    let is_streaming = crate::codegen::streaming_assertions::resolve_is_streaming(fixture, call_config.streaming);
622    let collect_snippet_opt = if is_streaming && !expects_error {
623        crate::codegen::streaming_assertions::StreamingFieldResolver::collect_snippet(lang, result_var, "chunks")
624    } else {
625        None
626    };
627    // When swift has streaming-virtual-field assertions but no collect snippet
628    // is available (the swift-bridge surface does not yet expose a typed
629    // `chatStream` async sequence we can drain into a typed
630    // `[ChatCompletionChunk]`), emit a skip stub rather than reference an
631    // undefined `chunks` local in the assertion expressions. This keeps the
632    // swift test target compiling while the binding catches up.
633    if is_streaming && !expects_error && collect_snippet_opt.is_none() {
634        if is_async {
635            let _ = writeln!(out, "    func test{method_name}() async throws {{");
636        } else {
637            let _ = writeln!(out, "    func test{method_name}() throws {{");
638        }
639        let _ = writeln!(out, "        // {description}");
640        let _ = writeln!(
641            out,
642            "        try XCTSkipIf(true, \"swift: streaming chunk collection is not yet supported via the swift-bridge surface (fixture: {})\")",
643            fixture.id
644        );
645        let _ = writeln!(out, "    }}");
646        return;
647    }
648    let collect_snippet = collect_snippet_opt.unwrap_or_default();
649
650    // Detect whether this call has any json_object args that cannot be constructed
651    // in Swift — swift-bridge opaque types do not provide a fromJson initialiser.
652    // When such args exist and no `options_via` is configured for swift, emit a
653    // skip stub so the test compiles but is recorded as skipped rather than
654    // generating invalid code that passes `nil` or a string literal where a
655    // strongly-typed request object is required.
656    let has_unresolvable_json_object_arg = {
657        let options_via = call_overrides.and_then(|o| o.options_via.as_deref());
658        options_via.is_none() && args.iter().any(|a| a.arg_type == "json_object" && a.name != "config")
659    };
660
661    if has_unresolvable_json_object_arg {
662        if is_async {
663            let _ = writeln!(out, "    func test{method_name}() async throws {{");
664        } else {
665            let _ = writeln!(out, "    func test{method_name}() throws {{");
666        }
667        let _ = writeln!(out, "        // {description}");
668        let _ = writeln!(
669            out,
670            "        try XCTSkipIf(true, \"swift: json_object request construction requires options_via configuration (fixture: {})\");",
671            fixture.id
672        );
673        let _ = writeln!(out, "    }}");
674        return;
675    }
676
677    // Visitor-driven fixtures: emit a class that conforms to `HtmlVisitorProtocol`
678    // and wrap it via `makeHtmlVisitorHandle(...)`. The handle is then threaded
679    // into the options via `conversionOptionsFromJsonWithVisitor(json, handle)`.
680    let mut visitor_setup_lines: Vec<String> = Vec::new();
681    let visitor_handle_expr: Option<String> = fixture
682        .visitor
683        .as_ref()
684        .map(|spec| super::swift_visitors::build_swift_visitor(&mut visitor_setup_lines, spec, &fixture.id));
685
686    // Resolve extra_args from per-call swift overrides (e.g. `nil` for optional
687    // query-param arguments on list_files/list_batches that have no fixture-level
688    // input field).
689    let extra_args: Vec<String> = call_overrides.map(|o| o.extra_args.clone()).unwrap_or_default();
690
691    // Merge per-call enum_fields keys into the effective enum set so that
692    // fields like "status" (BatchStatus, BatchObject) are treated as enum-typed
693    // even when they are not globally listed in fields_enum (they are context-
694    // dependent — BatchStatus on BatchObject but plain String on ResponseObject).
695    let effective_enum_fields: std::borrow::Cow<HashSet<String>> = {
696        let per_call = call_overrides.map(|o| &o.enum_fields);
697        if let Some(pc) = per_call {
698            if !pc.is_empty() {
699                let mut merged = enum_fields.clone();
700                merged.extend(pc.keys().cloned());
701                std::borrow::Cow::Owned(merged)
702            } else {
703                std::borrow::Cow::Borrowed(enum_fields)
704            }
705        } else {
706            std::borrow::Cow::Borrowed(enum_fields)
707        }
708    };
709
710    let options_via_str: Option<&str> = call_overrides.and_then(|o| o.options_via.as_deref());
711    let options_type_str: Option<&str> = call_overrides.and_then(|o| o.options_type.as_deref());
712    // Derive the Swift handle-config parsing function from the C override's
713    // `c_engine_factory` field. E.g. `"CrawlConfig"` → snake → `"crawl_config_from_json"`
714    // → camelCase → `"crawlConfigFromJson"`.
715    let handle_config_fn_owned: Option<String> = call_config
716        .overrides
717        .get("c")
718        .and_then(|c| c.c_engine_factory.as_deref())
719        .map(|ty| format!("{}_from_json", ty.to_snake_case()).to_lower_camel_case());
720    let (mut setup_lines, args_str) = build_args_and_setup(
721        &fixture.input,
722        args,
723        &fixture.id,
724        fixture.has_host_root_route(),
725        &function_name,
726        options_via_str,
727        options_type_str,
728        handle_config_fn_owned.as_deref(),
729        visitor_handle_expr.as_deref(),
730    );
731    // Prepend visitor class declarations (before any setup lines that reference the handle).
732    if !visitor_setup_lines.is_empty() {
733        visitor_setup_lines.extend(setup_lines);
734        setup_lines = visitor_setup_lines;
735    }
736
737    // Append extra_args to the argument list.
738    let args_str = if extra_args.is_empty() {
739        args_str
740    } else if args_str.is_empty() {
741        extra_args.join(", ")
742    } else {
743        format!("{args_str}, {}", extra_args.join(", "))
744    };
745
746    // When a client_factory is set, dispatch via a client instance:
747    //   let client = try <FactoryType>(apiKey: "test-key", baseUrl: <mock_url>)
748    //   try await client.<method>(args)
749    // Otherwise fall back to free-function call (Kreuzberg / non-client-factory libraries).
750    let has_mock = fixture.mock_response.is_some();
751    let (call_setup, call_expr) = if let Some(_factory) = client_factory {
752        let env_key = format!("MOCK_SERVER_{}", fixture.id.to_ascii_uppercase().replace('-', "_"));
753        let mock_url = if fixture.has_host_root_route() {
754            format!(
755                "ProcessInfo.processInfo.environment[\"{env_key}\"] ?? (ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{}\")",
756                fixture.id
757            )
758        } else {
759            format!(
760                "ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{}\"",
761                fixture.id
762            )
763        };
764        let client_constructor = if has_mock {
765            format!("let _client = try DefaultClient(apiKey: \"test-key\", baseUrl: {mock_url})")
766        } else {
767            // Live API: check for api_key_var; if not present use mock URL anyway.
768            if let Some(env_var) = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref()) {
769                format!(
770                    "let _apiKey = ProcessInfo.processInfo.environment[\"{env_var}\"]\n        \
771                     let _baseUrl: String? = _apiKey != nil ? nil : {mock_url}\n        \
772                     let _client = try DefaultClient(apiKey: _apiKey ?? \"test-key\", baseUrl: _baseUrl)"
773                )
774            } else {
775                format!("let _client = try DefaultClient(apiKey: \"test-key\", baseUrl: {mock_url})")
776            }
777        };
778        let expr = if is_async {
779            format!("try await _client.{function_name}({args_str})")
780        } else {
781            format!("try _client.{function_name}({args_str})")
782        };
783        (Some(client_constructor), expr)
784    } else {
785        // Free-function call (no client_factory).
786        let expr = if is_async {
787            format!("try await {function_name}({args_str})")
788        } else {
789            format!("try {function_name}({args_str})")
790        };
791        (None, expr)
792    };
793    // For backwards compatibility: qualified_function_name unused when client_factory is set.
794    let _ = function_name;
795
796    if is_async {
797        let _ = writeln!(out, "    func test{method_name}() async throws {{");
798    } else {
799        let _ = writeln!(out, "    func test{method_name}() throws {{");
800    }
801    let _ = writeln!(out, "        // {description}");
802
803    if expects_error {
804        // For error fixtures, setup may itself throw (e.g. config validation
805        // happens at engine construction). Wrap the whole pipeline — setup
806        // and the call — in a single do/catch so any throw counts as success.
807        if is_async {
808            // XCTAssertThrowsError is a synchronous macro; for async-throwing
809            // functions use a do/catch with explicit XCTFail to enforce that
810            // the throw actually happens. `await XCTAssertThrowsError(...)` is
811            // not valid Swift — it evaluates `await` against a non-async expr.
812            let _ = writeln!(out, "        do {{");
813            for line in &setup_lines {
814                let _ = writeln!(out, "            {line}");
815            }
816            if let Some(setup) = &call_setup {
817                let _ = writeln!(out, "            {setup}");
818            }
819            let _ = writeln!(out, "            _ = {call_expr}");
820            let _ = writeln!(out, "            XCTFail(\"expected to throw\")");
821            let _ = writeln!(out, "        }} catch {{");
822            let _ = writeln!(out, "            // success");
823            let _ = writeln!(out, "        }}");
824        } else {
825            // Synchronous: emit setup outside (it's expected to succeed) and
826            // wrap only the throwing call in XCTAssertThrowsError. If setup
827            // itself throws, that propagates as the test's own failure — but
828            // sync tests use `throws` so the test method itself rethrows,
829            // which XCTest still records as caught. Keep this simple: use a
830            // do/catch so setup-time throws also count as expected failures.
831            let _ = writeln!(out, "        do {{");
832            for line in &setup_lines {
833                let _ = writeln!(out, "            {line}");
834            }
835            if let Some(setup) = &call_setup {
836                let _ = writeln!(out, "            {setup}");
837            }
838            let _ = writeln!(out, "            _ = {call_expr}");
839            let _ = writeln!(out, "            XCTFail(\"expected to throw\")");
840            let _ = writeln!(out, "        }} catch {{");
841            let _ = writeln!(out, "            // success");
842            let _ = writeln!(out, "        }}");
843        }
844        let _ = writeln!(out, "    }}");
845        return;
846    }
847
848    for line in &setup_lines {
849        let _ = writeln!(out, "        {line}");
850    }
851
852    // Emit client construction if a client_factory is configured.
853    if let Some(setup) = &call_setup {
854        let _ = writeln!(out, "        {setup}");
855    }
856
857    let _ = writeln!(out, "        let {result_var} = {call_expr}");
858
859    // Emit the collect snippet for streaming fixtures (drains the async sequence into
860    // a local `chunks: [ChatCompletionChunk]` array used by streaming-virtual assertions).
861    if !collect_snippet.is_empty() {
862        for line in collect_snippet.lines() {
863            let _ = writeln!(out, "        {line}");
864        }
865    }
866
867    for assertion in &fixture.assertions {
868        render_assertion(
869            out,
870            assertion,
871            result_var,
872            field_resolver,
873            result_is_simple,
874            result_is_array,
875            result_is_option,
876            &effective_enum_fields,
877            is_streaming,
878        );
879    }
880
881    let _ = writeln!(out, "    }}");
882}
883
884#[allow(clippy::too_many_arguments)]
885/// Build setup lines and the argument list for the function call.
886///
887/// Swift-bridge wrappers require strongly-typed values that don't have implicit
888/// Swift literal conversions:
889///
890/// - `bytes` args become `RustVec<UInt8>` — fixture supplies a relative file path
891///   string which is read at test time and pushed into a `RustVec<UInt8>` setup
892///   variable. A literal byte array is base64-decoded or UTF-8 encoded inline.
893/// - `json_object` args become opaque `ExtractionConfig` (or sibling) instances —
894///   a JSON string is decoded via `extractionConfigFromJson(...)` in a setup line.
895/// - Optional args missing from the fixture must still appear at the call site
896///   as `nil` whenever a later positional arg is present, otherwise Swift slots
897///   subsequent values into the wrong parameter.
898fn build_args_and_setup(
899    input: &serde_json::Value,
900    args: &[crate::config::ArgMapping],
901    fixture_id: &str,
902    has_host_root_route: bool,
903    function_name: &str,
904    options_via: Option<&str>,
905    options_type: Option<&str>,
906    handle_config_fn: Option<&str>,
907    visitor_handle_expr: Option<&str>,
908) -> (Vec<String>, String) {
909    if args.is_empty() {
910        return (Vec::new(), String::new());
911    }
912
913    let mut setup_lines: Vec<String> = Vec::new();
914    let mut parts: Vec<String> = Vec::new();
915
916    // Pre-compute, for each arg index, whether any later arg has a fixture-provided
917    // value (or is required and will emit a default). When an optional arg is empty
918    // but a later arg WILL emit, we must keep the slot with `nil` so positional
919    // alignment is preserved.
920    let later_emits: Vec<bool> = (0..args.len())
921        .map(|i| {
922            args.iter().skip(i + 1).any(|a| {
923                let f = a.field.strip_prefix("input.").unwrap_or(&a.field);
924                let v = input.get(f);
925                let has_value = matches!(v, Some(x) if !x.is_null());
926                has_value || !a.optional || (a.arg_type == "json_object" && a.name == "config")
927            })
928        })
929        .collect();
930
931    for (idx, arg) in args.iter().enumerate() {
932        if arg.arg_type == "mock_url" {
933            let env_key = format!("MOCK_SERVER_{}", fixture_id.to_ascii_uppercase().replace('-', "_"));
934            let url_expr = if has_host_root_route {
935                format!(
936                    "ProcessInfo.processInfo.environment[\"{env_key}\"] ?? (ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{fixture_id}\")"
937                )
938            } else {
939                format!("ProcessInfo.processInfo.environment[\"MOCK_SERVER_URL\"]! + \"/fixtures/{fixture_id}\"")
940            };
941            setup_lines.push(format!("let {} = {url_expr}", arg.name));
942            parts.push(arg.name.clone());
943            continue;
944        }
945
946        if arg.arg_type == "handle" {
947            let var_name = format!("{}Obj", arg.name.to_lower_camel_case());
948            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
949            let config_val = input.get(field);
950            let has_config = config_val
951                .is_some_and(|v| !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty())));
952            if has_config {
953                if let Some(from_json_fn) = handle_config_fn {
954                    let json_str = serde_json::to_string(config_val.unwrap()).unwrap_or_default();
955                    let escaped = escape_swift_str(&json_str);
956                    let config_var = format!("{}Config", arg.name.to_lower_camel_case());
957                    setup_lines.push(format!("let {config_var} = try {from_json_fn}(\"{escaped}\")"));
958                    setup_lines.push(format!("let {var_name} = try createEngine({config_var})"));
959                } else {
960                    setup_lines.push(format!("let {var_name} = try createEngine(nil)"));
961                }
962            } else {
963                setup_lines.push(format!("let {var_name} = try createEngine(nil)"));
964            }
965            parts.push(var_name);
966            continue;
967        }
968
969        // bytes args: fixture stores a fixture-relative path string. Generate
970        // setup that reads it into a Data and pushes each byte into a
971        // RustVec<UInt8>. Literal byte arrays inline the bytes; missing values
972        // produce an empty vec (or `nil` when optional).
973        if arg.arg_type == "bytes" {
974            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
975            let val = input.get(field);
976            match val {
977                None | Some(serde_json::Value::Null) if arg.optional => {
978                    if later_emits[idx] {
979                        parts.push("nil".to_string());
980                    }
981                }
982                None | Some(serde_json::Value::Null) => {
983                    let var_name = format!("{}Vec", arg.name.to_lower_camel_case());
984                    setup_lines.push(format!("let {var_name} = RustVec<UInt8>()"));
985                    parts.push(var_name);
986                }
987                Some(serde_json::Value::String(s)) => {
988                    let escaped = escape_swift(s);
989                    let var_name = format!("{}Vec", arg.name.to_lower_camel_case());
990                    let data_var = format!("{}Data", arg.name.to_lower_camel_case());
991                    setup_lines.push(format!(
992                        "let {data_var} = try Data(contentsOf: URL(fileURLWithPath: \"{escaped}\"))"
993                    ));
994                    setup_lines.push(format!("let {var_name} = RustVec<UInt8>()"));
995                    setup_lines.push(format!("for _byte in {data_var} {{ {var_name}.push(value: _byte) }}"));
996                    parts.push(var_name);
997                }
998                Some(serde_json::Value::Array(arr)) => {
999                    let var_name = format!("{}Vec", arg.name.to_lower_camel_case());
1000                    setup_lines.push(format!("let {var_name} = RustVec<UInt8>()"));
1001                    for v in arr {
1002                        if let Some(n) = v.as_u64() {
1003                            setup_lines.push(format!("{var_name}.push(value: UInt8({n}))"));
1004                        }
1005                    }
1006                    parts.push(var_name);
1007                }
1008                Some(other) => {
1009                    // Fallback: encode the JSON serialisation as UTF-8 bytes.
1010                    let json_str = serde_json::to_string(other).unwrap_or_default();
1011                    let escaped = escape_swift(&json_str);
1012                    let var_name = format!("{}Vec", arg.name.to_lower_camel_case());
1013                    setup_lines.push(format!("let {var_name} = RustVec<UInt8>()"));
1014                    setup_lines.push(format!(
1015                        "for _byte in Array(\"{escaped}\".utf8) {{ {var_name}.push(value: _byte) }}"
1016                    ));
1017                    parts.push(var_name);
1018                }
1019            }
1020            continue;
1021        }
1022
1023        // json_object "config" args: the swift-bridge wrapper requires an opaque
1024        // config instance (e.g., `ExtractionConfig`, `ProcessConfig`), not a JSON string.
1025        // Derive the from-json helper name from options_type if available, else default
1026        // to kreuzberg's `extractionConfigFromJson` for backward compatibility.
1027        // Batch functions (batchExtract*) hardcode config internally — skip it.
1028        let is_config_arg = arg.name == "config" && arg.arg_type == "json_object";
1029        let is_batch_fn = function_name.starts_with("batch") || function_name.starts_with("Batch");
1030        if is_config_arg && !is_batch_fn {
1031            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1032            let val = input.get(field);
1033            let json_str = match val {
1034                None | Some(serde_json::Value::Null) => "{}".to_string(),
1035                Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
1036            };
1037            let escaped = escape_swift(&json_str);
1038            let var_name = format!("{}Obj", arg.name.to_lower_camel_case());
1039            // Derive the from-json helper name from options_type, or default to extractionConfigFromJson
1040            let from_json_fn = if let Some(type_name) = options_type {
1041                format!("{}FromJson", type_name.to_lower_camel_case())
1042            } else {
1043                "extractionConfigFromJson".to_string()
1044            };
1045            setup_lines.push(format!("let {var_name} = try {from_json_fn}(\"{escaped}\")"));
1046            parts.push(var_name);
1047            continue;
1048        }
1049
1050        // json_object non-config args with options_via = "from_json":
1051        // Use the generated `{typeCamelCase}FromJson(_:)` helper so the fixture JSON is
1052        // deserialised into the opaque swift-bridge type rather than passed as a raw string.
1053        // When arg.field == "input", the entire fixture input IS the request object.
1054        // When a visitor handle is present, use `{typeCamelCase}FromJsonWithVisitor(json, handle)`
1055        // instead to attach the visitor to the options in one step.
1056        if arg.arg_type == "json_object" && options_via == Some("from_json") {
1057            if let Some(type_name) = options_type {
1058                let resolved_val = super::resolve_field(input, &arg.field);
1059                let json_str = match resolved_val {
1060                    serde_json::Value::Null => "{}".to_string(),
1061                    v => serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string()),
1062                };
1063                let escaped = escape_swift(&json_str);
1064                let var_name = format!("_{}", arg.name.to_lower_camel_case());
1065                if let Some(handle_expr) = visitor_handle_expr {
1066                    // Use the visitor-aware helper: `{typeCamelCase}FromJsonWithVisitor(json, handle)`.
1067                    // The handle expression builds a VisitorHandle from the local class instance.
1068                    // The function name mirrors emit_options_field_options_helper: camelCase of
1069                    // `{options_snake}_from_json_with_visitor`.
1070                    let with_visitor_fn = format!("{}FromJsonWithVisitor", type_name.to_lower_camel_case());
1071                    let handle_var = format!("_visitorHandle_{}", var_name.trim_start_matches('_'));
1072                    setup_lines.push(format!("let {handle_var} = {handle_expr}"));
1073                    setup_lines.push(format!(
1074                        "let {var_name} = try {with_visitor_fn}(\"{escaped}\", {handle_var})"
1075                    ));
1076                } else {
1077                    let from_json_fn = format!("{}FromJson", type_name.to_lower_camel_case());
1078                    setup_lines.push(format!("let {var_name} = try {from_json_fn}(\"{escaped}\")"));
1079                }
1080                parts.push(var_name);
1081                continue;
1082            }
1083        }
1084
1085        let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1086        let val = input.get(field);
1087        match val {
1088            None | Some(serde_json::Value::Null) if arg.optional => {
1089                // Optional arg with no fixture value: keep the slot with `nil`
1090                // when a later arg will emit, so positional alignment matches
1091                // the swift-bridge wrapper signature.
1092                if later_emits[idx] {
1093                    parts.push("nil".to_string());
1094                }
1095            }
1096            None | Some(serde_json::Value::Null) => {
1097                let default_val = match arg.arg_type.as_str() {
1098                    "string" => "\"\"".to_string(),
1099                    "int" | "integer" => "0".to_string(),
1100                    "float" | "number" => "0.0".to_string(),
1101                    "bool" | "boolean" => "false".to_string(),
1102                    _ => "nil".to_string(),
1103                };
1104                parts.push(default_val);
1105            }
1106            Some(v) => {
1107                parts.push(json_to_swift(v));
1108            }
1109        }
1110    }
1111
1112    (setup_lines, parts.join(", "))
1113}
1114
1115#[allow(clippy::too_many_arguments)]
1116fn render_assertion(
1117    out: &mut String,
1118    assertion: &Assertion,
1119    result_var: &str,
1120    field_resolver: &FieldResolver,
1121    result_is_simple: bool,
1122    result_is_array: bool,
1123    result_is_option: bool,
1124    enum_fields: &HashSet<String>,
1125    is_streaming: bool,
1126) {
1127    // When the bare result is `Optional<T>` (no field path) the opaque class
1128    // exposed by swift-bridge has no `.toString()` method, so the usual
1129    // `.toString().isEmpty` pattern produces compile errors. Detect the
1130    // "bare result" case and prefer `XCTAssertNil` / `XCTAssertNotNil`.
1131    let bare_result_is_option = result_is_option && assertion.field.as_deref().filter(|f| !f.is_empty()).is_none();
1132    // Streaming virtual fields resolve against the `chunks` collected-array variable.
1133    // Intercept before is_valid_for_result so they are never skipped.
1134    // Also intercept `usage.*` deep-paths in streaming tests: `AsyncThrowingStream` does
1135    // not have a `usage()` method, so we must route them through the chunks accessor.
1136    if let Some(f) = &assertion.field {
1137        let is_streaming_usage_path =
1138            is_streaming && (f == "usage" || (f.starts_with("usage.") || f.starts_with("usage[")));
1139        if !f.is_empty()
1140            && (crate::codegen::streaming_assertions::is_streaming_virtual_field(f) || is_streaming_usage_path)
1141        {
1142            if let Some(expr) =
1143                crate::codegen::streaming_assertions::StreamingFieldResolver::accessor(f, "swift", "chunks")
1144            {
1145                let line = match assertion.assertion_type.as_str() {
1146                    "count_min" => {
1147                        if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1148                            format!("        XCTAssertGreaterThanOrEqual(chunks.count, {n})\n")
1149                        } else {
1150                            String::new()
1151                        }
1152                    }
1153                    "count_equals" => {
1154                        if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1155                            format!("        XCTAssertEqual(chunks.count, {n})\n")
1156                        } else {
1157                            String::new()
1158                        }
1159                    }
1160                    "equals" => {
1161                        if let Some(serde_json::Value::String(s)) = &assertion.value {
1162                            let escaped = escape_swift(s);
1163                            format!("        XCTAssertEqual({expr}, \"{escaped}\")\n")
1164                        } else if let Some(b) = assertion.value.as_ref().and_then(|v| v.as_bool()) {
1165                            format!("        XCTAssertEqual({expr}, {b})\n")
1166                        } else {
1167                            String::new()
1168                        }
1169                    }
1170                    "not_empty" => {
1171                        format!("        XCTAssertFalse({expr}.isEmpty, \"expected non-empty\")\n")
1172                    }
1173                    "is_empty" => {
1174                        format!("        XCTAssertTrue({expr}.isEmpty, \"expected empty\")\n")
1175                    }
1176                    "is_true" => {
1177                        format!("        XCTAssertTrue({expr})\n")
1178                    }
1179                    "is_false" => {
1180                        format!("        XCTAssertFalse({expr})\n")
1181                    }
1182                    "greater_than" => {
1183                        if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
1184                            format!("        XCTAssertGreaterThan(chunks.count, {n})\n")
1185                        } else {
1186                            String::new()
1187                        }
1188                    }
1189                    "contains" => {
1190                        if let Some(serde_json::Value::String(s)) = &assertion.value {
1191                            let escaped = escape_swift(s);
1192                            format!(
1193                                "        XCTAssertTrue({expr}.contains(\"{escaped}\"), \"expected to contain: {escaped}\")\n"
1194                            )
1195                        } else {
1196                            String::new()
1197                        }
1198                    }
1199                    _ => format!(
1200                        "        // streaming field '{f}': assertion type '{}' not rendered\n",
1201                        assertion.assertion_type
1202                    ),
1203                };
1204                if !line.is_empty() {
1205                    out.push_str(&line);
1206                }
1207            }
1208            return;
1209        }
1210    }
1211
1212    // Skip assertions on fields that don't exist on the result type.
1213    if let Some(f) = &assertion.field {
1214        if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1215            let _ = writeln!(out, "        // skipped: field '{f}' not available on result type");
1216            return;
1217        }
1218    }
1219
1220    // Skip assertions that traverse a tagged-union variant boundary.
1221    // In Swift, FormatMetadata and similar enum-backed opaque types are exposed as
1222    // plain classes by swift-bridge — variant accessor methods (e.g., `.excel()`)
1223    // are not generated, so such assertions cannot be expressed.
1224    if let Some(f) = &assertion.field {
1225        if !f.is_empty() && field_resolver.tagged_union_split(f).is_some() {
1226            let _ = writeln!(
1227                out,
1228                "        // skipped: field '{f}' crosses a tagged-union variant boundary (not expressible in Swift)"
1229            );
1230            return;
1231        }
1232    }
1233
1234    // Determine if this field is an enum type.
1235    let field_is_enum = assertion
1236        .field
1237        .as_deref()
1238        .is_some_and(|f| enum_fields.contains(f) || enum_fields.contains(field_resolver.resolve(f)));
1239
1240    let field_is_optional = assertion.field.as_deref().is_some_and(|f| {
1241        !f.is_empty() && (field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f)))
1242    });
1243    let field_is_array = assertion.field.as_deref().is_some_and(|f| {
1244        !f.is_empty()
1245            && (field_resolver.is_array(f)
1246                || field_resolver.is_array(field_resolver.resolve(f))
1247                || field_resolver.is_collection_root(f)
1248                || field_resolver.is_collection_root(field_resolver.resolve(f)))
1249    });
1250
1251    let field_expr_raw = if result_is_simple {
1252        result_var.to_string()
1253    } else {
1254        match &assertion.field {
1255            Some(f) if !f.is_empty() => field_resolver.accessor(f, "swift", result_var),
1256            _ => result_var.to_string(),
1257        }
1258    };
1259
1260    // swift-bridge `RustVec<T>` exposes its elements as `T.SelfRef`, which holds
1261    // a raw pointer into the parent Vec's storage. When the Vec is a temporary
1262    // (e.g. `result.json_ld()` called inline), Swift ARC may release it before
1263    // the ref is used, leaving the ref's pointer dangling. Materialise the
1264    // temporary into a local so it survives the full expression chain.
1265    //
1266    // The local name is suffixed with the assertion type plus a hash of the
1267    // assertion's discriminating fields so multiple assertions on the same
1268    // collection don't redeclare the same name.
1269    let local_suffix = {
1270        use std::hash::{Hash, Hasher};
1271        let mut hasher = std::collections::hash_map::DefaultHasher::new();
1272        assertion.field.hash(&mut hasher);
1273        assertion
1274            .value
1275            .as_ref()
1276            .map(|v| v.to_string())
1277            .unwrap_or_default()
1278            .hash(&mut hasher);
1279        format!(
1280            "{}_{:x}",
1281            assertion.assertion_type.replace(['-', '.'], "_"),
1282            hasher.finish() & 0xffff_ffff,
1283        )
1284    };
1285    let (vec_setup, field_expr, is_map_subscript) = materialise_vec_temporaries(&field_expr_raw, &local_suffix);
1286    // The `contains` / `not_contains` traversal branch builds its own
1287    // accessor from `field_resolver.accessor(array_part, ...)`, ignoring
1288    // `field_expr`. Emitting the vec_setup there would produce dead
1289    // `let _vec_… = …` lines, so skip it for those traversal cases.
1290    let field_uses_traversal = assertion.field.as_deref().is_some_and(|f| f.contains("[]."));
1291    let traversal_skips_field_expr = field_uses_traversal
1292        && matches!(
1293            assertion.assertion_type.as_str(),
1294            "contains" | "not_contains" | "not_empty" | "is_empty"
1295        );
1296    if !traversal_skips_field_expr {
1297        for line in &vec_setup {
1298            let _ = writeln!(out, "        {line}");
1299        }
1300    }
1301
1302    // In Swift, optional chaining with `?.` makes the result optional even if the
1303    // called method's return type isn't marked optional. For example:
1304    // `result.markdown()?.content()` returns `Optional<RustString>` because
1305    // `markdown()` is optional and the `?.` operator wraps the result.
1306    // Detect this by checking if the accessor contains `?.`.
1307    let accessor_is_optional = field_expr.contains("?.");
1308
1309    // For enum fields, need to handle the string representation differently in Swift.
1310    // Swift enums don't have `.rawValue` unless they're explicitly RawRepresentable.
1311    // Check if this is an enum type and handle accordingly.
1312    // For optional fields (Optional<RustString>), use optional chaining before toString().
1313    // For other fields: swift-bridge returns all Rust `String` fields as `RustString`.
1314    // We add .toString() here so string assertions (contains, hasPrefix, etc.) work.
1315    // Non-string opaque fields (DocumentStructure, etc.) should not appear in string
1316    // assertions — the fixture schema controls which assertions apply to which fields.
1317    let string_expr = if is_map_subscript {
1318        // The field_expr already evaluates to `String?` (from a JSON-decoded
1319        // `[String: String]` subscript). No `.toString()` chain needed —
1320        // coalesce the optional to "" and use the Swift String directly.
1321        format!("({field_expr} ?? \"\")")
1322    } else if field_is_enum && (field_is_optional || accessor_is_optional) {
1323        // Enum-typed fields that are also optional (e.g. `finish_reason() -> Optional<RustString>`)
1324        // must use optional chaining: `?.toString() ?? ""` to unwrap before converting to Swift String.
1325        format!("({field_expr}?.toString() ?? \"\")")
1326    } else if field_is_enum {
1327        // Enum-typed fields are now bridged as `String` (RustString in Swift) rather than
1328        // as opaque enum handles. The getter on the Rust side calls `to_string()` internally
1329        // and returns a `String` across the FFI. In Swift this arrives as `RustString`, so
1330        // `.toString()` converts it to a Swift `String` — one call, not two.
1331        format!("{field_expr}.toString()")
1332    } else if field_is_optional {
1333        // Leaf field itself is Optional<RustString> — need ?.toString() to unwrap.
1334        format!("({field_expr}?.toString() ?? \"\")")
1335    } else if accessor_is_optional {
1336        // Ancestor optional chain propagates; leaf is non-optional RustString within chain.
1337        // Use .toString() directly — the whole expr is Optional<String> due to propagation.
1338        format!("({field_expr}.toString() ?? \"\")")
1339    } else {
1340        format!("{field_expr}.toString()")
1341    };
1342
1343    match assertion.assertion_type.as_str() {
1344        "equals" => {
1345            if let Some(expected) = &assertion.value {
1346                let swift_val = json_to_swift(expected);
1347                if expected.is_string() {
1348                    if field_is_enum {
1349                        // Enum fields: `to_string()` (snake_case) returns RustString;
1350                        // `.toString()` converts it to a Swift String.
1351                        // `string_expr` already incorporates this call chain.
1352                        let trim_expr =
1353                            format!("{string_expr}.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)");
1354                        let _ = writeln!(out, "        XCTAssertEqual({trim_expr}, {swift_val})");
1355                    } else {
1356                        // For optional strings (String?), use ?? to coalesce before trimming.
1357                        // `.toString()` converts RustString → Swift String before calling
1358                        // `.trimmingCharacters`, which requires a concrete String type.
1359                        // string_expr already incorporates field_is_optional via ?.toString() ?? "".
1360                        let trim_expr =
1361                            format!("{string_expr}.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)");
1362                        let _ = writeln!(out, "        XCTAssertEqual({trim_expr}, {swift_val})");
1363                    }
1364                } else {
1365                    let _ = writeln!(out, "        XCTAssertEqual({field_expr}, {swift_val})");
1366                }
1367            }
1368        }
1369        "contains" => {
1370            if let Some(expected) = &assertion.value {
1371                let swift_val = json_to_swift(expected);
1372                // When the root result IS the array (result_is_simple + result_is_array) and
1373                // there is no field path, check array membership via map+contains.
1374                let no_field = assertion.field.as_deref().is_none_or(|f| f.is_empty());
1375                if result_is_simple && result_is_array && no_field {
1376                    // RustVec<RustString> iteration yields RustStringRef (no `toString()`);
1377                    // use `.as_str().toString()` to convert each element to a Swift String.
1378                    let _ = writeln!(
1379                        out,
1380                        "        XCTAssertTrue({result_var}.map {{ $0.as_str().toString() }}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1381                    );
1382                } else {
1383                    // []. traversal: field like "links[].url" → contains(where:) closure.
1384                    let traversal_handled = if let Some(f) = assertion.field.as_deref() {
1385                        if let Some(dot) = f.find("[].") {
1386                            let array_part = &f[..dot];
1387                            let elem_part = &f[dot + 3..];
1388                            let line = swift_traversal_contains_assert(
1389                                array_part,
1390                                elem_part,
1391                                f,
1392                                &swift_val,
1393                                result_var,
1394                                false,
1395                                &format!("expected to contain: \\({swift_val})"),
1396                                enum_fields,
1397                                field_resolver,
1398                            );
1399                            let _ = writeln!(out, "{line}");
1400                            true
1401                        } else {
1402                            false
1403                        }
1404                    } else {
1405                        false
1406                    };
1407                    if !traversal_handled {
1408                        // For array fields (RustVec<RustString>), check membership via map+contains.
1409                        let field_is_array = assertion
1410                            .field
1411                            .as_deref()
1412                            .is_some_and(|f| field_resolver.is_array(field_resolver.resolve(f)));
1413                        if field_is_array {
1414                            let contains_expr =
1415                                swift_array_contains_expr(assertion.field.as_deref(), result_var, field_resolver);
1416                            let _ = writeln!(
1417                                out,
1418                                "        XCTAssertTrue(({contains_expr} ?? []).contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1419                            );
1420                        } else if field_is_enum {
1421                            // Enum fields: use `toString().toString()` (via string_expr) to get the
1422                            // serde variant name as a Swift String, then check substring containment.
1423                            let _ = writeln!(
1424                                out,
1425                                "        XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1426                            );
1427                        } else {
1428                            let _ = writeln!(
1429                                out,
1430                                "        XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1431                            );
1432                        }
1433                    }
1434                }
1435            }
1436        }
1437        "contains_all" => {
1438            if let Some(values) = &assertion.values {
1439                // []. traversal: field like "links[].link_type" → contains(where:) per value.
1440                if let Some(f) = assertion.field.as_deref() {
1441                    if let Some(dot) = f.find("[].") {
1442                        let array_part = &f[..dot];
1443                        let elem_part = &f[dot + 3..];
1444                        for val in values {
1445                            let swift_val = json_to_swift(val);
1446                            let line = swift_traversal_contains_assert(
1447                                array_part,
1448                                elem_part,
1449                                f,
1450                                &swift_val,
1451                                result_var,
1452                                false,
1453                                &format!("expected to contain: \\({swift_val})"),
1454                                enum_fields,
1455                                field_resolver,
1456                            );
1457                            let _ = writeln!(out, "{line}");
1458                        }
1459                        // handled — skip remaining branches
1460                    } else {
1461                        // For array fields (RustVec<RustString>), check membership via map+contains.
1462                        let field_is_array = field_resolver.is_array(field_resolver.resolve(f));
1463                        if field_is_array {
1464                            let contains_expr =
1465                                swift_array_contains_expr(assertion.field.as_deref(), result_var, field_resolver);
1466                            for val in values {
1467                                let swift_val = json_to_swift(val);
1468                                let _ = writeln!(
1469                                    out,
1470                                    "        XCTAssertTrue(({contains_expr} ?? []).contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1471                                );
1472                            }
1473                        } else if field_is_enum {
1474                            // Enum fields: use `toString().toString()` (via string_expr) to get the
1475                            // serde variant name as a Swift String, then check substring containment.
1476                            for val in values {
1477                                let swift_val = json_to_swift(val);
1478                                let _ = writeln!(
1479                                    out,
1480                                    "        XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1481                                );
1482                            }
1483                        } else {
1484                            for val in values {
1485                                let swift_val = json_to_swift(val);
1486                                let _ = writeln!(
1487                                    out,
1488                                    "        XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1489                                );
1490                            }
1491                        }
1492                    }
1493                } else {
1494                    // No field — fall back to existing string_expr path.
1495                    for val in values {
1496                        let swift_val = json_to_swift(val);
1497                        let _ = writeln!(
1498                            out,
1499                            "        XCTAssertTrue({string_expr}.contains({swift_val}), \"expected to contain: \\({swift_val})\")"
1500                        );
1501                    }
1502                }
1503            }
1504        }
1505        "not_contains" => {
1506            if let Some(expected) = &assertion.value {
1507                let swift_val = json_to_swift(expected);
1508                // []. traversal: "links[].url" → XCTAssertFalse(array.contains(where:))
1509                let traversal_handled = if let Some(f) = assertion.field.as_deref() {
1510                    if let Some(dot) = f.find("[].") {
1511                        let array_part = &f[..dot];
1512                        let elem_part = &f[dot + 3..];
1513                        let line = swift_traversal_contains_assert(
1514                            array_part,
1515                            elem_part,
1516                            f,
1517                            &swift_val,
1518                            result_var,
1519                            true,
1520                            &format!("expected NOT to contain: \\({swift_val})"),
1521                            enum_fields,
1522                            field_resolver,
1523                        );
1524                        let _ = writeln!(out, "{line}");
1525                        true
1526                    } else {
1527                        false
1528                    }
1529                } else {
1530                    false
1531                };
1532                if !traversal_handled {
1533                    let _ = writeln!(
1534                        out,
1535                        "        XCTAssertFalse({string_expr}.contains({swift_val}), \"expected NOT to contain: \\({swift_val})\")"
1536                    );
1537                }
1538            }
1539        }
1540        "not_empty" => {
1541            // For optional fields (Optional<T>), check that the value is non-nil.
1542            // For array fields (RustVec<T>), check .isEmpty on the vec directly.
1543            // For result_is_simple (e.g. Data, String), use .isEmpty directly on
1544            // the result — avoids calling .toString() on non-RustString types.
1545            // For string fields, convert to Swift String and check .isEmpty.
1546            // []. traversal: "links[].url" → contains(where: { !elem.isEmpty })
1547            let traversal_not_empty_handled = if let Some(f) = assertion.field.as_deref() {
1548                if let Some(dot) = f.find("[].") {
1549                    let array_part = &f[..dot];
1550                    let elem_part = &f[dot + 3..];
1551                    let array_accessor = field_resolver.accessor(array_part, "swift", result_var);
1552                    let resolved_full = field_resolver.resolve(f);
1553                    let resolved_elem_part = resolved_full
1554                        .find("[].")
1555                        .map(|d| &resolved_full[d + 3..])
1556                        .unwrap_or(elem_part);
1557                    let elem_accessor = field_resolver.accessor(resolved_elem_part, "swift", "$0");
1558                    let elem_is_enum = enum_fields.contains(f) || enum_fields.contains(resolved_full);
1559                    let elem_is_optional = field_resolver.is_optional(resolved_elem_part)
1560                        || field_resolver.is_optional(field_resolver.resolve(resolved_elem_part));
1561                    let elem_str = if elem_is_enum {
1562                        format!("{elem_accessor}.to_string().toString()")
1563                    } else if elem_is_optional {
1564                        format!("({elem_accessor}?.toString() ?? \"\")")
1565                    } else {
1566                        format!("{elem_accessor}.toString()")
1567                    };
1568                    let _ = writeln!(
1569                        out,
1570                        "        XCTAssertTrue({array_accessor}.contains(where: {{ !{elem_str}.isEmpty }}), \"expected non-empty value\")"
1571                    );
1572                    true
1573                } else {
1574                    false
1575                }
1576            } else {
1577                false
1578            };
1579            if !traversal_not_empty_handled {
1580                if bare_result_is_option {
1581                    let _ = writeln!(out, "        XCTAssertNotNil({result_var}, \"expected non-nil value\")");
1582                } else if field_is_optional {
1583                    let _ = writeln!(out, "        XCTAssertNotNil({field_expr}, \"expected non-nil value\")");
1584                } else if field_is_array {
1585                    let _ = writeln!(
1586                        out,
1587                        "        XCTAssertFalse({field_expr}.isEmpty, \"expected non-empty value\")"
1588                    );
1589                } else if result_is_simple {
1590                    // result_is_simple: result is a primitive (Data, String, etc.) — use .isEmpty directly.
1591                    let _ = writeln!(
1592                        out,
1593                        "        XCTAssertFalse({result_var}.isEmpty, \"expected non-empty value\")"
1594                    );
1595                } else {
1596                    // Both `RustString` (via RustStringRef.len() -> UInt) and `RustVec<T>` (via
1597                    // len() -> Int) expose a `.len()` method. Using `.len() > 0` avoids the
1598                    // `.toString().isEmpty` path that fails to compile when the field returns
1599                    // `RustVec<T>` — `RustVec<T>` has no `.toString()` member.
1600                    //
1601                    // When the accessor contains a `?.` optional chain, `.len()` returns an
1602                    // Optional (e.g. `UInt?`) which Swift cannot compare directly to `0`;
1603                    // coalesce via `?? 0` so the assertion typechecks.
1604                    let len_expr = if accessor_is_optional {
1605                        format!("({field_expr}.len() ?? 0)")
1606                    } else {
1607                        format!("{field_expr}.len()")
1608                    };
1609                    let _ = writeln!(
1610                        out,
1611                        "        XCTAssertGreaterThan({len_expr}, 0, \"expected non-empty value\")"
1612                    );
1613                }
1614            }
1615        }
1616        "is_empty" => {
1617            if bare_result_is_option {
1618                let _ = writeln!(out, "        XCTAssertNil({result_var}, \"expected nil value\")");
1619            } else if field_is_optional {
1620                let _ = writeln!(out, "        XCTAssertNil({field_expr}, \"expected nil value\")");
1621            } else if field_is_array {
1622                let _ = writeln!(
1623                    out,
1624                    "        XCTAssertTrue({field_expr}.isEmpty, \"expected empty value\")"
1625                );
1626            } else {
1627                // Symmetric with not_empty: use .len() == 0 to avoid .toString() on
1628                // RustVec<T> fields that have no .toString() method. When the accessor
1629                // contains a `?.` optional chain, coalesce so the comparison typechecks.
1630                let len_expr = if accessor_is_optional {
1631                    format!("({field_expr}.len() ?? 0)")
1632                } else {
1633                    format!("{field_expr}.len()")
1634                };
1635                let _ = writeln!(out, "        XCTAssertEqual({len_expr}, 0, \"expected empty value\")");
1636            }
1637        }
1638        "contains_any" => {
1639            if let Some(values) = &assertion.values {
1640                let checks: Vec<String> = values
1641                    .iter()
1642                    .map(|v| {
1643                        let swift_val = json_to_swift(v);
1644                        format!("{string_expr}.contains({swift_val})")
1645                    })
1646                    .collect();
1647                let joined = checks.join(" || ");
1648                let _ = writeln!(
1649                    out,
1650                    "        XCTAssertTrue({joined}, \"expected to contain at least one of the specified values\")"
1651                );
1652            }
1653        }
1654        "greater_than" => {
1655            if let Some(val) = &assertion.value {
1656                let swift_val = json_to_swift(val);
1657                // For optional numeric fields (or when the accessor chain is optional),
1658                // coalesce to 0 before comparing so the expression is non-optional.
1659                let field_is_optional = accessor_is_optional
1660                    || assertion.field.as_deref().is_some_and(|f| {
1661                        field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f))
1662                    });
1663                let compare_expr = if field_is_optional {
1664                    format!("({field_expr} ?? 0)")
1665                } else {
1666                    field_expr.clone()
1667                };
1668                let _ = writeln!(out, "        XCTAssertGreaterThan({compare_expr}, {swift_val})");
1669            }
1670        }
1671        "less_than" => {
1672            if let Some(val) = &assertion.value {
1673                let swift_val = json_to_swift(val);
1674                let field_is_optional = accessor_is_optional
1675                    || assertion.field.as_deref().is_some_and(|f| {
1676                        field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f))
1677                    });
1678                let compare_expr = if field_is_optional {
1679                    format!("({field_expr} ?? 0)")
1680                } else {
1681                    field_expr.clone()
1682                };
1683                let _ = writeln!(out, "        XCTAssertLessThan({compare_expr}, {swift_val})");
1684            }
1685        }
1686        "greater_than_or_equal" => {
1687            if let Some(val) = &assertion.value {
1688                let swift_val = json_to_swift(val);
1689                // For optional numeric fields (or when the accessor chain is optional),
1690                // coalesce to 0 before comparing so the expression is non-optional.
1691                let field_is_optional = accessor_is_optional
1692                    || assertion.field.as_deref().is_some_and(|f| {
1693                        field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f))
1694                    });
1695                let compare_expr = if field_is_optional {
1696                    format!("({field_expr} ?? 0)")
1697                } else {
1698                    field_expr.clone()
1699                };
1700                let _ = writeln!(out, "        XCTAssertGreaterThanOrEqual({compare_expr}, {swift_val})");
1701            }
1702        }
1703        "less_than_or_equal" => {
1704            if let Some(val) = &assertion.value {
1705                let swift_val = json_to_swift(val);
1706                let field_is_optional = accessor_is_optional
1707                    || assertion.field.as_deref().is_some_and(|f| {
1708                        field_resolver.is_optional(f) || field_resolver.is_optional(field_resolver.resolve(f))
1709                    });
1710                let compare_expr = if field_is_optional {
1711                    format!("({field_expr} ?? 0)")
1712                } else {
1713                    field_expr.clone()
1714                };
1715                let _ = writeln!(out, "        XCTAssertLessThanOrEqual({compare_expr}, {swift_val})");
1716            }
1717        }
1718        "starts_with" => {
1719            if let Some(expected) = &assertion.value {
1720                let swift_val = json_to_swift(expected);
1721                let _ = writeln!(
1722                    out,
1723                    "        XCTAssertTrue({string_expr}.hasPrefix({swift_val}), \"expected to start with: \\({swift_val})\")"
1724                );
1725            }
1726        }
1727        "ends_with" => {
1728            if let Some(expected) = &assertion.value {
1729                let swift_val = json_to_swift(expected);
1730                let _ = writeln!(
1731                    out,
1732                    "        XCTAssertTrue({string_expr}.hasSuffix({swift_val}), \"expected to end with: \\({swift_val})\")"
1733                );
1734            }
1735        }
1736        "min_length" => {
1737            if let Some(val) = &assertion.value {
1738                if let Some(n) = val.as_u64() {
1739                    // Use string_expr.count: for RustString fields string_expr already has
1740                    // .toString() appended, giving a Swift String whose .count is character count.
1741                    let _ = writeln!(out, "        XCTAssertGreaterThanOrEqual({string_expr}.count, {n})");
1742                }
1743            }
1744        }
1745        "max_length" => {
1746            if let Some(val) = &assertion.value {
1747                if let Some(n) = val.as_u64() {
1748                    let _ = writeln!(out, "        XCTAssertLessThanOrEqual({string_expr}.count, {n})");
1749                }
1750            }
1751        }
1752        "count_min" => {
1753            if let Some(val) = &assertion.value {
1754                if let Some(n) = val.as_u64() {
1755                    // For fields nested inside an optional parent (e.g. document.nodes where
1756                    // document is Optional), the accessor generates `result.document().nodes()`
1757                    // which doesn't compile in Swift without optional chaining.
1758                    let count_expr = swift_array_count_expr(assertion.field.as_deref(), result_var, field_resolver);
1759                    let _ = writeln!(out, "        XCTAssertGreaterThanOrEqual({count_expr}, {n})");
1760                }
1761            }
1762        }
1763        "count_equals" => {
1764            if let Some(val) = &assertion.value {
1765                if let Some(n) = val.as_u64() {
1766                    let count_expr = swift_array_count_expr(assertion.field.as_deref(), result_var, field_resolver);
1767                    let _ = writeln!(out, "        XCTAssertEqual({count_expr}, {n})");
1768                }
1769            }
1770        }
1771        "is_true" => {
1772            let _ = writeln!(out, "        XCTAssertTrue({field_expr})");
1773        }
1774        "is_false" => {
1775            let _ = writeln!(out, "        XCTAssertFalse({field_expr})");
1776        }
1777        "matches_regex" => {
1778            if let Some(expected) = &assertion.value {
1779                let swift_val = json_to_swift(expected);
1780                let _ = writeln!(
1781                    out,
1782                    "        XCTAssertNotNil({string_expr}.range(of: {swift_val}, options: .regularExpression), \"expected value to match regex: \\({swift_val})\")"
1783                );
1784            }
1785        }
1786        "not_error" => {
1787            // Already handled by the call succeeding without exception.
1788        }
1789        "error" => {
1790            // Handled at the test method level.
1791        }
1792        "method_result" => {
1793            let _ = writeln!(out, "        // method_result assertions not yet implemented for Swift");
1794        }
1795        other => {
1796            panic!("Swift e2e generator: unsupported assertion type: {other}");
1797        }
1798    }
1799}
1800
1801/// Build a Swift accessor path for the given fixture field, inserting `()` on
1802/// every segment and `?` after every optional non-leaf segment.
1803///
1804/// This is the core helper for count/contains helpers that need to reconstruct
1805/// the path with correct optional chaining from the raw fixture field name.
1806///
1807/// Rewrite a Swift accessor expression to capture any `RustVec` temporaries
1808/// in a local before subscripting them. Returns `(setup_lines, rewritten_expr)`.
1809///
1810/// swift-bridge's `Vec_<T>$get` returns a raw pointer into the Vec's storage
1811/// wrapped in a `T.SelfRef`. If the Vec was a temporary, ARC may release it
1812/// before the ref is dereferenced, leaving the pointer dangling and reads
1813/// returning empty/garbage. Hoisting the Vec into a `let` binding ties the
1814/// Vec's lifetime to the enclosing function scope, so the ref stays valid.
1815///
1816/// Only the first `()[...]` occurrence per expression is materialised — that
1817/// covers all current fixture access patterns (single-level subscripts on a
1818/// result field). Nested subscripts are rare and would need a more elaborate
1819/// pass; if they appear, this returns conservative output (just the first
1820/// hoist) which is still correct.
1821/// Returns `(setup_lines, rewritten_expr, is_map_subscript)`. `is_map_subscript` is
1822/// true when the subscript key was a string literal, indicating the parent
1823/// accessor returns a JSON-encoded Map (RustString) and the rewritten expression
1824/// already evaluates to `String?` so callers should NOT append `.toString()`.
1825fn materialise_vec_temporaries(expr: &str, name_suffix: &str) -> (Vec<String>, String, bool) {
1826    let Some(idx) = expr.find("()[") else {
1827        return (Vec::new(), expr.to_string(), false);
1828    };
1829    let after_open = idx + 3; // position after `()[`
1830    let Some(close_rel) = expr[after_open..].find(']') else {
1831        return (Vec::new(), expr.to_string(), false);
1832    };
1833    let subscript_end = after_open + close_rel; // index of `]`
1834    let prefix = &expr[..idx + 2]; // includes `()`
1835    let subscript = &expr[idx + 2..=subscript_end]; // `[N]`
1836    let tail = &expr[subscript_end + 1..]; // everything after `]`
1837    let method_dot = expr[..idx].rfind('.').unwrap_or(0);
1838    let method = &expr[method_dot + 1..idx];
1839    let local = format!("_vec_{}_{}", method, name_suffix);
1840
1841    // String-key subscript (e.g. `["title"]`) signals a Map-like access. swift-bridge
1842    // serialises non-leaf Maps (e.g. `HashMap<String, String>`) as JSON-encoded
1843    // RustString rather than exposing a Swift dictionary. Decode the RustString to
1844    // `[String: String]` before subscripting so `_vec_X["title"]` works.
1845    let inner = subscript.trim_start_matches('[').trim_end_matches(']');
1846    let is_string_key = inner.starts_with('"') && inner.ends_with('"');
1847    let setup = if is_string_key {
1848        format!(
1849            "let {local} = (try? JSONSerialization.jsonObject(with: ({prefix}.toString() ?? \"{{}}\").data(using: .utf8)!) as? [String: String]) ?? [:]"
1850        )
1851    } else {
1852        format!("let {local} = {prefix}")
1853    };
1854
1855    let rewritten = format!("{local}{subscript}{tail}");
1856    (vec![setup], rewritten, is_string_key)
1857}
1858
1859/// Returns `(accessor_expr, has_optional)` where `has_optional` is true when
1860/// at least one `?.` was inserted.
1861fn swift_build_accessor(field: &str, result_var: &str, field_resolver: &FieldResolver) -> (String, bool) {
1862    let resolved = field_resolver.resolve(field);
1863    let parts: Vec<&str> = resolved.split('.').collect();
1864
1865    // Build a set of optional prefix paths for O(1) lookup during the walk.
1866    // We track path_so_far incrementally.
1867    let mut out = result_var.to_string();
1868    let mut has_optional = false;
1869    let mut path_so_far = String::new();
1870    let total = parts.len();
1871    for (i, part) in parts.iter().enumerate() {
1872        let is_leaf = i == total - 1;
1873        // Handle array index subscripts within a segment, e.g. `data[0]`.
1874        // `data[0]` must become `.data()[0]` not `.data[0]()`.
1875        // Split at the first `[` if present.
1876        let (field_name, subscript): (&str, Option<&str>) = if let Some(bracket_pos) = part.find('[') {
1877            (&part[..bracket_pos], Some(&part[bracket_pos..]))
1878        } else {
1879            (part, None)
1880        };
1881
1882        if !path_so_far.is_empty() {
1883            path_so_far.push('.');
1884        }
1885        // Build the base path (without subscript) for the optional check. When the
1886        // segment is e.g. `tool_calls[0]`, we want to check `is_optional` against
1887        // "choices[0].message.tool_calls" not "choices[0].message.tool_calls[0]".
1888        let base_path = {
1889            let mut p = path_so_far.clone();
1890            p.push_str(field_name);
1891            p
1892        };
1893        // Now push the full part (with subscript if any) so path_so_far is correct
1894        // for subsequent segment checks.
1895        path_so_far.push_str(part);
1896
1897        out.push('.');
1898        out.push_str(field_name);
1899        if let Some(sub) = subscript {
1900            // When the getter for this subscripted field is itself optional
1901            // (e.g. tool_calls returns Optional<RustVec<T>>), insert `?` before
1902            // the subscript so Swift unwraps the Optional before indexing.
1903            let field_is_optional = field_resolver.is_optional(&base_path);
1904            if field_is_optional {
1905                out.push_str("()?");
1906                has_optional = true;
1907            } else {
1908                out.push_str("()");
1909            }
1910            out.push_str(sub);
1911            // Do NOT append a trailing `?` after the subscript index: in Swift,
1912            // `optionalVec?[N]` via `Collection.subscript` returns the element
1913            // type `T` directly (the subscript is non-optional and the force-unwrap
1914            // inside RustVec's subscript is unconditional).  Optional chaining
1915            // already consumed the `?` in `?[N]`, so the result is `T` (non-optional
1916            // in the compiler's view), and a subsequent `?.member()` would be flagged
1917            // as "optional chaining on non-optional value".  The parent `has_optional`
1918            // flag is still set when `field_is_optional` is true, which causes the
1919            // enclosing expression to be wrapped in `(... ?? fallback)` correctly.
1920        } else {
1921            out.push_str("()");
1922            // Insert `?` after `()` for non-leaf optional fields so the next
1923            // member access becomes `?.`.
1924            if !is_leaf && field_resolver.is_optional(&base_path) {
1925                out.push('?');
1926                has_optional = true;
1927            }
1928        }
1929    }
1930    (out, has_optional)
1931}
1932
1933/// Generate a `[String]?` expression for a `RustVec<RustString>` (or optional variant) field
1934/// so that `contains` membership checks work against plain Swift Strings.
1935///
1936/// The result is `Optional<[String]>` — callers should coalesce with `?? []`.
1937///
1938/// We use `?.map { $0.as_str().toString() }` because:
1939/// 1. Iterating a `RustVec<RustString>` yields `RustStringRef` (not `RustString`), which
1940///    only has `as_str()` but not `toString()` directly.
1941/// 2. The accessor may end with an `Optional<RustVec<RustString>>` (e.g. `sheet_names()` is
1942///    `Option<Vec<String>>` in Rust, which becomes `Optional<RustVec<RustString>>` in Swift).
1943/// 3. Optional chaining from parent `?.` already produces `Optional<RustVec<T>>`.
1944///
1945/// `?.map { $0.as_str().toString() }` converts each `RustStringRef` to a Swift `String`,
1946/// giving `[String]` wrapped in `Optional`. The `?? []` in callers coalesces nil to an empty
1947/// array.
1948/// Generate a `XCTAssert{True|False}(array.contains(where: { elem_str.contains(val) }), msg)` line
1949/// for field paths that traverse a collection with `[].` notation (e.g. `links[].url`).
1950///
1951/// `array_part` — left side of `[].` (e.g. `"links"`)
1952/// `element_part` — right side (e.g. `"url"` or `"link_type"`)
1953/// `full_field` — original assertion.field (used for enum lookup against the full path)
1954#[allow(clippy::too_many_arguments)]
1955fn swift_traversal_contains_assert(
1956    array_part: &str,
1957    element_part: &str,
1958    full_field: &str,
1959    val_expr: &str,
1960    result_var: &str,
1961    negate: bool,
1962    msg: &str,
1963    enum_fields: &std::collections::HashSet<String>,
1964    field_resolver: &FieldResolver,
1965) -> String {
1966    let array_accessor = field_resolver.accessor(array_part, "swift", result_var);
1967    let resolved_full = field_resolver.resolve(full_field);
1968    let resolved_elem_part = resolved_full
1969        .find("[].")
1970        .map(|d| &resolved_full[d + 3..])
1971        .unwrap_or(element_part);
1972    let elem_accessor = field_resolver.accessor(resolved_elem_part, "swift", "$0");
1973    let elem_is_enum = enum_fields.contains(full_field) || enum_fields.contains(resolved_full);
1974    let elem_is_optional = field_resolver.is_optional(resolved_elem_part)
1975        || field_resolver.is_optional(field_resolver.resolve(resolved_elem_part));
1976    let elem_str = if elem_is_enum {
1977        // Enum-typed fields are bridged as `String` (RustString in Swift).
1978        // A single `.toString()` converts RustString → Swift String.
1979        format!("{elem_accessor}.toString()")
1980    } else if elem_is_optional {
1981        format!("({elem_accessor}?.toString() ?? \"\")")
1982    } else {
1983        format!("{elem_accessor}.toString()")
1984    };
1985    let assert_fn = if negate { "XCTAssertFalse" } else { "XCTAssertTrue" };
1986    format!("        {assert_fn}({array_accessor}.contains(where: {{ {elem_str}.contains({val_expr}) }}), \"{msg}\")")
1987}
1988
1989fn swift_array_contains_expr(field: Option<&str>, result_var: &str, field_resolver: &FieldResolver) -> String {
1990    let Some(f) = field else {
1991        return format!("{result_var}.map {{ $0.as_str().toString() }}");
1992    };
1993    let (accessor, _has_optional) = swift_build_accessor(f, result_var, field_resolver);
1994    // Always use `?.map` — the array field (sheet_names, etc.) may itself return
1995    // Optional<RustVec<T>> even if not listed in fields_optional.
1996    format!("{accessor}?.map {{ $0.as_str().toString() }}")
1997}
1998
1999/// Generate a `.count` expression for an array field that may be nested inside optional parents.
2000///
2001/// Swift-bridge exposes all Rust fields as methods with `()`. When ancestor segments are
2002/// optional, we use `?.` chaining. The final count is coalesced with `?? 0` when there
2003/// are optional ancestors so the XCTAssert macro receives a non-optional `Int`.
2004///
2005/// Also check if the field itself (the leaf) is optional, which happens when the field
2006/// returns Optional<RustVec<T>> (e.g., `links()` may return Optional).
2007fn swift_array_count_expr(field: Option<&str>, result_var: &str, field_resolver: &FieldResolver) -> String {
2008    let Some(f) = field else {
2009        return format!("{result_var}.count");
2010    };
2011    let (accessor, mut has_optional) = swift_build_accessor(f, result_var, field_resolver);
2012    // Also check if the leaf field itself is optional.
2013    if field_resolver.is_optional(f) {
2014        has_optional = true;
2015    }
2016    if has_optional {
2017        // In Swift, accessing .count on an optional with ?. returns Optional<Int>,
2018        // so we coalesce with ?? 0 to get a concrete Int for XCTAssert.
2019        if accessor.contains("?.") {
2020            format!("{accessor}.count ?? 0")
2021        } else {
2022            // If no ?. but field is optional, the field_expr itself is Optional<RustVec<T>>
2023            // so we need ?. to call count.
2024            format!("({accessor}?.count ?? 0)")
2025        }
2026    } else {
2027        format!("{accessor}.count")
2028    }
2029}
2030
2031/// Convert a `serde_json::Value` to a Swift literal string.
2032fn json_to_swift(value: &serde_json::Value) -> String {
2033    match value {
2034        serde_json::Value::String(s) => format!("\"{}\"", escape_swift(s)),
2035        serde_json::Value::Bool(b) => b.to_string(),
2036        serde_json::Value::Number(n) => n.to_string(),
2037        serde_json::Value::Null => "nil".to_string(),
2038        serde_json::Value::Array(arr) => {
2039            let items: Vec<String> = arr.iter().map(json_to_swift).collect();
2040            format!("[{}]", items.join(", "))
2041        }
2042        serde_json::Value::Object(_) => {
2043            let json_str = serde_json::to_string(value).unwrap_or_default();
2044            format!("\"{}\"", escape_swift(&json_str))
2045        }
2046    }
2047}
2048
2049/// Escape a string for embedding in a Swift double-quoted string literal.
2050fn escape_swift(s: &str) -> String {
2051    escape_swift_str(s)
2052}
2053
2054#[cfg(test)]
2055mod tests {
2056    use super::*;
2057    use crate::field_access::FieldResolver;
2058    use std::collections::{HashMap, HashSet};
2059
2060    fn make_resolver_tool_calls() -> FieldResolver {
2061        // Resolver for `choices[0].message.tool_calls[0].function.name`:
2062        //   - `choices` is a registered array field
2063        //   - `choices.message.tool_calls` is optional (Optional<RustVec<ToolCall>>)
2064        let mut optional = HashSet::new();
2065        optional.insert("choices.message.tool_calls".to_string());
2066        let mut arrays = HashSet::new();
2067        arrays.insert("choices".to_string());
2068        FieldResolver::new(&HashMap::new(), &optional, &HashSet::new(), &arrays, &HashSet::new())
2069    }
2070
2071    /// Regression: after `tool_calls()?[0]` the codegen must NOT append a trailing `?`
2072    /// before the next segment.  The Swift compiler sees `?[0]` as consuming the optional
2073    /// chain, yielding `ToolCallRef` (non-optional from the subscript's perspective), so
2074    /// `?.function()` triggers "cannot use optional chaining on non-optional value".
2075    ///
2076    /// The fix: do not emit `?` after the subscript index for non-leaf segments.
2077    #[test]
2078    fn optional_vec_subscript_does_not_emit_trailing_question_mark_before_next_segment() {
2079        let resolver = make_resolver_tool_calls();
2080        // Access `choices[0].message.tool_calls[0].function.name`:
2081        //   `tool_calls` is optional, `function` and `name` are non-optional.
2082        let (accessor, has_optional) =
2083            swift_build_accessor("choices[0].message.tool_calls[0].function.name", "result", &resolver);
2084        // `?` before `[0]` is correct (tool_calls is optional).
2085        // swift_build_accessor uses the raw field name without camelCase conversion.
2086        assert!(
2087            accessor.contains("tool_calls()?[0]"),
2088            "expected `tool_calls()?[0]` for optional tool_calls, got: {accessor}"
2089        );
2090        // There must NOT be `?[0]?` (trailing `?` after the index).
2091        assert!(
2092            !accessor.contains("?[0]?"),
2093            "must not emit trailing `?` after subscript index: {accessor}"
2094        );
2095        // The expression IS optional overall (tool_calls may be nil).
2096        assert!(has_optional, "expected has_optional=true for optional field chain");
2097        // Subsequent member access uses `.` (non-optional chain) not `?.`.
2098        assert!(
2099            accessor.contains("[0].function()"),
2100            "expected `.function()` (non-optional) after subscript: {accessor}"
2101        );
2102    }
2103}