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, HttpFixture};
7use alef_codegen::naming::{go_param_name, to_go_name};
8use alef_core::backend::GeneratedFile;
9use alef_core::config::AlefConfig;
10use alef_core::hash::{self, CommentStyle};
11use anyhow::Result;
12use heck::ToUpperCamelCase;
13use std::fmt::Write as FmtWrite;
14use std::path::PathBuf;
15
16use super::E2eCodegen;
17
18/// Go e2e code generator.
19pub struct GoCodegen;
20
21impl E2eCodegen for GoCodegen {
22    fn generate(
23        &self,
24        groups: &[FixtureGroup],
25        e2e_config: &E2eConfig,
26        alef_config: &AlefConfig,
27    ) -> Result<Vec<GeneratedFile>> {
28        let lang = self.language_name();
29        let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);
30
31        let mut files = Vec::new();
32
33        // Resolve call config with overrides (for module path and import alias).
34        let call = &e2e_config.call;
35        let overrides = call.overrides.get(lang);
36        let module_path = overrides
37            .and_then(|o| o.module.as_ref())
38            .cloned()
39            .unwrap_or_else(|| call.module.clone());
40        let import_alias = overrides
41            .and_then(|o| o.alias.as_ref())
42            .cloned()
43            .unwrap_or_else(|| "pkg".to_string());
44
45        // Resolve package config.
46        let go_pkg = e2e_config.resolve_package("go");
47        let go_module_path = go_pkg
48            .as_ref()
49            .and_then(|p| p.module.as_ref())
50            .cloned()
51            .unwrap_or_else(|| module_path.clone());
52        let replace_path = go_pkg.as_ref().and_then(|p| p.path.as_ref()).cloned();
53        let go_version = go_pkg
54            .as_ref()
55            .and_then(|p| p.version.as_ref())
56            .cloned()
57            .unwrap_or_else(|| {
58                alef_config
59                    .resolved_version()
60                    .map(|v| format!("v{v}"))
61                    .unwrap_or_else(|| "v0.0.0".to_string())
62            });
63        let field_resolver = FieldResolver::new(
64            &e2e_config.fields,
65            &e2e_config.fields_optional,
66            &e2e_config.result_fields,
67            &e2e_config.fields_array,
68        );
69
70        // Generate go.mod. In registry mode, omit the `replace` directive so the
71        // module is fetched from the Go module proxy.
72        let effective_replace = match e2e_config.dep_mode {
73            crate::config::DependencyMode::Registry => None,
74            crate::config::DependencyMode::Local => replace_path.as_deref().map(String::from),
75        };
76        files.push(GeneratedFile {
77            path: output_base.join("go.mod"),
78            content: render_go_mod(&go_module_path, effective_replace.as_deref(), &go_version),
79            generated_header: false,
80        });
81
82        // Generate test files per category.
83        for group in groups {
84            let active: Vec<&Fixture> = group
85                .fixtures
86                .iter()
87                .filter(|f| f.skip.as_ref().is_none_or(|s| !s.should_skip(lang)))
88                .collect();
89
90            if active.is_empty() {
91                continue;
92            }
93
94            let filename = format!("{}_test.go", sanitize_filename(&group.category));
95            let content = render_test_file(
96                &group.category,
97                &active,
98                &module_path,
99                &import_alias,
100                &field_resolver,
101                e2e_config,
102            );
103            files.push(GeneratedFile {
104                path: output_base.join(filename),
105                content,
106                generated_header: true,
107            });
108        }
109
110        Ok(files)
111    }
112
113    fn language_name(&self) -> &'static str {
114        "go"
115    }
116}
117
118fn render_go_mod(go_module_path: &str, replace_path: Option<&str>, version: &str) -> String {
119    let mut out = String::new();
120    let _ = writeln!(out, "module e2e_go");
121    let _ = writeln!(out);
122    let _ = writeln!(out, "go 1.26");
123    let _ = writeln!(out);
124    let _ = writeln!(out, "require (");
125    let _ = writeln!(out, "\t{go_module_path} {version}");
126    let _ = writeln!(out, "\tgithub.com/stretchr/testify v1.11.1");
127    let _ = writeln!(out, ")");
128
129    if let Some(path) = replace_path {
130        let _ = writeln!(out);
131        let _ = writeln!(out, "replace {go_module_path} => {path}");
132    }
133
134    out
135}
136
137fn render_test_file(
138    category: &str,
139    fixtures: &[&Fixture],
140    go_module_path: &str,
141    import_alias: &str,
142    field_resolver: &FieldResolver,
143    e2e_config: &crate::config::E2eConfig,
144) -> String {
145    let mut out = String::new();
146
147    // Go convention: generated file marker must appear before the package declaration.
148    out.push_str(&hash::header(CommentStyle::DoubleSlash));
149    let _ = writeln!(out);
150
151    // Determine if any fixture actually uses the pkg import.
152    // Fixtures without mock_response are emitted as t.Skip() stubs and don't reference the
153    // package — omit the import when no fixture needs it to avoid the Go "imported and not
154    // used" compile error.
155    let needs_pkg = fixtures.iter().any(|f| f.mock_response.is_some());
156
157    // Determine if we need the "os" import (mock_url args, or HTTP fixtures
158    // that read MOCK_SERVER_URL via os.Getenv).
159    let needs_os = fixtures.iter().any(|f| {
160        if f.is_http_test() {
161            return true;
162        }
163        let call_args = &e2e_config.resolve_call(f.call.as_deref()).args;
164        call_args.iter().any(|a| a.arg_type == "mock_url")
165    });
166
167    // Determine if we need "encoding/json" (handle args with non-null config,
168    // or json_object args that will be unmarshalled into a typed struct).
169    let needs_json = fixtures.iter().any(|f| {
170        let call = e2e_config.resolve_call(f.call.as_deref());
171        let call_args = &call.args;
172        // handle args with non-null config value
173        let has_handle = call_args.iter().any(|a| a.arg_type == "handle") && {
174            call_args.iter().filter(|a| a.arg_type == "handle").any(|a| {
175                let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
176                let v = f.input.get(field).unwrap_or(&serde_json::Value::Null);
177                !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty()))
178            })
179        };
180        // json_object args with options_type or array values (will use JSON unmarshal)
181        let go_override = call.overrides.get("go");
182        let opts_type = go_override.and_then(|o| o.options_type.as_deref()).or_else(|| {
183            e2e_config
184                .call
185                .overrides
186                .get("go")
187                .and_then(|o| o.options_type.as_deref())
188        });
189        let has_json_obj = call_args.iter().any(|a| {
190            if a.arg_type != "json_object" {
191                return false;
192            }
193            let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
194            let v = f.input.get(field).unwrap_or(&serde_json::Value::Null);
195            if v.is_array() {
196                return true;
197            } // array → []string unmarshal
198            opts_type.is_some() && v.is_object() && !v.as_object().is_some_and(|o| o.is_empty())
199        });
200        has_handle || has_json_obj
201    });
202
203    // Determine if we need "encoding/base64" (bytes-type args decoded at runtime).
204    let needs_base64 = fixtures.iter().any(|f| {
205        let call_args = &e2e_config.resolve_call(f.call.as_deref()).args;
206        call_args.iter().any(|a| {
207            if a.arg_type != "bytes" {
208                return false;
209            }
210            let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
211            matches!(f.input.get(field), Some(serde_json::Value::String(_)))
212        })
213    });
214
215    // Determine if we need the "fmt" import (CustomTemplate visitor actions with placeholders).
216    let needs_fmt = fixtures.iter().any(|f| {
217        f.visitor.as_ref().is_some_and(|v| {
218            v.callbacks.values().any(|action| {
219                if let CallbackAction::CustomTemplate { template } = action {
220                    template.contains('{')
221                } else {
222                    false
223                }
224            })
225        })
226    });
227
228    // Determine if we need the "strings" import.
229    // Only count assertions whose fields are actually valid for the result type.
230    let needs_strings = fixtures.iter().any(|f| {
231        f.assertions.iter().any(|a| {
232            let type_needs_strings = if a.assertion_type == "equals" {
233                // equals with string values needs strings.TrimSpace
234                a.value.as_ref().is_some_and(|v| v.is_string())
235            } else {
236                matches!(
237                    a.assertion_type.as_str(),
238                    "contains" | "contains_all" | "contains_any" | "not_contains" | "starts_with" | "ends_with"
239                )
240            };
241            let field_valid = a
242                .field
243                .as_ref()
244                .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
245                .unwrap_or(true);
246            type_needs_strings && field_valid
247        })
248    });
249
250    // Determine if we need the testify assert import (used for count_min, count_max,
251    // is_true, is_false, and method_result assertions).
252    let needs_assert = fixtures.iter().any(|f| {
253        f.assertions.iter().any(|a| {
254            let field_valid = a
255                .field
256                .as_ref()
257                .map(|f| f.is_empty() || field_resolver.is_valid_for_result(f))
258                .unwrap_or(true);
259            let type_needs_assert = matches!(
260                a.assertion_type.as_str(),
261                "count_min"
262                    | "count_max"
263                    | "is_true"
264                    | "is_false"
265                    | "method_result"
266                    | "min_length"
267                    | "max_length"
268                    | "matches_regex"
269            );
270            type_needs_assert && field_valid
271        })
272    });
273
274    // Determine if we need "net/http" and "io" (HTTP server tests via HTTP client).
275    let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());
276    let needs_http = has_http_fixtures;
277    // io.ReadAll is only emitted when an HTTP fixture has a body assertion.
278    let needs_io = fixtures
279        .iter()
280        .any(|f| f.http.as_ref().is_some_and(|h| h.expected_response.body.is_some()));
281
282    // Determine if we need "reflect" (for HTTP response body JSON comparison).
283    let needs_reflect = fixtures.iter().any(|f| {
284        if let Some(http) = &f.http {
285            if let Some(body) = &http.expected_response.body {
286                matches!(body, serde_json::Value::Object(_) | serde_json::Value::Array(_))
287            } else {
288                false
289            }
290        } else {
291            false
292        }
293    });
294
295    let _ = writeln!(out, "// E2e tests for category: {category}");
296    let _ = writeln!(out, "package e2e_test");
297    let _ = writeln!(out);
298    let _ = writeln!(out, "import (");
299    if needs_base64 {
300        let _ = writeln!(out, "\t\"encoding/base64\"");
301    }
302    if needs_json || needs_reflect {
303        let _ = writeln!(out, "\t\"encoding/json\"");
304    }
305    if needs_fmt {
306        let _ = writeln!(out, "\t\"fmt\"");
307    }
308    if needs_io {
309        let _ = writeln!(out, "\t\"io\"");
310    }
311    if needs_http {
312        let _ = writeln!(out, "\t\"net/http\"");
313    }
314    if needs_os {
315        let _ = writeln!(out, "\t\"os\"");
316    }
317    if needs_reflect {
318        let _ = writeln!(out, "\t\"reflect\"");
319    }
320    if needs_strings || needs_http {
321        let _ = writeln!(out, "\t\"strings\"");
322    }
323    let _ = writeln!(out, "\t\"testing\"");
324    if needs_assert {
325        let _ = writeln!(out);
326        let _ = writeln!(out, "\t\"github.com/stretchr/testify/assert\"");
327    }
328    if needs_pkg {
329        let _ = writeln!(out);
330        let _ = writeln!(out, "\t{import_alias} \"{go_module_path}\"");
331    }
332    let _ = writeln!(out, ")");
333    let _ = writeln!(out);
334
335    // Emit package-level visitor structs (must be outside any function in Go).
336    for fixture in fixtures.iter() {
337        if let Some(visitor_spec) = &fixture.visitor {
338            let struct_name = visitor_struct_name(&fixture.id);
339            emit_go_visitor_struct(&mut out, &struct_name, visitor_spec, import_alias);
340            let _ = writeln!(out);
341        }
342    }
343
344    for (i, fixture) in fixtures.iter().enumerate() {
345        render_test_function(&mut out, fixture, import_alias, field_resolver, e2e_config);
346        if i + 1 < fixtures.len() {
347            let _ = writeln!(out);
348        }
349    }
350
351    // Clean up trailing newlines.
352    while out.ends_with("\n\n") {
353        out.pop();
354    }
355    if !out.ends_with('\n') {
356        out.push('\n');
357    }
358    out
359}
360
361fn render_test_function(
362    out: &mut String,
363    fixture: &Fixture,
364    import_alias: &str,
365    field_resolver: &FieldResolver,
366    e2e_config: &crate::config::E2eConfig,
367) {
368    let fn_name = fixture.id.to_upper_camel_case();
369    let description = &fixture.description;
370
371    // Delegate HTTP fixtures to the HTTP-specific renderer.
372    if let Some(http) = &fixture.http {
373        render_http_test_function(out, fixture, http);
374        return;
375    }
376
377    // The Go binding wraps a C FFI layer and does not expose a HandleRequest
378    // (or equivalent) callable. Non-HTTP non-mock_response fixtures cannot be
379    // tested via Go — emit a documented stub to keep the generated package
380    // compilable. HTTP fixtures dispatch to the HTTP renderer above.
381    if fixture.mock_response.is_none() {
382        let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
383        let _ = writeln!(out, "\t// {description}");
384        let _ = writeln!(
385            out,
386            "\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")"
387        );
388        let _ = writeln!(out, "}}");
389        return;
390    }
391
392    // Resolve call config per-fixture (supports named calls via fixture.call).
393    let call_config = e2e_config.resolve_call(fixture.call.as_deref());
394    let lang = "go";
395    let overrides = call_config.overrides.get(lang);
396    let function_name = to_go_name(
397        overrides
398            .and_then(|o| o.function.as_ref())
399            .map(String::as_str)
400            .unwrap_or(&call_config.function),
401    );
402    let result_var = &call_config.result_var;
403    let args = &call_config.args;
404
405    // Whether the function returns (value, error) or just (error) or just (value).
406    // Check Go override first, fall back to call-level returns_result.
407    let returns_result = overrides
408        .and_then(|o| o.returns_result)
409        .unwrap_or(call_config.returns_result);
410
411    // Whether the function returns only error (no value component), i.e. Result<(), E>.
412    // When returns_result=true and returns_void=true, Go emits `err :=` not `_, err :=`.
413    let returns_void = call_config.returns_void;
414
415    // result_is_simple: result is a scalar (*string, *bool, etc.) not a struct.
416    // Check Go override first, then Rust override as fallback.
417    let result_is_simple = overrides.map(|o| o.result_is_simple).unwrap_or_else(|| {
418        call_config
419            .overrides
420            .get("rust")
421            .map(|o| o.result_is_simple)
422            .unwrap_or(false)
423    });
424
425    // result_is_array: the simple result is a slice/array type (e.g., []string).
426    // Only relevant when result_is_simple is true.
427    let result_is_array = overrides.map(|o| o.result_is_array).unwrap_or(false);
428
429    // Per-call Go options_type, falling back to the default call's Go override.
430    let call_options_type = overrides.and_then(|o| o.options_type.as_deref()).or_else(|| {
431        e2e_config
432            .call
433            .overrides
434            .get("go")
435            .and_then(|o| o.options_type.as_deref())
436    });
437
438    let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
439
440    let (mut setup_lines, args_str) =
441        build_args_and_setup(&fixture.input, args, import_alias, call_options_type, &fixture.id);
442
443    // Build visitor if present — struct is at package level, just instantiate here.
444    let mut visitor_arg = String::new();
445    if fixture.visitor.is_some() {
446        let struct_name = visitor_struct_name(&fixture.id);
447        setup_lines.push(format!("visitor := &{struct_name}{{}}"));
448        visitor_arg = "visitor".to_string();
449    }
450
451    let final_args = if visitor_arg.is_empty() {
452        args_str
453    } else {
454        format!("{args_str}, {visitor_arg}")
455    };
456
457    let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
458    let _ = writeln!(out, "\t// {description}");
459
460    for line in &setup_lines {
461        let _ = writeln!(out, "\t{line}");
462    }
463
464    if expects_error {
465        if returns_result && !returns_void {
466            let _ = writeln!(out, "\t_, err := {import_alias}.{function_name}({final_args})");
467        } else {
468            let _ = writeln!(out, "\terr := {import_alias}.{function_name}({final_args})");
469        }
470        let _ = writeln!(out, "\tif err == nil {{");
471        let _ = writeln!(out, "\t\tt.Errorf(\"expected an error, but call succeeded\")");
472        let _ = writeln!(out, "\t}}");
473        let _ = writeln!(out, "}}");
474        return;
475    }
476
477    // Check if any assertion actually uses the result variable.
478    // If all assertions are skipped (field not on result type), use `_` to avoid
479    // Go's "declared and not used" compile error.
480    let has_usable_assertion = fixture.assertions.iter().any(|a| {
481        if a.assertion_type == "not_error" || a.assertion_type == "error" {
482            return false;
483        }
484        // method_result assertions always use the result variable.
485        if a.assertion_type == "method_result" {
486            return true;
487        }
488        match &a.field {
489            Some(f) if !f.is_empty() => field_resolver.is_valid_for_result(f),
490            _ => true,
491        }
492    });
493
494    // For result_is_simple functions, the result variable IS the value (e.g. *string, *bool).
495    // We create a local `value` that dereferences it so assertions can use a plain type.
496    // For functions that return (value, error): emit `result, err :=`
497    // For functions that return only error: emit `err :=`
498    // For functions that return only a value (result_is_simple, no error): emit `result :=`
499    if !returns_result && result_is_simple {
500        // Function returns a single value, no error (e.g. *string, *bool).
501        let result_binding = if has_usable_assertion {
502            result_var.to_string()
503        } else {
504            "_".to_string()
505        };
506        // In Go, `_ :=` is invalid — must use `_ =` for the blank identifier.
507        let assign_op = if result_binding == "_" { "=" } else { ":=" };
508        let _ = writeln!(
509            out,
510            "\t{result_binding} {assign_op} {import_alias}.{function_name}({final_args})"
511        );
512        if has_usable_assertion && result_binding != "_" {
513            // Emit nil check and dereference for simple pointer results.
514            let _ = writeln!(out, "\tif {result_var} == nil {{");
515            let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
516            let _ = writeln!(out, "\t}}");
517            let _ = writeln!(out, "\tvalue := *{result_var}");
518        }
519    } else if !returns_result || returns_void {
520        // Function returns only error (either returns_result=false, or returns_result=true
521        // with returns_void=true meaning the Go function signature is `func(...) error`).
522        let _ = writeln!(out, "\terr := {import_alias}.{function_name}({final_args})");
523        let _ = writeln!(out, "\tif err != nil {{");
524        let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
525        let _ = writeln!(out, "\t}}");
526        // No result variable to use in assertions.
527        let _ = writeln!(out, "}}");
528        return;
529    } else {
530        // returns_result = true, returns_void = false: function returns (value, error).
531        let result_binding = if has_usable_assertion {
532            result_var.to_string()
533        } else {
534            "_".to_string()
535        };
536        let _ = writeln!(
537            out,
538            "\t{result_binding}, err := {import_alias}.{function_name}({final_args})"
539        );
540        let _ = writeln!(out, "\tif err != nil {{");
541        let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
542        let _ = writeln!(out, "\t}}");
543        if result_is_simple && has_usable_assertion && result_binding != "_" {
544            // Emit nil check and dereference for simple pointer results.
545            let _ = writeln!(out, "\tif {result_var} == nil {{");
546            let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
547            let _ = writeln!(out, "\t}}");
548            let _ = writeln!(out, "\tvalue := *{result_var}");
549        }
550    }
551
552    // For result_is_simple functions, assertions reference `value` (the dereferenced result).
553    let effective_result_var = if result_is_simple && has_usable_assertion {
554        "value".to_string()
555    } else {
556        result_var.to_string()
557    };
558
559    // Collect optional fields referenced by assertions and emit nil-safe
560    // dereference blocks so that assertions can use plain string locals.
561    // Only dereference fields whose assertion values are strings (or that are
562    // used in string-oriented assertions like equals/contains with string values).
563    let mut optional_locals: std::collections::HashMap<String, String> = std::collections::HashMap::new();
564    for assertion in &fixture.assertions {
565        if let Some(f) = &assertion.field {
566            if !f.is_empty() {
567                let resolved = field_resolver.resolve(f);
568                if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
569                    // Only create deref locals for string-valued fields that are NOT arrays.
570                    // Array fields (e.g., *[]string) must keep their pointer form so
571                    // render_assertion can emit strings.Join(*field, " ") rather than
572                    // treating them as plain strings.
573                    let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
574                    let is_array_field = field_resolver.is_array(resolved);
575                    if !is_string_field || is_array_field {
576                        // Non-string optional fields (e.g., *uint64) and array optional
577                        // fields (e.g., *[]string) are handled by nil guards in render_assertion.
578                        continue;
579                    }
580                    let field_expr = field_resolver.accessor(f, "go", &effective_result_var);
581                    let local_var = go_param_name(&resolved.replace(['.', '[', ']'], "_"));
582                    if field_resolver.has_map_access(f) {
583                        // Go map access returns a value type (string), not a pointer.
584                        // Use the value directly — empty string means not present.
585                        let _ = writeln!(out, "\t{local_var} := {field_expr}");
586                    } else {
587                        let _ = writeln!(out, "\tvar {local_var} string");
588                        let _ = writeln!(out, "\tif {field_expr} != nil {{");
589                        let _ = writeln!(out, "\t\t{local_var} = *{field_expr}");
590                        let _ = writeln!(out, "\t}}");
591                    }
592                    optional_locals.insert(f.clone(), local_var);
593                }
594            }
595        }
596    }
597
598    // Emit assertions, wrapping in nil guards when an intermediate path segment is optional.
599    for assertion in &fixture.assertions {
600        if let Some(f) = &assertion.field {
601            if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
602                // Check if any prefix of the dotted path is optional (pointer in Go).
603                // e.g., "document.nodes" — if "document" is optional, guard the whole block.
604                let parts: Vec<&str> = f.split('.').collect();
605                let mut guard_expr: Option<String> = None;
606                for i in 1..parts.len() {
607                    let prefix = parts[..i].join(".");
608                    let resolved_prefix = field_resolver.resolve(&prefix);
609                    if field_resolver.is_optional(resolved_prefix) {
610                        let accessor = field_resolver.accessor(&prefix, "go", &effective_result_var);
611                        guard_expr = Some(accessor);
612                        break;
613                    }
614                }
615                if let Some(guard) = guard_expr {
616                    // Only emit nil guard if the assertion will actually produce code
617                    // (not just a skip comment), to avoid empty branches (SA9003).
618                    if field_resolver.is_valid_for_result(f) {
619                        let _ = writeln!(out, "\tif {guard} != nil {{");
620                        // Render into a temporary buffer so we can re-indent by one
621                        // tab level to sit inside the nil-guard block.
622                        let mut nil_buf = String::new();
623                        render_assertion(
624                            &mut nil_buf,
625                            assertion,
626                            &effective_result_var,
627                            import_alias,
628                            field_resolver,
629                            &optional_locals,
630                            result_is_simple,
631                            result_is_array,
632                        );
633                        for line in nil_buf.lines() {
634                            let _ = writeln!(out, "\t{line}");
635                        }
636                        let _ = writeln!(out, "\t}}");
637                    } else {
638                        render_assertion(
639                            out,
640                            assertion,
641                            &effective_result_var,
642                            import_alias,
643                            field_resolver,
644                            &optional_locals,
645                            result_is_simple,
646                            result_is_array,
647                        );
648                    }
649                    continue;
650                }
651            }
652        }
653        render_assertion(
654            out,
655            assertion,
656            &effective_result_var,
657            import_alias,
658            field_resolver,
659            &optional_locals,
660            result_is_simple,
661            result_is_array,
662        );
663    }
664
665    let _ = writeln!(out, "}}");
666}
667
668/// Render an HTTP server test function using net/http against MOCK_SERVER_URL.
669///
670/// The mock server registers each fixture at `/fixtures/<fixture_id>` and returns the
671/// pre-canned response. Tests send the correct HTTP method and headers to that endpoint.
672fn render_http_test_function(out: &mut String, fixture: &Fixture, http: &HttpFixture) {
673    let fn_name = fixture.id.to_upper_camel_case();
674    let description = &fixture.description;
675    let request = &http.request;
676    let expected = &http.expected_response;
677    let method = request.method.to_uppercase();
678    let fixture_id = &fixture.id;
679    let expected_status = expected.status_code;
680
681    let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
682    let _ = writeln!(out, "\t// {description}");
683    let _ = writeln!(out, "\tbaseURL := os.Getenv(\"MOCK_SERVER_URL\")");
684    let _ = writeln!(out, "\tif baseURL == \"\" {{");
685    let _ = writeln!(out, "\t\tbaseURL = \"http://localhost:8080\"");
686    let _ = writeln!(out, "\t}}");
687
688    // Build request body.
689    let body_expr = if let Some(body) = &request.body {
690        let json = serde_json::to_string(body).unwrap_or_default();
691        let escaped = go_string_literal(&json);
692        format!("strings.NewReader({})", escaped)
693    } else {
694        "strings.NewReader(\"\")".to_string()
695    };
696
697    let _ = writeln!(out, "\tbody := {body_expr}");
698    let _ = writeln!(
699        out,
700        "\treq, err := http.NewRequest(\"{method}\", baseURL+\"/fixtures/{fixture_id}\", body)"
701    );
702    let _ = writeln!(out, "\tif err != nil {{");
703    let _ = writeln!(out, "\t\tt.Fatalf(\"new request failed: %v\", err)");
704    let _ = writeln!(out, "\t}}");
705
706    // Set headers.
707    let content_type = request.content_type.as_deref().unwrap_or("application/json");
708    if request.body.is_some() {
709        let _ = writeln!(out, "\treq.Header.Set(\"Content-Type\", \"{content_type}\")");
710    }
711
712    for (name, value) in &request.headers {
713        let escaped_name = go_string_literal(name);
714        let escaped_value = go_string_literal(value);
715        let _ = writeln!(out, "\treq.Header.Set({escaped_name}, {escaped_value})");
716    }
717
718    // Add cookies.
719    if !request.cookies.is_empty() {
720        for (name, value) in &request.cookies {
721            let escaped_name = go_string_literal(name);
722            let escaped_value = go_string_literal(value);
723            let _ = writeln!(
724                out,
725                "\treq.AddCookie(&http.Cookie{{Name: {escaped_name}, Value: {escaped_value}}})"
726            );
727        }
728    }
729
730    // Send request.
731    let _ = writeln!(out, "\tresp, err := http.DefaultClient.Do(req)");
732    let _ = writeln!(out, "\tif err != nil {{");
733    let _ = writeln!(out, "\t\tt.Fatalf(\"request failed: %v\", err)");
734    let _ = writeln!(out, "\t}}");
735    let _ = writeln!(out, "\tdefer resp.Body.Close()");
736
737    // Read body — only declare bodyBytes if a body assertion will use it.
738    let body_used = expected.body.is_some();
739    if body_used {
740        let _ = writeln!(out, "\tbodyBytes, err := io.ReadAll(resp.Body)");
741        let _ = writeln!(out, "\tif err != nil {{");
742        let _ = writeln!(out, "\t\tt.Fatalf(\"read body failed: %v\", err)");
743        let _ = writeln!(out, "\t}}");
744    }
745
746    // Assert status code.
747    let _ = writeln!(out, "\tif resp.StatusCode != {expected_status} {{");
748    let _ = writeln!(
749        out,
750        "\t\tt.Fatalf(\"status: got %d want {expected_status}\", resp.StatusCode)"
751    );
752    let _ = writeln!(out, "\t}}");
753
754    // Assert body if expected.
755    if let Some(expected_body) = &expected.body {
756        match expected_body {
757            serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
758                let json_str = serde_json::to_string(expected_body).unwrap_or_default();
759                let escaped = go_string_literal(&json_str);
760                // Use `any` so JSON objects and arrays both decode correctly.
761                let _ = writeln!(out, "\tvar got any");
762                let _ = writeln!(out, "\tvar want any");
763                let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &got); err != nil {{");
764                let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal got: %v\", err)");
765                let _ = writeln!(out, "\t}}");
766                let _ = writeln!(
767                    out,
768                    "\tif err := json.Unmarshal([]byte({escaped}), &want); err != nil {{"
769                );
770                let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal want: %v\", err)");
771                let _ = writeln!(out, "\t}}");
772                let _ = writeln!(out, "\tif !reflect.DeepEqual(got, want) {{");
773                let _ = writeln!(out, "\t\tt.Fatalf(\"body mismatch: got %v want %v\", got, want)");
774                let _ = writeln!(out, "\t}}");
775            }
776            serde_json::Value::String(s) => {
777                let escaped = go_string_literal(s);
778                let _ = writeln!(out, "\twant := {escaped}");
779                let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
780                let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
781                let _ = writeln!(out, "\t}}");
782            }
783            other => {
784                let escaped = go_string_literal(&other.to_string());
785                let _ = writeln!(out, "\twant := {escaped}");
786                let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
787                let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
788                let _ = writeln!(out, "\t}}");
789            }
790        }
791    }
792
793    // Assert response headers if specified (skip special tokens and non-applicable headers).
794    for (name, value) in &expected.headers {
795        if value == "<<absent>>" || value == "<<present>>" || value == "<<uuid>>" {
796            // Skip special-token assertions for now.
797            continue;
798        }
799        let escaped_name = go_string_literal(name);
800        let escaped_value = go_string_literal(value);
801        let _ = writeln!(
802            out,
803            "\tif !strings.Contains(resp.Header.Get({escaped_name}), {escaped_value}) {{"
804        );
805        let _ = writeln!(
806            out,
807            "\t\tt.Fatalf(\"header %s mismatch: got %q want to contain %q\", {escaped_name}, resp.Header.Get({escaped_name}), {escaped_value})"
808        );
809        let _ = writeln!(out, "\t}}");
810    }
811
812    let _ = writeln!(out, "}}");
813}
814
815/// Build setup lines (e.g. handle creation) and the argument list for the function call.
816///
817/// Returns `(setup_lines, args_string)`.
818fn build_args_and_setup(
819    input: &serde_json::Value,
820    args: &[crate::config::ArgMapping],
821    import_alias: &str,
822    options_type: Option<&str>,
823    fixture_id: &str,
824) -> (Vec<String>, String) {
825    use heck::ToUpperCamelCase;
826
827    if args.is_empty() {
828        return (Vec::new(), String::new());
829    }
830
831    let mut setup_lines: Vec<String> = Vec::new();
832    let mut parts: Vec<String> = Vec::new();
833
834    for arg in args {
835        if arg.arg_type == "mock_url" {
836            setup_lines.push(format!(
837                "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
838                arg.name,
839            ));
840            parts.push(arg.name.clone());
841            continue;
842        }
843
844        if arg.arg_type == "handle" {
845            // Generate a CreateEngine (or equivalent) call and pass the variable.
846            let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
847            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
848            let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
849            if config_value.is_null()
850                || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
851            {
852                setup_lines.push(format!(
853                    "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}",
854                    name = arg.name,
855                ));
856            } else {
857                let json_str = serde_json::to_string(config_value).unwrap_or_default();
858                let go_literal = go_string_literal(&json_str);
859                let name = &arg.name;
860                setup_lines.push(format!(
861                    "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}}"
862                ));
863                setup_lines.push(format!(
864                    "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}"
865                ));
866            }
867            parts.push(arg.name.clone());
868            continue;
869        }
870
871        let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
872        let val = input.get(field);
873
874        // Handle bytes type: fixture stores base64-encoded bytes.
875        // Emit a Go base64.StdEncoding.DecodeString call to decode at runtime.
876        if arg.arg_type == "bytes" {
877            let var_name = format!("{}Bytes", arg.name);
878            match val {
879                None | Some(serde_json::Value::Null) => {
880                    if arg.optional {
881                        parts.push("nil".to_string());
882                    } else {
883                        parts.push("[]byte{}".to_string());
884                    }
885                }
886                Some(serde_json::Value::String(s)) => {
887                    let go_b64 = go_string_literal(s);
888                    setup_lines.push(format!("{var_name}, _ := base64.StdEncoding.DecodeString({go_b64})"));
889                    parts.push(var_name);
890                }
891                Some(other) => {
892                    parts.push(format!("[]byte({})", json_to_go(other)));
893                }
894            }
895            continue;
896        }
897
898        match val {
899            None | Some(serde_json::Value::Null) if arg.optional => {
900                // Optional arg absent: emit Go zero/nil for the type.
901                match arg.arg_type.as_str() {
902                    "string" => {
903                        // Optional string in Go bindings is *string → nil.
904                        parts.push("nil".to_string());
905                    }
906                    "json_object" => {
907                        // Optional config struct → zero-value struct.
908                        if let Some(opts_type) = options_type {
909                            parts.push(format!("{import_alias}.{opts_type}{{}}"));
910                        } else {
911                            parts.push("nil".to_string());
912                        }
913                    }
914                    _ => {
915                        parts.push("nil".to_string());
916                    }
917                }
918            }
919            None | Some(serde_json::Value::Null) => {
920                // Required arg with no fixture value: pass a language-appropriate default.
921                let default_val = match arg.arg_type.as_str() {
922                    "string" => "\"\"".to_string(),
923                    "int" | "integer" | "i64" => "0".to_string(),
924                    "float" | "number" => "0.0".to_string(),
925                    "bool" | "boolean" => "false".to_string(),
926                    "json_object" => {
927                        if let Some(opts_type) = options_type {
928                            format!("{import_alias}.{opts_type}{{}}")
929                        } else {
930                            "nil".to_string()
931                        }
932                    }
933                    _ => "nil".to_string(),
934                };
935                parts.push(default_val);
936            }
937            Some(v) => {
938                match arg.arg_type.as_str() {
939                    "json_object" => {
940                        // JSON arrays unmarshal into []string (Go slices).
941                        // JSON objects with a known options_type unmarshal into that type.
942                        let is_array = v.is_array();
943                        let is_empty_obj = !is_array && v.is_object() && v.as_object().is_some_and(|o| o.is_empty());
944                        if is_empty_obj {
945                            if let Some(opts_type) = options_type {
946                                parts.push(format!("{import_alias}.{opts_type}{{}}"));
947                            } else {
948                                parts.push("nil".to_string());
949                            }
950                        } else if is_array {
951                            // Array type — unmarshal into []string (typical for paths/texts).
952                            let json_str = serde_json::to_string(v).unwrap_or_default();
953                            let go_literal = go_string_literal(&json_str);
954                            let var_name = &arg.name;
955                            setup_lines.push(format!(
956                                "var {var_name} []string\n\tif err := json.Unmarshal([]byte({go_literal}), &{var_name}); err != nil {{\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}}"
957                            ));
958                            parts.push(var_name.to_string());
959                        } else if let Some(opts_type) = options_type {
960                            // Object with known type — unmarshal into typed struct.
961                            let json_str = serde_json::to_string(v).unwrap_or_default();
962                            let go_literal = go_string_literal(&json_str);
963                            let var_name = &arg.name;
964                            setup_lines.push(format!(
965                                "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}}"
966                            ));
967                            parts.push(var_name.to_string());
968                        } else {
969                            parts.push(json_to_go(v));
970                        }
971                    }
972                    "string" if arg.optional => {
973                        // Optional string in Go is *string — take address of a local.
974                        let var_name = format!("{}Val", arg.name);
975                        let go_val = json_to_go(v);
976                        setup_lines.push(format!("{var_name} := {go_val}"));
977                        parts.push(format!("&{var_name}"));
978                    }
979                    _ => {
980                        parts.push(json_to_go(v));
981                    }
982                }
983            }
984        }
985    }
986
987    (setup_lines, parts.join(", "))
988}
989
990#[allow(clippy::too_many_arguments)]
991fn render_assertion(
992    out: &mut String,
993    assertion: &Assertion,
994    result_var: &str,
995    import_alias: &str,
996    field_resolver: &FieldResolver,
997    optional_locals: &std::collections::HashMap<String, String>,
998    result_is_simple: bool,
999    result_is_array: bool,
1000) {
1001    // Handle synthetic / derived fields before the is_valid_for_result check
1002    // so they are never treated as struct field accesses on the result.
1003    if !result_is_simple {
1004        if let Some(f) = &assertion.field {
1005            // embed_texts returns *[][]float32; the embedding matrix is *result_var.
1006            // We emit inline func() expressions so we don't need additional variables.
1007            let embed_deref = format!("(*{result_var})");
1008            match f.as_str() {
1009                "chunks_have_content" => {
1010                    let pred = format!(
1011                        "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range *chunks {{ if c.Content == \"\" {{ return false }} }}; return true }}()"
1012                    );
1013                    match assertion.assertion_type.as_str() {
1014                        "is_true" => {
1015                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1016                        }
1017                        "is_false" => {
1018                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1019                        }
1020                        _ => {
1021                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1022                        }
1023                    }
1024                    return;
1025                }
1026                "chunks_have_embeddings" => {
1027                    let pred = format!(
1028                        "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 }}()"
1029                    );
1030                    match assertion.assertion_type.as_str() {
1031                        "is_true" => {
1032                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1033                        }
1034                        "is_false" => {
1035                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1036                        }
1037                        _ => {
1038                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1039                        }
1040                    }
1041                    return;
1042                }
1043                "embeddings" => {
1044                    match assertion.assertion_type.as_str() {
1045                        "count_equals" => {
1046                            if let Some(val) = &assertion.value {
1047                                if let Some(n) = val.as_u64() {
1048                                    let _ = writeln!(
1049                                        out,
1050                                        "\tassert.Equal(t, {n}, len({embed_deref}), \"expected exactly {n} elements\")"
1051                                    );
1052                                }
1053                            }
1054                        }
1055                        "count_min" => {
1056                            if let Some(val) = &assertion.value {
1057                                if let Some(n) = val.as_u64() {
1058                                    let _ = writeln!(
1059                                        out,
1060                                        "\tassert.GreaterOrEqual(t, len({embed_deref}), {n}, \"expected at least {n} elements\")"
1061                                    );
1062                                }
1063                            }
1064                        }
1065                        "not_empty" => {
1066                            let _ = writeln!(
1067                                out,
1068                                "\tassert.NotEmpty(t, {embed_deref}, \"expected non-empty embeddings\")"
1069                            );
1070                        }
1071                        "is_empty" => {
1072                            let _ = writeln!(out, "\tassert.Empty(t, {embed_deref}, \"expected empty embeddings\")");
1073                        }
1074                        _ => {
1075                            let _ = writeln!(
1076                                out,
1077                                "\t// skipped: unsupported assertion type on synthetic field 'embeddings'"
1078                            );
1079                        }
1080                    }
1081                    return;
1082                }
1083                "embedding_dimensions" => {
1084                    let expr = format!(
1085                        "func() int {{ if len({embed_deref}) == 0 {{ return 0 }}; return len({embed_deref}[0]) }}()"
1086                    );
1087                    match assertion.assertion_type.as_str() {
1088                        "equals" => {
1089                            if let Some(val) = &assertion.value {
1090                                if let Some(n) = val.as_u64() {
1091                                    let _ = writeln!(
1092                                        out,
1093                                        "\tif {expr} != {n} {{\n\t\tt.Errorf(\"equals mismatch: got %v\", {expr})\n\t}}"
1094                                    );
1095                                }
1096                            }
1097                        }
1098                        "greater_than" => {
1099                            if let Some(val) = &assertion.value {
1100                                if let Some(n) = val.as_u64() {
1101                                    let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
1102                                }
1103                            }
1104                        }
1105                        _ => {
1106                            let _ = writeln!(
1107                                out,
1108                                "\t// skipped: unsupported assertion type on synthetic field 'embedding_dimensions'"
1109                            );
1110                        }
1111                    }
1112                    return;
1113                }
1114                "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
1115                    let pred = match f.as_str() {
1116                        "embeddings_valid" => {
1117                            format!(
1118                                "func() bool {{ for _, e := range {embed_deref} {{ if len(e) == 0 {{ return false }} }}; return true }}()"
1119                            )
1120                        }
1121                        "embeddings_finite" => {
1122                            format!(
1123                                "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 }}()"
1124                            )
1125                        }
1126                        "embeddings_non_zero" => {
1127                            format!(
1128                                "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 }}()"
1129                            )
1130                        }
1131                        "embeddings_normalized" => {
1132                            format!(
1133                                "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 }}()"
1134                            )
1135                        }
1136                        _ => unreachable!(),
1137                    };
1138                    match assertion.assertion_type.as_str() {
1139                        "is_true" => {
1140                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1141                        }
1142                        "is_false" => {
1143                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1144                        }
1145                        _ => {
1146                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1147                        }
1148                    }
1149                    return;
1150                }
1151                // ---- keywords / keywords_count ----
1152                // Go ExtractionResult does not expose extracted_keywords; skip.
1153                "keywords" | "keywords_count" => {
1154                    let _ = writeln!(out, "\t// skipped: field '{f}' not available on Go ExtractionResult");
1155                    return;
1156                }
1157                _ => {}
1158            }
1159        }
1160    }
1161
1162    // Skip assertions on fields that don't exist on the result type.
1163    // When result_is_simple, all field assertions operate on the scalar result directly.
1164    if !result_is_simple {
1165        if let Some(f) = &assertion.field {
1166            if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1167                let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
1168                return;
1169            }
1170        }
1171    }
1172
1173    let field_expr = if result_is_simple {
1174        // The result IS the value — field access is irrelevant.
1175        result_var.to_string()
1176    } else {
1177        match &assertion.field {
1178            Some(f) if !f.is_empty() => {
1179                // Use the local variable if the field was dereferenced above.
1180                if let Some(local_var) = optional_locals.get(f.as_str()) {
1181                    local_var.clone()
1182                } else {
1183                    field_resolver.accessor(f, "go", result_var)
1184                }
1185            }
1186            _ => result_var.to_string(),
1187        }
1188    };
1189
1190    // Check if the field (after resolution) is optional, which means it's a pointer in Go.
1191    // Also check if a `.length` suffix's parent is optional (e.g., metadata.headings.length
1192    // where metadata.headings is optional → len() needs dereference).
1193    let is_optional = assertion
1194        .field
1195        .as_ref()
1196        .map(|f| {
1197            let resolved = field_resolver.resolve(f);
1198            let check_path = resolved
1199                .strip_suffix(".length")
1200                .or_else(|| resolved.strip_suffix(".count"))
1201                .or_else(|| resolved.strip_suffix(".size"))
1202                .unwrap_or(resolved);
1203            field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
1204        })
1205        .unwrap_or(false);
1206
1207    // When field_expr is `len(X)` and X is an optional (pointer) field, rewrite to `len(*X)`
1208    // and we'll wrap with a nil guard in the assertion handlers.
1209    let field_expr = if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') {
1210        let inner = &field_expr[4..field_expr.len() - 1];
1211        format!("len(*{inner})")
1212    } else {
1213        field_expr
1214    };
1215    // Build the nil-guard expression for the inner pointer (without len wrapper).
1216    let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
1217        Some(field_expr[5..field_expr.len() - 1].to_string())
1218    } else {
1219        None
1220    };
1221
1222    // For optional non-string fields that weren't dereferenced into locals,
1223    // we need to dereference the pointer in comparisons.
1224    let deref_field_expr = if is_optional && !field_expr.starts_with("len(") {
1225        format!("*{field_expr}")
1226    } else {
1227        field_expr.clone()
1228    };
1229
1230    // Detect array element access (e.g., `result.Assets[0].ContentHash`).
1231    // When the field_expr contains `[0]`, we must guard against an out-of-bounds
1232    // panic by checking that the array is non-empty first.
1233    // Extract the array slice expression (everything before `[0]`).
1234    let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
1235        let array_expr = &field_expr[..idx];
1236        Some(array_expr.to_string())
1237    } else {
1238        None
1239    };
1240
1241    // Render the assertion into a temporary buffer first, then wrap with the array
1242    // bounds guard (if needed) by adding one extra level of indentation.
1243    let mut assertion_buf = String::new();
1244    let out_ref = &mut assertion_buf;
1245
1246    match assertion.assertion_type.as_str() {
1247        "equals" => {
1248            if let Some(expected) = &assertion.value {
1249                let go_val = json_to_go(expected);
1250                // For string equality, trim whitespace to handle trailing newlines from the converter.
1251                if expected.is_string() {
1252                    // Wrap field expression with strings.TrimSpace() for string comparisons.
1253                    let trimmed_field = if is_optional && !field_expr.starts_with("len(") {
1254                        format!("strings.TrimSpace(*{field_expr})")
1255                    } else {
1256                        format!("strings.TrimSpace({field_expr})")
1257                    };
1258                    if is_optional && !field_expr.starts_with("len(") {
1259                        let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
1260                    } else {
1261                        let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
1262                    }
1263                } else if is_optional && !field_expr.starts_with("len(") {
1264                    let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
1265                } else {
1266                    let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
1267                }
1268                let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
1269                let _ = writeln!(out_ref, "\t}}");
1270            }
1271        }
1272        "contains" => {
1273            if let Some(expected) = &assertion.value {
1274                let go_val = json_to_go(expected);
1275                // Determine the "string view" of the field expression.
1276                // - *[]string → strings.Join(*field_expr, " ") for a nil-guarded check
1277                // - *string → string(*field_expr)
1278                // - string → string(field_expr) (or just field_expr for plain strings)
1279                // - result_is_array (result_is_simple + array result) → strings.Join(field_expr, " ")
1280                let resolved_field = assertion.field.as_deref().unwrap_or("");
1281                let resolved_name = field_resolver.resolve(resolved_field);
1282                let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
1283                let is_opt =
1284                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
1285                let field_for_contains = if is_opt && field_is_array {
1286                    format!("strings.Join(*{field_expr}, \" \")")
1287                } else if is_opt {
1288                    format!("string(*{field_expr})")
1289                } else if field_is_array {
1290                    format!("strings.Join({field_expr}, \" \")")
1291                } else {
1292                    format!("string({field_expr})")
1293                };
1294                if is_opt {
1295                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1296                    let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
1297                    let _ = writeln!(
1298                        out_ref,
1299                        "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
1300                    );
1301                    let _ = writeln!(out_ref, "\t}}");
1302                    let _ = writeln!(out_ref, "\t}}");
1303                } else {
1304                    let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
1305                    let _ = writeln!(
1306                        out_ref,
1307                        "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
1308                    );
1309                    let _ = writeln!(out_ref, "\t}}");
1310                }
1311            }
1312        }
1313        "contains_all" => {
1314            if let Some(values) = &assertion.values {
1315                let resolved_field = assertion.field.as_deref().unwrap_or("");
1316                let resolved_name = field_resolver.resolve(resolved_field);
1317                let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
1318                let is_opt =
1319                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
1320                for val in values {
1321                    let go_val = json_to_go(val);
1322                    let field_for_contains = if is_opt && field_is_array {
1323                        format!("strings.Join(*{field_expr}, \" \")")
1324                    } else if is_opt {
1325                        format!("string(*{field_expr})")
1326                    } else if field_is_array {
1327                        format!("strings.Join({field_expr}, \" \")")
1328                    } else {
1329                        format!("string({field_expr})")
1330                    };
1331                    if is_opt {
1332                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1333                        let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
1334                        let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
1335                        let _ = writeln!(out_ref, "\t}}");
1336                        let _ = writeln!(out_ref, "\t}}");
1337                    } else {
1338                        let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
1339                        let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
1340                        let _ = writeln!(out_ref, "\t}}");
1341                    }
1342                }
1343            }
1344        }
1345        "not_contains" => {
1346            if let Some(expected) = &assertion.value {
1347                let go_val = json_to_go(expected);
1348                let resolved_field = assertion.field.as_deref().unwrap_or("");
1349                let resolved_name = field_resolver.resolve(resolved_field);
1350                let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
1351                let is_opt =
1352                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
1353                let field_for_contains = if is_opt && field_is_array {
1354                    format!("strings.Join(*{field_expr}, \" \")")
1355                } else if is_opt {
1356                    format!("string(*{field_expr})")
1357                } else if field_is_array {
1358                    format!("strings.Join({field_expr}, \" \")")
1359                } else {
1360                    format!("string({field_expr})")
1361                };
1362                let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
1363                let _ = writeln!(
1364                    out_ref,
1365                    "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
1366                );
1367                let _ = writeln!(out_ref, "\t}}");
1368            }
1369        }
1370        "not_empty" => {
1371            // For optional struct pointers (not arrays), just check != nil.
1372            // For optional slice/string pointers, check nil and len.
1373            let field_is_array = {
1374                let rf = assertion.field.as_deref().unwrap_or("");
1375                let rn = field_resolver.resolve(rf);
1376                field_resolver.is_array(rn)
1377            };
1378            if is_optional && !field_is_array {
1379                // Struct pointer: non-empty means not nil.
1380                let _ = writeln!(out_ref, "\tif {field_expr} == nil {{");
1381            } else if is_optional {
1382                let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
1383            } else {
1384                let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
1385            }
1386            let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
1387            let _ = writeln!(out_ref, "\t}}");
1388        }
1389        "is_empty" => {
1390            let field_is_array = {
1391                let rf = assertion.field.as_deref().unwrap_or("");
1392                let rn = field_resolver.resolve(rf);
1393                field_resolver.is_array(rn)
1394            };
1395            if is_optional && !field_is_array {
1396                // Struct pointer: empty means nil.
1397                let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1398            } else if is_optional {
1399                let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
1400            } else {
1401                let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
1402            }
1403            let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
1404            let _ = writeln!(out_ref, "\t}}");
1405        }
1406        "contains_any" => {
1407            if let Some(values) = &assertion.values {
1408                let resolved_field = assertion.field.as_deref().unwrap_or("");
1409                let resolved_name = field_resolver.resolve(resolved_field);
1410                let field_is_array = field_resolver.is_array(resolved_name);
1411                let is_opt =
1412                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
1413                let field_for_contains = if is_opt && field_is_array {
1414                    format!("strings.Join(*{field_expr}, \" \")")
1415                } else if is_opt {
1416                    format!("*{field_expr}")
1417                } else if field_is_array {
1418                    format!("strings.Join({field_expr}, \" \")")
1419                } else {
1420                    field_expr.clone()
1421                };
1422                let _ = writeln!(out_ref, "\t{{");
1423                let _ = writeln!(out_ref, "\t\tfound := false");
1424                for val in values {
1425                    let go_val = json_to_go(val);
1426                    let _ = writeln!(
1427                        out_ref,
1428                        "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
1429                    );
1430                }
1431                let _ = writeln!(out_ref, "\t\tif !found {{");
1432                let _ = writeln!(
1433                    out_ref,
1434                    "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
1435                );
1436                let _ = writeln!(out_ref, "\t\t}}");
1437                let _ = writeln!(out_ref, "\t}}");
1438            }
1439        }
1440        "greater_than" => {
1441            if let Some(val) = &assertion.value {
1442                let go_val = json_to_go(val);
1443                // Use `< N+1` instead of `<= N` to avoid golangci-lint sloppyLen
1444                // warning when N is 0 (len(x) <= 0 → len(x) < 1).
1445                // For optional (pointer) fields, dereference and guard with nil check.
1446                if is_optional {
1447                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1448                    if let Some(n) = val.as_u64() {
1449                        let next = n + 1;
1450                        let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {next} {{");
1451                    } else {
1452                        let _ = writeln!(out_ref, "\t\tif {deref_field_expr} <= {go_val} {{");
1453                    }
1454                    let _ = writeln!(
1455                        out_ref,
1456                        "\t\t\tt.Errorf(\"expected > {go_val}, got %v\", {deref_field_expr})"
1457                    );
1458                    let _ = writeln!(out_ref, "\t\t}}");
1459                    let _ = writeln!(out_ref, "\t}}");
1460                } else if let Some(n) = val.as_u64() {
1461                    let next = n + 1;
1462                    let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
1463                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
1464                    let _ = writeln!(out_ref, "\t}}");
1465                } else {
1466                    let _ = writeln!(out_ref, "\tif {field_expr} <= {go_val} {{");
1467                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
1468                    let _ = writeln!(out_ref, "\t}}");
1469                }
1470            }
1471        }
1472        "less_than" => {
1473            if let Some(val) = &assertion.value {
1474                let go_val = json_to_go(val);
1475                let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
1476                let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
1477                let _ = writeln!(out_ref, "\t}}");
1478            }
1479        }
1480        "greater_than_or_equal" => {
1481            if let Some(val) = &assertion.value {
1482                let go_val = json_to_go(val);
1483                if let Some(ref guard) = nil_guard_expr {
1484                    let _ = writeln!(out_ref, "\tif {guard} != nil {{");
1485                    let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
1486                    let _ = writeln!(
1487                        out_ref,
1488                        "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
1489                    );
1490                    let _ = writeln!(out_ref, "\t\t}}");
1491                    let _ = writeln!(out_ref, "\t}}");
1492                } else if is_optional && !field_expr.starts_with("len(") {
1493                    // Optional pointer field: nil-guard and dereference before comparison.
1494                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1495                    let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {go_val} {{");
1496                    let _ = writeln!(
1497                        out_ref,
1498                        "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {deref_field_expr})"
1499                    );
1500                    let _ = writeln!(out_ref, "\t\t}}");
1501                    let _ = writeln!(out_ref, "\t}}");
1502                } else {
1503                    let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
1504                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
1505                    let _ = writeln!(out_ref, "\t}}");
1506                }
1507            }
1508        }
1509        "less_than_or_equal" => {
1510            if let Some(val) = &assertion.value {
1511                let go_val = json_to_go(val);
1512                let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
1513                let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
1514                let _ = writeln!(out_ref, "\t}}");
1515            }
1516        }
1517        "starts_with" => {
1518            if let Some(expected) = &assertion.value {
1519                let go_val = json_to_go(expected);
1520                let field_for_prefix = if is_optional
1521                    && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
1522                {
1523                    format!("string(*{field_expr})")
1524                } else {
1525                    format!("string({field_expr})")
1526                };
1527                let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
1528                let _ = writeln!(
1529                    out_ref,
1530                    "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
1531                );
1532                let _ = writeln!(out_ref, "\t}}");
1533            }
1534        }
1535        "count_min" => {
1536            if let Some(val) = &assertion.value {
1537                if let Some(n) = val.as_u64() {
1538                    if is_optional {
1539                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1540                        let _ = writeln!(
1541                            out_ref,
1542                            "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected at least {n} elements\")"
1543                        );
1544                        let _ = writeln!(out_ref, "\t}}");
1545                    } else {
1546                        let _ = writeln!(
1547                            out_ref,
1548                            "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
1549                        );
1550                    }
1551                }
1552            }
1553        }
1554        "count_equals" => {
1555            if let Some(val) = &assertion.value {
1556                if let Some(n) = val.as_u64() {
1557                    if is_optional {
1558                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1559                        let _ = writeln!(
1560                            out_ref,
1561                            "\t\tassert.Equal(t, len(*{field_expr}), {n}, \"expected exactly {n} elements\")"
1562                        );
1563                        let _ = writeln!(out_ref, "\t}}");
1564                    } else {
1565                        let _ = writeln!(
1566                            out_ref,
1567                            "\tassert.Equal(t, len({field_expr}), {n}, \"expected exactly {n} elements\")"
1568                        );
1569                    }
1570                }
1571            }
1572        }
1573        "is_true" => {
1574            if is_optional {
1575                let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1576                let _ = writeln!(out_ref, "\t\tassert.True(t, *{field_expr}, \"expected true\")");
1577                let _ = writeln!(out_ref, "\t}}");
1578            } else {
1579                let _ = writeln!(out_ref, "\tassert.True(t, {field_expr}, \"expected true\")");
1580            }
1581        }
1582        "is_false" => {
1583            if is_optional {
1584                let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1585                let _ = writeln!(out_ref, "\t\tassert.False(t, *{field_expr}, \"expected false\")");
1586                let _ = writeln!(out_ref, "\t}}");
1587            } else {
1588                let _ = writeln!(out_ref, "\tassert.False(t, {field_expr}, \"expected false\")");
1589            }
1590        }
1591        "method_result" => {
1592            if let Some(method_name) = &assertion.method {
1593                let info = build_go_method_call(result_var, method_name, assertion.args.as_ref(), import_alias);
1594                let check = assertion.check.as_deref().unwrap_or("is_true");
1595                // For pointer-returning functions, dereference with `*`. Value-returning
1596                // functions (e.g., NodeInfo field access) are used directly.
1597                let deref_expr = if info.is_pointer {
1598                    format!("*{}", info.call_expr)
1599                } else {
1600                    info.call_expr.clone()
1601                };
1602                match check {
1603                    "equals" => {
1604                        if let Some(val) = &assertion.value {
1605                            if val.is_boolean() {
1606                                if val.as_bool() == Some(true) {
1607                                    let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
1608                                } else {
1609                                    let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
1610                                }
1611                            } else {
1612                                // Apply type cast to numeric literals when the method returns
1613                                // a typed uint (e.g., *uint) to avoid reflect.DeepEqual
1614                                // mismatches between int and uint in testify's assert.Equal.
1615                                let go_val = if let Some(cast) = info.value_cast {
1616                                    if val.is_number() {
1617                                        format!("{cast}({})", json_to_go(val))
1618                                    } else {
1619                                        json_to_go(val)
1620                                    }
1621                                } else {
1622                                    json_to_go(val)
1623                                };
1624                                let _ = writeln!(
1625                                    out_ref,
1626                                    "\tassert.Equal(t, {go_val}, {deref_expr}, \"method_result equals assertion failed\")"
1627                                );
1628                            }
1629                        }
1630                    }
1631                    "is_true" => {
1632                        let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
1633                    }
1634                    "is_false" => {
1635                        let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
1636                    }
1637                    "greater_than_or_equal" => {
1638                        if let Some(val) = &assertion.value {
1639                            let n = val.as_u64().unwrap_or(0);
1640                            // Use the value_cast type if available (e.g., uint for named_children_count).
1641                            let cast = info.value_cast.unwrap_or("uint");
1642                            let _ = writeln!(
1643                                out_ref,
1644                                "\tassert.GreaterOrEqual(t, {deref_expr}, {cast}({n}), \"expected >= {n}\")"
1645                            );
1646                        }
1647                    }
1648                    "count_min" => {
1649                        if let Some(val) = &assertion.value {
1650                            let n = val.as_u64().unwrap_or(0);
1651                            let _ = writeln!(
1652                                out_ref,
1653                                "\tassert.GreaterOrEqual(t, len({deref_expr}), {n}, \"expected at least {n} elements\")"
1654                            );
1655                        }
1656                    }
1657                    "contains" => {
1658                        if let Some(val) = &assertion.value {
1659                            let go_val = json_to_go(val);
1660                            let _ = writeln!(
1661                                out_ref,
1662                                "\tassert.Contains(t, {deref_expr}, {go_val}, \"expected result to contain value\")"
1663                            );
1664                        }
1665                    }
1666                    "is_error" => {
1667                        let _ = writeln!(out_ref, "\t{{");
1668                        let _ = writeln!(out_ref, "\t\t_, methodErr := {}", info.call_expr);
1669                        let _ = writeln!(out_ref, "\t\tassert.Error(t, methodErr)");
1670                        let _ = writeln!(out_ref, "\t}}");
1671                    }
1672                    other_check => {
1673                        panic!("Go e2e generator: unsupported method_result check type: {other_check}");
1674                    }
1675                }
1676            } else {
1677                panic!("Go e2e generator: method_result assertion missing 'method' field");
1678            }
1679        }
1680        "min_length" => {
1681            if let Some(val) = &assertion.value {
1682                if let Some(n) = val.as_u64() {
1683                    if is_optional {
1684                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1685                        let _ = writeln!(
1686                            out_ref,
1687                            "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected length >= {n}\")"
1688                        );
1689                        let _ = writeln!(out_ref, "\t}}");
1690                    } else {
1691                        let _ = writeln!(
1692                            out_ref,
1693                            "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected length >= {n}\")"
1694                        );
1695                    }
1696                }
1697            }
1698        }
1699        "max_length" => {
1700            if let Some(val) = &assertion.value {
1701                if let Some(n) = val.as_u64() {
1702                    if is_optional {
1703                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1704                        let _ = writeln!(
1705                            out_ref,
1706                            "\t\tassert.LessOrEqual(t, len(*{field_expr}), {n}, \"expected length <= {n}\")"
1707                        );
1708                        let _ = writeln!(out_ref, "\t}}");
1709                    } else {
1710                        let _ = writeln!(
1711                            out_ref,
1712                            "\tassert.LessOrEqual(t, len({field_expr}), {n}, \"expected length <= {n}\")"
1713                        );
1714                    }
1715                }
1716            }
1717        }
1718        "ends_with" => {
1719            if let Some(expected) = &assertion.value {
1720                let go_val = json_to_go(expected);
1721                let field_for_suffix = if is_optional
1722                    && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
1723                {
1724                    format!("string(*{field_expr})")
1725                } else {
1726                    format!("string({field_expr})")
1727                };
1728                let _ = writeln!(out_ref, "\tif !strings.HasSuffix({field_for_suffix}, {go_val}) {{");
1729                let _ = writeln!(
1730                    out_ref,
1731                    "\t\tt.Errorf(\"expected to end with %s, got %v\", {go_val}, {field_expr})"
1732                );
1733                let _ = writeln!(out_ref, "\t}}");
1734            }
1735        }
1736        "matches_regex" => {
1737            if let Some(expected) = &assertion.value {
1738                let go_val = json_to_go(expected);
1739                let field_for_regex = if is_optional
1740                    && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
1741                {
1742                    format!("*{field_expr}")
1743                } else {
1744                    field_expr.clone()
1745                };
1746                let _ = writeln!(
1747                    out_ref,
1748                    "\tassert.Regexp(t, {go_val}, {field_for_regex}, \"expected value to match regex\")"
1749                );
1750            }
1751        }
1752        "not_error" => {
1753            // Already handled by the `if err != nil` check above.
1754        }
1755        "error" => {
1756            // Handled at the test function level.
1757        }
1758        other => {
1759            panic!("Go e2e generator: unsupported assertion type: {other}");
1760        }
1761    }
1762
1763    // If the assertion accesses an array element via [0], wrap the generated code in a
1764    // bounds check to prevent an index-out-of-range panic when the array is empty.
1765    if let Some(ref arr) = array_guard {
1766        if !assertion_buf.is_empty() {
1767            let _ = writeln!(out, "\tif len({arr}) > 0 {{");
1768            // Re-indent each line by one additional tab level.
1769            for line in assertion_buf.lines() {
1770                let _ = writeln!(out, "\t{line}");
1771            }
1772            let _ = writeln!(out, "\t}}");
1773        }
1774    } else {
1775        out.push_str(&assertion_buf);
1776    }
1777}
1778
1779/// Metadata about the return type of a Go method call for `method_result` assertions.
1780struct GoMethodCallInfo {
1781    /// The call expression string.
1782    call_expr: String,
1783    /// Whether the return type is a pointer (needs `*` dereference for value comparison).
1784    is_pointer: bool,
1785    /// Optional Go type cast to apply to numeric literal values in `equals` assertions
1786    /// (e.g., `"uint"` so that `0` becomes `uint(0)` to match `*uint` deref type).
1787    value_cast: Option<&'static str>,
1788}
1789
1790/// Build a Go call expression for a `method_result` assertion on a tree-sitter Tree.
1791///
1792/// Maps method names to the appropriate Go function calls, matching the Go binding API
1793/// in `packages/go/binding.go`. Returns a [`GoMethodCallInfo`] describing the call and
1794/// its return type characteristics.
1795///
1796/// Return types by method:
1797/// - `has_error_nodes`, `contains_node_type` → `*bool` (pointer)
1798/// - `error_count` → `*uint` (pointer, value_cast = "uint")
1799/// - `tree_to_sexp` → `*string` (pointer)
1800/// - `root_node_type` → `string` via `RootNodeInfo(tree).Kind` (value)
1801/// - `named_children_count` → `uint` via `RootNodeInfo(tree).NamedChildCount` (value, value_cast = "uint")
1802/// - `find_nodes_by_type` → `*[]NodeInfo` (pointer to slice)
1803/// - `run_query` → `(*[]QueryMatch, error)` (pointer + error; use `is_error` check type)
1804fn build_go_method_call(
1805    result_var: &str,
1806    method_name: &str,
1807    args: Option<&serde_json::Value>,
1808    import_alias: &str,
1809) -> GoMethodCallInfo {
1810    match method_name {
1811        "root_node_type" => GoMethodCallInfo {
1812            call_expr: format!("{import_alias}.RootNodeInfo({result_var}).Kind"),
1813            is_pointer: false,
1814            value_cast: None,
1815        },
1816        "named_children_count" => GoMethodCallInfo {
1817            call_expr: format!("{import_alias}.RootNodeInfo({result_var}).NamedChildCount"),
1818            is_pointer: false,
1819            value_cast: Some("uint"),
1820        },
1821        "has_error_nodes" => GoMethodCallInfo {
1822            call_expr: format!("{import_alias}.TreeHasErrorNodes({result_var})"),
1823            is_pointer: true,
1824            value_cast: None,
1825        },
1826        "error_count" | "tree_error_count" => GoMethodCallInfo {
1827            call_expr: format!("{import_alias}.TreeErrorCount({result_var})"),
1828            is_pointer: true,
1829            value_cast: Some("uint"),
1830        },
1831        "tree_to_sexp" => GoMethodCallInfo {
1832            call_expr: format!("{import_alias}.TreeToSexp({result_var})"),
1833            is_pointer: true,
1834            value_cast: None,
1835        },
1836        "contains_node_type" => {
1837            let node_type = args
1838                .and_then(|a| a.get("node_type"))
1839                .and_then(|v| v.as_str())
1840                .unwrap_or("");
1841            GoMethodCallInfo {
1842                call_expr: format!("{import_alias}.TreeContainsNodeType({result_var}, \"{node_type}\")"),
1843                is_pointer: true,
1844                value_cast: None,
1845            }
1846        }
1847        "find_nodes_by_type" => {
1848            let node_type = args
1849                .and_then(|a| a.get("node_type"))
1850                .and_then(|v| v.as_str())
1851                .unwrap_or("");
1852            GoMethodCallInfo {
1853                call_expr: format!("{import_alias}.FindNodesByType({result_var}, \"{node_type}\")"),
1854                is_pointer: true,
1855                value_cast: None,
1856            }
1857        }
1858        "run_query" => {
1859            let query_source = args
1860                .and_then(|a| a.get("query_source"))
1861                .and_then(|v| v.as_str())
1862                .unwrap_or("");
1863            let language = args
1864                .and_then(|a| a.get("language"))
1865                .and_then(|v| v.as_str())
1866                .unwrap_or("");
1867            let query_lit = go_string_literal(query_source);
1868            let lang_lit = go_string_literal(language);
1869            // RunQuery returns (*[]QueryMatch, error) — use is_error check type.
1870            GoMethodCallInfo {
1871                call_expr: format!("{import_alias}.RunQuery({result_var}, {lang_lit}, {query_lit}, []byte(source))"),
1872                is_pointer: false,
1873                value_cast: None,
1874            }
1875        }
1876        other => {
1877            let method_pascal = other.to_upper_camel_case();
1878            GoMethodCallInfo {
1879                call_expr: format!("{result_var}.{method_pascal}()"),
1880                is_pointer: false,
1881                value_cast: None,
1882            }
1883        }
1884    }
1885}
1886
1887/// Convert a `serde_json::Value` to a Go literal string.
1888fn json_to_go(value: &serde_json::Value) -> String {
1889    match value {
1890        serde_json::Value::String(s) => go_string_literal(s),
1891        serde_json::Value::Bool(b) => b.to_string(),
1892        serde_json::Value::Number(n) => n.to_string(),
1893        serde_json::Value::Null => "nil".to_string(),
1894        // For complex types, serialize to JSON string and pass as literal.
1895        other => go_string_literal(&other.to_string()),
1896    }
1897}
1898
1899// ---------------------------------------------------------------------------
1900// Visitor generation
1901// ---------------------------------------------------------------------------
1902
1903/// Derive a unique, exported Go struct name for a visitor from a fixture ID.
1904///
1905/// E.g. `visitor_continue_default` → `visitorContinueDefault` (unexported, avoids
1906/// polluting the exported API of the test package while still being package-level).
1907fn visitor_struct_name(fixture_id: &str) -> String {
1908    use heck::ToUpperCamelCase;
1909    // Use UpperCamelCase so Go treats it as exported — required for method sets.
1910    format!("testVisitor{}", fixture_id.to_upper_camel_case())
1911}
1912
1913/// Emit a package-level Go struct declaration and all its visitor methods.
1914fn emit_go_visitor_struct(
1915    out: &mut String,
1916    struct_name: &str,
1917    visitor_spec: &crate::fixture::VisitorSpec,
1918    import_alias: &str,
1919) {
1920    let _ = writeln!(out, "type {struct_name} struct{{}}");
1921    for (method_name, action) in &visitor_spec.callbacks {
1922        emit_go_visitor_method(out, struct_name, method_name, action, import_alias);
1923    }
1924}
1925
1926/// Emit a Go visitor method for a callback action on the named struct.
1927fn emit_go_visitor_method(
1928    out: &mut String,
1929    struct_name: &str,
1930    method_name: &str,
1931    action: &CallbackAction,
1932    import_alias: &str,
1933) {
1934    let camel_method = method_to_camel(method_name);
1935    let params = match method_name {
1936        "visit_link" => format!("_ {import_alias}.NodeContext, href, text, title string"),
1937        "visit_image" => format!("_ {import_alias}.NodeContext, src, alt, title string"),
1938        "visit_heading" => format!("_ {import_alias}.NodeContext, level int, text, id string"),
1939        "visit_code_block" => format!("_ {import_alias}.NodeContext, lang, code string"),
1940        "visit_code_inline"
1941        | "visit_strong"
1942        | "visit_emphasis"
1943        | "visit_strikethrough"
1944        | "visit_underline"
1945        | "visit_subscript"
1946        | "visit_superscript"
1947        | "visit_mark"
1948        | "visit_button"
1949        | "visit_summary"
1950        | "visit_figcaption"
1951        | "visit_definition_term"
1952        | "visit_definition_description" => format!("_ {import_alias}.NodeContext, text string"),
1953        "visit_text" => format!("_ {import_alias}.NodeContext, text string"),
1954        "visit_list_item" => {
1955            format!("_ {import_alias}.NodeContext, ordered bool, marker, text string")
1956        }
1957        "visit_blockquote" => format!("_ {import_alias}.NodeContext, content string, depth int"),
1958        "visit_table_row" => format!("_ {import_alias}.NodeContext, cells []string, isHeader bool"),
1959        "visit_custom_element" => format!("_ {import_alias}.NodeContext, tagName, html string"),
1960        "visit_form" => format!("_ {import_alias}.NodeContext, actionUrl, method string"),
1961        "visit_input" => format!("_ {import_alias}.NodeContext, inputType, name, value string"),
1962        "visit_audio" | "visit_video" | "visit_iframe" => {
1963            format!("_ {import_alias}.NodeContext, src string")
1964        }
1965        "visit_details" => format!("_ {import_alias}.NodeContext, isOpen bool"),
1966        "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
1967            format!("_ {import_alias}.NodeContext, output string")
1968        }
1969        "visit_list_start" => format!("_ {import_alias}.NodeContext, ordered bool"),
1970        "visit_list_end" => format!("_ {import_alias}.NodeContext, ordered bool, output string"),
1971        _ => format!("_ {import_alias}.NodeContext"),
1972    };
1973
1974    let _ = writeln!(
1975        out,
1976        "func (v *{struct_name}) {camel_method}({params}) {import_alias}.VisitResult {{"
1977    );
1978    match action {
1979        CallbackAction::Skip => {
1980            let _ = writeln!(out, "\treturn {import_alias}.VisitResultSkip");
1981        }
1982        CallbackAction::Continue => {
1983            let _ = writeln!(out, "\treturn {import_alias}.VisitResultContinue");
1984        }
1985        CallbackAction::PreserveHtml => {
1986            let _ = writeln!(out, "\treturn {import_alias}.VisitResultPreserveHtml");
1987        }
1988        CallbackAction::Custom { output } => {
1989            let escaped = go_string_literal(output);
1990            let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped})");
1991        }
1992        CallbackAction::CustomTemplate { template } => {
1993            // Convert {var} placeholders to %s format verbs and collect arg names.
1994            // E.g. `QUOTE: "{text}"` → fmt.Sprintf("QUOTE: \"%s\"", text)
1995            let (fmt_str, fmt_args) = template_to_sprintf(template);
1996            let escaped_fmt = go_string_literal(&fmt_str);
1997            if fmt_args.is_empty() {
1998                let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped_fmt})");
1999            } else {
2000                let args_str = fmt_args.join(", ");
2001                let _ = writeln!(
2002                    out,
2003                    "\treturn {import_alias}.VisitResultCustom(fmt.Sprintf({escaped_fmt}, {args_str}))"
2004                );
2005            }
2006        }
2007    }
2008    let _ = writeln!(out, "}}");
2009}
2010
2011/// Convert a `{var}` template string into a `fmt.Sprintf` format string and argument list.
2012///
2013/// For example, `QUOTE: "{text}"` becomes `("QUOTE: \"%s\"", vec!["text"])`.
2014fn template_to_sprintf(template: &str) -> (String, Vec<String>) {
2015    let mut fmt_str = String::new();
2016    let mut args: Vec<String> = Vec::new();
2017    let mut chars = template.chars().peekable();
2018    while let Some(c) = chars.next() {
2019        if c == '{' {
2020            // Collect placeholder name until '}'.
2021            let mut name = String::new();
2022            for inner in chars.by_ref() {
2023                if inner == '}' {
2024                    break;
2025                }
2026                name.push(inner);
2027            }
2028            fmt_str.push_str("%s");
2029            args.push(name);
2030        } else {
2031            fmt_str.push(c);
2032        }
2033    }
2034    (fmt_str, args)
2035}
2036
2037/// Convert snake_case method names to Go camelCase.
2038fn method_to_camel(snake: &str) -> String {
2039    use heck::ToUpperCamelCase;
2040    snake.to_upper_camel_case()
2041}
2042
2043#[cfg(test)]
2044mod tests {
2045    use super::*;
2046    use crate::config::{CallConfig, E2eConfig};
2047    use crate::field_access::FieldResolver;
2048    use crate::fixture::{Assertion, Fixture};
2049
2050    fn make_fixture(id: &str) -> Fixture {
2051        Fixture {
2052            id: id.to_string(),
2053            category: None,
2054            description: "test fixture".to_string(),
2055            tags: vec![],
2056            skip: None,
2057            call: None,
2058            input: serde_json::Value::Null,
2059            mock_response: None,
2060            source: String::new(),
2061            http: None,
2062            assertions: vec![Assertion {
2063                assertion_type: "not_error".to_string(),
2064                field: None,
2065                value: None,
2066                values: None,
2067                method: None,
2068                args: None,
2069                check: None,
2070            }],
2071            visitor: None,
2072        }
2073    }
2074
2075    /// snake_case function names in `[e2e.call]` must be routed through `to_go_name`
2076    /// so the emitted Go call uses the idiomatic CamelCase (e.g. `CleanExtractedText`
2077    /// instead of `clean_extracted_text`).
2078    #[test]
2079    fn test_go_method_name_uses_go_casing() {
2080        let e2e_config = E2eConfig {
2081            call: CallConfig {
2082                function: "clean_extracted_text".to_string(),
2083                module: "github.com/example/mylib".to_string(),
2084                result_var: "result".to_string(),
2085                r#async: false,
2086                path: None,
2087                method: None,
2088                args: vec![],
2089                overrides: std::collections::HashMap::new(),
2090                returns_result: true,
2091                returns_void: false,
2092                skip_languages: vec![],
2093            },
2094            ..E2eConfig::default()
2095        };
2096
2097        let fixture = make_fixture("basic_text");
2098        let resolver = FieldResolver::new(
2099            &std::collections::HashMap::new(),
2100            &std::collections::HashSet::new(),
2101            &std::collections::HashSet::new(),
2102            &std::collections::HashSet::new(),
2103        );
2104        let mut out = String::new();
2105        render_test_function(&mut out, &fixture, "kreuzberg", &resolver, &e2e_config);
2106
2107        assert!(
2108            out.contains("kreuzberg.CleanExtractedText("),
2109            "expected Go-cased method name 'CleanExtractedText', got:\n{out}"
2110        );
2111        assert!(
2112            !out.contains("kreuzberg.clean_extracted_text("),
2113            "must not emit raw snake_case method name, got:\n{out}"
2114        );
2115    }
2116}