Skip to main content

alef_e2e/codegen/
go.rs

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