Skip to main content

alef_e2e/codegen/
go.rs

1//! Go e2e test generator using testing.T.
2
3use crate::config::E2eConfig;
4use crate::escape::{go_string_literal, sanitize_filename};
5use crate::field_access::FieldResolver;
6use crate::fixture::{Assertion, CallbackAction, Fixture, FixtureGroup, ValidationErrorExpectation};
7use alef_codegen::naming::{go_param_name, to_go_name};
8use alef_core::backend::GeneratedFile;
9use alef_core::config::Language;
10use alef_core::config::ResolvedCrateConfig;
11use alef_core::hash::{self, CommentStyle};
12use anyhow::Result;
13use heck::ToUpperCamelCase;
14use std::fmt::Write as FmtWrite;
15use std::path::PathBuf;
16
17use super::E2eCodegen;
18use super::client;
19
20/// Go e2e code generator.
21pub struct GoCodegen;
22
23impl E2eCodegen for GoCodegen {
24    fn generate(
25        &self,
26        groups: &[FixtureGroup],
27        e2e_config: &E2eConfig,
28        config: &ResolvedCrateConfig,
29        _type_defs: &[alef_core::ir::TypeDef],
30    ) -> Result<Vec<GeneratedFile>> {
31        let lang = self.language_name();
32        let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
33
34        let mut files = Vec::new();
35
36        // Resolve call config with overrides (for module path and import alias).
37        let call = &e2e_config.call;
38        let overrides = call.overrides.get(lang);
39        let configured_go_module_path = config.go.as_ref().and_then(|go| go.module.as_ref()).cloned();
40        let module_path = overrides
41            .and_then(|o| o.module.as_ref())
42            .cloned()
43            .or_else(|| configured_go_module_path.clone())
44            .unwrap_or_else(|| call.module.clone());
45        let import_alias = overrides
46            .and_then(|o| o.alias.as_ref())
47            .cloned()
48            .unwrap_or_else(|| "pkg".to_string());
49
50        // Resolve package config.
51        let go_pkg = e2e_config.resolve_package("go");
52        let go_module_path = go_pkg
53            .as_ref()
54            .and_then(|p| p.module.as_ref())
55            .cloned()
56            .or_else(|| configured_go_module_path.clone())
57            .unwrap_or_else(|| module_path.clone());
58        let replace_path = go_pkg
59            .as_ref()
60            .and_then(|p| p.path.as_ref())
61            .cloned()
62            .or_else(|| Some(format!("../../{}", config.package_dir(Language::Go))));
63        let go_version = go_pkg
64            .as_ref()
65            .and_then(|p| p.version.as_ref())
66            .cloned()
67            .unwrap_or_else(|| {
68                config
69                    .resolved_version()
70                    .map(|v| format!("v{v}"))
71                    .unwrap_or_else(|| "v0.0.0".to_string())
72            });
73        let field_resolver = FieldResolver::new(
74            &e2e_config.fields,
75            &e2e_config.fields_optional,
76            &e2e_config.result_fields,
77            &e2e_config.fields_array,
78            &std::collections::HashSet::new(),
79        );
80
81        // Generate go.mod. In registry mode, omit the `replace` directive so the
82        // module is fetched from the Go module proxy.
83        let effective_replace = match e2e_config.dep_mode {
84            crate::config::DependencyMode::Registry => None,
85            crate::config::DependencyMode::Local => replace_path.as_deref().map(String::from),
86        };
87        // In local mode with a `replace` directive the version in `require` is a
88        // placeholder.  Go requires that for a major-version module path (`/vN`, N ≥ 2)
89        // the placeholder version must start with `vN.`, e.g. `v3.0.0`.  A version like
90        // `v0.0.0` is rejected with "should be v3, not v0".  Fix the placeholder when the
91        // module path ends with `/vN` and the configured version doesn't match.
92        let effective_go_version = if effective_replace.is_some() {
93            fix_go_major_version(&go_module_path, &go_version)
94        } else {
95            go_version.clone()
96        };
97        files.push(GeneratedFile {
98            path: output_base.join("go.mod"),
99            content: render_go_mod(&go_module_path, effective_replace.as_deref(), &effective_go_version),
100            generated_header: false,
101        });
102
103        // Determine if any fixture needs jsonString helper across all groups.
104        let emits_executable_test =
105            |fixture: &Fixture| fixture.is_http_test() || fixture_has_go_callable(fixture, e2e_config);
106        let needs_json_stringify = groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| {
107            emits_executable_test(f)
108                && f.assertions.iter().any(|a| {
109                    matches!(
110                        a.assertion_type.as_str(),
111                        "contains" | "contains_all" | "contains_any" | "not_contains"
112                    ) && {
113                        if a.field.as_ref().is_none_or(|f| f.is_empty()) {
114                            e2e_config
115                                .resolve_call_for_fixture(f.call.as_deref(), &f.input)
116                                .result_is_array
117                        } else {
118                            let resolved_name = field_resolver.resolve(a.field.as_deref().unwrap_or(""));
119                            field_resolver.is_array(resolved_name)
120                        }
121                    }
122                })
123        });
124
125        // Generate helpers_test.go with jsonString if needed, emitted exactly once.
126        if needs_json_stringify {
127            files.push(GeneratedFile {
128                path: output_base.join("helpers_test.go"),
129                content: render_helpers_test_go(),
130                generated_header: true,
131            });
132        }
133
134        // Generate main_test.go with TestMain when:
135        // 1. Any fixture needs the mock server (has mock_response), or
136        // 2. Any fixture is client_factory-based (reads MOCK_SERVER_URL), or
137        // 3. Any fixture is file-based (requires test_documents directory setup).
138        //
139        // TestMain runs before all tests and changes to the test_documents directory,
140        // ensuring that relative file paths like "pdf/fake_memo.pdf" resolve correctly.
141        let has_file_fixtures = groups
142            .iter()
143            .flat_map(|g| g.fixtures.iter())
144            .any(|f| f.http.is_none() && !f.needs_mock_server());
145
146        let needs_main_test = has_file_fixtures
147            || groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| {
148                if f.needs_mock_server() {
149                    return true;
150                }
151                let cc = e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.input);
152                let go_override = cc.overrides.get("go").or_else(|| e2e_config.call.overrides.get("go"));
153                go_override.and_then(|o| o.client_factory.as_deref()).is_some()
154            });
155
156        if needs_main_test {
157            files.push(GeneratedFile {
158                path: output_base.join("main_test.go"),
159                content: render_main_test_go(&e2e_config.test_documents_dir),
160                generated_header: true,
161            });
162        }
163
164        // Generate test files per category.
165        for group in groups {
166            let active: Vec<&Fixture> = group
167                .fixtures
168                .iter()
169                .filter(|f| super::should_include_fixture(f, lang, e2e_config))
170                .collect();
171
172            if active.is_empty() {
173                continue;
174            }
175
176            let filename = format!("{}_test.go", sanitize_filename(&group.category));
177            let content = render_test_file(
178                &group.category,
179                &active,
180                &module_path,
181                &import_alias,
182                &field_resolver,
183                e2e_config,
184            );
185            files.push(GeneratedFile {
186                path: output_base.join(filename),
187                content,
188                generated_header: true,
189            });
190        }
191
192        Ok(files)
193    }
194
195    fn language_name(&self) -> &'static str {
196        "go"
197    }
198}
199
200/// Fix a Go module version so it is valid for a major-version module path.
201///
202/// Go requires that a module path ending in `/vN` (N ≥ 2) uses a version
203/// whose major component matches N.  In local-replace mode we use a synthetic
204/// placeholder version; if that placeholder (e.g. `v0.0.0`) doesn't match the
205/// major suffix, fix it to `vN.0.0` so `go mod` accepts the go.mod.
206fn fix_go_major_version(module_path: &str, version: &str) -> String {
207    // Extract `/vN` suffix from the module path (N must be ≥ 2).
208    let major = module_path
209        .rsplit('/')
210        .next()
211        .and_then(|seg| seg.strip_prefix('v'))
212        .and_then(|n| n.parse::<u64>().ok())
213        .filter(|&n| n >= 2);
214
215    let Some(n) = major else {
216        return version.to_string();
217    };
218
219    // If the version already starts with `vN.`, it is valid — leave it alone.
220    let expected_prefix = format!("v{n}.");
221    if version.starts_with(&expected_prefix) {
222        return version.to_string();
223    }
224
225    format!("v{n}.0.0")
226}
227
228fn render_go_mod(go_module_path: &str, replace_path: Option<&str>, version: &str) -> String {
229    let mut out = String::new();
230    let _ = writeln!(out, "module e2e_go");
231    let _ = writeln!(out);
232    let _ = writeln!(out, "go 1.26");
233    let _ = writeln!(out);
234    let _ = writeln!(out, "require (");
235    let _ = writeln!(out, "\t{go_module_path} {version}");
236    let _ = writeln!(out, "\tgithub.com/stretchr/testify v1.11.1");
237    let _ = writeln!(out, ")");
238
239    if let Some(path) = replace_path {
240        let _ = writeln!(out);
241        let _ = writeln!(out, "replace {go_module_path} => {path}");
242    }
243
244    out
245}
246
247/// Generate `main_test.go` that starts the mock HTTP server before all tests run.
248///
249/// The binary is expected at `../rust/target/release/mock-server` relative to the Go e2e
250/// directory.  The server prints `MOCK_SERVER_URL=http://...` on stdout; we read that line
251/// and export the variable so all test files can call `os.Getenv("MOCK_SERVER_URL")`.
252fn render_main_test_go(test_documents_dir: &str) -> String {
253    // NOTE: the generated-file header is injected by the caller (generated_header: true).
254    let mut out = String::new();
255    let _ = writeln!(out, "package e2e_test");
256    let _ = writeln!(out);
257    let _ = writeln!(out, "import (");
258    let _ = writeln!(out, "\t\"bufio\"");
259    let _ = writeln!(out, "\t\"encoding/json\"");
260    let _ = writeln!(out, "\t\"io\"");
261    let _ = writeln!(out, "\t\"os\"");
262    let _ = writeln!(out, "\t\"os/exec\"");
263    let _ = writeln!(out, "\t\"path/filepath\"");
264    let _ = writeln!(out, "\t\"runtime\"");
265    let _ = writeln!(out, "\t\"strings\"");
266    let _ = writeln!(out, "\t\"testing\"");
267    let _ = writeln!(out, ")");
268    let _ = writeln!(out);
269    let _ = writeln!(out, "func TestMain(m *testing.M) {{");
270    let _ = writeln!(out, "\t_, filename, _, _ := runtime.Caller(0)");
271    let _ = writeln!(out, "\tdir := filepath.Dir(filename)");
272    let _ = writeln!(out);
273    let _ = writeln!(
274        out,
275        "\t// Change to the configured test-documents directory so that fixture file paths like"
276    );
277    let _ = writeln!(
278        out,
279        "\t// \"pdf/fake_memo.pdf\" resolve correctly when running go test from e2e/go/."
280    );
281    let _ = writeln!(
282        out,
283        "\ttestDocumentsDir := filepath.Join(dir, \"..\", \"..\", \"{test_documents_dir}\")"
284    );
285    let _ = writeln!(out, "\tif err := os.Chdir(testDocumentsDir); err != nil {{");
286    let _ = writeln!(out, "\t\tpanic(err)");
287    let _ = writeln!(out, "\t}}");
288    let _ = writeln!(out);
289    let _ = writeln!(out, "\t// Start the mock HTTP server if it exists.");
290    let _ = writeln!(
291        out,
292        "\tmockServerBin := filepath.Join(dir, \"..\", \"rust\", \"target\", \"release\", \"mock-server\")"
293    );
294    let _ = writeln!(out, "\tif _, err := os.Stat(mockServerBin); err == nil {{");
295    let _ = writeln!(
296        out,
297        "\t\tfixturesDir := filepath.Join(dir, \"..\", \"..\", \"fixtures\")"
298    );
299    let _ = writeln!(out, "\t\tcmd := exec.Command(mockServerBin, fixturesDir)");
300    let _ = writeln!(out, "\t\tcmd.Stderr = os.Stderr");
301    let _ = writeln!(out, "\t\tstdout, err := cmd.StdoutPipe()");
302    let _ = writeln!(out, "\t\tif err != nil {{");
303    let _ = writeln!(out, "\t\t\tpanic(err)");
304    let _ = writeln!(out, "\t\t}}");
305    let _ = writeln!(out, "\t\t// Keep a writable pipe to the mock-server's stdin so the");
306    let _ = writeln!(
307        out,
308        "\t\t// server does not see EOF and exit immediately. The mock-server"
309    );
310    let _ = writeln!(out, "\t\t// blocks reading stdin until the parent closes the pipe.");
311    let _ = writeln!(out, "\t\tstdin, err := cmd.StdinPipe()");
312    let _ = writeln!(out, "\t\tif err != nil {{");
313    let _ = writeln!(out, "\t\t\tpanic(err)");
314    let _ = writeln!(out, "\t\t}}");
315    let _ = writeln!(out, "\t\tif err := cmd.Start(); err != nil {{");
316    let _ = writeln!(out, "\t\t\tpanic(err)");
317    let _ = writeln!(out, "\t\t}}");
318    let _ = writeln!(out, "\t\tscanner := bufio.NewScanner(stdout)");
319    let _ = writeln!(out, "\t\tfor scanner.Scan() {{");
320    let _ = writeln!(out, "\t\t\tline := scanner.Text()");
321    let _ = writeln!(out, "\t\t\tif strings.HasPrefix(line, \"MOCK_SERVER_URL=\") {{");
322    let _ = writeln!(
323        out,
324        "\t\t\t\t_ = os.Setenv(\"MOCK_SERVER_URL\", strings.TrimPrefix(line, \"MOCK_SERVER_URL=\"))"
325    );
326    let _ = writeln!(out, "\t\t\t}} else if strings.HasPrefix(line, \"MOCK_SERVERS=\") {{");
327    let _ = writeln!(out, "\t\t\t\t_jsonVal := strings.TrimPrefix(line, \"MOCK_SERVERS=\")");
328    let _ = writeln!(out, "\t\t\t\t_ = os.Setenv(\"MOCK_SERVERS\", _jsonVal)");
329    let _ = writeln!(
330        out,
331        "\t\t\t\t// Parse the JSON map and set per-fixture env vars (MOCK_SERVER_<FIXTURE_ID>)."
332    );
333    let _ = writeln!(out, "\t\t\t\tvar _perFixture map[string]string");
334    let _ = writeln!(
335        out,
336        "\t\t\t\tif err := json.Unmarshal([]byte(_jsonVal), &_perFixture); err == nil {{"
337    );
338    let _ = writeln!(out, "\t\t\t\t\tfor _fid, _furl := range _perFixture {{");
339    let _ = writeln!(
340        out,
341        "\t\t\t\t\t\t_ = os.Setenv(\"MOCK_SERVER_\"+strings.ToUpper(_fid), _furl)"
342    );
343    let _ = writeln!(out, "\t\t\t\t\t}}");
344    let _ = writeln!(out, "\t\t\t\t}}");
345    let _ = writeln!(out, "\t\t\t\tbreak");
346    let _ = writeln!(out, "\t\t\t}} else if os.Getenv(\"MOCK_SERVER_URL\") != \"\" {{");
347    let _ = writeln!(out, "\t\t\t\tbreak");
348    let _ = writeln!(out, "\t\t\t}}");
349    let _ = writeln!(out, "\t\t}}");
350    let _ = writeln!(out, "\t\tgo func() {{ _, _ = io.Copy(io.Discard, stdout) }}()");
351    let _ = writeln!(out, "\t\tcode := m.Run()");
352    let _ = writeln!(out, "\t\t_ = stdin.Close()");
353    let _ = writeln!(out, "\t\t_ = cmd.Process.Signal(os.Interrupt)");
354    let _ = writeln!(out, "\t\t_ = cmd.Wait()");
355    let _ = writeln!(out, "\t\tos.Exit(code)");
356    let _ = writeln!(out, "\t}} else {{");
357    let _ = writeln!(out, "\t\tcode := m.Run()");
358    let _ = writeln!(out, "\t\tos.Exit(code)");
359    let _ = writeln!(out, "\t}}");
360    let _ = writeln!(out, "}}");
361    out
362}
363
364/// Generate `helpers_test.go` with the jsonString helper function.
365/// This is emitted once per package to avoid duplicate function definitions.
366fn render_helpers_test_go() -> String {
367    let mut out = String::new();
368    let _ = writeln!(out, "package e2e_test");
369    let _ = writeln!(out);
370    let _ = writeln!(out, "import \"encoding/json\"");
371    let _ = writeln!(out);
372    let _ = writeln!(out, "// jsonString converts a value to its JSON string representation.");
373    let _ = writeln!(
374        out,
375        "// Array fields use jsonString instead of fmt.Sprint to preserve structure."
376    );
377    let _ = writeln!(out, "func jsonString(value any) string {{");
378    let _ = writeln!(out, "\tencoded, err := json.Marshal(value)");
379    let _ = writeln!(out, "\tif err != nil {{");
380    let _ = writeln!(out, "\t\treturn \"\"");
381    let _ = writeln!(out, "\t}}");
382    let _ = writeln!(out, "\treturn string(encoded)");
383    let _ = writeln!(out, "}}");
384    out
385}
386
387fn render_test_file(
388    category: &str,
389    fixtures: &[&Fixture],
390    go_module_path: &str,
391    import_alias: &str,
392    field_resolver: &FieldResolver,
393    e2e_config: &crate::config::E2eConfig,
394) -> String {
395    let mut out = String::new();
396    let emits_executable_test =
397        |fixture: &Fixture| fixture.is_http_test() || fixture_has_go_callable(fixture, e2e_config);
398
399    // Go convention: generated file marker must appear before the package declaration.
400    out.push_str(&hash::header(CommentStyle::DoubleSlash));
401    let _ = writeln!(out);
402
403    // Determine if any fixture actually uses the pkg import.
404    // Fixtures without mock_response are emitted as t.Skip() stubs and don't reference the
405    // package — omit the import when no fixture needs it to avoid the Go "imported and not
406    // used" compile error. Visitor fixtures reference the package types (NodeContext,
407    // VisitResult, VisitResult* helpers) in struct method signatures emitted at file scope,
408    // so they also require the import even when the test body itself is a Skip stub.
409    // Direct-callable fixtures (non-HTTP, non-mock, with a resolved Go function) also
410    // reference the package when a Go override function is configured.
411    let needs_pkg = fixtures
412        .iter()
413        .any(|f| fixture_has_go_callable(f, e2e_config) || f.is_http_test() || f.visitor.is_some());
414
415    // Determine if we need the "os" import (mock_url args, HTTP fixtures, or
416    // client_factory fixtures that read MOCK_SERVER_URL via os.Getenv).
417    let needs_os = fixtures.iter().any(|f| {
418        if f.is_http_test() {
419            return true;
420        }
421        if !emits_executable_test(f) {
422            return false;
423        }
424        let call_config = e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.input);
425        let go_override = call_config
426            .overrides
427            .get("go")
428            .or_else(|| e2e_config.call.overrides.get("go"));
429        if go_override.and_then(|o| o.client_factory.as_deref()).is_some() {
430            return true;
431        }
432        let call_args = &call_config.args;
433        // Need "os" for mock_url args, or for bytes args with a string fixture value
434        // (fixture-relative path loaded via os.ReadFile at test-run time).
435        if call_args.iter().any(|a| a.arg_type == "mock_url") {
436            return true;
437        }
438        call_args.iter().any(|a| {
439            if a.arg_type != "bytes" {
440                return false;
441            }
442            // alef.toml field paths are dotted (e.g. "input.data"). The fixture's `input`
443            // field already strips the "input." prefix, so we walk the remaining segments.
444            let mut current = &f.input;
445            let path = a.field.strip_prefix("input.").unwrap_or(&a.field);
446            for segment in path.split('.') {
447                match current.get(segment) {
448                    Some(next) => current = next,
449                    None => return false,
450                }
451            }
452            current.is_string()
453        })
454    });
455
456    // Note: file_path args are passed directly as relative strings — the e2e/go
457    // TestMain in main_test.go already chdir's into the repo-root test_documents/.
458    let needs_filepath = false;
459
460    let _needs_json_stringify = fixtures.iter().any(|f| {
461        emits_executable_test(f)
462            && f.assertions.iter().any(|a| {
463                matches!(
464                    a.assertion_type.as_str(),
465                    "contains" | "contains_all" | "contains_any" | "not_contains"
466                ) && {
467                    // Check if this assertion operates on an array field.
468                    // If no field is specified, check if the result itself is an array.
469                    if a.field.as_ref().is_none_or(|f| f.is_empty()) {
470                        // No field specified: check if result is an array
471                        e2e_config
472                            .resolve_call_for_fixture(f.call.as_deref(), &f.input)
473                            .result_is_array
474                    } else {
475                        // Field specified: check if that field is an array
476                        let resolved_name = field_resolver.resolve(a.field.as_deref().unwrap_or(""));
477                        field_resolver.is_array(resolved_name)
478                    }
479                }
480            })
481    });
482
483    // Determine if we need "encoding/json" (handle args with non-null config,
484    // json_object args that will be unmarshalled into a typed struct, or HTTP
485    // body/partial/validation-error assertions that use json.Unmarshal).
486    let needs_json = fixtures.iter().any(|f| {
487        // HTTP body assertions use json.Unmarshal for Object/Array bodies;
488        // partial body and validation-error assertions always use json.Unmarshal.
489        if let Some(http) = &f.http {
490            let body_needs_json = http
491                .expected_response
492                .body
493                .as_ref()
494                .is_some_and(|b| matches!(b, serde_json::Value::Object(_) | serde_json::Value::Array(_)));
495            let partial_needs_json = http.expected_response.body_partial.is_some();
496            let ve_needs_json = http
497                .expected_response
498                .validation_errors
499                .as_ref()
500                .is_some_and(|v| !v.is_empty());
501            if body_needs_json || partial_needs_json || ve_needs_json {
502                return true;
503            }
504        }
505        if !emits_executable_test(f) {
506            return false;
507        }
508
509        let call = e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.input);
510        let call_args = &call.args;
511        // handle args with non-null config value
512        let has_handle = call_args.iter().any(|a| a.arg_type == "handle") && {
513            call_args.iter().filter(|a| a.arg_type == "handle").any(|a| {
514                let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
515                let v = f.input.get(field).unwrap_or(&serde_json::Value::Null);
516                !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty()))
517            })
518        };
519        // json_object args with options_type or array values (will use JSON unmarshal)
520        let go_override = call.overrides.get("go");
521        let opts_type = go_override.and_then(|o| o.options_type.as_deref()).or_else(|| {
522            e2e_config
523                .call
524                .overrides
525                .get("go")
526                .and_then(|o| o.options_type.as_deref())
527        });
528        let has_json_obj = call_args.iter().any(|a| {
529            if a.arg_type != "json_object" {
530                return false;
531            }
532            let v = if a.field == "input" {
533                &f.input
534            } else {
535                let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
536                f.input.get(field).unwrap_or(&serde_json::Value::Null)
537            };
538            if v.is_array() {
539                return true;
540            } // array → []string unmarshal
541            opts_type.is_some() && v.is_object() && !v.as_object().is_some_and(|o| o.is_empty())
542        });
543        has_handle || has_json_obj
544    });
545
546    // No runtime base64 calls remain in generated Go code. Bytes args with string values
547    // are now loaded via os.ReadFile (see needs_os) and HTTP body byte arrays are
548    // base64-encoded at codegen time and embedded as literal strings in the json.Unmarshal
549    // call, which doesn't require the `encoding/base64` import in the test file.
550    let needs_base64 = false;
551
552    // Determine if we need the "fmt" import (CustomTemplate visitor actions
553    // with placeholders or string assertions rendered through fmt.Sprint).
554    // Note: jsonString is now in helpers_test.go (uses encoding/json, not fmt),
555    // so individual test files do NOT need fmt just for calling jsonString.
556    let needs_fmt = fixtures.iter().any(|f| {
557        f.visitor.as_ref().is_some_and(|v| {
558            v.callbacks.values().any(|action| {
559                if let CallbackAction::CustomTemplate { template, .. } = action {
560                    template.contains('{')
561                } else {
562                    false
563                }
564            })
565        }) || (emits_executable_test(f)
566            && f.assertions.iter().any(|a| {
567                matches!(
568                    a.assertion_type.as_str(),
569                    "contains" | "contains_all" | "contains_any" | "not_contains"
570                ) && {
571                    // Check if this assertion uses fmt.Sprint (non-array fields).
572                    // Array fields use jsonString instead, which also needs fmt.
573                    // Also verify the field is valid for the result type — assertions
574                    // on invalid fields are skipped without emitting any fmt.Sprint call.
575                    if a.field.as_ref().is_none_or(|f| f.is_empty()) {
576                        // No field: fmt.Sprint only if result is not an array
577                        !e2e_config
578                            .resolve_call_for_fixture(f.call.as_deref(), &f.input)
579                            .result_is_array
580                    } else {
581                        // Field specified: fmt.Sprint only if that field is not an array
582                        // and the field is actually valid for the result type (otherwise
583                        // the assertion is skipped and fmt.Sprint is never emitted).
584                        let field = a.field.as_deref().unwrap_or("");
585                        let resolved_name = field_resolver.resolve(field);
586                        !field_resolver.is_array(resolved_name) && field_resolver.is_valid_for_result(field)
587                    }
588                }
589            }))
590    });
591
592    // Determine if we need the "strings" import.
593    // Only count assertions whose fields are actually valid for the result type.
594    let needs_strings = fixtures.iter().any(|f| {
595        if !emits_executable_test(f) {
596            return false;
597        }
598        f.assertions.iter().any(|a| {
599            let type_needs_strings = if a.assertion_type == "equals" {
600                // equals with string values needs strings.TrimSpace
601                a.value.as_ref().is_some_and(|v| v.is_string())
602            } else {
603                matches!(
604                    a.assertion_type.as_str(),
605                    "contains" | "contains_all" | "contains_any" | "not_contains" | "starts_with" | "ends_with"
606                )
607            };
608            let field_valid = a
609                .field
610                .as_ref()
611                .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
612                .unwrap_or(true);
613            type_needs_strings && field_valid
614        })
615    });
616
617    // Determine if we need the testify assert import.
618    let needs_assert = fixtures.iter().any(|f| {
619        if !emits_executable_test(f) {
620            return false;
621        }
622        // Validation-category fixtures with an `error` assertion emit
623        // `assert.Error(t, createErr)` in their setup block, requiring testify.
624        // Other categories (e.g. `error`) use t.Errorf/t.Fatalf and do NOT need testify.
625        if f.resolved_category() == "validation" && f.assertions.iter().any(|a| a.assertion_type == "error") {
626            return true;
627        }
628        // Streaming fixtures emit `assert.X` calls against the collected `chunks`
629        // list — the field path resolves against the streaming virtual-field
630        // accessor table, not the result type. Treat any streaming-virtual field
631        // reference as `field_valid`.
632        let is_streaming_fixture = f.is_streaming_mock();
633        f.assertions.iter().any(|a| {
634            let field_is_streaming_virtual = a
635                .field
636                .as_deref()
637                .is_some_and(|s| !s.is_empty() && crate::codegen::streaming_assertions::is_streaming_virtual_field(s));
638            let field_valid = a
639                .field
640                .as_ref()
641                .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
642                .unwrap_or(true)
643                || (is_streaming_fixture && field_is_streaming_virtual);
644            let synthetic_field_needs_assert = match a.field.as_deref() {
645                Some("chunks_have_content" | "chunks_have_embeddings") => {
646                    matches!(a.assertion_type.as_str(), "is_true" | "is_false")
647                }
648                Some("embeddings") => {
649                    matches!(
650                        a.assertion_type.as_str(),
651                        "count_equals" | "count_min" | "not_empty" | "is_empty"
652                    )
653                }
654                _ => false,
655            };
656            let type_needs_assert = matches!(
657                a.assertion_type.as_str(),
658                "count_equals"
659                    | "count_min"
660                    | "count_max"
661                    | "is_true"
662                    | "is_false"
663                    | "method_result"
664                    | "min_length"
665                    | "max_length"
666                    | "matches_regex"
667            );
668            synthetic_field_needs_assert || type_needs_assert && field_valid
669        })
670    });
671
672    // Determine if we need "net/http" and "io" (HTTP server tests via HTTP client).
673    let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());
674    let needs_http = has_http_fixtures;
675    // io.ReadAll is emitted for every HTTP fixture (render_call always reads the body).
676    let needs_io = has_http_fixtures;
677
678    // Determine if we need "reflect" (for HTTP response body JSON comparison
679    // and partial-body assertions, both of which use reflect.DeepEqual).
680    let needs_reflect = fixtures.iter().any(|f| {
681        if let Some(http) = &f.http {
682            let body_needs_reflect = http
683                .expected_response
684                .body
685                .as_ref()
686                .is_some_and(|b| matches!(b, serde_json::Value::Object(_) | serde_json::Value::Array(_)));
687            let partial_needs_reflect = http.expected_response.body_partial.is_some();
688            body_needs_reflect || partial_needs_reflect
689        } else {
690            false
691        }
692    });
693
694    let _ = writeln!(out, "// E2e tests for category: {category}");
695    let _ = writeln!(out, "package e2e_test");
696    let _ = writeln!(out);
697    let _ = writeln!(out, "import (");
698    if needs_base64 {
699        let _ = writeln!(out, "\t\"encoding/base64\"");
700    }
701    if needs_json || needs_reflect {
702        let _ = writeln!(out, "\t\"encoding/json\"");
703    }
704    if needs_fmt {
705        let _ = writeln!(out, "\t\"fmt\"");
706    }
707    if needs_io {
708        let _ = writeln!(out, "\t\"io\"");
709    }
710    if needs_http {
711        let _ = writeln!(out, "\t\"net/http\"");
712    }
713    if needs_os {
714        let _ = writeln!(out, "\t\"os\"");
715    }
716    let _ = needs_filepath; // reserved for future use; currently always false
717    if needs_reflect {
718        let _ = writeln!(out, "\t\"reflect\"");
719    }
720    if needs_strings {
721        let _ = writeln!(out, "\t\"strings\"");
722    }
723    let _ = writeln!(out, "\t\"testing\"");
724    if needs_assert {
725        let _ = writeln!(out);
726        let _ = writeln!(out, "\t\"github.com/stretchr/testify/assert\"");
727    }
728    if needs_pkg {
729        let _ = writeln!(out);
730        let _ = writeln!(out, "\t{import_alias} \"{go_module_path}\"");
731    }
732    let _ = writeln!(out, ")");
733    let _ = writeln!(out);
734
735    // Emit package-level visitor structs (must be outside any function in Go).
736    for fixture in fixtures.iter() {
737        if let Some(visitor_spec) = &fixture.visitor {
738            let struct_name = visitor_struct_name(&fixture.id);
739            emit_go_visitor_struct(&mut out, &struct_name, visitor_spec, import_alias);
740            let _ = writeln!(out);
741        }
742    }
743
744    for (i, fixture) in fixtures.iter().enumerate() {
745        render_test_function(&mut out, fixture, import_alias, field_resolver, e2e_config);
746        if i + 1 < fixtures.len() {
747            let _ = writeln!(out);
748        }
749    }
750
751    // Clean up trailing newlines.
752    while out.ends_with("\n\n") {
753        out.pop();
754    }
755    if !out.ends_with('\n') {
756        out.push('\n');
757    }
758    out
759}
760
761/// Return `true` when a non-HTTP fixture can be exercised by calling the Go
762/// binding directly.
763///
764/// A fixture is Go-callable when the resolved call config provides a non-empty
765/// function name — either via a Go-specific override (`[e2e.call.overrides.go]
766/// function`) or via the base call `function` field.  The Go binding exposes all
767/// public functions from the Rust core as PascalCase exports, so any non-empty
768/// function name can be resolved to a valid Go symbol via `to_go_name`.
769fn fixture_has_go_callable(fixture: &Fixture, e2e_config: &crate::config::E2eConfig) -> bool {
770    // HTTP fixtures are handled by render_http_test_function — not our concern here.
771    if fixture.is_http_test() {
772        return false;
773    }
774    let call_config = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
775    // Honor per-call `skip_languages`: when the resolved call's `skip_languages`
776    // contains `"go"`, the Go binding doesn't expose this function.
777    if call_config.skip_languages.iter().any(|l| l == "go") {
778        return false;
779    }
780    let go_override = call_config
781        .overrides
782        .get("go")
783        .or_else(|| e2e_config.call.overrides.get("go"));
784    // When a client_factory is configured, the fixture is callable via the
785    // client-method pattern even when the base function name is empty.
786    if go_override.and_then(|o| o.client_factory.as_deref()).is_some() {
787        return true;
788    }
789    // Prefer a Go-specific override function name; fall back to the base function name.
790    // Any non-empty function name is callable: the Go binding exports all public
791    // Rust functions as PascalCase symbols (snake_case → PascalCase via to_go_name).
792    let fn_name = go_override
793        .and_then(|o| o.function.as_deref())
794        .filter(|s| !s.is_empty())
795        .unwrap_or(call_config.function.as_str());
796    !fn_name.is_empty()
797}
798
799fn render_test_function(
800    out: &mut String,
801    fixture: &Fixture,
802    import_alias: &str,
803    field_resolver: &FieldResolver,
804    e2e_config: &crate::config::E2eConfig,
805) {
806    let fn_name = fixture.id.to_upper_camel_case();
807    let description = &fixture.description;
808
809    // Delegate HTTP fixtures to the shared driver via GoTestClientRenderer.
810    if fixture.http.is_some() {
811        render_http_test_function(out, fixture);
812        return;
813    }
814
815    // Non-HTTP fixtures are tested directly when the call config provides a
816    // callable Go function.  Emit a t.Skip() stub when:
817    //   - No mock response and no callable (non-HTTP, non-mock, unreachable), or
818    //   - The call's skip_languages includes "go" (e.g. streaming not supported).
819    if !fixture_has_go_callable(fixture, e2e_config) {
820        let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
821        let _ = writeln!(out, "\t// {description}");
822        let _ = writeln!(
823            out,
824            "\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")"
825        );
826        let _ = writeln!(out, "}}");
827        return;
828    }
829
830    // Resolve call config per-fixture (supports named calls via fixture.call).
831    let call_config = e2e_config.resolve_call_for_fixture(fixture.call.as_deref(), &fixture.input);
832    let lang = "go";
833    let overrides = call_config.overrides.get(lang);
834
835    // Select the function name: Go bindings now integrate visitor support into
836    // the main Convert() function via ConversionOptions.Visitor field.
837    // (In other languages, there may be separate visitor_function overrides, but Go uses a single function.)
838    let base_function_name = overrides
839        .and_then(|o| o.function.as_deref())
840        .unwrap_or(&call_config.function);
841    let function_name = to_go_name(base_function_name);
842    let result_var = &call_config.result_var;
843    let args = &call_config.args;
844
845    // Whether the function returns (value, error) or just (error) or just (value).
846    // Check Go override first, fall back to call-level returns_result.
847    let returns_result = overrides
848        .and_then(|o| o.returns_result)
849        .unwrap_or(call_config.returns_result);
850
851    // Whether the function returns only error (no value component), i.e. Result<(), E>.
852    // When returns_result=true and returns_void=true, Go emits `err :=` not `_, err :=`.
853    let returns_void = call_config.returns_void;
854
855    // result_is_simple: result is a scalar (*string, *bool, etc.) not a struct.
856    // Priority: Go override > call-level (canonical source) > Rust override (legacy compat).
857    let result_is_simple = overrides.map(|o| o.result_is_simple).unwrap_or_else(|| {
858        if call_config.result_is_simple {
859            return true;
860        }
861        call_config
862            .overrides
863            .get("rust")
864            .map(|o| o.result_is_simple)
865            .unwrap_or(false)
866    });
867
868    // result_is_array: the simple result is a slice/array type (e.g., []string).
869    // Priority: Go override > call-level (canonical source).
870    let result_is_array = overrides
871        .map(|o| o.result_is_array)
872        .unwrap_or(call_config.result_is_array);
873
874    // Per-call Go options_type, falling back to the default call's Go override.
875    let call_options_type = overrides.and_then(|o| o.options_type.as_deref()).or_else(|| {
876        e2e_config
877            .call
878            .overrides
879            .get("go")
880            .and_then(|o| o.options_type.as_deref())
881    });
882
883    // Whether json_object options are passed as a pointer (*OptionsType).
884    let call_options_ptr = overrides.map(|o| o.options_ptr).unwrap_or_else(|| {
885        e2e_config
886            .call
887            .overrides
888            .get("go")
889            .map(|o| o.options_ptr)
890            .unwrap_or(false)
891    });
892
893    let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
894    // Validation-category fixtures expect engine *creation* to fail. Other expects_error
895    // fixtures (error_*) construct a valid engine and expect the *operation* to fail —
896    // engine creation should not be wrapped in assert.Error there.
897    let validation_creation_failure = expects_error && fixture.resolved_category() == "validation";
898
899    // Client factory: when set, the test creates a client via `pkg.Factory("test-key", baseURL)`
900    // and calls methods on the instance rather than top-level package functions.
901    let client_factory = overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
902        e2e_config
903            .call
904            .overrides
905            .get(lang)
906            .and_then(|o| o.client_factory.as_deref())
907    });
908
909    let (mut setup_lines, args_str) = build_args_and_setup(
910        &fixture.input,
911        args,
912        import_alias,
913        call_options_type,
914        fixture,
915        call_options_ptr,
916        validation_creation_failure,
917    );
918
919    // Build visitor if present — integrate into options instead of separate parameter.
920    // Go binding's Convert() checks options.Visitor and delegates to convertWithVisitorHelper when set.
921    let mut visitor_opts_var: Option<String> = None;
922    if fixture.visitor.is_some() {
923        let struct_name = visitor_struct_name(&fixture.id);
924        setup_lines.push(format!("visitor := &{struct_name}{{}}"));
925        // Create a fresh opts variable with the visitor attached.
926        let opts_type = call_options_type.unwrap_or("ConversionOptions");
927        let opts_var = "opts".to_string();
928        setup_lines.push(format!("opts := &{import_alias}.{opts_type}{{}}"));
929        setup_lines.push("opts.Visitor = visitor".to_string());
930        visitor_opts_var = Some(opts_var);
931    }
932
933    let go_extra_args = overrides.map(|o| o.extra_args.as_slice()).unwrap_or(&[]).to_vec();
934    let final_args = {
935        let mut parts: Vec<String> = Vec::new();
936        if !args_str.is_empty() {
937            // When visitor is present, replace trailing ", nil" with ", opts"
938            let processed_args = if let Some(ref opts_var) = visitor_opts_var {
939                args_str.trim_end_matches(", nil").to_string() + ", " + opts_var
940            } else {
941                args_str
942            };
943            parts.push(processed_args);
944        }
945        parts.extend(go_extra_args);
946        parts.join(", ")
947    };
948
949    let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
950    let _ = writeln!(out, "\t// {description}");
951
952    // Live-API fixtures use `env.api_key_var` to mark the env var that
953    // supplies the real API key. Skip the test when the env var is unset
954    // (mirrors Python's pytest.skip and Node's early-return pattern).
955    let has_mock = fixture.mock_response.is_some() || fixture.http.is_some();
956    let api_key_var = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref());
957    if let Some(var) = api_key_var {
958        if has_mock {
959            // Env-fallback branch: when the real API key is set use the live
960            // provider; otherwise fall back to the mock server so the test
961            // always runs in CI without credentials.
962            let fixture_id = &fixture.id;
963            let _ = writeln!(out, "\tapiKey := os.Getenv(\"{var}\")");
964            let _ = writeln!(out, "\tvar baseURL *string");
965            let _ = writeln!(out, "\tif apiKey != \"\" {{");
966            let _ = writeln!(out, "\t\tt.Logf(\"{fixture_id}: using real API ({var} is set)\")");
967            let _ = writeln!(out, "\t}} else {{");
968            let _ = writeln!(out, "\t\tt.Logf(\"{fixture_id}: using mock server ({var} not set)\")");
969            let _ = writeln!(
970                out,
971                "\t\tu := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
972            );
973            let _ = writeln!(out, "\t\tbaseURL = &u");
974            let _ = writeln!(out, "\t\tapiKey = \"test-key\"");
975            let _ = writeln!(out, "\t}}");
976        } else {
977            let _ = writeln!(out, "\tapiKey := os.Getenv(\"{var}\")");
978            let _ = writeln!(out, "\tif apiKey == \"\" {{");
979            let _ = writeln!(out, "\t\tt.Skipf(\"{var} not set\")");
980            let _ = writeln!(out, "\t}}");
981        }
982    }
983
984    for line in &setup_lines {
985        let _ = writeln!(out, "\t{line}");
986    }
987
988    // Client factory: emit client creation before the call.
989    // Each test creates a fresh client pointed at MOCK_SERVER_URL/fixtures/<id>
990    // so the mock server can serve the fixture response via prefix routing.
991    let call_prefix = if let Some(factory) = client_factory {
992        let factory_name = to_go_name(factory);
993        let fixture_id = &fixture.id;
994        // Determine how to express the API key and base URL for the client
995        // constructor call, depending on which code path was emitted above.
996        let (api_key_expr, base_url_expr) = if has_mock && api_key_var.is_some() {
997            // Env-fallback: local vars emitted above carry the right values.
998            ("apiKey".to_string(), "baseURL".to_string())
999        } else if api_key_var.is_some() {
1000            // Skip-unless-set: live API only, no mock fallback.
1001            ("apiKey".to_string(), "nil".to_string())
1002        } else if fixture.has_host_root_route() {
1003            let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1004            let _ = writeln!(out, "\tmockURL := os.Getenv(\"{env_key}\")");
1005            let _ = writeln!(out, "\tif mockURL == \"\" {{");
1006            let _ = writeln!(
1007                out,
1008                "\t\tmockURL = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1009            );
1010            let _ = writeln!(out, "\t}}");
1011            ("\"test-key\"".to_string(), "&mockURL".to_string())
1012        } else {
1013            let _ = writeln!(
1014                out,
1015                "\tmockURL := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
1016            );
1017            ("\"test-key\"".to_string(), "&mockURL".to_string())
1018        };
1019        let _ = writeln!(
1020            out,
1021            "\tclient, clientErr := {import_alias}.{factory_name}({api_key_expr}, {base_url_expr}, nil, nil, nil)"
1022        );
1023        let _ = writeln!(out, "\tif clientErr != nil {{");
1024        let _ = writeln!(out, "\t\tt.Fatalf(\"create client failed: %v\", clientErr)");
1025        let _ = writeln!(out, "\t}}");
1026        "client".to_string()
1027    } else {
1028        import_alias.to_string()
1029    };
1030
1031    // The Go binding generator wraps the FFI call in `(T, error)` whenever any
1032    // param requires JSON marshalling, even when the underlying Rust function
1033    // does not return Result. Detect that so error-expecting tests emit `_, err :=`
1034    // instead of `err :=` when the binding has a value component.
1035    let binding_returns_error_pre = args
1036        .iter()
1037        .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
1038    let effective_returns_result_pre = returns_result || binding_returns_error_pre || client_factory.is_some();
1039
1040    if expects_error {
1041        if effective_returns_result_pre && !returns_void {
1042            let _ = writeln!(out, "\t_, err := {call_prefix}.{function_name}({final_args})");
1043        } else {
1044            let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
1045        }
1046        let _ = writeln!(out, "\tif err == nil {{");
1047        let _ = writeln!(out, "\t\tt.Errorf(\"expected an error, but call succeeded\")");
1048        let _ = writeln!(out, "\t}}");
1049        let _ = writeln!(out, "}}");
1050        return;
1051    }
1052
1053    // Detect streaming fixtures (call-level `streaming` opt-out is honored).
1054    let is_streaming = crate::codegen::streaming_assertions::resolve_is_streaming(fixture, call_config.streaming);
1055
1056    // Check if any assertion actually uses the result variable.
1057    // If all assertions are skipped (field not on result type), use `_` to avoid
1058    // Go's "declared and not used" compile error.
1059    // For streaming fixtures: streaming virtual fields count as usable.
1060    let has_usable_assertion = fixture.assertions.iter().any(|a| {
1061        if a.assertion_type == "not_error" || a.assertion_type == "error" {
1062            return false;
1063        }
1064        // method_result assertions always use the result variable.
1065        if a.assertion_type == "method_result" {
1066            return true;
1067        }
1068        match &a.field {
1069            Some(f) if !f.is_empty() => {
1070                if is_streaming && crate::codegen::streaming_assertions::is_streaming_virtual_field(f) {
1071                    return true;
1072                }
1073                field_resolver.is_valid_for_result(f)
1074            }
1075            _ => true,
1076        }
1077    });
1078
1079    // The Go binding generator (alef-backend-go) wraps the FFI call in `(T, error)`
1080    // whenever any param requires JSON marshalling (Vec, Map, Named struct), even when
1081    // the underlying Rust function does not return Result. So a result_is_simple call
1082    // like `generate_cache_key(parts: &[(String, String)]) -> String` still surfaces in
1083    // Go as `func GenerateCacheKey(parts [][]string) (*string, error)`. Detect that
1084    // here so the test emits `_, err :=` / `result, err :=` instead of `result :=`.
1085    let binding_returns_error = args
1086        .iter()
1087        .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
1088    // Client-factory methods always return (value, error) in the Go binding.
1089    let effective_returns_result = returns_result || binding_returns_error || client_factory.is_some();
1090
1091    // For result_is_simple functions, the result variable IS the value (e.g. *string, *bool).
1092    // We create a local `value` that dereferences it so assertions can use a plain type.
1093    // For functions that return (value, error): emit `result, err :=`
1094    // For functions that return only error: emit `err :=`
1095    // For functions that return only a value (result_is_simple, no error): emit `result :=`
1096    if !effective_returns_result && result_is_simple {
1097        // Function returns a single value, no error (e.g. *string, *bool).
1098        let result_binding = if has_usable_assertion {
1099            result_var.to_string()
1100        } else {
1101            "_".to_string()
1102        };
1103        // In Go, `_ :=` is invalid — must use `_ =` for the blank identifier.
1104        let assign_op = if result_binding == "_" { "=" } else { ":=" };
1105        let _ = writeln!(
1106            out,
1107            "\t{result_binding} {assign_op} {call_prefix}.{function_name}({final_args})"
1108        );
1109        if has_usable_assertion && result_binding != "_" {
1110            if result_is_array {
1111                // Array results are slices (not pointers); assign directly without dereference.
1112                let _ = writeln!(out, "\tvalue := {result_var}");
1113            } else {
1114                // Check if ALL simple-result assertions are is_empty/is_null with no field.
1115                // If so, skip dereference — we'll use the pointer directly.
1116                let only_nil_assertions = fixture
1117                    .assertions
1118                    .iter()
1119                    .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1120                    .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1121                    .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1122
1123                if !only_nil_assertions {
1124                    // Emit nil check and dereference for simple pointer results.
1125                    let _ = writeln!(out, "\tif {result_var} == nil {{");
1126                    let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
1127                    let _ = writeln!(out, "\t}}");
1128                    let _ = writeln!(out, "\tvalue := *{result_var}");
1129                }
1130            }
1131        }
1132    } else if !effective_returns_result || returns_void {
1133        // Function returns only error (either returns_result=false, or returns_result=true
1134        // with returns_void=true meaning the Go function signature is `func(...) error`).
1135        let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
1136        let _ = writeln!(out, "\tif err != nil {{");
1137        let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
1138        let _ = writeln!(out, "\t}}");
1139        // No result variable to use in assertions.
1140        let _ = writeln!(out, "}}");
1141        return;
1142    } else {
1143        // returns_result = true, returns_void = false: function returns (value, error).
1144        // For streaming fixtures, always capture the channel as `stream`.
1145        let result_binding = if is_streaming {
1146            "stream".to_string()
1147        } else if has_usable_assertion {
1148            result_var.to_string()
1149        } else {
1150            "_".to_string()
1151        };
1152        let _ = writeln!(
1153            out,
1154            "\t{result_binding}, err := {call_prefix}.{function_name}({final_args})"
1155        );
1156        let _ = writeln!(out, "\tif err != nil {{");
1157        let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
1158        let _ = writeln!(out, "\t}}");
1159        // For streaming fixtures: drain the channel into a []T slice.
1160        if is_streaming {
1161            let _ = writeln!(out, "\tvar chunks []{import_alias}.ChatCompletionChunk");
1162            let _ = writeln!(out, "\tfor chunk := range stream {{");
1163            let _ = writeln!(out, "\t\tchunks = append(chunks, chunk)");
1164            let _ = writeln!(out, "\t}}");
1165        }
1166        if result_is_simple && has_usable_assertion && result_binding != "_" {
1167            if result_is_array {
1168                // Array results are slices (not pointers); assign directly without dereference.
1169                let _ = writeln!(out, "\tvalue := {}", result_var);
1170            } else {
1171                // Check if ALL simple-result assertions are is_empty/is_null with no field.
1172                // If so, skip dereference — we'll use the pointer directly.
1173                let only_nil_assertions = fixture
1174                    .assertions
1175                    .iter()
1176                    .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1177                    .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1178                    .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1179
1180                if !only_nil_assertions {
1181                    // Emit nil check and dereference for simple pointer results.
1182                    let _ = writeln!(out, "\tif {} == nil {{", result_var);
1183                    let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
1184                    let _ = writeln!(out, "\t}}");
1185                    let _ = writeln!(out, "\tvalue := *{}", result_var);
1186                }
1187            }
1188        }
1189    }
1190
1191    // For result_is_simple functions, determine if we created a dereferenced `value` variable.
1192    // We skip dereferencing if all simple-result assertions are is_empty/is_null with no field.
1193    let has_deref_value = if result_is_simple && has_usable_assertion && !result_is_array {
1194        let only_nil_assertions = fixture
1195            .assertions
1196            .iter()
1197            .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
1198            .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
1199            .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
1200        !only_nil_assertions
1201    } else {
1202        result_is_simple && has_usable_assertion
1203    };
1204
1205    let effective_result_var = if has_deref_value {
1206        "value".to_string()
1207    } else {
1208        result_var.to_string()
1209    };
1210
1211    // Collect optional fields referenced by assertions and emit nil-safe
1212    // dereference blocks so that assertions can use plain string locals.
1213    // Only dereference fields whose assertion values are strings (or that are
1214    // used in string-oriented assertions like equals/contains with string values).
1215    let mut optional_locals: std::collections::HashMap<String, String> = std::collections::HashMap::new();
1216    for assertion in &fixture.assertions {
1217        if let Some(f) = &assertion.field {
1218            if !f.is_empty() {
1219                let resolved = field_resolver.resolve(f);
1220                if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
1221                    // Only create deref locals for string-valued fields that are NOT arrays.
1222                    // Array fields (e.g., *[]string) must keep their pointer form so
1223                    // render_assertion can emit strings.Join(*field, " ") rather than
1224                    // treating them as plain strings.
1225                    let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
1226                    let is_array_field = field_resolver.is_array(resolved);
1227                    if !is_string_field || is_array_field {
1228                        // Non-string optional fields (e.g., *uint64) and array optional
1229                        // fields (e.g., *[]string) are handled by nil guards in render_assertion.
1230                        continue;
1231                    }
1232                    let field_expr = field_resolver.accessor(f, "go", &effective_result_var);
1233                    let local_var = go_param_name(&resolved.replace(['.', '[', ']'], "_"));
1234                    if field_resolver.has_map_access(f) {
1235                        // Go map access returns a value type (string), not a pointer.
1236                        // Use the value directly — empty string means not present.
1237                        let _ = writeln!(out, "\t{local_var} := {field_expr}");
1238                    } else {
1239                        let _ = writeln!(out, "\tvar {local_var} string");
1240                        let _ = writeln!(out, "\tif {field_expr} != nil {{");
1241                        // Use string() cast to handle named string types (e.g. *FinishReason) in
1242                        // addition to plain *string fields — string(*ptr) is a no-op for *string
1243                        // and a safe coercion for any named type whose underlying type is string.
1244                        let _ = writeln!(out, "\t\t{local_var} = string(*{field_expr})");
1245                        let _ = writeln!(out, "\t}}");
1246                    }
1247                    optional_locals.insert(f.clone(), local_var);
1248                }
1249            }
1250        }
1251    }
1252
1253    // Emit assertions, wrapping in nil guards when an intermediate path segment is optional.
1254    for assertion in &fixture.assertions {
1255        if let Some(f) = &assertion.field {
1256            if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
1257                // Check if any prefix of the dotted path is optional (pointer in Go).
1258                // e.g., "document.nodes" — if "document" is optional, guard the whole block.
1259                let parts: Vec<&str> = f.split('.').collect();
1260                let mut guard_expr: Option<String> = None;
1261                for i in 1..parts.len() {
1262                    let prefix = parts[..i].join(".");
1263                    let resolved_prefix = field_resolver.resolve(&prefix);
1264                    if field_resolver.is_optional(resolved_prefix) {
1265                        // If the prefix ends with a numeric index (e.g. "segments[0]"),
1266                        // the element itself is a value type in Go — it cannot be nil.
1267                        // Use the array field without the index (e.g. "segments") as the
1268                        // nil guard instead, so we emit `result.Segments != nil` rather
1269                        // than the invalid `result.Segments[0] != nil`.
1270                        let guard_prefix = if let Some(bracket_pos) = resolved_prefix.rfind('[') {
1271                            let suffix = &resolved_prefix[bracket_pos + 1..];
1272                            let is_numeric_index = suffix.trim_end_matches(']').chars().all(|c| c.is_ascii_digit());
1273                            if is_numeric_index {
1274                                &resolved_prefix[..bracket_pos]
1275                            } else {
1276                                resolved_prefix
1277                            }
1278                        } else {
1279                            resolved_prefix
1280                        };
1281                        let accessor = field_resolver.accessor(guard_prefix, "go", &effective_result_var);
1282                        guard_expr = Some(accessor);
1283                        break;
1284                    }
1285                }
1286                if let Some(guard) = guard_expr {
1287                    // Only emit nil guard if the assertion will actually produce code
1288                    // (not just a skip comment), to avoid empty branches (SA9003).
1289                    if field_resolver.is_valid_for_result(f) {
1290                        let _ = writeln!(out, "\tif {guard} != nil {{");
1291                        // Render into a temporary buffer so we can re-indent by one
1292                        // tab level to sit inside the nil-guard block.
1293                        let mut nil_buf = String::new();
1294                        render_assertion(
1295                            &mut nil_buf,
1296                            assertion,
1297                            &effective_result_var,
1298                            import_alias,
1299                            field_resolver,
1300                            &optional_locals,
1301                            result_is_simple,
1302                            result_is_array,
1303                            is_streaming,
1304                        );
1305                        for line in nil_buf.lines() {
1306                            let _ = writeln!(out, "\t{line}");
1307                        }
1308                        let _ = writeln!(out, "\t}}");
1309                    } else {
1310                        render_assertion(
1311                            out,
1312                            assertion,
1313                            &effective_result_var,
1314                            import_alias,
1315                            field_resolver,
1316                            &optional_locals,
1317                            result_is_simple,
1318                            result_is_array,
1319                            is_streaming,
1320                        );
1321                    }
1322                    continue;
1323                }
1324            }
1325        }
1326        render_assertion(
1327            out,
1328            assertion,
1329            &effective_result_var,
1330            import_alias,
1331            field_resolver,
1332            &optional_locals,
1333            result_is_simple,
1334            result_is_array,
1335            is_streaming,
1336        );
1337    }
1338
1339    let _ = writeln!(out, "}}");
1340}
1341
1342/// Render an HTTP server test function using net/http against MOCK_SERVER_URL.
1343///
1344/// Delegates to the shared driver [`client::http_call::render_http_test`] via
1345/// [`GoTestClientRenderer`]. The emitted test shape is unchanged: `func Test_<Name>(t *testing.T)`
1346/// with a `net/http` client that hits `$MOCK_SERVER_URL/fixtures/<id>`.
1347fn render_http_test_function(out: &mut String, fixture: &Fixture) {
1348    client::http_call::render_http_test(out, &GoTestClientRenderer, fixture);
1349}
1350
1351// ---------------------------------------------------------------------------
1352// HTTP test rendering — GoTestClientRenderer
1353// ---------------------------------------------------------------------------
1354
1355/// Go `net/http` test renderer.
1356///
1357/// Go HTTP e2e tests send a request to `$MOCK_SERVER_URL/fixtures/<id>` using
1358/// the standard library `net/http` client. The trait primitives emit the
1359/// request-build, response-capture, and assertion code that the previous
1360/// monolithic renderer produced, so generated output is unchanged after the
1361/// migration.
1362struct GoTestClientRenderer;
1363
1364impl client::TestClientRenderer for GoTestClientRenderer {
1365    fn language_name(&self) -> &'static str {
1366        "go"
1367    }
1368
1369    /// Go test names use `UpperCamelCase` so they form valid exported identifiers
1370    /// (e.g. `Test_MyFixtureId`). Override the default `sanitize_ident` which
1371    /// produces `lower_snake_case`.
1372    fn sanitize_test_name(&self, id: &str) -> String {
1373        id.to_upper_camel_case()
1374    }
1375
1376    /// Emit `func Test_<fn_name>(t *testing.T) {`, a description comment, and the
1377    /// `baseURL` / request scaffolding. Skipped fixtures get `t.Skip(...)` inline.
1378    fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
1379        let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
1380        let _ = writeln!(out, "\t// {description}");
1381        if let Some(reason) = skip_reason {
1382            let escaped = go_string_literal(reason);
1383            let _ = writeln!(out, "\tt.Skip({escaped})");
1384        }
1385    }
1386
1387    fn render_test_close(&self, out: &mut String) {
1388        let _ = writeln!(out, "}}");
1389    }
1390
1391    /// Emit the full `net/http` request scaffolding: URL construction, body,
1392    /// headers, cookies, a no-redirect client, and `io.ReadAll` for the body.
1393    ///
1394    /// `bodyBytes` is always declared (with `_ = bodyBytes` to avoid the Go
1395    /// "declared and not used" compile error on tests with no body assertion).
1396    fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
1397        let method = ctx.method.to_uppercase();
1398        let path = ctx.path;
1399
1400        let _ = writeln!(out, "\tbaseURL := os.Getenv(\"MOCK_SERVER_URL\")");
1401        let _ = writeln!(out, "\tif baseURL == \"\" {{");
1402        let _ = writeln!(out, "\t\tbaseURL = \"http://localhost:8080\"");
1403        let _ = writeln!(out, "\t}}");
1404
1405        // Build request body expression.
1406        let body_expr = if let Some(body) = ctx.body {
1407            let json = serde_json::to_string(body).unwrap_or_default();
1408            let escaped = go_string_literal(&json);
1409            format!("strings.NewReader({})", escaped)
1410        } else {
1411            "strings.NewReader(\"\")".to_string()
1412        };
1413
1414        let _ = writeln!(out, "\tbody := {body_expr}");
1415        let _ = writeln!(
1416            out,
1417            "\treq, err := http.NewRequest(\"{method}\", baseURL+\"{path}\", body)"
1418        );
1419        let _ = writeln!(out, "\tif err != nil {{");
1420        let _ = writeln!(out, "\t\tt.Fatalf(\"new request failed: %v\", err)");
1421        let _ = writeln!(out, "\t}}");
1422
1423        // Content-Type header (only when a body is present).
1424        if ctx.body.is_some() {
1425            let content_type = ctx.content_type.unwrap_or("application/json");
1426            let _ = writeln!(out, "\treq.Header.Set(\"Content-Type\", \"{content_type}\")");
1427        }
1428
1429        // Explicit request headers (sorted for deterministic output).
1430        let mut header_names: Vec<&String> = ctx.headers.keys().collect();
1431        header_names.sort();
1432        for name in header_names {
1433            let value = &ctx.headers[name];
1434            let escaped_name = go_string_literal(name);
1435            let escaped_value = go_string_literal(value);
1436            let _ = writeln!(out, "\treq.Header.Set({escaped_name}, {escaped_value})");
1437        }
1438
1439        // Cookies.
1440        if !ctx.cookies.is_empty() {
1441            let mut cookie_names: Vec<&String> = ctx.cookies.keys().collect();
1442            cookie_names.sort();
1443            for name in cookie_names {
1444                let value = &ctx.cookies[name];
1445                let escaped_name = go_string_literal(name);
1446                let escaped_value = go_string_literal(value);
1447                let _ = writeln!(
1448                    out,
1449                    "\treq.AddCookie(&http.Cookie{{Name: {escaped_name}, Value: {escaped_value}}})"
1450                );
1451            }
1452        }
1453
1454        // No-redirect client so 3xx fixtures assert the redirect response itself.
1455        let _ = writeln!(out, "\tnoRedirectClient := &http.Client{{");
1456        let _ = writeln!(
1457            out,
1458            "\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {{"
1459        );
1460        let _ = writeln!(out, "\t\t\treturn http.ErrUseLastResponse");
1461        let _ = writeln!(out, "\t\t}},");
1462        let _ = writeln!(out, "\t}}");
1463        let _ = writeln!(out, "\tresp, err := noRedirectClient.Do(req)");
1464        let _ = writeln!(out, "\tif err != nil {{");
1465        let _ = writeln!(out, "\t\tt.Fatalf(\"request failed: %v\", err)");
1466        let _ = writeln!(out, "\t}}");
1467        let _ = writeln!(out, "\tdefer resp.Body.Close()");
1468
1469        // Always read the response body so body-assertion methods can reference
1470        // `bodyBytes`. Suppress the "declared and not used" compile error with
1471        // `_ = bodyBytes` for tests that have no body assertion.
1472        let _ = writeln!(out, "\tbodyBytes, err := io.ReadAll(resp.Body)");
1473        let _ = writeln!(out, "\tif err != nil {{");
1474        let _ = writeln!(out, "\t\tt.Fatalf(\"read body failed: %v\", err)");
1475        let _ = writeln!(out, "\t}}");
1476        let _ = writeln!(out, "\t_ = bodyBytes");
1477    }
1478
1479    fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
1480        let _ = writeln!(out, "\tif resp.StatusCode != {status} {{");
1481        let _ = writeln!(out, "\t\tt.Fatalf(\"status: got %d want {status}\", resp.StatusCode)");
1482        let _ = writeln!(out, "\t}}");
1483    }
1484
1485    /// Emit a header assertion, skipping special tokens (`<<present>>`, `<<absent>>`,
1486    /// `<<uuid>>`) and hop-by-hop headers (`Connection`) that `net/http` strips.
1487    fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
1488        // Skip special-token assertions.
1489        if matches!(expected, "<<absent>>" | "<<present>>" | "<<uuid>>") {
1490            return;
1491        }
1492        // Connection is a hop-by-hop header that Go's net/http strips.
1493        if name.eq_ignore_ascii_case("connection") {
1494            return;
1495        }
1496        let escaped_name = go_string_literal(name);
1497        let escaped_value = go_string_literal(expected);
1498        let _ = writeln!(
1499            out,
1500            "\tif !strings.Contains(resp.Header.Get({escaped_name}), {escaped_value}) {{"
1501        );
1502        let _ = writeln!(
1503            out,
1504            "\t\tt.Fatalf(\"header %s mismatch: got %q want to contain %q\", {escaped_name}, resp.Header.Get({escaped_name}), {escaped_value})"
1505        );
1506        let _ = writeln!(out, "\t}}");
1507    }
1508
1509    /// Emit an exact-equality body assertion.
1510    ///
1511    /// JSON objects and arrays are round-tripped via `json.Unmarshal` + `reflect.DeepEqual`.
1512    /// Scalar values are compared as trimmed strings.
1513    fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1514        match expected {
1515            serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
1516                let json_str = serde_json::to_string(expected).unwrap_or_default();
1517                let escaped = go_string_literal(&json_str);
1518                let _ = writeln!(out, "\tvar got any");
1519                let _ = writeln!(out, "\tvar want any");
1520                let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &got); err != nil {{");
1521                let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal got: %v\", err)");
1522                let _ = writeln!(out, "\t}}");
1523                let _ = writeln!(
1524                    out,
1525                    "\tif err := json.Unmarshal([]byte({escaped}), &want); err != nil {{"
1526                );
1527                let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal want: %v\", err)");
1528                let _ = writeln!(out, "\t}}");
1529                let _ = writeln!(out, "\tif !reflect.DeepEqual(got, want) {{");
1530                let _ = writeln!(out, "\t\tt.Fatalf(\"body mismatch: got %v want %v\", got, want)");
1531                let _ = writeln!(out, "\t}}");
1532            }
1533            serde_json::Value::String(s) => {
1534                let escaped = go_string_literal(s);
1535                let _ = writeln!(out, "\twant := {escaped}");
1536                let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1537                let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1538                let _ = writeln!(out, "\t}}");
1539            }
1540            other => {
1541                let escaped = go_string_literal(&other.to_string());
1542                let _ = writeln!(out, "\twant := {escaped}");
1543                let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
1544                let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
1545                let _ = writeln!(out, "\t}}");
1546            }
1547        }
1548    }
1549
1550    /// Emit partial-body assertions: every key in `expected` must appear in the
1551    /// parsed JSON response with the matching value.
1552    fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
1553        if let Some(obj) = expected.as_object() {
1554            let _ = writeln!(out, "\tvar _partialGot map[string]any");
1555            let _ = writeln!(
1556                out,
1557                "\tif err := json.Unmarshal(bodyBytes, &_partialGot); err != nil {{"
1558            );
1559            let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal partial: %v\", err)");
1560            let _ = writeln!(out, "\t}}");
1561            for (key, val) in obj {
1562                let escaped_key = go_string_literal(key);
1563                let json_val = serde_json::to_string(val).unwrap_or_default();
1564                let escaped_val = go_string_literal(&json_val);
1565                let _ = writeln!(out, "\t{{");
1566                let _ = writeln!(out, "\t\tvar _wantVal any");
1567                let _ = writeln!(
1568                    out,
1569                    "\t\tif err := json.Unmarshal([]byte({escaped_val}), &_wantVal); err != nil {{"
1570                );
1571                let _ = writeln!(out, "\t\t\tt.Fatalf(\"json unmarshal partial want: %v\", err)");
1572                let _ = writeln!(out, "\t\t}}");
1573                let _ = writeln!(
1574                    out,
1575                    "\t\tif !reflect.DeepEqual(_partialGot[{escaped_key}], _wantVal) {{"
1576                );
1577                let _ = writeln!(
1578                    out,
1579                    "\t\t\tt.Fatalf(\"partial body field {key}: got %v want %v\", _partialGot[{escaped_key}], _wantVal)"
1580                );
1581                let _ = writeln!(out, "\t\t}}");
1582                let _ = writeln!(out, "\t}}");
1583            }
1584        }
1585    }
1586
1587    /// Emit validation-error assertions for 422 responses.
1588    ///
1589    /// Checks that each expected `msg` appears in at least one element of the
1590    /// parsed body's `"errors"` array.
1591    fn render_assert_validation_errors(
1592        &self,
1593        out: &mut String,
1594        _response_var: &str,
1595        errors: &[ValidationErrorExpectation],
1596    ) {
1597        let _ = writeln!(out, "\tvar _veBody map[string]any");
1598        let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &_veBody); err != nil {{");
1599        let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal validation errors: %v\", err)");
1600        let _ = writeln!(out, "\t}}");
1601        let _ = writeln!(out, "\t_veErrors, _ := _veBody[\"errors\"].([]any)");
1602        for ve in errors {
1603            let escaped_msg = go_string_literal(&ve.msg);
1604            let _ = writeln!(out, "\t{{");
1605            let _ = writeln!(out, "\t\t_found := false");
1606            let _ = writeln!(out, "\t\tfor _, _e := range _veErrors {{");
1607            let _ = writeln!(out, "\t\t\tif _em, ok := _e.(map[string]any); ok {{");
1608            let _ = writeln!(
1609                out,
1610                "\t\t\t\tif _msg, ok := _em[\"msg\"].(string); ok && strings.Contains(_msg, {escaped_msg}) {{"
1611            );
1612            let _ = writeln!(out, "\t\t\t\t\t_found = true");
1613            let _ = writeln!(out, "\t\t\t\t\tbreak");
1614            let _ = writeln!(out, "\t\t\t\t}}");
1615            let _ = writeln!(out, "\t\t\t}}");
1616            let _ = writeln!(out, "\t\t}}");
1617            let _ = writeln!(out, "\t\tif !_found {{");
1618            let _ = writeln!(
1619                out,
1620                "\t\t\tt.Fatalf(\"validation error with msg containing %q not found in errors\", {escaped_msg})"
1621            );
1622            let _ = writeln!(out, "\t\t}}");
1623            let _ = writeln!(out, "\t}}");
1624        }
1625    }
1626}
1627
1628/// Build setup lines (e.g. handle creation) and the argument list for the function call.
1629///
1630/// Returns `(setup_lines, args_string)`.
1631///
1632/// `options_ptr` — when `true`, `json_object` args with an `options_type` are
1633/// passed as a Go pointer (`*OptionsType`): absent/empty → `nil`, present →
1634/// `&varName` after JSON unmarshal.
1635fn build_args_and_setup(
1636    input: &serde_json::Value,
1637    args: &[crate::config::ArgMapping],
1638    import_alias: &str,
1639    options_type: Option<&str>,
1640    fixture: &crate::fixture::Fixture,
1641    options_ptr: bool,
1642    expects_error: bool,
1643) -> (Vec<String>, String) {
1644    let fixture_id = &fixture.id;
1645    use heck::ToUpperCamelCase;
1646
1647    if args.is_empty() {
1648        return (Vec::new(), String::new());
1649    }
1650
1651    let mut setup_lines: Vec<String> = Vec::new();
1652    let mut parts: Vec<String> = Vec::new();
1653
1654    for arg in args {
1655        if arg.arg_type == "mock_url" {
1656            if fixture.has_host_root_route() {
1657                let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
1658                setup_lines.push(format!("{} := os.Getenv(\"{env_key}\")", arg.name));
1659                setup_lines.push(format!(
1660                    "if {} == \"\" {{ {} = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\" }}",
1661                    arg.name, arg.name
1662                ));
1663            } else {
1664                setup_lines.push(format!(
1665                    "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
1666                    arg.name,
1667                ));
1668            }
1669            parts.push(arg.name.clone());
1670            continue;
1671        }
1672
1673        if arg.arg_type == "handle" {
1674            // Generate a CreateEngine (or equivalent) call and pass the variable.
1675            let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
1676            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1677            let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
1678            // When the fixture expects an error (validation test), engine creation
1679            // is the error source. Assert the error and return so the test passes
1680            // without proceeding to the (unreachable) function call.
1681            let create_err_handler = if expects_error {
1682                "assert.Error(t, createErr)\n\t\treturn".to_string()
1683            } else {
1684                "t.Fatalf(\"create handle failed: %v\", createErr)".to_string()
1685            };
1686            if config_value.is_null()
1687                || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
1688            {
1689                setup_lines.push(format!(
1690                    "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}",
1691                    name = arg.name,
1692                ));
1693            } else {
1694                let json_str = serde_json::to_string(config_value).unwrap_or_default();
1695                let go_literal = go_string_literal(&json_str);
1696                let name = &arg.name;
1697                setup_lines.push(format!(
1698                    "var {name}Config {import_alias}.CrawlConfig\n\tif err := json.Unmarshal([]byte({go_literal}), &{name}Config); err != nil {{\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}}"
1699                ));
1700                setup_lines.push(format!(
1701                    "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}"
1702                ));
1703            }
1704            parts.push(arg.name.clone());
1705            continue;
1706        }
1707
1708        let val: Option<&serde_json::Value> = if arg.field == "input" {
1709            Some(input)
1710        } else {
1711            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
1712            input.get(field)
1713        };
1714
1715        // file_path args are fixture-relative paths under `test_documents/`. The Go test
1716        // runner's TestMain (in main_test.go) already does `os.Chdir(test_documents)` so
1717        // tests can pass these relative strings directly; no additional resolution needed.
1718
1719        // Handle bytes type: fixture stores base64-encoded bytes.
1720        // Emit a Go base64.StdEncoding.DecodeString call to decode at runtime.
1721        if arg.arg_type == "bytes" {
1722            let var_name = format!("{}Bytes", arg.name);
1723            match val {
1724                None | Some(serde_json::Value::Null) => {
1725                    if arg.optional {
1726                        parts.push("nil".to_string());
1727                    } else {
1728                        parts.push("[]byte{}".to_string());
1729                    }
1730                }
1731                Some(serde_json::Value::String(s)) => {
1732                    // Bytes args whose fixture value is a string are fixture-relative paths into
1733                    // the repo-root `test_documents/` directory (matching the rust e2e codegen
1734                    // convention). The Go test runner's TestMain chdirs into test_documents/, so
1735                    // a bare relative path resolves correctly via os.ReadFile.
1736                    let go_path = go_string_literal(s);
1737                    setup_lines.push(format!(
1738                        "{var_name}, {var_name}Err := os.ReadFile({go_path})\n\tif {var_name}Err != nil {{\n\t\tt.Fatalf(\"read fixture {s}: %v\", {var_name}Err)\n\t}}"
1739                    ));
1740                    parts.push(var_name);
1741                }
1742                Some(other) => {
1743                    parts.push(format!("[]byte({})", json_to_go(other)));
1744                }
1745            }
1746            continue;
1747        }
1748
1749        match val {
1750            None | Some(serde_json::Value::Null) if arg.optional => {
1751                // Optional arg absent: emit Go zero/nil for the type.
1752                match arg.arg_type.as_str() {
1753                    "string" => {
1754                        // Optional string in Go bindings is *string → nil.
1755                        parts.push("nil".to_string());
1756                    }
1757                    "json_object" => {
1758                        if options_ptr {
1759                            // Pointer options type (*OptionsType): absent → nil.
1760                            parts.push("nil".to_string());
1761                        } else if let Some(opts_type) = options_type {
1762                            // Value options type: zero-value struct.
1763                            parts.push(format!("{import_alias}.{opts_type}{{}}"));
1764                        } else {
1765                            parts.push("nil".to_string());
1766                        }
1767                    }
1768                    _ => {
1769                        parts.push("nil".to_string());
1770                    }
1771                }
1772            }
1773            None | Some(serde_json::Value::Null) => {
1774                // Required arg with no fixture value: pass a language-appropriate default.
1775                let default_val = match arg.arg_type.as_str() {
1776                    "string" => "\"\"".to_string(),
1777                    "int" | "integer" | "i64" => "0".to_string(),
1778                    "float" | "number" => "0.0".to_string(),
1779                    "bool" | "boolean" => "false".to_string(),
1780                    "json_object" => {
1781                        if options_ptr {
1782                            // Pointer options type (*OptionsType): absent → nil.
1783                            "nil".to_string()
1784                        } else if let Some(opts_type) = options_type {
1785                            format!("{import_alias}.{opts_type}{{}}")
1786                        } else {
1787                            "nil".to_string()
1788                        }
1789                    }
1790                    _ => "nil".to_string(),
1791                };
1792                parts.push(default_val);
1793            }
1794            Some(v) => {
1795                match arg.arg_type.as_str() {
1796                    "json_object" => {
1797                        // JSON arrays unmarshal into []string (Go slices).
1798                        // JSON objects with a known options_type unmarshal into that type.
1799                        let is_array = v.is_array();
1800                        let is_empty_obj = !is_array && v.is_object() && v.as_object().is_some_and(|o| o.is_empty());
1801                        if is_empty_obj {
1802                            if options_ptr {
1803                                // Pointer options type: empty object → nil.
1804                                parts.push("nil".to_string());
1805                            } else if let Some(opts_type) = options_type {
1806                                parts.push(format!("{import_alias}.{opts_type}{{}}"));
1807                            } else {
1808                                parts.push("nil".to_string());
1809                            }
1810                        } else if is_array {
1811                            // Array type — unmarshal into a Go slice. Honor `go_type` for a
1812                            // fully explicit Go type (e.g. `"kreuzberg.BatchBytesItem"`), fall
1813                            // back to deriving the slice type from `element_type`, defaulting
1814                            // to `[]string` for unknown types.
1815                            let go_slice_type = if let Some(go_t) = arg.go_type.as_deref() {
1816                                // go_type is the slice element type — wrap it in [].
1817                                // If it already starts with '[' the user specified the full
1818                                // slice type; use it verbatim.
1819                                if go_t.starts_with('[') {
1820                                    go_t.to_string()
1821                                } else {
1822                                    // Qualify unqualified types (e.g., "BatchBytesItem" → "kreuzberg.BatchBytesItem")
1823                                    let qualified = if go_t.contains('.') {
1824                                        go_t.to_string()
1825                                    } else {
1826                                        format!("{import_alias}.{go_t}")
1827                                    };
1828                                    format!("[]{qualified}")
1829                                }
1830                            } else {
1831                                element_type_to_go_slice(arg.element_type.as_deref(), import_alias)
1832                            };
1833                            // Convert JSON for Go compatibility (e.g., byte arrays → base64 strings)
1834                            let converted_v = convert_json_for_go(v.clone());
1835                            let json_str = serde_json::to_string(&converted_v).unwrap_or_default();
1836                            let go_literal = go_string_literal(&json_str);
1837                            let var_name = &arg.name;
1838                            setup_lines.push(format!(
1839                                "var {var_name} {go_slice_type}\n\tif err := json.Unmarshal([]byte({go_literal}), &{var_name}); err != nil {{\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}}"
1840                            ));
1841                            parts.push(var_name.to_string());
1842                        } else if let Some(opts_type) = options_type {
1843                            // Object with known type — unmarshal into typed struct.
1844                            // When options_ptr is set, the Go struct uses snake_case JSON
1845                            // field tags and lowercase/snake_case enum values.  Remap the
1846                            // fixture's camelCase keys and PascalCase enum string values.
1847                            let remapped_v = if options_ptr {
1848                                convert_json_for_go(v.clone())
1849                            } else {
1850                                v.clone()
1851                            };
1852                            let json_str = serde_json::to_string(&remapped_v).unwrap_or_default();
1853                            let go_literal = go_string_literal(&json_str);
1854                            let var_name = &arg.name;
1855                            setup_lines.push(format!(
1856                                "var {var_name} {import_alias}.{opts_type}\n\tif err := json.Unmarshal([]byte({go_literal}), &{var_name}); err != nil {{\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}}"
1857                            ));
1858                            // Pass as pointer when options_ptr is set.
1859                            let arg_expr = if options_ptr {
1860                                format!("&{var_name}")
1861                            } else {
1862                                var_name.to_string()
1863                            };
1864                            parts.push(arg_expr);
1865                        } else {
1866                            parts.push(json_to_go(v));
1867                        }
1868                    }
1869                    "string" if arg.optional => {
1870                        // Optional string in Go is *string — take address of a local.
1871                        let var_name = format!("{}Val", arg.name);
1872                        let go_val = json_to_go(v);
1873                        setup_lines.push(format!("{var_name} := {go_val}"));
1874                        parts.push(format!("&{var_name}"));
1875                    }
1876                    _ => {
1877                        parts.push(json_to_go(v));
1878                    }
1879                }
1880            }
1881        }
1882    }
1883
1884    (setup_lines, parts.join(", "))
1885}
1886
1887#[allow(clippy::too_many_arguments)]
1888fn render_assertion(
1889    out: &mut String,
1890    assertion: &Assertion,
1891    result_var: &str,
1892    import_alias: &str,
1893    field_resolver: &FieldResolver,
1894    optional_locals: &std::collections::HashMap<String, String>,
1895    result_is_simple: bool,
1896    result_is_array: bool,
1897    is_streaming: bool,
1898) {
1899    // Handle synthetic / derived fields before the is_valid_for_result check
1900    // so they are never treated as struct field accesses on the result.
1901    if !result_is_simple {
1902        if let Some(f) = &assertion.field {
1903            // embed_texts returns *[][]float32; the embedding matrix is *result_var.
1904            // We emit inline func() expressions so we don't need additional variables.
1905            let embed_deref = format!("(*{result_var})");
1906            match f.as_str() {
1907                "chunks_have_content" => {
1908                    let pred = format!(
1909                        "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range *chunks {{ if c.Content == \"\" {{ return false }} }}; return true }}()"
1910                    );
1911                    match assertion.assertion_type.as_str() {
1912                        "is_true" => {
1913                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1914                        }
1915                        "is_false" => {
1916                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1917                        }
1918                        _ => {
1919                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1920                        }
1921                    }
1922                    return;
1923                }
1924                "chunks_have_embeddings" => {
1925                    let pred = format!(
1926                        "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range *chunks {{ if c.Embedding == nil || len(*c.Embedding) == 0 {{ return false }} }}; return true }}()"
1927                    );
1928                    match assertion.assertion_type.as_str() {
1929                        "is_true" => {
1930                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1931                        }
1932                        "is_false" => {
1933                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1934                        }
1935                        _ => {
1936                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1937                        }
1938                    }
1939                    return;
1940                }
1941                "embeddings" => {
1942                    match assertion.assertion_type.as_str() {
1943                        "count_equals" => {
1944                            if let Some(val) = &assertion.value {
1945                                if let Some(n) = val.as_u64() {
1946                                    let _ = writeln!(
1947                                        out,
1948                                        "\tassert.Equal(t, {n}, len({embed_deref}), \"expected exactly {n} elements\")"
1949                                    );
1950                                }
1951                            }
1952                        }
1953                        "count_min" => {
1954                            if let Some(val) = &assertion.value {
1955                                if let Some(n) = val.as_u64() {
1956                                    let _ = writeln!(
1957                                        out,
1958                                        "\tassert.GreaterOrEqual(t, len({embed_deref}), {n}, \"expected at least {n} elements\")"
1959                                    );
1960                                }
1961                            }
1962                        }
1963                        "not_empty" => {
1964                            let _ = writeln!(
1965                                out,
1966                                "\tassert.NotEmpty(t, {embed_deref}, \"expected non-empty embeddings\")"
1967                            );
1968                        }
1969                        "is_empty" => {
1970                            let _ = writeln!(out, "\tassert.Empty(t, {embed_deref}, \"expected empty embeddings\")");
1971                        }
1972                        _ => {
1973                            let _ = writeln!(
1974                                out,
1975                                "\t// skipped: unsupported assertion type on synthetic field 'embeddings'"
1976                            );
1977                        }
1978                    }
1979                    return;
1980                }
1981                "embedding_dimensions" => {
1982                    let expr = format!(
1983                        "func() int {{ if len({embed_deref}) == 0 {{ return 0 }}; return len({embed_deref}[0]) }}()"
1984                    );
1985                    match assertion.assertion_type.as_str() {
1986                        "equals" => {
1987                            if let Some(val) = &assertion.value {
1988                                if let Some(n) = val.as_u64() {
1989                                    let _ = writeln!(
1990                                        out,
1991                                        "\tif {expr} != {n} {{\n\t\tt.Errorf(\"equals mismatch: got %v\", {expr})\n\t}}"
1992                                    );
1993                                }
1994                            }
1995                        }
1996                        "greater_than" => {
1997                            if let Some(val) = &assertion.value {
1998                                if let Some(n) = val.as_u64() {
1999                                    let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
2000                                }
2001                            }
2002                        }
2003                        _ => {
2004                            let _ = writeln!(
2005                                out,
2006                                "\t// skipped: unsupported assertion type on synthetic field 'embedding_dimensions'"
2007                            );
2008                        }
2009                    }
2010                    return;
2011                }
2012                "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
2013                    let pred = match f.as_str() {
2014                        "embeddings_valid" => {
2015                            format!(
2016                                "func() bool {{ for _, e := range {embed_deref} {{ if len(e) == 0 {{ return false }} }}; return true }}()"
2017                            )
2018                        }
2019                        "embeddings_finite" => {
2020                            format!(
2021                                "func() bool {{ for _, e := range {embed_deref} {{ for _, v := range e {{ if v != v || v == float32(1.0/0.0) || v == float32(-1.0/0.0) {{ return false }} }} }}; return true }}()"
2022                            )
2023                        }
2024                        "embeddings_non_zero" => {
2025                            format!(
2026                                "func() bool {{ for _, e := range {embed_deref} {{ hasNonZero := false; for _, v := range e {{ if v != 0 {{ hasNonZero = true; break }} }}; if !hasNonZero {{ return false }} }}; return true }}()"
2027                            )
2028                        }
2029                        "embeddings_normalized" => {
2030                            format!(
2031                                "func() bool {{ for _, e := range {embed_deref} {{ var n float64; for _, v := range e {{ n += float64(v) * float64(v) }}; if n < 0.999 || n > 1.001 {{ return false }} }}; return true }}()"
2032                            )
2033                        }
2034                        _ => unreachable!(),
2035                    };
2036                    match assertion.assertion_type.as_str() {
2037                        "is_true" => {
2038                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
2039                        }
2040                        "is_false" => {
2041                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
2042                        }
2043                        _ => {
2044                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
2045                        }
2046                    }
2047                    return;
2048                }
2049                // ---- keywords / keywords_count ----
2050                // Go ExtractionResult does not expose extracted_keywords; skip.
2051                "keywords" | "keywords_count" => {
2052                    let _ = writeln!(out, "\t// skipped: field '{f}' not available on Go ExtractionResult");
2053                    return;
2054                }
2055                _ => {}
2056            }
2057        }
2058    }
2059
2060    // Streaming virtual fields: intercept before is_valid_for_result so they are
2061    // never skipped.  These fields resolve against the `chunks` collected-list variable.
2062    // Skip the streaming interception entirely when the call has opted out
2063    // (`[e2e.calls.<name>] streaming = false`) — `chunks` then names a plain
2064    // field on the synchronous result struct and must flow through normal
2065    // accessor resolution (e.g. `result.Chunks`).
2066    if !result_is_simple && is_streaming {
2067        if let Some(f) = &assertion.field {
2068            if !f.is_empty() && crate::codegen::streaming_assertions::is_streaming_virtual_field(f) {
2069                if let Some(expr) =
2070                    crate::codegen::streaming_assertions::StreamingFieldResolver::accessor(f, "go", "chunks")
2071                {
2072                    match assertion.assertion_type.as_str() {
2073                        "count_min" => {
2074                            if let Some(val) = &assertion.value {
2075                                if let Some(n) = val.as_u64() {
2076                                    let _ = writeln!(
2077                                        out,
2078                                        "\tassert.GreaterOrEqual(t, len({expr}), {n}, \"expected >= {n} chunks\")"
2079                                    );
2080                                }
2081                            }
2082                        }
2083                        "count_equals" => {
2084                            if let Some(val) = &assertion.value {
2085                                if let Some(n) = val.as_u64() {
2086                                    let _ = writeln!(
2087                                        out,
2088                                        "\tassert.Equal(t, {n}, len({expr}), \"expected exactly {n} chunks\")"
2089                                    );
2090                                }
2091                            }
2092                        }
2093                        "equals" => {
2094                            if let Some(serde_json::Value::String(s)) = &assertion.value {
2095                                let escaped = crate::escape::go_string_literal(s);
2096                                // Deep-path streaming-virtual fields like `tool_calls[0].function.name`
2097                                // resolve to pointer-typed Go fields (`*string`). The flat virtual
2098                                // accessors `stream_content` / `finish_reason` already return `string`.
2099                                // Wrap only the deep-path case in a safe-deref IIFE.
2100                                let is_deep_path = f.contains('.') || f.contains('[');
2101                                let safe_expr = if is_deep_path {
2102                                    format!(
2103                                        "func() string {{ v := {expr}; if v == nil {{ return \"\" }}; return *v }}()"
2104                                    )
2105                                } else {
2106                                    expr.clone()
2107                                };
2108                                let _ = writeln!(out, "\tassert.Equal(t, {escaped}, {safe_expr})");
2109                            } else if let Some(val) = &assertion.value {
2110                                if let Some(n) = val.as_u64() {
2111                                    let _ = writeln!(out, "\tassert.Equal(t, {n}, {expr})");
2112                                }
2113                            }
2114                        }
2115                        "not_empty" => {
2116                            let _ = writeln!(out, "\tassert.NotEmpty(t, {expr}, \"expected non-empty\")");
2117                        }
2118                        "is_empty" => {
2119                            let _ = writeln!(out, "\tassert.Empty(t, {expr}, \"expected empty\")");
2120                        }
2121                        "is_true" => {
2122                            let _ = writeln!(out, "\tassert.True(t, {expr}, \"expected true\")");
2123                        }
2124                        "is_false" => {
2125                            let _ = writeln!(out, "\tassert.False(t, {expr}, \"expected false\")");
2126                        }
2127                        "greater_than" => {
2128                            if let Some(val) = &assertion.value {
2129                                if let Some(n) = val.as_u64() {
2130                                    let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
2131                                }
2132                            }
2133                        }
2134                        "greater_than_or_equal" => {
2135                            if let Some(val) = &assertion.value {
2136                                if let Some(n) = val.as_u64() {
2137                                    let _ =
2138                                        writeln!(out, "\tassert.GreaterOrEqual(t, {expr}, {n}, \"expected >= {n}\")");
2139                                }
2140                            }
2141                        }
2142                        "contains" => {
2143                            if let Some(serde_json::Value::String(s)) = &assertion.value {
2144                                let escaped = crate::escape::go_string_literal(s);
2145                                let _ =
2146                                    writeln!(out, "\tassert.Contains(t, {expr}, {escaped}, \"expected to contain\")");
2147                            }
2148                        }
2149                        _ => {
2150                            let _ = writeln!(
2151                                out,
2152                                "\t// streaming field '{f}': assertion type '{}' not rendered",
2153                                assertion.assertion_type
2154                            );
2155                        }
2156                    }
2157                }
2158                return;
2159            }
2160        }
2161    }
2162
2163    // Skip assertions on fields that don't exist on the result type.
2164    // When result_is_simple, all field assertions operate on the scalar result directly.
2165    if !result_is_simple {
2166        if let Some(f) = &assertion.field {
2167            if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
2168                let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
2169                return;
2170            }
2171        }
2172    }
2173
2174    let field_expr = if result_is_simple {
2175        // The result IS the value — field access is irrelevant.
2176        result_var.to_string()
2177    } else {
2178        match &assertion.field {
2179            Some(f) if !f.is_empty() => {
2180                // Use the local variable if the field was dereferenced above.
2181                if let Some(local_var) = optional_locals.get(f.as_str()) {
2182                    local_var.clone()
2183                } else {
2184                    field_resolver.accessor(f, "go", result_var)
2185                }
2186            }
2187            _ => result_var.to_string(),
2188        }
2189    };
2190
2191    // Check if the field (after resolution) is optional, which means it's a pointer in Go.
2192    // Also check if a `.length` suffix's parent is optional (e.g., metadata.headings.length
2193    // where metadata.headings is optional → len() needs dereference).
2194    let is_optional = assertion
2195        .field
2196        .as_ref()
2197        .map(|f| {
2198            let resolved = field_resolver.resolve(f);
2199            let check_path = resolved
2200                .strip_suffix(".length")
2201                .or_else(|| resolved.strip_suffix(".count"))
2202                .or_else(|| resolved.strip_suffix(".size"))
2203                .unwrap_or(resolved);
2204            field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
2205        })
2206        .unwrap_or(false);
2207
2208    // When field_expr is `len(X)` and X is an optional (pointer) field, rewrite to `len(*X)`
2209    // and we'll wrap with a nil guard in the assertion handlers.
2210    // However, slices are already nil-able and should not be dereferenced.
2211    let field_is_array_for_len = assertion
2212        .field
2213        .as_ref()
2214        .map(|f| {
2215            let resolved = field_resolver.resolve(f);
2216            let check_path = resolved
2217                .strip_suffix(".length")
2218                .or_else(|| resolved.strip_suffix(".count"))
2219                .or_else(|| resolved.strip_suffix(".size"))
2220                .unwrap_or(resolved);
2221            field_resolver.is_array(check_path)
2222        })
2223        .unwrap_or(false);
2224    let field_expr =
2225        if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') && !field_is_array_for_len {
2226            let inner = &field_expr[4..field_expr.len() - 1];
2227            format!("len(*{inner})")
2228        } else {
2229            field_expr
2230        };
2231    // Build the nil-guard expression for the inner pointer (without len wrapper).
2232    let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
2233        Some(field_expr[5..field_expr.len() - 1].to_string())
2234    } else {
2235        None
2236    };
2237
2238    // For optional non-string fields that weren't dereferenced into locals,
2239    // we need to dereference the pointer in comparisons.
2240    // However, slices are already nil-able and should not be dereferenced.
2241    let field_is_slice = assertion
2242        .field
2243        .as_ref()
2244        .map(|f| field_resolver.is_array(field_resolver.resolve(f)))
2245        .unwrap_or(false);
2246    let deref_field_expr = if is_optional && !field_expr.starts_with("len(") && !field_is_slice {
2247        format!("*{field_expr}")
2248    } else {
2249        field_expr.clone()
2250    };
2251
2252    // Detect array element access (e.g., `result.Assets[0].ContentHash`).
2253    // When the field_expr contains `[0]`, we must guard against an out-of-bounds
2254    // panic by checking that the array is non-empty first.
2255    // Extract the array slice expression (everything before `[0]`).
2256    let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
2257        let mut array_expr = field_expr[..idx].to_string();
2258        if let Some(stripped) = array_expr.strip_prefix("len(") {
2259            array_expr = stripped.to_string();
2260        }
2261        Some(array_expr)
2262    } else {
2263        None
2264    };
2265
2266    // Render the assertion into a temporary buffer first, then wrap with the array
2267    // bounds guard (if needed) by adding one extra level of indentation.
2268    let mut assertion_buf = String::new();
2269    let out_ref = &mut assertion_buf;
2270
2271    match assertion.assertion_type.as_str() {
2272        "equals" => {
2273            if let Some(expected) = &assertion.value {
2274                let go_val = json_to_go(expected);
2275                // For string equality, trim whitespace to handle trailing newlines from the converter.
2276                if expected.is_string() {
2277                    // Wrap field expression with strings.TrimSpace() for string comparisons.
2278                    // Use string() cast to handle named string types (e.g. BatchStatus, FinishReason).
2279                    let trimmed_field = if is_optional && !field_expr.starts_with("len(") {
2280                        format!("strings.TrimSpace(string(*{field_expr}))")
2281                    } else {
2282                        format!("strings.TrimSpace(string({field_expr}))")
2283                    };
2284                    if is_optional && !field_expr.starts_with("len(") {
2285                        let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
2286                    } else {
2287                        let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
2288                    }
2289                } else if is_optional && !field_expr.starts_with("len(") {
2290                    let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
2291                } else {
2292                    let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
2293                }
2294                let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
2295                let _ = writeln!(out_ref, "\t}}");
2296            }
2297        }
2298        "contains" => {
2299            if let Some(expected) = &assertion.value {
2300                let go_val = json_to_go(expected);
2301                // Determine the "string view" of the field expression.
2302                // - []string (optional) → jsonString(field_expr) — Go slices are nil-able, no `*` needed
2303                // - *string → string(*field_expr)
2304                // - string → string(field_expr) (or just field_expr for plain strings)
2305                // - result_is_array (result_is_simple + array result) → jsonString(field_expr)
2306                let resolved_field = assertion.field.as_deref().unwrap_or("");
2307                let resolved_name = field_resolver.resolve(resolved_field);
2308                let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2309                let is_opt =
2310                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2311                let field_for_contains = if is_opt && field_is_array {
2312                    // Go slices are nil-able directly — no pointer dereference needed.
2313                    format!("jsonString({field_expr})")
2314                } else if is_opt {
2315                    format!("fmt.Sprint(*{field_expr})")
2316                } else if field_is_array {
2317                    format!("jsonString({field_expr})")
2318                } else {
2319                    format!("fmt.Sprint({field_expr})")
2320                };
2321                if is_opt {
2322                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2323                    let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2324                    let _ = writeln!(
2325                        out_ref,
2326                        "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2327                    );
2328                    let _ = writeln!(out_ref, "\t}}");
2329                    let _ = writeln!(out_ref, "\t}}");
2330                } else {
2331                    let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2332                    let _ = writeln!(
2333                        out_ref,
2334                        "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
2335                    );
2336                    let _ = writeln!(out_ref, "\t}}");
2337                }
2338            }
2339        }
2340        "contains_all" => {
2341            if let Some(values) = &assertion.values {
2342                let resolved_field = assertion.field.as_deref().unwrap_or("");
2343                let resolved_name = field_resolver.resolve(resolved_field);
2344                let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2345                let is_opt =
2346                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2347                for val in values {
2348                    let go_val = json_to_go(val);
2349                    let field_for_contains = if is_opt && field_is_array {
2350                        // Go slices are nil-able directly — no pointer dereference needed.
2351                        format!("jsonString({field_expr})")
2352                    } else if is_opt {
2353                        format!("fmt.Sprint(*{field_expr})")
2354                    } else if field_is_array {
2355                        format!("jsonString({field_expr})")
2356                    } else {
2357                        format!("fmt.Sprint({field_expr})")
2358                    };
2359                    if is_opt {
2360                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2361                        let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2362                        let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2363                        let _ = writeln!(out_ref, "\t}}");
2364                        let _ = writeln!(out_ref, "\t}}");
2365                    } else {
2366                        let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
2367                        let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
2368                        let _ = writeln!(out_ref, "\t}}");
2369                    }
2370                }
2371            }
2372        }
2373        "not_contains" => {
2374            if let Some(expected) = &assertion.value {
2375                let go_val = json_to_go(expected);
2376                let resolved_field = assertion.field.as_deref().unwrap_or("");
2377                let resolved_name = field_resolver.resolve(resolved_field);
2378                let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
2379                let is_opt =
2380                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2381                let field_for_contains = if is_opt && field_is_array {
2382                    // Go slices are nil-able directly — no pointer dereference needed.
2383                    format!("jsonString({field_expr})")
2384                } else if is_opt {
2385                    format!("fmt.Sprint(*{field_expr})")
2386                } else if field_is_array {
2387                    format!("jsonString({field_expr})")
2388                } else {
2389                    format!("fmt.Sprint({field_expr})")
2390                };
2391                let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
2392                let _ = writeln!(
2393                    out_ref,
2394                    "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
2395                );
2396                let _ = writeln!(out_ref, "\t}}");
2397            }
2398        }
2399        "not_empty" => {
2400            // For optional struct pointers (not arrays), just check != nil.
2401            // For optional slice/string pointers, check nil and len.
2402            let field_is_array = {
2403                let rf = assertion.field.as_deref().unwrap_or("");
2404                let rn = field_resolver.resolve(rf);
2405                field_resolver.is_array(rn)
2406            };
2407            if is_optional && !field_is_array {
2408                // Struct pointer: non-empty means not nil.
2409                let _ = writeln!(out_ref, "\tif {field_expr} == nil {{");
2410            } else if is_optional && field_is_slice {
2411                // Slice optional: Go slices are already nil-able — no dereference needed.
2412                let _ = writeln!(out_ref, "\tif {field_expr} == nil || len({field_expr}) == 0 {{");
2413            } else if is_optional {
2414                // Pointer-to-slice (*[]T): dereference then len.
2415                let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
2416            } else if result_is_simple && result_is_array {
2417                // Simple array result ([]T) — direct slice, not a pointer; check length only.
2418                let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2419            } else {
2420                let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
2421            }
2422            let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
2423            let _ = writeln!(out_ref, "\t}}");
2424        }
2425        "is_empty" => {
2426            let field_is_array = {
2427                let rf = assertion.field.as_deref().unwrap_or("");
2428                let rn = field_resolver.resolve(rf);
2429                field_resolver.is_array(rn)
2430            };
2431            // Special case: result_is_simple && !result_is_array && no field means the result is a pointer.
2432            // Empty means nil.
2433            if result_is_simple && !result_is_array && assertion.field.as_ref().is_none_or(|f| f.is_empty()) {
2434                // Pointer result (not dereferenced): empty means nil.
2435                let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2436            } else if is_optional && !field_is_array {
2437                // Struct pointer: empty means nil.
2438                let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2439            } else if is_optional && field_is_slice {
2440                // Slice optional: Go slices are already nil-able — no dereference needed.
2441                let _ = writeln!(out_ref, "\tif {field_expr} != nil && len({field_expr}) != 0 {{");
2442            } else if is_optional {
2443                // Pointer-to-slice (*[]T): dereference then len.
2444                let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
2445            } else {
2446                let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
2447            }
2448            let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
2449            let _ = writeln!(out_ref, "\t}}");
2450        }
2451        "contains_any" => {
2452            if let Some(values) = &assertion.values {
2453                let resolved_field = assertion.field.as_deref().unwrap_or("");
2454                let resolved_name = field_resolver.resolve(resolved_field);
2455                let field_is_array = field_resolver.is_array(resolved_name);
2456                let is_opt =
2457                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
2458                let field_for_contains = if is_opt && field_is_array {
2459                    // Go slices are nil-able directly — no pointer dereference needed.
2460                    format!("jsonString({field_expr})")
2461                } else if is_opt {
2462                    format!("fmt.Sprint(*{field_expr})")
2463                } else if field_is_array {
2464                    format!("jsonString({field_expr})")
2465                } else {
2466                    format!("fmt.Sprint({field_expr})")
2467                };
2468                let _ = writeln!(out_ref, "\t{{");
2469                let _ = writeln!(out_ref, "\t\tfound := false");
2470                for val in values {
2471                    let go_val = json_to_go(val);
2472                    let _ = writeln!(
2473                        out_ref,
2474                        "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
2475                    );
2476                }
2477                let _ = writeln!(out_ref, "\t\tif !found {{");
2478                let _ = writeln!(
2479                    out_ref,
2480                    "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
2481                );
2482                let _ = writeln!(out_ref, "\t\t}}");
2483                let _ = writeln!(out_ref, "\t}}");
2484            }
2485        }
2486        "greater_than" => {
2487            if let Some(val) = &assertion.value {
2488                let go_val = json_to_go(val);
2489                // Use `< N+1` instead of `<= N` to avoid golangci-lint sloppyLen
2490                // warning when N is 0 (len(x) <= 0 → len(x) < 1).
2491                // For optional (pointer) fields, dereference and guard with nil check.
2492                if is_optional {
2493                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2494                    if let Some(n) = val.as_u64() {
2495                        let next = n + 1;
2496                        let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {next} {{");
2497                    } else {
2498                        let _ = writeln!(out_ref, "\t\tif {deref_field_expr} <= {go_val} {{");
2499                    }
2500                    let _ = writeln!(
2501                        out_ref,
2502                        "\t\t\tt.Errorf(\"expected > {go_val}, got %v\", {deref_field_expr})"
2503                    );
2504                    let _ = writeln!(out_ref, "\t\t}}");
2505                    let _ = writeln!(out_ref, "\t}}");
2506                } else if let Some(n) = val.as_u64() {
2507                    let next = n + 1;
2508                    let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
2509                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2510                    let _ = writeln!(out_ref, "\t}}");
2511                } else {
2512                    let _ = writeln!(out_ref, "\tif {field_expr} <= {go_val} {{");
2513                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
2514                    let _ = writeln!(out_ref, "\t}}");
2515                }
2516            }
2517        }
2518        "less_than" => {
2519            if let Some(val) = &assertion.value {
2520                let go_val = json_to_go(val);
2521                let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
2522                let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
2523                let _ = writeln!(out_ref, "\t}}");
2524            }
2525        }
2526        "greater_than_or_equal" => {
2527            if let Some(val) = &assertion.value {
2528                let go_val = json_to_go(val);
2529                if let Some(ref guard) = nil_guard_expr {
2530                    let _ = writeln!(out_ref, "\tif {guard} != nil {{");
2531                    let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
2532                    let _ = writeln!(
2533                        out_ref,
2534                        "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
2535                    );
2536                    let _ = writeln!(out_ref, "\t\t}}");
2537                    let _ = writeln!(out_ref, "\t}}");
2538                } else if is_optional && !field_expr.starts_with("len(") {
2539                    // Optional pointer field: nil-guard and dereference before comparison.
2540                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2541                    let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {go_val} {{");
2542                    let _ = writeln!(
2543                        out_ref,
2544                        "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {deref_field_expr})"
2545                    );
2546                    let _ = writeln!(out_ref, "\t\t}}");
2547                    let _ = writeln!(out_ref, "\t}}");
2548                } else {
2549                    let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
2550                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
2551                    let _ = writeln!(out_ref, "\t}}");
2552                }
2553            }
2554        }
2555        "less_than_or_equal" => {
2556            if let Some(val) = &assertion.value {
2557                let go_val = json_to_go(val);
2558                let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
2559                let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
2560                let _ = writeln!(out_ref, "\t}}");
2561            }
2562        }
2563        "starts_with" => {
2564            if let Some(expected) = &assertion.value {
2565                let go_val = json_to_go(expected);
2566                let field_for_prefix = if is_optional
2567                    && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2568                {
2569                    format!("string(*{field_expr})")
2570                } else {
2571                    format!("string({field_expr})")
2572                };
2573                let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
2574                let _ = writeln!(
2575                    out_ref,
2576                    "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
2577                );
2578                let _ = writeln!(out_ref, "\t}}");
2579            }
2580        }
2581        "count_min" => {
2582            if let Some(val) = &assertion.value {
2583                if let Some(n) = val.as_u64() {
2584                    if is_optional {
2585                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2586                        // Slices are value types in Go — use len(slice) not len(*slice).
2587                        let len_expr = if field_is_slice {
2588                            format!("len({field_expr})")
2589                        } else {
2590                            format!("len(*{field_expr})")
2591                        };
2592                        let _ = writeln!(
2593                            out_ref,
2594                            "\t\tassert.GreaterOrEqual(t, {len_expr}, {n}, \"expected at least {n} elements\")"
2595                        );
2596                        let _ = writeln!(out_ref, "\t}}");
2597                    } else {
2598                        let _ = writeln!(
2599                            out_ref,
2600                            "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
2601                        );
2602                    }
2603                }
2604            }
2605        }
2606        "count_equals" => {
2607            if let Some(val) = &assertion.value {
2608                if let Some(n) = val.as_u64() {
2609                    if is_optional {
2610                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2611                        // Slices are value types in Go — use len(slice) not len(*slice).
2612                        let len_expr = if field_is_slice {
2613                            format!("len({field_expr})")
2614                        } else {
2615                            format!("len(*{field_expr})")
2616                        };
2617                        let _ = writeln!(
2618                            out_ref,
2619                            "\t\tassert.Equal(t, {len_expr}, {n}, \"expected exactly {n} elements\")"
2620                        );
2621                        let _ = writeln!(out_ref, "\t}}");
2622                    } else {
2623                        let _ = writeln!(
2624                            out_ref,
2625                            "\tassert.Equal(t, len({field_expr}), {n}, \"expected exactly {n} elements\")"
2626                        );
2627                    }
2628                }
2629            }
2630        }
2631        "is_true" => {
2632            if is_optional {
2633                let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2634                let _ = writeln!(out_ref, "\t\tassert.True(t, *{field_expr}, \"expected true\")");
2635                let _ = writeln!(out_ref, "\t}}");
2636            } else {
2637                let _ = writeln!(out_ref, "\tassert.True(t, {field_expr}, \"expected true\")");
2638            }
2639        }
2640        "is_false" => {
2641            if is_optional {
2642                let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2643                let _ = writeln!(out_ref, "\t\tassert.False(t, *{field_expr}, \"expected false\")");
2644                let _ = writeln!(out_ref, "\t}}");
2645            } else {
2646                let _ = writeln!(out_ref, "\tassert.False(t, {field_expr}, \"expected false\")");
2647            }
2648        }
2649        "method_result" => {
2650            if let Some(method_name) = &assertion.method {
2651                let info = build_go_method_call(result_var, method_name, assertion.args.as_ref(), import_alias);
2652                let check = assertion.check.as_deref().unwrap_or("is_true");
2653                // For pointer-returning functions, dereference with `*`. Value-returning
2654                // functions (e.g., NodeInfo field access) are used directly.
2655                let deref_expr = if info.is_pointer {
2656                    format!("*{}", info.call_expr)
2657                } else {
2658                    info.call_expr.clone()
2659                };
2660                match check {
2661                    "equals" => {
2662                        if let Some(val) = &assertion.value {
2663                            if val.is_boolean() {
2664                                if val.as_bool() == Some(true) {
2665                                    let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
2666                                } else {
2667                                    let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
2668                                }
2669                            } else {
2670                                // Apply type cast to numeric literals when the method returns
2671                                // a typed uint (e.g., *uint) to avoid reflect.DeepEqual
2672                                // mismatches between int and uint in testify's assert.Equal.
2673                                let go_val = if let Some(cast) = info.value_cast {
2674                                    if val.is_number() {
2675                                        format!("{cast}({})", json_to_go(val))
2676                                    } else {
2677                                        json_to_go(val)
2678                                    }
2679                                } else {
2680                                    json_to_go(val)
2681                                };
2682                                let _ = writeln!(
2683                                    out_ref,
2684                                    "\tassert.Equal(t, {go_val}, {deref_expr}, \"method_result equals assertion failed\")"
2685                                );
2686                            }
2687                        }
2688                    }
2689                    "is_true" => {
2690                        let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
2691                    }
2692                    "is_false" => {
2693                        let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
2694                    }
2695                    "greater_than_or_equal" => {
2696                        if let Some(val) = &assertion.value {
2697                            let n = val.as_u64().unwrap_or(0);
2698                            // Use the value_cast type if available (e.g., uint for named_children_count).
2699                            let cast = info.value_cast.unwrap_or("uint");
2700                            let _ = writeln!(
2701                                out_ref,
2702                                "\tassert.GreaterOrEqual(t, {deref_expr}, {cast}({n}), \"expected >= {n}\")"
2703                            );
2704                        }
2705                    }
2706                    "count_min" => {
2707                        if let Some(val) = &assertion.value {
2708                            let n = val.as_u64().unwrap_or(0);
2709                            let _ = writeln!(
2710                                out_ref,
2711                                "\tassert.GreaterOrEqual(t, len({deref_expr}), {n}, \"expected at least {n} elements\")"
2712                            );
2713                        }
2714                    }
2715                    "contains" => {
2716                        if let Some(val) = &assertion.value {
2717                            let go_val = json_to_go(val);
2718                            let _ = writeln!(
2719                                out_ref,
2720                                "\tassert.Contains(t, {deref_expr}, {go_val}, \"expected result to contain value\")"
2721                            );
2722                        }
2723                    }
2724                    "is_error" => {
2725                        let _ = writeln!(out_ref, "\t{{");
2726                        let _ = writeln!(out_ref, "\t\t_, methodErr := {}", info.call_expr);
2727                        let _ = writeln!(out_ref, "\t\tassert.Error(t, methodErr)");
2728                        let _ = writeln!(out_ref, "\t}}");
2729                    }
2730                    other_check => {
2731                        panic!("Go e2e generator: unsupported method_result check type: {other_check}");
2732                    }
2733                }
2734            } else {
2735                panic!("Go e2e generator: method_result assertion missing 'method' field");
2736            }
2737        }
2738        "min_length" => {
2739            if let Some(val) = &assertion.value {
2740                if let Some(n) = val.as_u64() {
2741                    if is_optional {
2742                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2743                        let _ = writeln!(
2744                            out_ref,
2745                            "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected length >= {n}\")"
2746                        );
2747                        let _ = writeln!(out_ref, "\t}}");
2748                    } else if field_expr.starts_with("len(") {
2749                        let _ = writeln!(
2750                            out_ref,
2751                            "\tassert.GreaterOrEqual(t, {field_expr}, {n}, \"expected length >= {n}\")"
2752                        );
2753                    } else {
2754                        let _ = writeln!(
2755                            out_ref,
2756                            "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected length >= {n}\")"
2757                        );
2758                    }
2759                }
2760            }
2761        }
2762        "max_length" => {
2763            if let Some(val) = &assertion.value {
2764                if let Some(n) = val.as_u64() {
2765                    if is_optional {
2766                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
2767                        let _ = writeln!(
2768                            out_ref,
2769                            "\t\tassert.LessOrEqual(t, len(*{field_expr}), {n}, \"expected length <= {n}\")"
2770                        );
2771                        let _ = writeln!(out_ref, "\t}}");
2772                    } else if field_expr.starts_with("len(") {
2773                        let _ = writeln!(
2774                            out_ref,
2775                            "\tassert.LessOrEqual(t, {field_expr}, {n}, \"expected length <= {n}\")"
2776                        );
2777                    } else {
2778                        let _ = writeln!(
2779                            out_ref,
2780                            "\tassert.LessOrEqual(t, len({field_expr}), {n}, \"expected length <= {n}\")"
2781                        );
2782                    }
2783                }
2784            }
2785        }
2786        "ends_with" => {
2787            if let Some(expected) = &assertion.value {
2788                let go_val = json_to_go(expected);
2789                let field_for_suffix = if is_optional
2790                    && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2791                {
2792                    format!("string(*{field_expr})")
2793                } else {
2794                    format!("string({field_expr})")
2795                };
2796                let _ = writeln!(out_ref, "\tif !strings.HasSuffix({field_for_suffix}, {go_val}) {{");
2797                let _ = writeln!(
2798                    out_ref,
2799                    "\t\tt.Errorf(\"expected to end with %s, got %v\", {go_val}, {field_expr})"
2800                );
2801                let _ = writeln!(out_ref, "\t}}");
2802            }
2803        }
2804        "matches_regex" => {
2805            if let Some(expected) = &assertion.value {
2806                let go_val = json_to_go(expected);
2807                let field_for_regex = if is_optional
2808                    && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
2809                {
2810                    format!("*{field_expr}")
2811                } else {
2812                    field_expr.clone()
2813                };
2814                let _ = writeln!(
2815                    out_ref,
2816                    "\tassert.Regexp(t, {go_val}, {field_for_regex}, \"expected value to match regex\")"
2817                );
2818            }
2819        }
2820        "not_error" => {
2821            // Already handled by the `if err != nil` check above.
2822        }
2823        "error" => {
2824            // Handled at the test function level.
2825        }
2826        other => {
2827            panic!("Go e2e generator: unsupported assertion type: {other}");
2828        }
2829    }
2830
2831    // If the assertion accesses an array element via [0], wrap the generated code in a
2832    // bounds check to prevent an index-out-of-range panic when the array is empty.
2833    if let Some(ref arr) = array_guard {
2834        if !assertion_buf.is_empty() {
2835            let _ = writeln!(out, "\tif len({arr}) > 0 {{");
2836            // Re-indent each line by one additional tab level.
2837            for line in assertion_buf.lines() {
2838                let _ = writeln!(out, "\t{line}");
2839            }
2840            let _ = writeln!(out, "\t}}");
2841        }
2842    } else {
2843        out.push_str(&assertion_buf);
2844    }
2845}
2846
2847/// Metadata about the return type of a Go method call for `method_result` assertions.
2848struct GoMethodCallInfo {
2849    /// The call expression string.
2850    call_expr: String,
2851    /// Whether the return type is a pointer (needs `*` dereference for value comparison).
2852    is_pointer: bool,
2853    /// Optional Go type cast to apply to numeric literal values in `equals` assertions
2854    /// (e.g., `"uint"` so that `0` becomes `uint(0)` to match `*uint` deref type).
2855    value_cast: Option<&'static str>,
2856}
2857
2858/// Build a Go call expression for a `method_result` assertion on a tree-sitter Tree.
2859///
2860/// Maps method names to the appropriate Go function calls, matching the Go binding API
2861/// in `packages/go/binding.go`. Returns a [`GoMethodCallInfo`] describing the call and
2862/// its return type characteristics.
2863///
2864/// Return types by method:
2865/// - `has_error_nodes`, `contains_node_type` → `*bool` (pointer)
2866/// - `error_count` → `*uint` (pointer, value_cast = "uint")
2867/// - `tree_to_sexp` → `*string` (pointer)
2868/// - `root_node_type` → `string` via `RootNodeInfo(tree).Kind` (value)
2869/// - `named_children_count` → `uint` via `RootNodeInfo(tree).NamedChildCount` (value, value_cast = "uint")
2870/// - `find_nodes_by_type` → `*[]NodeInfo` (pointer to slice)
2871/// - `run_query` → `(*[]QueryMatch, error)` (pointer + error; use `is_error` check type)
2872fn build_go_method_call(
2873    result_var: &str,
2874    method_name: &str,
2875    args: Option<&serde_json::Value>,
2876    import_alias: &str,
2877) -> GoMethodCallInfo {
2878    match method_name {
2879        "root_node_type" => GoMethodCallInfo {
2880            call_expr: format!("{import_alias}.RootNodeInfo({result_var}).Kind"),
2881            is_pointer: false,
2882            value_cast: None,
2883        },
2884        "named_children_count" => GoMethodCallInfo {
2885            call_expr: format!("{import_alias}.RootNodeInfo({result_var}).NamedChildCount"),
2886            is_pointer: false,
2887            value_cast: Some("uint"),
2888        },
2889        "has_error_nodes" => GoMethodCallInfo {
2890            call_expr: format!("{import_alias}.TreeHasErrorNodes({result_var})"),
2891            is_pointer: true,
2892            value_cast: None,
2893        },
2894        "error_count" | "tree_error_count" => GoMethodCallInfo {
2895            call_expr: format!("{import_alias}.TreeErrorCount({result_var})"),
2896            is_pointer: true,
2897            value_cast: Some("uint"),
2898        },
2899        "tree_to_sexp" => GoMethodCallInfo {
2900            call_expr: format!("{import_alias}.TreeToSexp({result_var})"),
2901            is_pointer: true,
2902            value_cast: None,
2903        },
2904        "contains_node_type" => {
2905            let node_type = args
2906                .and_then(|a| a.get("node_type"))
2907                .and_then(|v| v.as_str())
2908                .unwrap_or("");
2909            GoMethodCallInfo {
2910                call_expr: format!("{import_alias}.TreeContainsNodeType({result_var}, \"{node_type}\")"),
2911                is_pointer: true,
2912                value_cast: None,
2913            }
2914        }
2915        "find_nodes_by_type" => {
2916            let node_type = args
2917                .and_then(|a| a.get("node_type"))
2918                .and_then(|v| v.as_str())
2919                .unwrap_or("");
2920            GoMethodCallInfo {
2921                call_expr: format!("{import_alias}.FindNodesByType({result_var}, \"{node_type}\")"),
2922                is_pointer: true,
2923                value_cast: None,
2924            }
2925        }
2926        "run_query" => {
2927            let query_source = args
2928                .and_then(|a| a.get("query_source"))
2929                .and_then(|v| v.as_str())
2930                .unwrap_or("");
2931            let language = args
2932                .and_then(|a| a.get("language"))
2933                .and_then(|v| v.as_str())
2934                .unwrap_or("");
2935            let query_lit = go_string_literal(query_source);
2936            let lang_lit = go_string_literal(language);
2937            // RunQuery returns (*[]QueryMatch, error) — use is_error check type.
2938            GoMethodCallInfo {
2939                call_expr: format!("{import_alias}.RunQuery({result_var}, {lang_lit}, {query_lit}, []byte(source))"),
2940                is_pointer: false,
2941                value_cast: None,
2942            }
2943        }
2944        other => {
2945            let method_pascal = other.to_upper_camel_case();
2946            GoMethodCallInfo {
2947                call_expr: format!("{result_var}.{method_pascal}()"),
2948                is_pointer: false,
2949                value_cast: None,
2950            }
2951        }
2952    }
2953}
2954
2955/// Convert a `serde_json::Value` to a Go literal string.
2956/// Recursively convert a JSON value for Go struct unmarshalling.
2957///
2958/// The Go binding's `ConversionOptions` struct uses:
2959/// - `snake_case` JSON field tags (e.g. `"code_block_style"` not `"codeBlockStyle"`)
2960/// - lowercase/snake_case string values for enums (e.g. `"indented"`, `"atx_closed"`)
2961///
2962/// Fixture JSON uses camelCase keys and PascalCase enum values (Python/TS conventions).
2963/// This function remaps both so the generated Go tests can unmarshal correctly.
2964fn convert_json_for_go(value: serde_json::Value) -> serde_json::Value {
2965    match value {
2966        serde_json::Value::Object(map) => {
2967            let new_map: serde_json::Map<String, serde_json::Value> = map
2968                .into_iter()
2969                .map(|(k, v)| (camel_to_snake_case(&k), convert_json_for_go(v)))
2970                .collect();
2971            serde_json::Value::Object(new_map)
2972        }
2973        serde_json::Value::Array(arr) => {
2974            // Check if this is a byte array (array of integers 0-255).
2975            // If so, encode as base64 string for Go json.Unmarshal compatibility.
2976            if is_byte_array(&arr) {
2977                let bytes: Vec<u8> = arr
2978                    .iter()
2979                    .filter_map(|v| v.as_u64().and_then(|n| if n <= 255 { Some(n as u8) } else { None }))
2980                    .collect();
2981                // Encode bytes as base64 for Go json.Unmarshal (Go expects []byte as base64 strings)
2982                let encoded = base64_encode(&bytes);
2983                serde_json::Value::String(encoded)
2984            } else {
2985                serde_json::Value::Array(arr.into_iter().map(convert_json_for_go).collect())
2986            }
2987        }
2988        serde_json::Value::String(s) => {
2989            // Convert PascalCase enum values to snake_case.
2990            // Only convert values that look like PascalCase (start with uppercase, no spaces).
2991            serde_json::Value::String(pascal_to_snake_case(&s))
2992        }
2993        other => other,
2994    }
2995}
2996
2997/// Check if an array looks like a byte array (all elements are integers 0-255).
2998fn is_byte_array(arr: &[serde_json::Value]) -> bool {
2999    if arr.is_empty() {
3000        return false;
3001    }
3002    arr.iter().all(|v| {
3003        if let serde_json::Value::Number(n) = v {
3004            n.is_u64() && n.as_u64().is_some_and(|u| u <= 255)
3005        } else {
3006            false
3007        }
3008    })
3009}
3010
3011/// Encode bytes as base64 string (standard alphabet without padding in this output,
3012/// though Go's json.Unmarshal handles both).
3013fn base64_encode(bytes: &[u8]) -> String {
3014    const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3015    let mut result = String::new();
3016    let mut i = 0;
3017
3018    while i + 2 < bytes.len() {
3019        let b1 = bytes[i];
3020        let b2 = bytes[i + 1];
3021        let b3 = bytes[i + 2];
3022
3023        result.push(TABLE[(b1 >> 2) as usize] as char);
3024        result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
3025        result.push(TABLE[(((b2 & 0x0f) << 2) | (b3 >> 6)) as usize] as char);
3026        result.push(TABLE[(b3 & 0x3f) as usize] as char);
3027
3028        i += 3;
3029    }
3030
3031    // Handle remaining bytes
3032    if i < bytes.len() {
3033        let b1 = bytes[i];
3034        result.push(TABLE[(b1 >> 2) as usize] as char);
3035
3036        if i + 1 < bytes.len() {
3037            let b2 = bytes[i + 1];
3038            result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
3039            result.push(TABLE[((b2 & 0x0f) << 2) as usize] as char);
3040            result.push('=');
3041        } else {
3042            result.push(TABLE[((b1 & 0x03) << 4) as usize] as char);
3043            result.push_str("==");
3044        }
3045    }
3046
3047    result
3048}
3049
3050/// Convert a camelCase or PascalCase string to snake_case.
3051fn camel_to_snake_case(s: &str) -> String {
3052    let mut result = String::new();
3053    let mut prev_upper = false;
3054    for (i, c) in s.char_indices() {
3055        if c.is_uppercase() {
3056            if i > 0 && !prev_upper {
3057                result.push('_');
3058            }
3059            result.push(c.to_lowercase().next().unwrap_or(c));
3060            prev_upper = true;
3061        } else {
3062            if prev_upper && i > 1 {
3063                // Handles sequences like "URLPath" → "url_path": insert _ before last uppercase
3064                // when transitioning from a run of uppercase back to lowercase.
3065                // This is tricky — use simple approach: detect Aa pattern.
3066            }
3067            result.push(c);
3068            prev_upper = false;
3069        }
3070    }
3071    result
3072}
3073
3074/// Convert a PascalCase string to snake_case (for enum values).
3075///
3076/// Only converts if the string looks like PascalCase (starts uppercase, no spaces/underscores).
3077/// Values that are already lowercase/snake_case are returned unchanged.
3078fn pascal_to_snake_case(s: &str) -> String {
3079    // Skip conversion for strings that already contain underscores, spaces, or start lowercase.
3080    let first_char = s.chars().next();
3081    if first_char.is_none() || !first_char.unwrap().is_uppercase() || s.contains('_') || s.contains(' ') {
3082        return s.to_string();
3083    }
3084    camel_to_snake_case(s)
3085}
3086
3087/// Map an `ArgMapping.element_type` to a Go slice type. Used for `json_object` args
3088/// whose fixture value is a JSON array. The element type is wrapped in `[]…` so an
3089/// element of `String` becomes `[]string` and `Vec<String>` becomes `[][]string`.
3090fn element_type_to_go_slice(element_type: Option<&str>, import_alias: &str) -> String {
3091    let elem = element_type.unwrap_or("String").trim();
3092    let go_elem = rust_type_to_go(elem, import_alias);
3093    format!("[]{go_elem}")
3094}
3095
3096/// Map a small subset of Rust scalar / `Vec<T>` types to their Go equivalents.
3097/// For unknown types, qualify with the import alias (e.g., "kreuzberg.BatchBytesItem").
3098fn rust_type_to_go(rust: &str, import_alias: &str) -> String {
3099    let trimmed = rust.trim();
3100    if let Some(inner) = trimmed.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>')) {
3101        return format!("[]{}", rust_type_to_go(inner, import_alias));
3102    }
3103    match trimmed {
3104        "String" | "&str" | "str" => "string".to_string(),
3105        "bool" => "bool".to_string(),
3106        "f32" => "float32".to_string(),
3107        "f64" => "float64".to_string(),
3108        "i8" => "int8".to_string(),
3109        "i16" => "int16".to_string(),
3110        "i32" => "int32".to_string(),
3111        "i64" | "isize" => "int64".to_string(),
3112        "u8" => "uint8".to_string(),
3113        "u16" => "uint16".to_string(),
3114        "u32" => "uint32".to_string(),
3115        "u64" | "usize" => "uint64".to_string(),
3116        _ => format!("{import_alias}.{trimmed}"),
3117    }
3118}
3119
3120fn json_to_go(value: &serde_json::Value) -> String {
3121    match value {
3122        serde_json::Value::String(s) => go_string_literal(s),
3123        serde_json::Value::Bool(b) => b.to_string(),
3124        serde_json::Value::Number(n) => n.to_string(),
3125        serde_json::Value::Null => "nil".to_string(),
3126        // For complex types, serialize to JSON string and pass as literal.
3127        other => go_string_literal(&other.to_string()),
3128    }
3129}
3130
3131// ---------------------------------------------------------------------------
3132// Visitor generation
3133// ---------------------------------------------------------------------------
3134
3135/// Derive a unique, exported Go struct name for a visitor from a fixture ID.
3136///
3137/// E.g. `visitor_continue_default` → `visitorContinueDefault` (unexported, avoids
3138/// polluting the exported API of the test package while still being package-level).
3139fn visitor_struct_name(fixture_id: &str) -> String {
3140    use heck::ToUpperCamelCase;
3141    // Use UpperCamelCase so Go treats it as exported — required for method sets.
3142    format!("testVisitor{}", fixture_id.to_upper_camel_case())
3143}
3144
3145/// Emit a package-level Go struct declaration and all its visitor methods.
3146///
3147/// The struct embeds `BaseVisitor` to satisfy all interface methods not
3148/// explicitly overridden by the fixture callbacks.
3149fn emit_go_visitor_struct(
3150    out: &mut String,
3151    struct_name: &str,
3152    visitor_spec: &crate::fixture::VisitorSpec,
3153    import_alias: &str,
3154) {
3155    let _ = writeln!(out, "type {struct_name} struct{{");
3156    let _ = writeln!(out, "\t{import_alias}.BaseVisitor");
3157    let _ = writeln!(out, "}}");
3158    for (method_name, action) in &visitor_spec.callbacks {
3159        emit_go_visitor_method(out, struct_name, method_name, action, import_alias);
3160    }
3161}
3162
3163/// Emit a Go visitor method for a callback action on the named struct.
3164fn emit_go_visitor_method(
3165    out: &mut String,
3166    struct_name: &str,
3167    method_name: &str,
3168    action: &CallbackAction,
3169    import_alias: &str,
3170) {
3171    let camel_method = method_to_camel(method_name);
3172    // Parameter signatures must exactly match the htmltomarkdown.Visitor interface.
3173    // Optional fields use pointer types (*string, *uint32, etc.) to indicate nil-ability.
3174    let params = match method_name {
3175        "visit_link" => format!("_ {import_alias}.NodeContext, href string, text string, title *string"),
3176        "visit_image" => format!("_ {import_alias}.NodeContext, src string, alt string, title *string"),
3177        "visit_heading" => format!("_ {import_alias}.NodeContext, level uint32, text string, id *string"),
3178        "visit_code_block" => format!("_ {import_alias}.NodeContext, lang *string, code string"),
3179        "visit_code_inline"
3180        | "visit_strong"
3181        | "visit_emphasis"
3182        | "visit_strikethrough"
3183        | "visit_underline"
3184        | "visit_subscript"
3185        | "visit_superscript"
3186        | "visit_mark"
3187        | "visit_button"
3188        | "visit_summary"
3189        | "visit_figcaption"
3190        | "visit_definition_term"
3191        | "visit_definition_description" => format!("_ {import_alias}.NodeContext, text string"),
3192        "visit_text" => format!("_ {import_alias}.NodeContext, text string"),
3193        "visit_list_item" => {
3194            format!("_ {import_alias}.NodeContext, ordered bool, marker string, text string")
3195        }
3196        "visit_blockquote" => format!("_ {import_alias}.NodeContext, content string, depth uint"),
3197        "visit_table_row" => format!("_ {import_alias}.NodeContext, cells []string, isHeader bool"),
3198        "visit_custom_element" => format!("_ {import_alias}.NodeContext, tagName string, html string"),
3199        "visit_form" => format!("_ {import_alias}.NodeContext, action *string, method *string"),
3200        "visit_input" => {
3201            format!("_ {import_alias}.NodeContext, inputType string, name *string, value *string")
3202        }
3203        "visit_audio" | "visit_video" | "visit_iframe" => {
3204            format!("_ {import_alias}.NodeContext, src *string")
3205        }
3206        "visit_details" => format!("_ {import_alias}.NodeContext, open bool"),
3207        "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
3208            format!("_ {import_alias}.NodeContext, output string")
3209        }
3210        "visit_list_start" => format!("_ {import_alias}.NodeContext, ordered bool"),
3211        "visit_list_end" => format!("_ {import_alias}.NodeContext, ordered bool, output string"),
3212        _ => format!("_ {import_alias}.NodeContext"),
3213    };
3214
3215    let _ = writeln!(
3216        out,
3217        "func (v *{struct_name}) {camel_method}({params}) {import_alias}.VisitResult {{"
3218    );
3219    match action {
3220        CallbackAction::Skip => {
3221            let _ = writeln!(out, "\treturn {import_alias}.VisitResultSkip()");
3222        }
3223        CallbackAction::Continue => {
3224            let _ = writeln!(out, "\treturn {import_alias}.VisitResultContinue()");
3225        }
3226        CallbackAction::PreserveHtml => {
3227            let _ = writeln!(out, "\treturn {import_alias}.VisitResultPreserveHTML()");
3228        }
3229        CallbackAction::Custom { output } => {
3230            let escaped = go_string_literal(output);
3231            let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped})");
3232        }
3233        CallbackAction::CustomTemplate { template, .. } => {
3234            // Convert {var} placeholders to %s format verbs and collect arg names.
3235            // E.g. `QUOTE: "{text}"` → fmt.Sprintf("QUOTE: \"%s\"", text)
3236            //
3237            // For pointer-typed params (e.g. `src *string`), dereference with `*`
3238            // — the test fixtures always supply a non-nil value for methods that
3239            // fire a custom template, so this is safe in practice.
3240            let ptr_params = go_visitor_ptr_params(method_name);
3241            let (fmt_str, fmt_args) = template_to_sprintf(template, &ptr_params);
3242            let escaped_fmt = go_string_literal(&fmt_str);
3243            if fmt_args.is_empty() {
3244                let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped_fmt})");
3245            } else {
3246                let args_str = fmt_args.join(", ");
3247                let _ = writeln!(
3248                    out,
3249                    "\treturn {import_alias}.VisitResultCustom(fmt.Sprintf({escaped_fmt}, {args_str}))"
3250                );
3251            }
3252        }
3253    }
3254    let _ = writeln!(out, "}}");
3255}
3256
3257/// Return the set of camelCase parameter names that are pointer types (`*string`) for a
3258/// given visitor method name.  Used to dereference pointers in template `fmt.Sprintf` calls.
3259fn go_visitor_ptr_params(method_name: &str) -> std::collections::HashSet<&'static str> {
3260    match method_name {
3261        "visit_link" => ["title"].into(),
3262        "visit_image" => ["title"].into(),
3263        "visit_heading" => ["id"].into(),
3264        "visit_code_block" => ["lang"].into(),
3265        "visit_form" => ["action", "method"].into(),
3266        "visit_input" => ["name", "value"].into(),
3267        "visit_audio" | "visit_video" | "visit_iframe" => ["src"].into(),
3268        _ => std::collections::HashSet::new(),
3269    }
3270}
3271
3272/// Convert a `{var}` template string into a `fmt.Sprintf` format string and argument list.
3273///
3274/// For example, `QUOTE: "{text}"` becomes `("QUOTE: \"%s\"", vec!["text"])`.
3275///
3276/// Placeholder names in the template use snake_case (matching fixture field names); they
3277/// are converted to Go camelCase parameter names using `go_param_name` so they match the
3278/// generated visitor method signatures (e.g. `{input_type}` → `inputType`).
3279///
3280/// `ptr_params` — camelCase names of parameters that are `*string`; these are
3281/// dereferenced with `*` when used as `fmt.Sprintf` arguments.  The fixtures that
3282/// use `custom_template` on pointer-param methods always supply a non-nil value.
3283fn template_to_sprintf(template: &str, ptr_params: &std::collections::HashSet<&str>) -> (String, Vec<String>) {
3284    let mut fmt_str = String::new();
3285    let mut args: Vec<String> = Vec::new();
3286    let mut chars = template.chars().peekable();
3287    while let Some(c) = chars.next() {
3288        if c == '{' {
3289            // Collect placeholder name until '}'.
3290            let mut name = String::new();
3291            for inner in chars.by_ref() {
3292                if inner == '}' {
3293                    break;
3294                }
3295                name.push(inner);
3296            }
3297            fmt_str.push_str("%s");
3298            // Convert snake_case placeholder to Go camelCase to match method param names.
3299            let go_name = go_param_name(&name);
3300            // Dereference pointer params so fmt.Sprintf receives a string value.
3301            let arg_expr = if ptr_params.contains(go_name.as_str()) {
3302                format!("*{go_name}")
3303            } else {
3304                go_name
3305            };
3306            args.push(arg_expr);
3307        } else {
3308            fmt_str.push(c);
3309        }
3310    }
3311    (fmt_str, args)
3312}
3313
3314/// Convert snake_case method names to Go camelCase.
3315fn method_to_camel(snake: &str) -> String {
3316    use heck::ToUpperCamelCase;
3317    snake.to_upper_camel_case()
3318}
3319
3320#[cfg(test)]
3321mod tests {
3322    use super::*;
3323    use crate::config::{CallConfig, E2eConfig};
3324    use crate::field_access::FieldResolver;
3325    use crate::fixture::{Assertion, Fixture};
3326
3327    fn make_fixture(id: &str) -> Fixture {
3328        Fixture {
3329            id: id.to_string(),
3330            category: None,
3331            description: "test fixture".to_string(),
3332            tags: vec![],
3333            skip: None,
3334            env: None,
3335            call: None,
3336            input: serde_json::Value::Null,
3337            mock_response: Some(crate::fixture::MockResponse {
3338                status: 200,
3339                body: Some(serde_json::Value::Null),
3340                stream_chunks: None,
3341                headers: std::collections::HashMap::new(),
3342            }),
3343            source: String::new(),
3344            http: None,
3345            assertions: vec![Assertion {
3346                assertion_type: "not_error".to_string(),
3347                ..Default::default()
3348            }],
3349            visitor: None,
3350        }
3351    }
3352
3353    /// snake_case function names in `[e2e.call]` must be routed through `to_go_name`
3354    /// so the emitted Go call uses the idiomatic CamelCase (e.g. `CleanExtractedText`
3355    /// instead of `clean_extracted_text`).
3356    #[test]
3357    fn test_go_method_name_uses_go_casing() {
3358        let e2e_config = E2eConfig {
3359            call: CallConfig {
3360                function: "clean_extracted_text".to_string(),
3361                module: "github.com/example/mylib".to_string(),
3362                result_var: "result".to_string(),
3363                returns_result: true,
3364                ..CallConfig::default()
3365            },
3366            ..E2eConfig::default()
3367        };
3368
3369        let fixture = make_fixture("basic_text");
3370        let resolver = FieldResolver::new(
3371            &std::collections::HashMap::new(),
3372            &std::collections::HashSet::new(),
3373            &std::collections::HashSet::new(),
3374            &std::collections::HashSet::new(),
3375            &std::collections::HashSet::new(),
3376        );
3377        let mut out = String::new();
3378        render_test_function(&mut out, &fixture, "kreuzberg", &resolver, &e2e_config);
3379
3380        assert!(
3381            out.contains("kreuzberg.CleanExtractedText("),
3382            "expected Go-cased method name 'CleanExtractedText', got:\n{out}"
3383        );
3384        assert!(
3385            !out.contains("kreuzberg.clean_extracted_text("),
3386            "must not emit raw snake_case method name, got:\n{out}"
3387        );
3388    }
3389
3390    #[test]
3391    fn test_streaming_fixture_emits_collect_snippet() {
3392        // A streaming fixture should emit `stream, err :=` and the collect loop.
3393        let streaming_fixture_json = r#"{
3394            "id": "basic_stream",
3395            "description": "basic streaming test",
3396            "call": "chat_stream",
3397            "input": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
3398            "mock_response": {
3399                "status": 200,
3400                "stream_chunks": [{"delta": "hello"}]
3401            },
3402            "assertions": [
3403                {"type": "count_min", "field": "chunks", "value": 1}
3404            ]
3405        }"#;
3406        let fixture: Fixture = serde_json::from_str(streaming_fixture_json).unwrap();
3407        assert!(fixture.is_streaming_mock(), "fixture should be detected as streaming");
3408
3409        let e2e_config = E2eConfig {
3410            call: CallConfig {
3411                function: "chat_stream".to_string(),
3412                module: "github.com/example/mylib".to_string(),
3413                result_var: "result".to_string(),
3414                returns_result: true,
3415                r#async: true,
3416                ..CallConfig::default()
3417            },
3418            ..E2eConfig::default()
3419        };
3420
3421        let resolver = FieldResolver::new(
3422            &std::collections::HashMap::new(),
3423            &std::collections::HashSet::new(),
3424            &std::collections::HashSet::new(),
3425            &std::collections::HashSet::new(),
3426            &std::collections::HashSet::new(),
3427        );
3428
3429        let mut out = String::new();
3430        render_test_function(&mut out, &fixture, "pkg", &resolver, &e2e_config);
3431
3432        assert!(out.contains("stream, err :="), "should use stream binding, got:\n{out}");
3433        assert!(
3434            out.contains("for chunk := range stream"),
3435            "should emit collect loop, got:\n{out}"
3436        );
3437    }
3438
3439    #[test]
3440    fn test_streaming_with_client_factory_and_json_arg() {
3441        // Mimics the real liter-llm setup: no returns_result on the call,
3442        // json_object arg (binding_returns_error=true), and client_factory from
3443        // the default Go call override.
3444        use alef_core::config::e2e::{ArgMapping, CallOverride};
3445        let streaming_fixture_json = r#"{
3446            "id": "basic_stream_client",
3447            "description": "basic streaming test with client",
3448            "call": "chat_stream",
3449            "input": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
3450            "mock_response": {
3451                "status": 200,
3452                "stream_chunks": [{"delta": "hello"}]
3453            },
3454            "assertions": [
3455                {"type": "count_min", "field": "chunks", "value": 1}
3456            ]
3457        }"#;
3458        let fixture: Fixture = serde_json::from_str(streaming_fixture_json).unwrap();
3459        assert!(fixture.is_streaming_mock(), "fixture should be detected as streaming");
3460
3461        let go_override = CallOverride {
3462            client_factory: Some("CreateClient".to_string()),
3463            ..Default::default()
3464        };
3465
3466        let mut call_overrides = std::collections::HashMap::new();
3467        call_overrides.insert("go".to_string(), go_override);
3468
3469        let e2e_config = E2eConfig {
3470            call: CallConfig {
3471                function: "chat_stream".to_string(),
3472                module: "github.com/example/mylib".to_string(),
3473                result_var: "result".to_string(),
3474                returns_result: false, // NOT true — like real liter-llm
3475                r#async: true,
3476                args: vec![ArgMapping {
3477                    name: "request".to_string(),
3478                    field: "input".to_string(),
3479                    arg_type: "json_object".to_string(),
3480                    optional: false,
3481                    owned: true,
3482                    element_type: None,
3483                    go_type: None,
3484                }],
3485                overrides: call_overrides,
3486                ..CallConfig::default()
3487            },
3488            ..E2eConfig::default()
3489        };
3490
3491        let resolver = FieldResolver::new(
3492            &std::collections::HashMap::new(),
3493            &std::collections::HashSet::new(),
3494            &std::collections::HashSet::new(),
3495            &std::collections::HashSet::new(),
3496            &std::collections::HashSet::new(),
3497        );
3498
3499        let mut out = String::new();
3500        render_test_function(&mut out, &fixture, "pkg", &resolver, &e2e_config);
3501
3502        eprintln!("generated:\n{out}");
3503        assert!(out.contains("stream, err :="), "should use stream binding, got:\n{out}");
3504        assert!(
3505            out.contains("for chunk := range stream"),
3506            "should emit collect loop, got:\n{out}"
3507        );
3508    }
3509
3510    /// When `segments` is an optional field (Option<Vec<T>>) and a fixture asserts on
3511    /// `segments[0].id`, the prefix guard must be `result.Segments != nil` — NOT
3512    /// `result.Segments[0] != nil`, which is a compile error for a value-typed element.
3513    #[test]
3514    fn test_indexed_element_prefix_guard_uses_array_not_element() {
3515        let e2e_config = E2eConfig {
3516            call: CallConfig {
3517                function: "transcribe".to_string(),
3518                module: "github.com/example/mylib".to_string(),
3519                result_var: "result".to_string(),
3520                returns_result: true,
3521                ..CallConfig::default()
3522            },
3523            ..E2eConfig::default()
3524        };
3525
3526        let fixture = Fixture {
3527            id: "edge_transcribe_with_timestamps".to_string(),
3528            category: None,
3529            description: "Transcription with timestamp segments".to_string(),
3530            tags: vec![],
3531            skip: None,
3532            env: None,
3533            call: None,
3534            input: serde_json::Value::Null,
3535            mock_response: Some(crate::fixture::MockResponse {
3536                status: 200,
3537                body: Some(serde_json::Value::Null),
3538                stream_chunks: None,
3539                headers: std::collections::HashMap::new(),
3540            }),
3541            source: String::new(),
3542            http: None,
3543            assertions: vec![
3544                Assertion {
3545                    assertion_type: "not_error".to_string(),
3546                    ..Default::default()
3547                },
3548                Assertion {
3549                    assertion_type: "equals".to_string(),
3550                    field: Some("segments[0].id".to_string()),
3551                    value: Some(serde_json::Value::Number(serde_json::Number::from(0u64))),
3552                    ..Default::default()
3553                },
3554            ],
3555            visitor: None,
3556        };
3557
3558        let mut optional_fields = std::collections::HashSet::new();
3559        // segments is Option<Vec<TranscriptionSegment>> — the whole field is optional.
3560        optional_fields.insert("segments".to_string());
3561
3562        let mut array_fields = std::collections::HashSet::new();
3563        array_fields.insert("segments".to_string());
3564
3565        let resolver = FieldResolver::new(
3566            &std::collections::HashMap::new(),
3567            &optional_fields,
3568            &array_fields,
3569            &std::collections::HashSet::new(),
3570            &std::collections::HashSet::new(),
3571        );
3572
3573        let mut out = String::new();
3574        render_test_function(&mut out, &fixture, "pkg", &resolver, &e2e_config);
3575
3576        eprintln!("generated:\n{out}");
3577
3578        // Must guard on the slice itself — not on the element.
3579        assert!(
3580            out.contains("result.Segments != nil"),
3581            "guard must be on Segments (the slice), not an element; got:\n{out}"
3582        );
3583        // Must NOT emit the invalid element nil check.
3584        assert!(
3585            !out.contains("result.Segments[0] != nil"),
3586            "must not emit Segments[0] != nil for a value-type element; got:\n{out}"
3587        );
3588    }
3589}