Skip to main content

alef_e2e/codegen/
swift.rs

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