Skip to main content

alef_e2e/codegen/
go.rs

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