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    // Disable auto-follow so redirect-status fixtures (3xx) can assert the
732    // server's response status code rather than the followed target's status.
733    let _ = writeln!(out, "\tnoRedirectClient := &http.Client{{");
734    let _ = writeln!(
735        out,
736        "\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {{"
737    );
738    let _ = writeln!(out, "\t\t\treturn http.ErrUseLastResponse");
739    let _ = writeln!(out, "\t\t}},");
740    let _ = writeln!(out, "\t}}");
741    let _ = writeln!(out, "\tresp, err := noRedirectClient.Do(req)");
742    let _ = writeln!(out, "\tif err != nil {{");
743    let _ = writeln!(out, "\t\tt.Fatalf(\"request failed: %v\", err)");
744    let _ = writeln!(out, "\t}}");
745    let _ = writeln!(out, "\tdefer resp.Body.Close()");
746
747    // Read body — only declare bodyBytes if a body assertion will use it.
748    let body_used = expected.body.is_some();
749    if body_used {
750        let _ = writeln!(out, "\tbodyBytes, err := io.ReadAll(resp.Body)");
751        let _ = writeln!(out, "\tif err != nil {{");
752        let _ = writeln!(out, "\t\tt.Fatalf(\"read body failed: %v\", err)");
753        let _ = writeln!(out, "\t}}");
754    }
755
756    // Assert status code.
757    let _ = writeln!(out, "\tif resp.StatusCode != {expected_status} {{");
758    let _ = writeln!(
759        out,
760        "\t\tt.Fatalf(\"status: got %d want {expected_status}\", resp.StatusCode)"
761    );
762    let _ = writeln!(out, "\t}}");
763
764    // Assert body if expected.
765    if let Some(expected_body) = &expected.body {
766        match expected_body {
767            serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
768                let json_str = serde_json::to_string(expected_body).unwrap_or_default();
769                let escaped = go_string_literal(&json_str);
770                // Use `any` so JSON objects and arrays both decode correctly.
771                let _ = writeln!(out, "\tvar got any");
772                let _ = writeln!(out, "\tvar want any");
773                let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &got); err != nil {{");
774                let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal got: %v\", err)");
775                let _ = writeln!(out, "\t}}");
776                let _ = writeln!(
777                    out,
778                    "\tif err := json.Unmarshal([]byte({escaped}), &want); err != nil {{"
779                );
780                let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal want: %v\", err)");
781                let _ = writeln!(out, "\t}}");
782                let _ = writeln!(out, "\tif !reflect.DeepEqual(got, want) {{");
783                let _ = writeln!(out, "\t\tt.Fatalf(\"body mismatch: got %v want %v\", got, want)");
784                let _ = writeln!(out, "\t}}");
785            }
786            serde_json::Value::String(s) => {
787                let escaped = go_string_literal(s);
788                let _ = writeln!(out, "\twant := {escaped}");
789                let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
790                let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
791                let _ = writeln!(out, "\t}}");
792            }
793            other => {
794                let escaped = go_string_literal(&other.to_string());
795                let _ = writeln!(out, "\twant := {escaped}");
796                let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
797                let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
798                let _ = writeln!(out, "\t}}");
799            }
800        }
801    }
802
803    // Assert response headers if specified (skip special tokens and non-applicable headers).
804    for (name, value) in &expected.headers {
805        if value == "<<absent>>" || value == "<<present>>" || value == "<<uuid>>" {
806            // Skip special-token assertions for now.
807            continue;
808        }
809        // Skip headers the mock server cannot reproduce: content-encoding (no
810        // compression) and Connection (Go's net/http strips hop-by-hop headers
811        // when reading the response).
812        let lower = name.to_ascii_lowercase();
813        if lower == "content-encoding" || lower == "connection" {
814            continue;
815        }
816        let escaped_name = go_string_literal(name);
817        let escaped_value = go_string_literal(value);
818        let _ = writeln!(
819            out,
820            "\tif !strings.Contains(resp.Header.Get({escaped_name}), {escaped_value}) {{"
821        );
822        let _ = writeln!(
823            out,
824            "\t\tt.Fatalf(\"header %s mismatch: got %q want to contain %q\", {escaped_name}, resp.Header.Get({escaped_name}), {escaped_value})"
825        );
826        let _ = writeln!(out, "\t}}");
827    }
828
829    let _ = writeln!(out, "}}");
830}
831
832/// Build setup lines (e.g. handle creation) and the argument list for the function call.
833///
834/// Returns `(setup_lines, args_string)`.
835fn build_args_and_setup(
836    input: &serde_json::Value,
837    args: &[crate::config::ArgMapping],
838    import_alias: &str,
839    options_type: Option<&str>,
840    fixture_id: &str,
841) -> (Vec<String>, String) {
842    use heck::ToUpperCamelCase;
843
844    if args.is_empty() {
845        return (Vec::new(), String::new());
846    }
847
848    let mut setup_lines: Vec<String> = Vec::new();
849    let mut parts: Vec<String> = Vec::new();
850
851    for arg in args {
852        if arg.arg_type == "mock_url" {
853            setup_lines.push(format!(
854                "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
855                arg.name,
856            ));
857            parts.push(arg.name.clone());
858            continue;
859        }
860
861        if arg.arg_type == "handle" {
862            // Generate a CreateEngine (or equivalent) call and pass the variable.
863            let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
864            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
865            let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
866            if config_value.is_null()
867                || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
868            {
869                setup_lines.push(format!(
870                    "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}",
871                    name = arg.name,
872                ));
873            } else {
874                let json_str = serde_json::to_string(config_value).unwrap_or_default();
875                let go_literal = go_string_literal(&json_str);
876                let name = &arg.name;
877                setup_lines.push(format!(
878                    "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}}"
879                ));
880                setup_lines.push(format!(
881                    "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\tt.Fatalf(\"create handle failed: %v\", createErr)\n\t}}"
882                ));
883            }
884            parts.push(arg.name.clone());
885            continue;
886        }
887
888        let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
889        let val = input.get(field);
890
891        // Handle bytes type: fixture stores base64-encoded bytes.
892        // Emit a Go base64.StdEncoding.DecodeString call to decode at runtime.
893        if arg.arg_type == "bytes" {
894            let var_name = format!("{}Bytes", arg.name);
895            match val {
896                None | Some(serde_json::Value::Null) => {
897                    if arg.optional {
898                        parts.push("nil".to_string());
899                    } else {
900                        parts.push("[]byte{}".to_string());
901                    }
902                }
903                Some(serde_json::Value::String(s)) => {
904                    let go_b64 = go_string_literal(s);
905                    setup_lines.push(format!("{var_name}, _ := base64.StdEncoding.DecodeString({go_b64})"));
906                    parts.push(var_name);
907                }
908                Some(other) => {
909                    parts.push(format!("[]byte({})", json_to_go(other)));
910                }
911            }
912            continue;
913        }
914
915        match val {
916            None | Some(serde_json::Value::Null) if arg.optional => {
917                // Optional arg absent: emit Go zero/nil for the type.
918                match arg.arg_type.as_str() {
919                    "string" => {
920                        // Optional string in Go bindings is *string → nil.
921                        parts.push("nil".to_string());
922                    }
923                    "json_object" => {
924                        // Optional config struct → zero-value struct.
925                        if let Some(opts_type) = options_type {
926                            parts.push(format!("{import_alias}.{opts_type}{{}}"));
927                        } else {
928                            parts.push("nil".to_string());
929                        }
930                    }
931                    _ => {
932                        parts.push("nil".to_string());
933                    }
934                }
935            }
936            None | Some(serde_json::Value::Null) => {
937                // Required arg with no fixture value: pass a language-appropriate default.
938                let default_val = match arg.arg_type.as_str() {
939                    "string" => "\"\"".to_string(),
940                    "int" | "integer" | "i64" => "0".to_string(),
941                    "float" | "number" => "0.0".to_string(),
942                    "bool" | "boolean" => "false".to_string(),
943                    "json_object" => {
944                        if let Some(opts_type) = options_type {
945                            format!("{import_alias}.{opts_type}{{}}")
946                        } else {
947                            "nil".to_string()
948                        }
949                    }
950                    _ => "nil".to_string(),
951                };
952                parts.push(default_val);
953            }
954            Some(v) => {
955                match arg.arg_type.as_str() {
956                    "json_object" => {
957                        // JSON arrays unmarshal into []string (Go slices).
958                        // JSON objects with a known options_type unmarshal into that type.
959                        let is_array = v.is_array();
960                        let is_empty_obj = !is_array && v.is_object() && v.as_object().is_some_and(|o| o.is_empty());
961                        if is_empty_obj {
962                            if let Some(opts_type) = options_type {
963                                parts.push(format!("{import_alias}.{opts_type}{{}}"));
964                            } else {
965                                parts.push("nil".to_string());
966                            }
967                        } else if is_array {
968                            // Array type — unmarshal into []string (typical for paths/texts).
969                            let json_str = serde_json::to_string(v).unwrap_or_default();
970                            let go_literal = go_string_literal(&json_str);
971                            let var_name = &arg.name;
972                            setup_lines.push(format!(
973                                "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}}"
974                            ));
975                            parts.push(var_name.to_string());
976                        } else if let Some(opts_type) = options_type {
977                            // Object with known type — unmarshal into typed struct.
978                            let json_str = serde_json::to_string(v).unwrap_or_default();
979                            let go_literal = go_string_literal(&json_str);
980                            let var_name = &arg.name;
981                            setup_lines.push(format!(
982                                "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}}"
983                            ));
984                            parts.push(var_name.to_string());
985                        } else {
986                            parts.push(json_to_go(v));
987                        }
988                    }
989                    "string" if arg.optional => {
990                        // Optional string in Go is *string — take address of a local.
991                        let var_name = format!("{}Val", arg.name);
992                        let go_val = json_to_go(v);
993                        setup_lines.push(format!("{var_name} := {go_val}"));
994                        parts.push(format!("&{var_name}"));
995                    }
996                    _ => {
997                        parts.push(json_to_go(v));
998                    }
999                }
1000            }
1001        }
1002    }
1003
1004    (setup_lines, parts.join(", "))
1005}
1006
1007#[allow(clippy::too_many_arguments)]
1008fn render_assertion(
1009    out: &mut String,
1010    assertion: &Assertion,
1011    result_var: &str,
1012    import_alias: &str,
1013    field_resolver: &FieldResolver,
1014    optional_locals: &std::collections::HashMap<String, String>,
1015    result_is_simple: bool,
1016    result_is_array: bool,
1017) {
1018    // Handle synthetic / derived fields before the is_valid_for_result check
1019    // so they are never treated as struct field accesses on the result.
1020    if !result_is_simple {
1021        if let Some(f) = &assertion.field {
1022            // embed_texts returns *[][]float32; the embedding matrix is *result_var.
1023            // We emit inline func() expressions so we don't need additional variables.
1024            let embed_deref = format!("(*{result_var})");
1025            match f.as_str() {
1026                "chunks_have_content" => {
1027                    let pred = format!(
1028                        "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range *chunks {{ if c.Content == \"\" {{ 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                "chunks_have_embeddings" => {
1044                    let pred = format!(
1045                        "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 }}()"
1046                    );
1047                    match assertion.assertion_type.as_str() {
1048                        "is_true" => {
1049                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1050                        }
1051                        "is_false" => {
1052                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1053                        }
1054                        _ => {
1055                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1056                        }
1057                    }
1058                    return;
1059                }
1060                "embeddings" => {
1061                    match assertion.assertion_type.as_str() {
1062                        "count_equals" => {
1063                            if let Some(val) = &assertion.value {
1064                                if let Some(n) = val.as_u64() {
1065                                    let _ = writeln!(
1066                                        out,
1067                                        "\tassert.Equal(t, {n}, len({embed_deref}), \"expected exactly {n} elements\")"
1068                                    );
1069                                }
1070                            }
1071                        }
1072                        "count_min" => {
1073                            if let Some(val) = &assertion.value {
1074                                if let Some(n) = val.as_u64() {
1075                                    let _ = writeln!(
1076                                        out,
1077                                        "\tassert.GreaterOrEqual(t, len({embed_deref}), {n}, \"expected at least {n} elements\")"
1078                                    );
1079                                }
1080                            }
1081                        }
1082                        "not_empty" => {
1083                            let _ = writeln!(
1084                                out,
1085                                "\tassert.NotEmpty(t, {embed_deref}, \"expected non-empty embeddings\")"
1086                            );
1087                        }
1088                        "is_empty" => {
1089                            let _ = writeln!(out, "\tassert.Empty(t, {embed_deref}, \"expected empty embeddings\")");
1090                        }
1091                        _ => {
1092                            let _ = writeln!(
1093                                out,
1094                                "\t// skipped: unsupported assertion type on synthetic field 'embeddings'"
1095                            );
1096                        }
1097                    }
1098                    return;
1099                }
1100                "embedding_dimensions" => {
1101                    let expr = format!(
1102                        "func() int {{ if len({embed_deref}) == 0 {{ return 0 }}; return len({embed_deref}[0]) }}()"
1103                    );
1104                    match assertion.assertion_type.as_str() {
1105                        "equals" => {
1106                            if let Some(val) = &assertion.value {
1107                                if let Some(n) = val.as_u64() {
1108                                    let _ = writeln!(
1109                                        out,
1110                                        "\tif {expr} != {n} {{\n\t\tt.Errorf(\"equals mismatch: got %v\", {expr})\n\t}}"
1111                                    );
1112                                }
1113                            }
1114                        }
1115                        "greater_than" => {
1116                            if let Some(val) = &assertion.value {
1117                                if let Some(n) = val.as_u64() {
1118                                    let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
1119                                }
1120                            }
1121                        }
1122                        _ => {
1123                            let _ = writeln!(
1124                                out,
1125                                "\t// skipped: unsupported assertion type on synthetic field 'embedding_dimensions'"
1126                            );
1127                        }
1128                    }
1129                    return;
1130                }
1131                "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
1132                    let pred = match f.as_str() {
1133                        "embeddings_valid" => {
1134                            format!(
1135                                "func() bool {{ for _, e := range {embed_deref} {{ if len(e) == 0 {{ return false }} }}; return true }}()"
1136                            )
1137                        }
1138                        "embeddings_finite" => {
1139                            format!(
1140                                "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 }}()"
1141                            )
1142                        }
1143                        "embeddings_non_zero" => {
1144                            format!(
1145                                "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 }}()"
1146                            )
1147                        }
1148                        "embeddings_normalized" => {
1149                            format!(
1150                                "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 }}()"
1151                            )
1152                        }
1153                        _ => unreachable!(),
1154                    };
1155                    match assertion.assertion_type.as_str() {
1156                        "is_true" => {
1157                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
1158                        }
1159                        "is_false" => {
1160                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
1161                        }
1162                        _ => {
1163                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
1164                        }
1165                    }
1166                    return;
1167                }
1168                // ---- keywords / keywords_count ----
1169                // Go ExtractionResult does not expose extracted_keywords; skip.
1170                "keywords" | "keywords_count" => {
1171                    let _ = writeln!(out, "\t// skipped: field '{f}' not available on Go ExtractionResult");
1172                    return;
1173                }
1174                _ => {}
1175            }
1176        }
1177    }
1178
1179    // Skip assertions on fields that don't exist on the result type.
1180    // When result_is_simple, all field assertions operate on the scalar result directly.
1181    if !result_is_simple {
1182        if let Some(f) = &assertion.field {
1183            if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
1184                let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
1185                return;
1186            }
1187        }
1188    }
1189
1190    let field_expr = if result_is_simple {
1191        // The result IS the value — field access is irrelevant.
1192        result_var.to_string()
1193    } else {
1194        match &assertion.field {
1195            Some(f) if !f.is_empty() => {
1196                // Use the local variable if the field was dereferenced above.
1197                if let Some(local_var) = optional_locals.get(f.as_str()) {
1198                    local_var.clone()
1199                } else {
1200                    field_resolver.accessor(f, "go", result_var)
1201                }
1202            }
1203            _ => result_var.to_string(),
1204        }
1205    };
1206
1207    // Check if the field (after resolution) is optional, which means it's a pointer in Go.
1208    // Also check if a `.length` suffix's parent is optional (e.g., metadata.headings.length
1209    // where metadata.headings is optional → len() needs dereference).
1210    let is_optional = assertion
1211        .field
1212        .as_ref()
1213        .map(|f| {
1214            let resolved = field_resolver.resolve(f);
1215            let check_path = resolved
1216                .strip_suffix(".length")
1217                .or_else(|| resolved.strip_suffix(".count"))
1218                .or_else(|| resolved.strip_suffix(".size"))
1219                .unwrap_or(resolved);
1220            field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
1221        })
1222        .unwrap_or(false);
1223
1224    // When field_expr is `len(X)` and X is an optional (pointer) field, rewrite to `len(*X)`
1225    // and we'll wrap with a nil guard in the assertion handlers.
1226    let field_expr = if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') {
1227        let inner = &field_expr[4..field_expr.len() - 1];
1228        format!("len(*{inner})")
1229    } else {
1230        field_expr
1231    };
1232    // Build the nil-guard expression for the inner pointer (without len wrapper).
1233    let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
1234        Some(field_expr[5..field_expr.len() - 1].to_string())
1235    } else {
1236        None
1237    };
1238
1239    // For optional non-string fields that weren't dereferenced into locals,
1240    // we need to dereference the pointer in comparisons.
1241    let deref_field_expr = if is_optional && !field_expr.starts_with("len(") {
1242        format!("*{field_expr}")
1243    } else {
1244        field_expr.clone()
1245    };
1246
1247    // Detect array element access (e.g., `result.Assets[0].ContentHash`).
1248    // When the field_expr contains `[0]`, we must guard against an out-of-bounds
1249    // panic by checking that the array is non-empty first.
1250    // Extract the array slice expression (everything before `[0]`).
1251    let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
1252        let array_expr = &field_expr[..idx];
1253        Some(array_expr.to_string())
1254    } else {
1255        None
1256    };
1257
1258    // Render the assertion into a temporary buffer first, then wrap with the array
1259    // bounds guard (if needed) by adding one extra level of indentation.
1260    let mut assertion_buf = String::new();
1261    let out_ref = &mut assertion_buf;
1262
1263    match assertion.assertion_type.as_str() {
1264        "equals" => {
1265            if let Some(expected) = &assertion.value {
1266                let go_val = json_to_go(expected);
1267                // For string equality, trim whitespace to handle trailing newlines from the converter.
1268                if expected.is_string() {
1269                    // Wrap field expression with strings.TrimSpace() for string comparisons.
1270                    let trimmed_field = if is_optional && !field_expr.starts_with("len(") {
1271                        format!("strings.TrimSpace(*{field_expr})")
1272                    } else {
1273                        format!("strings.TrimSpace({field_expr})")
1274                    };
1275                    if is_optional && !field_expr.starts_with("len(") {
1276                        let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
1277                    } else {
1278                        let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
1279                    }
1280                } else if is_optional && !field_expr.starts_with("len(") {
1281                    let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
1282                } else {
1283                    let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
1284                }
1285                let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
1286                let _ = writeln!(out_ref, "\t}}");
1287            }
1288        }
1289        "contains" => {
1290            if let Some(expected) = &assertion.value {
1291                let go_val = json_to_go(expected);
1292                // Determine the "string view" of the field expression.
1293                // - *[]string → strings.Join(*field_expr, " ") for a nil-guarded check
1294                // - *string → string(*field_expr)
1295                // - string → string(field_expr) (or just field_expr for plain strings)
1296                // - result_is_array (result_is_simple + array result) → strings.Join(field_expr, " ")
1297                let resolved_field = assertion.field.as_deref().unwrap_or("");
1298                let resolved_name = field_resolver.resolve(resolved_field);
1299                let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
1300                let is_opt =
1301                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
1302                let field_for_contains = if is_opt && field_is_array {
1303                    format!("strings.Join(*{field_expr}, \" \")")
1304                } else if is_opt {
1305                    format!("string(*{field_expr})")
1306                } else if field_is_array {
1307                    format!("strings.Join({field_expr}, \" \")")
1308                } else {
1309                    format!("string({field_expr})")
1310                };
1311                if is_opt {
1312                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1313                    let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
1314                    let _ = writeln!(
1315                        out_ref,
1316                        "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
1317                    );
1318                    let _ = writeln!(out_ref, "\t}}");
1319                    let _ = writeln!(out_ref, "\t}}");
1320                } else {
1321                    let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
1322                    let _ = writeln!(
1323                        out_ref,
1324                        "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
1325                    );
1326                    let _ = writeln!(out_ref, "\t}}");
1327                }
1328            }
1329        }
1330        "contains_all" => {
1331            if let Some(values) = &assertion.values {
1332                let resolved_field = assertion.field.as_deref().unwrap_or("");
1333                let resolved_name = field_resolver.resolve(resolved_field);
1334                let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
1335                let is_opt =
1336                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
1337                for val in values {
1338                    let go_val = json_to_go(val);
1339                    let field_for_contains = if is_opt && field_is_array {
1340                        format!("strings.Join(*{field_expr}, \" \")")
1341                    } else if is_opt {
1342                        format!("string(*{field_expr})")
1343                    } else if field_is_array {
1344                        format!("strings.Join({field_expr}, \" \")")
1345                    } else {
1346                        format!("string({field_expr})")
1347                    };
1348                    if is_opt {
1349                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1350                        let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
1351                        let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
1352                        let _ = writeln!(out_ref, "\t}}");
1353                        let _ = writeln!(out_ref, "\t}}");
1354                    } else {
1355                        let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
1356                        let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
1357                        let _ = writeln!(out_ref, "\t}}");
1358                    }
1359                }
1360            }
1361        }
1362        "not_contains" => {
1363            if let Some(expected) = &assertion.value {
1364                let go_val = json_to_go(expected);
1365                let resolved_field = assertion.field.as_deref().unwrap_or("");
1366                let resolved_name = field_resolver.resolve(resolved_field);
1367                let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
1368                let is_opt =
1369                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
1370                let field_for_contains = if is_opt && field_is_array {
1371                    format!("strings.Join(*{field_expr}, \" \")")
1372                } else if is_opt {
1373                    format!("string(*{field_expr})")
1374                } else if field_is_array {
1375                    format!("strings.Join({field_expr}, \" \")")
1376                } else {
1377                    format!("string({field_expr})")
1378                };
1379                let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
1380                let _ = writeln!(
1381                    out_ref,
1382                    "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
1383                );
1384                let _ = writeln!(out_ref, "\t}}");
1385            }
1386        }
1387        "not_empty" => {
1388            // For optional struct pointers (not arrays), just check != nil.
1389            // For optional slice/string pointers, check nil and len.
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: non-empty means not 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 non-empty value\")");
1404            let _ = writeln!(out_ref, "\t}}");
1405        }
1406        "is_empty" => {
1407            let field_is_array = {
1408                let rf = assertion.field.as_deref().unwrap_or("");
1409                let rn = field_resolver.resolve(rf);
1410                field_resolver.is_array(rn)
1411            };
1412            if is_optional && !field_is_array {
1413                // Struct pointer: empty means nil.
1414                let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1415            } else if is_optional {
1416                let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
1417            } else {
1418                let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
1419            }
1420            let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
1421            let _ = writeln!(out_ref, "\t}}");
1422        }
1423        "contains_any" => {
1424            if let Some(values) = &assertion.values {
1425                let resolved_field = assertion.field.as_deref().unwrap_or("");
1426                let resolved_name = field_resolver.resolve(resolved_field);
1427                let field_is_array = field_resolver.is_array(resolved_name);
1428                let is_opt =
1429                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
1430                let field_for_contains = if is_opt && field_is_array {
1431                    format!("strings.Join(*{field_expr}, \" \")")
1432                } else if is_opt {
1433                    format!("*{field_expr}")
1434                } else if field_is_array {
1435                    format!("strings.Join({field_expr}, \" \")")
1436                } else {
1437                    field_expr.clone()
1438                };
1439                let _ = writeln!(out_ref, "\t{{");
1440                let _ = writeln!(out_ref, "\t\tfound := false");
1441                for val in values {
1442                    let go_val = json_to_go(val);
1443                    let _ = writeln!(
1444                        out_ref,
1445                        "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
1446                    );
1447                }
1448                let _ = writeln!(out_ref, "\t\tif !found {{");
1449                let _ = writeln!(
1450                    out_ref,
1451                    "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
1452                );
1453                let _ = writeln!(out_ref, "\t\t}}");
1454                let _ = writeln!(out_ref, "\t}}");
1455            }
1456        }
1457        "greater_than" => {
1458            if let Some(val) = &assertion.value {
1459                let go_val = json_to_go(val);
1460                // Use `< N+1` instead of `<= N` to avoid golangci-lint sloppyLen
1461                // warning when N is 0 (len(x) <= 0 → len(x) < 1).
1462                // For optional (pointer) fields, dereference and guard with nil check.
1463                if is_optional {
1464                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1465                    if let Some(n) = val.as_u64() {
1466                        let next = n + 1;
1467                        let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {next} {{");
1468                    } else {
1469                        let _ = writeln!(out_ref, "\t\tif {deref_field_expr} <= {go_val} {{");
1470                    }
1471                    let _ = writeln!(
1472                        out_ref,
1473                        "\t\t\tt.Errorf(\"expected > {go_val}, got %v\", {deref_field_expr})"
1474                    );
1475                    let _ = writeln!(out_ref, "\t\t}}");
1476                    let _ = writeln!(out_ref, "\t}}");
1477                } else if let Some(n) = val.as_u64() {
1478                    let next = n + 1;
1479                    let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
1480                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
1481                    let _ = writeln!(out_ref, "\t}}");
1482                } else {
1483                    let _ = writeln!(out_ref, "\tif {field_expr} <= {go_val} {{");
1484                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
1485                    let _ = writeln!(out_ref, "\t}}");
1486                }
1487            }
1488        }
1489        "less_than" => {
1490            if let Some(val) = &assertion.value {
1491                let go_val = json_to_go(val);
1492                let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
1493                let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
1494                let _ = writeln!(out_ref, "\t}}");
1495            }
1496        }
1497        "greater_than_or_equal" => {
1498            if let Some(val) = &assertion.value {
1499                let go_val = json_to_go(val);
1500                if let Some(ref guard) = nil_guard_expr {
1501                    let _ = writeln!(out_ref, "\tif {guard} != nil {{");
1502                    let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
1503                    let _ = writeln!(
1504                        out_ref,
1505                        "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
1506                    );
1507                    let _ = writeln!(out_ref, "\t\t}}");
1508                    let _ = writeln!(out_ref, "\t}}");
1509                } else if is_optional && !field_expr.starts_with("len(") {
1510                    // Optional pointer field: nil-guard and dereference before comparison.
1511                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1512                    let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {go_val} {{");
1513                    let _ = writeln!(
1514                        out_ref,
1515                        "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {deref_field_expr})"
1516                    );
1517                    let _ = writeln!(out_ref, "\t\t}}");
1518                    let _ = writeln!(out_ref, "\t}}");
1519                } else {
1520                    let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
1521                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
1522                    let _ = writeln!(out_ref, "\t}}");
1523                }
1524            }
1525        }
1526        "less_than_or_equal" => {
1527            if let Some(val) = &assertion.value {
1528                let go_val = json_to_go(val);
1529                let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
1530                let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
1531                let _ = writeln!(out_ref, "\t}}");
1532            }
1533        }
1534        "starts_with" => {
1535            if let Some(expected) = &assertion.value {
1536                let go_val = json_to_go(expected);
1537                let field_for_prefix = if is_optional
1538                    && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
1539                {
1540                    format!("string(*{field_expr})")
1541                } else {
1542                    format!("string({field_expr})")
1543                };
1544                let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
1545                let _ = writeln!(
1546                    out_ref,
1547                    "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
1548                );
1549                let _ = writeln!(out_ref, "\t}}");
1550            }
1551        }
1552        "count_min" => {
1553            if let Some(val) = &assertion.value {
1554                if let Some(n) = val.as_u64() {
1555                    if is_optional {
1556                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1557                        let _ = writeln!(
1558                            out_ref,
1559                            "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected at least {n} elements\")"
1560                        );
1561                        let _ = writeln!(out_ref, "\t}}");
1562                    } else {
1563                        let _ = writeln!(
1564                            out_ref,
1565                            "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
1566                        );
1567                    }
1568                }
1569            }
1570        }
1571        "count_equals" => {
1572            if let Some(val) = &assertion.value {
1573                if let Some(n) = val.as_u64() {
1574                    if is_optional {
1575                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1576                        let _ = writeln!(
1577                            out_ref,
1578                            "\t\tassert.Equal(t, len(*{field_expr}), {n}, \"expected exactly {n} elements\")"
1579                        );
1580                        let _ = writeln!(out_ref, "\t}}");
1581                    } else {
1582                        let _ = writeln!(
1583                            out_ref,
1584                            "\tassert.Equal(t, len({field_expr}), {n}, \"expected exactly {n} elements\")"
1585                        );
1586                    }
1587                }
1588            }
1589        }
1590        "is_true" => {
1591            if is_optional {
1592                let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1593                let _ = writeln!(out_ref, "\t\tassert.True(t, *{field_expr}, \"expected true\")");
1594                let _ = writeln!(out_ref, "\t}}");
1595            } else {
1596                let _ = writeln!(out_ref, "\tassert.True(t, {field_expr}, \"expected true\")");
1597            }
1598        }
1599        "is_false" => {
1600            if is_optional {
1601                let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1602                let _ = writeln!(out_ref, "\t\tassert.False(t, *{field_expr}, \"expected false\")");
1603                let _ = writeln!(out_ref, "\t}}");
1604            } else {
1605                let _ = writeln!(out_ref, "\tassert.False(t, {field_expr}, \"expected false\")");
1606            }
1607        }
1608        "method_result" => {
1609            if let Some(method_name) = &assertion.method {
1610                let info = build_go_method_call(result_var, method_name, assertion.args.as_ref(), import_alias);
1611                let check = assertion.check.as_deref().unwrap_or("is_true");
1612                // For pointer-returning functions, dereference with `*`. Value-returning
1613                // functions (e.g., NodeInfo field access) are used directly.
1614                let deref_expr = if info.is_pointer {
1615                    format!("*{}", info.call_expr)
1616                } else {
1617                    info.call_expr.clone()
1618                };
1619                match check {
1620                    "equals" => {
1621                        if let Some(val) = &assertion.value {
1622                            if val.is_boolean() {
1623                                if val.as_bool() == Some(true) {
1624                                    let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
1625                                } else {
1626                                    let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
1627                                }
1628                            } else {
1629                                // Apply type cast to numeric literals when the method returns
1630                                // a typed uint (e.g., *uint) to avoid reflect.DeepEqual
1631                                // mismatches between int and uint in testify's assert.Equal.
1632                                let go_val = if let Some(cast) = info.value_cast {
1633                                    if val.is_number() {
1634                                        format!("{cast}({})", json_to_go(val))
1635                                    } else {
1636                                        json_to_go(val)
1637                                    }
1638                                } else {
1639                                    json_to_go(val)
1640                                };
1641                                let _ = writeln!(
1642                                    out_ref,
1643                                    "\tassert.Equal(t, {go_val}, {deref_expr}, \"method_result equals assertion failed\")"
1644                                );
1645                            }
1646                        }
1647                    }
1648                    "is_true" => {
1649                        let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
1650                    }
1651                    "is_false" => {
1652                        let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
1653                    }
1654                    "greater_than_or_equal" => {
1655                        if let Some(val) = &assertion.value {
1656                            let n = val.as_u64().unwrap_or(0);
1657                            // Use the value_cast type if available (e.g., uint for named_children_count).
1658                            let cast = info.value_cast.unwrap_or("uint");
1659                            let _ = writeln!(
1660                                out_ref,
1661                                "\tassert.GreaterOrEqual(t, {deref_expr}, {cast}({n}), \"expected >= {n}\")"
1662                            );
1663                        }
1664                    }
1665                    "count_min" => {
1666                        if let Some(val) = &assertion.value {
1667                            let n = val.as_u64().unwrap_or(0);
1668                            let _ = writeln!(
1669                                out_ref,
1670                                "\tassert.GreaterOrEqual(t, len({deref_expr}), {n}, \"expected at least {n} elements\")"
1671                            );
1672                        }
1673                    }
1674                    "contains" => {
1675                        if let Some(val) = &assertion.value {
1676                            let go_val = json_to_go(val);
1677                            let _ = writeln!(
1678                                out_ref,
1679                                "\tassert.Contains(t, {deref_expr}, {go_val}, \"expected result to contain value\")"
1680                            );
1681                        }
1682                    }
1683                    "is_error" => {
1684                        let _ = writeln!(out_ref, "\t{{");
1685                        let _ = writeln!(out_ref, "\t\t_, methodErr := {}", info.call_expr);
1686                        let _ = writeln!(out_ref, "\t\tassert.Error(t, methodErr)");
1687                        let _ = writeln!(out_ref, "\t}}");
1688                    }
1689                    other_check => {
1690                        panic!("Go e2e generator: unsupported method_result check type: {other_check}");
1691                    }
1692                }
1693            } else {
1694                panic!("Go e2e generator: method_result assertion missing 'method' field");
1695            }
1696        }
1697        "min_length" => {
1698            if let Some(val) = &assertion.value {
1699                if let Some(n) = val.as_u64() {
1700                    if is_optional {
1701                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1702                        let _ = writeln!(
1703                            out_ref,
1704                            "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected length >= {n}\")"
1705                        );
1706                        let _ = writeln!(out_ref, "\t}}");
1707                    } else {
1708                        let _ = writeln!(
1709                            out_ref,
1710                            "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected length >= {n}\")"
1711                        );
1712                    }
1713                }
1714            }
1715        }
1716        "max_length" => {
1717            if let Some(val) = &assertion.value {
1718                if let Some(n) = val.as_u64() {
1719                    if is_optional {
1720                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
1721                        let _ = writeln!(
1722                            out_ref,
1723                            "\t\tassert.LessOrEqual(t, len(*{field_expr}), {n}, \"expected length <= {n}\")"
1724                        );
1725                        let _ = writeln!(out_ref, "\t}}");
1726                    } else {
1727                        let _ = writeln!(
1728                            out_ref,
1729                            "\tassert.LessOrEqual(t, len({field_expr}), {n}, \"expected length <= {n}\")"
1730                        );
1731                    }
1732                }
1733            }
1734        }
1735        "ends_with" => {
1736            if let Some(expected) = &assertion.value {
1737                let go_val = json_to_go(expected);
1738                let field_for_suffix = if is_optional
1739                    && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
1740                {
1741                    format!("string(*{field_expr})")
1742                } else {
1743                    format!("string({field_expr})")
1744                };
1745                let _ = writeln!(out_ref, "\tif !strings.HasSuffix({field_for_suffix}, {go_val}) {{");
1746                let _ = writeln!(
1747                    out_ref,
1748                    "\t\tt.Errorf(\"expected to end with %s, got %v\", {go_val}, {field_expr})"
1749                );
1750                let _ = writeln!(out_ref, "\t}}");
1751            }
1752        }
1753        "matches_regex" => {
1754            if let Some(expected) = &assertion.value {
1755                let go_val = json_to_go(expected);
1756                let field_for_regex = if is_optional
1757                    && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
1758                {
1759                    format!("*{field_expr}")
1760                } else {
1761                    field_expr.clone()
1762                };
1763                let _ = writeln!(
1764                    out_ref,
1765                    "\tassert.Regexp(t, {go_val}, {field_for_regex}, \"expected value to match regex\")"
1766                );
1767            }
1768        }
1769        "not_error" => {
1770            // Already handled by the `if err != nil` check above.
1771        }
1772        "error" => {
1773            // Handled at the test function level.
1774        }
1775        other => {
1776            panic!("Go e2e generator: unsupported assertion type: {other}");
1777        }
1778    }
1779
1780    // If the assertion accesses an array element via [0], wrap the generated code in a
1781    // bounds check to prevent an index-out-of-range panic when the array is empty.
1782    if let Some(ref arr) = array_guard {
1783        if !assertion_buf.is_empty() {
1784            let _ = writeln!(out, "\tif len({arr}) > 0 {{");
1785            // Re-indent each line by one additional tab level.
1786            for line in assertion_buf.lines() {
1787                let _ = writeln!(out, "\t{line}");
1788            }
1789            let _ = writeln!(out, "\t}}");
1790        }
1791    } else {
1792        out.push_str(&assertion_buf);
1793    }
1794}
1795
1796/// Metadata about the return type of a Go method call for `method_result` assertions.
1797struct GoMethodCallInfo {
1798    /// The call expression string.
1799    call_expr: String,
1800    /// Whether the return type is a pointer (needs `*` dereference for value comparison).
1801    is_pointer: bool,
1802    /// Optional Go type cast to apply to numeric literal values in `equals` assertions
1803    /// (e.g., `"uint"` so that `0` becomes `uint(0)` to match `*uint` deref type).
1804    value_cast: Option<&'static str>,
1805}
1806
1807/// Build a Go call expression for a `method_result` assertion on a tree-sitter Tree.
1808///
1809/// Maps method names to the appropriate Go function calls, matching the Go binding API
1810/// in `packages/go/binding.go`. Returns a [`GoMethodCallInfo`] describing the call and
1811/// its return type characteristics.
1812///
1813/// Return types by method:
1814/// - `has_error_nodes`, `contains_node_type` → `*bool` (pointer)
1815/// - `error_count` → `*uint` (pointer, value_cast = "uint")
1816/// - `tree_to_sexp` → `*string` (pointer)
1817/// - `root_node_type` → `string` via `RootNodeInfo(tree).Kind` (value)
1818/// - `named_children_count` → `uint` via `RootNodeInfo(tree).NamedChildCount` (value, value_cast = "uint")
1819/// - `find_nodes_by_type` → `*[]NodeInfo` (pointer to slice)
1820/// - `run_query` → `(*[]QueryMatch, error)` (pointer + error; use `is_error` check type)
1821fn build_go_method_call(
1822    result_var: &str,
1823    method_name: &str,
1824    args: Option<&serde_json::Value>,
1825    import_alias: &str,
1826) -> GoMethodCallInfo {
1827    match method_name {
1828        "root_node_type" => GoMethodCallInfo {
1829            call_expr: format!("{import_alias}.RootNodeInfo({result_var}).Kind"),
1830            is_pointer: false,
1831            value_cast: None,
1832        },
1833        "named_children_count" => GoMethodCallInfo {
1834            call_expr: format!("{import_alias}.RootNodeInfo({result_var}).NamedChildCount"),
1835            is_pointer: false,
1836            value_cast: Some("uint"),
1837        },
1838        "has_error_nodes" => GoMethodCallInfo {
1839            call_expr: format!("{import_alias}.TreeHasErrorNodes({result_var})"),
1840            is_pointer: true,
1841            value_cast: None,
1842        },
1843        "error_count" | "tree_error_count" => GoMethodCallInfo {
1844            call_expr: format!("{import_alias}.TreeErrorCount({result_var})"),
1845            is_pointer: true,
1846            value_cast: Some("uint"),
1847        },
1848        "tree_to_sexp" => GoMethodCallInfo {
1849            call_expr: format!("{import_alias}.TreeToSexp({result_var})"),
1850            is_pointer: true,
1851            value_cast: None,
1852        },
1853        "contains_node_type" => {
1854            let node_type = args
1855                .and_then(|a| a.get("node_type"))
1856                .and_then(|v| v.as_str())
1857                .unwrap_or("");
1858            GoMethodCallInfo {
1859                call_expr: format!("{import_alias}.TreeContainsNodeType({result_var}, \"{node_type}\")"),
1860                is_pointer: true,
1861                value_cast: None,
1862            }
1863        }
1864        "find_nodes_by_type" => {
1865            let node_type = args
1866                .and_then(|a| a.get("node_type"))
1867                .and_then(|v| v.as_str())
1868                .unwrap_or("");
1869            GoMethodCallInfo {
1870                call_expr: format!("{import_alias}.FindNodesByType({result_var}, \"{node_type}\")"),
1871                is_pointer: true,
1872                value_cast: None,
1873            }
1874        }
1875        "run_query" => {
1876            let query_source = args
1877                .and_then(|a| a.get("query_source"))
1878                .and_then(|v| v.as_str())
1879                .unwrap_or("");
1880            let language = args
1881                .and_then(|a| a.get("language"))
1882                .and_then(|v| v.as_str())
1883                .unwrap_or("");
1884            let query_lit = go_string_literal(query_source);
1885            let lang_lit = go_string_literal(language);
1886            // RunQuery returns (*[]QueryMatch, error) — use is_error check type.
1887            GoMethodCallInfo {
1888                call_expr: format!("{import_alias}.RunQuery({result_var}, {lang_lit}, {query_lit}, []byte(source))"),
1889                is_pointer: false,
1890                value_cast: None,
1891            }
1892        }
1893        other => {
1894            let method_pascal = other.to_upper_camel_case();
1895            GoMethodCallInfo {
1896                call_expr: format!("{result_var}.{method_pascal}()"),
1897                is_pointer: false,
1898                value_cast: None,
1899            }
1900        }
1901    }
1902}
1903
1904/// Convert a `serde_json::Value` to a Go literal string.
1905fn json_to_go(value: &serde_json::Value) -> String {
1906    match value {
1907        serde_json::Value::String(s) => go_string_literal(s),
1908        serde_json::Value::Bool(b) => b.to_string(),
1909        serde_json::Value::Number(n) => n.to_string(),
1910        serde_json::Value::Null => "nil".to_string(),
1911        // For complex types, serialize to JSON string and pass as literal.
1912        other => go_string_literal(&other.to_string()),
1913    }
1914}
1915
1916// ---------------------------------------------------------------------------
1917// Visitor generation
1918// ---------------------------------------------------------------------------
1919
1920/// Derive a unique, exported Go struct name for a visitor from a fixture ID.
1921///
1922/// E.g. `visitor_continue_default` → `visitorContinueDefault` (unexported, avoids
1923/// polluting the exported API of the test package while still being package-level).
1924fn visitor_struct_name(fixture_id: &str) -> String {
1925    use heck::ToUpperCamelCase;
1926    // Use UpperCamelCase so Go treats it as exported — required for method sets.
1927    format!("testVisitor{}", fixture_id.to_upper_camel_case())
1928}
1929
1930/// Emit a package-level Go struct declaration and all its visitor methods.
1931fn emit_go_visitor_struct(
1932    out: &mut String,
1933    struct_name: &str,
1934    visitor_spec: &crate::fixture::VisitorSpec,
1935    import_alias: &str,
1936) {
1937    let _ = writeln!(out, "type {struct_name} struct{{}}");
1938    for (method_name, action) in &visitor_spec.callbacks {
1939        emit_go_visitor_method(out, struct_name, method_name, action, import_alias);
1940    }
1941}
1942
1943/// Emit a Go visitor method for a callback action on the named struct.
1944fn emit_go_visitor_method(
1945    out: &mut String,
1946    struct_name: &str,
1947    method_name: &str,
1948    action: &CallbackAction,
1949    import_alias: &str,
1950) {
1951    let camel_method = method_to_camel(method_name);
1952    let params = match method_name {
1953        "visit_link" => format!("_ {import_alias}.NodeContext, href, text, title string"),
1954        "visit_image" => format!("_ {import_alias}.NodeContext, src, alt, title string"),
1955        "visit_heading" => format!("_ {import_alias}.NodeContext, level int, text, id string"),
1956        "visit_code_block" => format!("_ {import_alias}.NodeContext, lang, code string"),
1957        "visit_code_inline"
1958        | "visit_strong"
1959        | "visit_emphasis"
1960        | "visit_strikethrough"
1961        | "visit_underline"
1962        | "visit_subscript"
1963        | "visit_superscript"
1964        | "visit_mark"
1965        | "visit_button"
1966        | "visit_summary"
1967        | "visit_figcaption"
1968        | "visit_definition_term"
1969        | "visit_definition_description" => format!("_ {import_alias}.NodeContext, text string"),
1970        "visit_text" => format!("_ {import_alias}.NodeContext, text string"),
1971        "visit_list_item" => {
1972            format!("_ {import_alias}.NodeContext, ordered bool, marker, text string")
1973        }
1974        "visit_blockquote" => format!("_ {import_alias}.NodeContext, content string, depth int"),
1975        "visit_table_row" => format!("_ {import_alias}.NodeContext, cells []string, isHeader bool"),
1976        "visit_custom_element" => format!("_ {import_alias}.NodeContext, tagName, html string"),
1977        "visit_form" => format!("_ {import_alias}.NodeContext, actionUrl, method string"),
1978        "visit_input" => format!("_ {import_alias}.NodeContext, inputType, name, value string"),
1979        "visit_audio" | "visit_video" | "visit_iframe" => {
1980            format!("_ {import_alias}.NodeContext, src string")
1981        }
1982        "visit_details" => format!("_ {import_alias}.NodeContext, isOpen bool"),
1983        "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
1984            format!("_ {import_alias}.NodeContext, output string")
1985        }
1986        "visit_list_start" => format!("_ {import_alias}.NodeContext, ordered bool"),
1987        "visit_list_end" => format!("_ {import_alias}.NodeContext, ordered bool, output string"),
1988        _ => format!("_ {import_alias}.NodeContext"),
1989    };
1990
1991    let _ = writeln!(
1992        out,
1993        "func (v *{struct_name}) {camel_method}({params}) {import_alias}.VisitResult {{"
1994    );
1995    match action {
1996        CallbackAction::Skip => {
1997            let _ = writeln!(out, "\treturn {import_alias}.VisitResultSkip");
1998        }
1999        CallbackAction::Continue => {
2000            let _ = writeln!(out, "\treturn {import_alias}.VisitResultContinue");
2001        }
2002        CallbackAction::PreserveHtml => {
2003            let _ = writeln!(out, "\treturn {import_alias}.VisitResultPreserveHtml");
2004        }
2005        CallbackAction::Custom { output } => {
2006            let escaped = go_string_literal(output);
2007            let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped})");
2008        }
2009        CallbackAction::CustomTemplate { template } => {
2010            // Convert {var} placeholders to %s format verbs and collect arg names.
2011            // E.g. `QUOTE: "{text}"` → fmt.Sprintf("QUOTE: \"%s\"", text)
2012            let (fmt_str, fmt_args) = template_to_sprintf(template);
2013            let escaped_fmt = go_string_literal(&fmt_str);
2014            if fmt_args.is_empty() {
2015                let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped_fmt})");
2016            } else {
2017                let args_str = fmt_args.join(", ");
2018                let _ = writeln!(
2019                    out,
2020                    "\treturn {import_alias}.VisitResultCustom(fmt.Sprintf({escaped_fmt}, {args_str}))"
2021                );
2022            }
2023        }
2024    }
2025    let _ = writeln!(out, "}}");
2026}
2027
2028/// Convert a `{var}` template string into a `fmt.Sprintf` format string and argument list.
2029///
2030/// For example, `QUOTE: "{text}"` becomes `("QUOTE: \"%s\"", vec!["text"])`.
2031fn template_to_sprintf(template: &str) -> (String, Vec<String>) {
2032    let mut fmt_str = String::new();
2033    let mut args: Vec<String> = Vec::new();
2034    let mut chars = template.chars().peekable();
2035    while let Some(c) = chars.next() {
2036        if c == '{' {
2037            // Collect placeholder name until '}'.
2038            let mut name = String::new();
2039            for inner in chars.by_ref() {
2040                if inner == '}' {
2041                    break;
2042                }
2043                name.push(inner);
2044            }
2045            fmt_str.push_str("%s");
2046            args.push(name);
2047        } else {
2048            fmt_str.push(c);
2049        }
2050    }
2051    (fmt_str, args)
2052}
2053
2054/// Convert snake_case method names to Go camelCase.
2055fn method_to_camel(snake: &str) -> String {
2056    use heck::ToUpperCamelCase;
2057    snake.to_upper_camel_case()
2058}
2059
2060#[cfg(test)]
2061mod tests {
2062    use super::*;
2063    use crate::config::{CallConfig, E2eConfig};
2064    use crate::field_access::FieldResolver;
2065    use crate::fixture::{Assertion, Fixture};
2066
2067    fn make_fixture(id: &str) -> Fixture {
2068        Fixture {
2069            id: id.to_string(),
2070            category: None,
2071            description: "test fixture".to_string(),
2072            tags: vec![],
2073            skip: None,
2074            call: None,
2075            input: serde_json::Value::Null,
2076            mock_response: Some(crate::fixture::MockResponse {
2077                status: 200,
2078                body: Some(serde_json::Value::Null),
2079                stream_chunks: None,
2080                headers: std::collections::HashMap::new(),
2081            }),
2082            source: String::new(),
2083            http: None,
2084            assertions: vec![Assertion {
2085                assertion_type: "not_error".to_string(),
2086                field: None,
2087                value: None,
2088                values: None,
2089                method: None,
2090                args: None,
2091                check: None,
2092            }],
2093            visitor: None,
2094        }
2095    }
2096
2097    /// snake_case function names in `[e2e.call]` must be routed through `to_go_name`
2098    /// so the emitted Go call uses the idiomatic CamelCase (e.g. `CleanExtractedText`
2099    /// instead of `clean_extracted_text`).
2100    #[test]
2101    fn test_go_method_name_uses_go_casing() {
2102        let e2e_config = E2eConfig {
2103            call: CallConfig {
2104                function: "clean_extracted_text".to_string(),
2105                module: "github.com/example/mylib".to_string(),
2106                result_var: "result".to_string(),
2107                r#async: false,
2108                path: None,
2109                method: None,
2110                args: vec![],
2111                overrides: std::collections::HashMap::new(),
2112                returns_result: true,
2113                returns_void: false,
2114                skip_languages: vec![],
2115            },
2116            ..E2eConfig::default()
2117        };
2118
2119        let fixture = make_fixture("basic_text");
2120        let resolver = FieldResolver::new(
2121            &std::collections::HashMap::new(),
2122            &std::collections::HashSet::new(),
2123            &std::collections::HashSet::new(),
2124            &std::collections::HashSet::new(),
2125        );
2126        let mut out = String::new();
2127        render_test_function(&mut out, &fixture, "kreuzberg", &resolver, &e2e_config);
2128
2129        assert!(
2130            out.contains("kreuzberg.CleanExtractedText("),
2131            "expected Go-cased method name 'CleanExtractedText', got:\n{out}"
2132        );
2133        assert!(
2134            !out.contains("kreuzberg.clean_extracted_text("),
2135            "must not emit raw snake_case method name, got:\n{out}"
2136        );
2137    }
2138}