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