Skip to main content

alef_e2e/codegen/
go.rs

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