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