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