//! Go e2e test generator using testing.T.

use crate::codegen::naming::{go_param_name, to_go_name};
use crate::core::backend::GeneratedFile;
use crate::core::config::Language;
use crate::core::config::ResolvedCrateConfig;
use crate::core::hash::{self, CommentStyle};
use crate::e2e::config::E2eConfig;
use crate::e2e::escape::{go_string_literal, sanitize_filename};
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::{Assertion, CallbackAction, Fixture, FixtureGroup, ValidationErrorExpectation};
use anyhow::Result;
use heck::ToUpperCamelCase;
use std::fmt::Write as FmtWrite;
use std::path::PathBuf;

use super::E2eCodegen;
use super::client;

/// Go e2e code generator.
pub struct GoCodegen;

impl E2eCodegen for GoCodegen {
    fn generate(
        &self,
        groups: &[FixtureGroup],
        e2e_config: &E2eConfig,
        config: &ResolvedCrateConfig,
        type_defs: &[crate::core::ir::TypeDef],
        enums: &[crate::core::ir::EnumDef],
    ) -> Result<Vec<GeneratedFile>> {
        let lang = self.language_name();
        let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);

        let mut files = Vec::new();

        // Identify data-enum (sum-type) names: enums where at least one variant has named fields.
        // These require special unmarshaling in Go (via Unmarshal<Type> discriminator).
        let data_enum_names: std::collections::HashSet<&str> = enums
            .iter()
            .filter(|e| {
                e.variants
                    .iter()
                    .any(|v| !v.fields.is_empty() && v.fields.iter().any(|f| !f.name.is_empty()))
            })
            .map(|e| e.name.as_str())
            .collect();

        // Resolve call config with overrides (for module path and import alias).
        let call = &e2e_config.call;
        let overrides = call.overrides.get(lang);
        let configured_go_module_path = config.go.as_ref().and_then(|go| go.module.as_ref()).cloned();
        let module_path = overrides
            .and_then(|o| o.module.as_ref())
            .cloned()
            .or_else(|| configured_go_module_path.clone())
            .unwrap_or_else(|| call.module.clone());
        let import_alias = overrides
            .and_then(|o| o.alias.as_ref())
            .cloned()
            .unwrap_or_else(|| "pkg".to_string());

        // Resolve package config.
        let go_pkg = e2e_config.resolve_package("go");
        let go_module_path = go_pkg
            .as_ref()
            .and_then(|p| p.module.as_ref())
            .cloned()
            .or_else(|| configured_go_module_path.clone())
            .unwrap_or_else(|| module_path.clone());
        let replace_path = go_pkg
            .as_ref()
            .and_then(|p| p.path.as_ref())
            .cloned()
            .or_else(|| Some(format!("../../{}", config.package_dir(Language::Go))));
        // In Local mode the version field is a placeholder for the
        // `require <module> <version>` line that pairs with a `replace`
        // directive — `v0.0.0` is the common idiom and gets corrected by
        // `fix_go_major_version` if the module is `/vN` for N >= 2.
        //
        // In Registry mode the test app is consuming the published module
        // from the Go proxy, so the version must match the real release tag.
        // The merged `PackageRef.version` falls through to the Local-mode
        // `[crates.e2e.packages.go].version` placeholder when no
        // `[crates.e2e.registry.packages.go].version` is configured, which
        // wrote `v0.0.0` into the published test_app's go.mod. Prefer the
        // crate-wide resolved version (from `version_from`) in Registry mode
        // and fall back to the package's version only if no resolved version
        // is available.
        let go_version = match e2e_config.dep_mode {
            crate::e2e::config::DependencyMode::Registry => {
                let registry_version = e2e_config
                    .registry
                    .packages
                    .get("go")
                    .and_then(|p| p.version.as_ref())
                    .cloned();
                registry_version
                    .or_else(|| config.resolved_version().map(|v| format!("v{v}")))
                    .or_else(|| go_pkg.as_ref().and_then(|p| p.version.as_ref()).cloned())
                    .unwrap_or_else(|| "v0.0.0".to_string())
            }
            crate::e2e::config::DependencyMode::Local => go_pkg
                .as_ref()
                .and_then(|p| p.version.as_ref())
                .cloned()
                .unwrap_or_else(|| {
                    config
                        .resolved_version()
                        .map(|v| format!("v{v}"))
                        .unwrap_or_else(|| "v0.0.0".to_string())
                }),
        };
        // Generate go.mod. In registry mode, omit the `replace` directive so the
        // module is fetched from the Go module proxy.
        let effective_replace = match e2e_config.dep_mode {
            crate::e2e::config::DependencyMode::Registry => None,
            crate::e2e::config::DependencyMode::Local => replace_path.as_deref().map(String::from),
        };
        // In local mode with a `replace` directive the version in `require` is a
        // placeholder.  Go requires that for a major-version module path (`/vN`, N ≥ 2)
        // the placeholder version must start with `vN.`, e.g. `v3.0.0`.  A version like
        // `v0.0.0` is rejected with "should be v3, not v0".  Fix the placeholder when the
        // module path ends with `/vN` and the configured version doesn't match.
        let effective_go_version = if effective_replace.is_some() {
            fix_go_major_version(&go_module_path, &go_version)
        } else {
            go_version.clone()
        };
        files.push(GeneratedFile {
            path: output_base.join("go.mod"),
            content: render_go_mod(&go_module_path, effective_replace.as_deref(), &effective_go_version),
            generated_header: false,
        });

        // Determine if any fixture needs jsonString helper across all groups.
        let emits_executable_test =
            |fixture: &Fixture| fixture.is_http_test() || fixture_has_go_callable(fixture, e2e_config);
        let needs_json_stringify = groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| {
            emits_executable_test(f)
                && f.assertions.iter().any(|a| {
                    matches!(
                        a.assertion_type.as_str(),
                        "contains" | "contains_all" | "contains_any" | "not_contains"
                    ) && {
                        if a.field.as_ref().is_none_or(|f| f.is_empty()) {
                            e2e_config
                                .resolve_call_for_fixture(
                                    f.call.as_deref(),
                                    &f.id,
                                    &f.resolved_category(),
                                    &f.tags,
                                    &f.input,
                                )
                                .result_is_array
                        } else {
                            let cc = e2e_config.resolve_call_for_fixture(
                                f.call.as_deref(),
                                &f.id,
                                &f.resolved_category(),
                                &f.tags,
                                &f.input,
                            );
                            let per_call_resolver = FieldResolver::new(
                                e2e_config.effective_fields(cc),
                                e2e_config.effective_fields_optional(cc),
                                e2e_config.effective_result_fields(cc),
                                e2e_config.effective_fields_array(cc),
                                &std::collections::HashSet::new(),
                            );
                            let resolved_name = per_call_resolver.resolve(a.field.as_deref().unwrap_or(""));
                            per_call_resolver.is_array(resolved_name)
                        }
                    }
                })
        });

        // Generate helpers_test.go with jsonString if needed, emitted exactly once.
        if needs_json_stringify {
            files.push(GeneratedFile {
                path: output_base.join("helpers_test.go"),
                content: render_helpers_test_go(),
                generated_header: true,
            });
        }

        // Generate main_test.go with TestMain when:
        // 1. Any fixture needs the mock server (has mock_response), or
        // 2. Any fixture is client_factory-based (reads MOCK_SERVER_URL), or
        // 3. Any fixture is file-based (requires test_documents directory setup).
        //
        // TestMain runs before all tests and changes to the test_documents directory,
        // ensuring that relative file paths like "pdf/fake_memo.pdf" resolve correctly.
        let has_file_fixtures = groups
            .iter()
            .flat_map(|g| g.fixtures.iter())
            .any(|f| f.http.is_none() && !f.needs_mock_server());

        let needs_main_test = has_file_fixtures
            || groups.iter().flat_map(|g| g.fixtures.iter()).any(|f| {
                if f.needs_mock_server() {
                    return true;
                }
                let cc = e2e_config.resolve_call_for_fixture(
                    f.call.as_deref(),
                    &f.id,
                    &f.resolved_category(),
                    &f.tags,
                    &f.input,
                );
                let go_override = cc.overrides.get("go").or_else(|| e2e_config.call.overrides.get("go"));
                go_override.and_then(|o| o.client_factory.as_deref()).is_some()
            });

        if needs_main_test {
            files.push(GeneratedFile {
                path: output_base.join("main_test.go"),
                content: render_main_test_go(&e2e_config.test_documents_dir),
                generated_header: true,
            });
        }

        // Generate test files per category.
        for group in groups {
            let active: Vec<&Fixture> = group
                .fixtures
                .iter()
                .filter(|f| super::should_include_fixture(f, lang, e2e_config))
                .collect();

            if active.is_empty() {
                continue;
            }

            let filename = format!("{}_test.go", sanitize_filename(&group.category));
            let content = render_test_file(
                &group.category,
                &active,
                GoTestFileContext {
                    go_module_path: &module_path,
                    import_alias: &import_alias,
                    e2e_config,
                    adapters: &config.adapters,
                    data_enum_names: &data_enum_names,
                    config,
                    type_defs,
                    enums,
                },
            );
            files.push(GeneratedFile {
                path: output_base.join(filename),
                content,
                generated_header: true,
            });
        }

        Ok(files)
    }

    fn language_name(&self) -> &'static str {
        "go"
    }
}

/// Fix a Go module version so it is valid for a major-version module path.
///
/// Go requires that a module path ending in `/vN` (N ≥ 2) uses a version
/// whose major component matches N.  In local-replace mode we use a synthetic
/// placeholder version; if that placeholder (e.g. `v0.0.0`) doesn't match the
/// major suffix, fix it to `vN.0.0` so `go mod` accepts the go.mod.
fn fix_go_major_version(module_path: &str, version: &str) -> String {
    // Extract `/vN` suffix from the module path (N must be ≥ 2).
    let major = module_path
        .rsplit('/')
        .next()
        .and_then(|seg| seg.strip_prefix('v'))
        .and_then(|n| n.parse::<u64>().ok())
        .filter(|&n| n >= 2);

    let Some(n) = major else {
        return version.to_string();
    };

    // If the version already starts with `vN.`, it is valid — leave it alone.
    let expected_prefix = format!("v{n}.");
    if version.starts_with(&expected_prefix) {
        return version.to_string();
    }

    format!("v{n}.0.0")
}

fn render_go_mod(go_module_path: &str, replace_path: Option<&str>, version: &str) -> String {
    let mut out = String::new();
    let _ = writeln!(out, "module e2e_go");
    let _ = writeln!(out);
    let _ = writeln!(out, "go 1.26");
    let _ = writeln!(out);
    let _ = writeln!(out, "require (");
    let _ = writeln!(out, "\t{go_module_path} {version}");
    let _ = writeln!(out, "\tgithub.com/stretchr/testify v1.11.1");
    let _ = writeln!(out, ")");

    if let Some(path) = replace_path {
        let _ = writeln!(out);
        let _ = writeln!(out, "replace {go_module_path} => {path}");
    }

    out
}

/// Generate `main_test.go` that starts the mock HTTP server before all tests run.
///
/// The binary is expected at `../rust/target/release/mock-server` relative to the Go e2e
/// directory.  The server prints `MOCK_SERVER_URL=http://...` on stdout; we read that line
/// and export the variable so all test files can call `os.Getenv("MOCK_SERVER_URL")`.
fn render_main_test_go(test_documents_dir: &str) -> String {
    // NOTE: the generated-file header is injected by the caller (generated_header: true).
    let mut out = String::new();
    let _ = writeln!(out, "package e2e_test");
    let _ = writeln!(out);
    let _ = writeln!(out, "import (");
    let _ = writeln!(out, "\t\"bufio\"");
    let _ = writeln!(out, "\t\"encoding/json\"");
    let _ = writeln!(out, "\t\"io\"");
    let _ = writeln!(out, "\t\"os\"");
    let _ = writeln!(out, "\t\"os/exec\"");
    let _ = writeln!(out, "\t\"path/filepath\"");
    let _ = writeln!(out, "\t\"runtime\"");
    let _ = writeln!(out, "\t\"strings\"");
    let _ = writeln!(out, "\t\"testing\"");
    let _ = writeln!(out, ")");
    let _ = writeln!(out);
    let _ = writeln!(out, "func TestMain(m *testing.M) {{");
    let _ = writeln!(out, "\t_, filename, _, _ := runtime.Caller(0)");
    let _ = writeln!(out, "\tdir := filepath.Dir(filename)");
    let _ = writeln!(out);
    let _ = writeln!(
        out,
        "\t// Change to the configured test-documents directory (if it exists) so that fixture"
    );
    let _ = writeln!(
        out,
        "\t// file paths like \"pdf/fake_memo.pdf\" resolve correctly when running go test"
    );
    let _ = writeln!(
        out,
        "\t// from e2e/go/. Repos without document fixtures (web crawler, network clients) do"
    );
    let _ = writeln!(out, "\t// not ship this directory — skip chdir and run from e2e/go/.");
    let _ = writeln!(
        out,
        "\ttestDocumentsDir := filepath.Join(dir, \"..\", \"..\", \"{test_documents_dir}\")"
    );
    let _ = writeln!(
        out,
        "\tif info, err := os.Stat(testDocumentsDir); err == nil && info.IsDir() {{"
    );
    let _ = writeln!(out, "\t\tif err := os.Chdir(testDocumentsDir); err != nil {{");
    let _ = writeln!(out, "\t\t\tpanic(err)");
    let _ = writeln!(out, "\t\t}}");
    let _ = writeln!(out, "\t}}");
    let _ = writeln!(out);
    let _ = writeln!(
        out,
        "\t// If MOCK_SERVER_URL is already set, a parent process (e.g. `alef test-apps run`)"
    );
    let _ = writeln!(
        out,
        "\t// started a shared mock-server and exported its URL (plus any MOCK_SERVERS /"
    );
    let _ = writeln!(
        out,
        "\t// MOCK_SERVER_<FIXTURE_ID> vars). Use it as-is and do NOT spawn our own server."
    );
    let _ = writeln!(out, "\tif os.Getenv(\"MOCK_SERVER_URL\") != \"\" {{");
    let _ = writeln!(out, "\t\tos.Exit(m.Run())");
    let _ = writeln!(out, "\t}}");
    let _ = writeln!(out);
    let _ = writeln!(out, "\t// Start the mock HTTP server if it exists.");
    let _ = writeln!(
        out,
        "\tmockServerBin := filepath.Join(dir, \"..\", \"rust\", \"target\", \"release\", \"mock-server\")"
    );
    let _ = writeln!(out, "\tif _, err := os.Stat(mockServerBin); err == nil {{");
    let _ = writeln!(
        out,
        "\t\tfixturesDir := filepath.Join(dir, \"..\", \"..\", \"fixtures\")"
    );
    let _ = writeln!(out, "\t\tcmd := exec.Command(mockServerBin, fixturesDir)");
    let _ = writeln!(out, "\t\tcmd.Stderr = os.Stderr");
    let _ = writeln!(out, "\t\tstdout, err := cmd.StdoutPipe()");
    let _ = writeln!(out, "\t\tif err != nil {{");
    let _ = writeln!(out, "\t\t\tpanic(err)");
    let _ = writeln!(out, "\t\t}}");
    let _ = writeln!(out, "\t\t// Keep a writable pipe to the mock-server's stdin so the");
    let _ = writeln!(
        out,
        "\t\t// server does not see EOF and exit immediately. The mock-server"
    );
    let _ = writeln!(out, "\t\t// blocks reading stdin until the parent closes the pipe.");
    let _ = writeln!(out, "\t\tstdin, err := cmd.StdinPipe()");
    let _ = writeln!(out, "\t\tif err != nil {{");
    let _ = writeln!(out, "\t\t\tpanic(err)");
    let _ = writeln!(out, "\t\t}}");
    let _ = writeln!(out, "\t\tif err := cmd.Start(); err != nil {{");
    let _ = writeln!(out, "\t\t\tpanic(err)");
    let _ = writeln!(out, "\t\t}}");
    let _ = writeln!(out, "\t\tscanner := bufio.NewScanner(stdout)");
    let _ = writeln!(out, "\t\tfor scanner.Scan() {{");
    let _ = writeln!(out, "\t\t\tline := scanner.Text()");
    let _ = writeln!(out, "\t\t\tif strings.HasPrefix(line, \"MOCK_SERVER_URL=\") {{");
    let _ = writeln!(
        out,
        "\t\t\t\t_ = os.Setenv(\"MOCK_SERVER_URL\", strings.TrimPrefix(line, \"MOCK_SERVER_URL=\"))"
    );
    let _ = writeln!(out, "\t\t\t}} else if strings.HasPrefix(line, \"MOCK_SERVERS=\") {{");
    let _ = writeln!(out, "\t\t\t\t_jsonVal := strings.TrimPrefix(line, \"MOCK_SERVERS=\")");
    let _ = writeln!(out, "\t\t\t\t_ = os.Setenv(\"MOCK_SERVERS\", _jsonVal)");
    let _ = writeln!(
        out,
        "\t\t\t\t// Parse the JSON map and set per-fixture env vars (MOCK_SERVER_<FIXTURE_ID>)."
    );
    let _ = writeln!(out, "\t\t\t\tvar _perFixture map[string]string");
    let _ = writeln!(
        out,
        "\t\t\t\tif err := json.Unmarshal([]byte(_jsonVal), &_perFixture); err == nil {{"
    );
    let _ = writeln!(out, "\t\t\t\t\tfor _fid, _furl := range _perFixture {{");
    let _ = writeln!(
        out,
        "\t\t\t\t\t\t_ = os.Setenv(\"MOCK_SERVER_\"+strings.ToUpper(_fid), _furl)"
    );
    let _ = writeln!(out, "\t\t\t\t\t}}");
    let _ = writeln!(out, "\t\t\t\t}}");
    let _ = writeln!(out, "\t\t\t\tbreak");
    let _ = writeln!(out, "\t\t\t}} else if os.Getenv(\"MOCK_SERVER_URL\") != \"\" {{");
    let _ = writeln!(out, "\t\t\t\tbreak");
    let _ = writeln!(out, "\t\t\t}}");
    let _ = writeln!(out, "\t\t}}");
    let _ = writeln!(out, "\t\tgo func() {{ _, _ = io.Copy(io.Discard, stdout) }}()");
    let _ = writeln!(out, "\t\tcode := m.Run()");
    let _ = writeln!(out, "\t\t_ = stdin.Close()");
    let _ = writeln!(out, "\t\t_ = cmd.Process.Signal(os.Interrupt)");
    let _ = writeln!(out, "\t\t_ = cmd.Wait()");
    let _ = writeln!(out, "\t\tos.Exit(code)");
    let _ = writeln!(out, "\t}} else {{");
    let _ = writeln!(out, "\t\tcode := m.Run()");
    let _ = writeln!(out, "\t\tos.Exit(code)");
    let _ = writeln!(out, "\t}}");
    let _ = writeln!(out, "}}");
    out
}

/// Generate `helpers_test.go` with the jsonString helper function.
/// This is emitted once per package to avoid duplicate function definitions.
fn render_helpers_test_go() -> String {
    let mut out = String::new();
    let _ = writeln!(out, "package e2e_test");
    let _ = writeln!(out);
    let _ = writeln!(out, "import \"encoding/json\"");
    let _ = writeln!(out);
    let _ = writeln!(out, "// jsonString converts a value to its JSON string representation.");
    let _ = writeln!(
        out,
        "// Array fields use jsonString instead of fmt.Sprint to preserve structure."
    );
    let _ = writeln!(out, "func jsonString(value any) string {{");
    let _ = writeln!(out, "\tencoded, err := json.Marshal(value)");
    let _ = writeln!(out, "\tif err != nil {{");
    let _ = writeln!(out, "\t\treturn \"\"");
    let _ = writeln!(out, "\t}}");
    let _ = writeln!(out, "\treturn string(encoded)");
    let _ = writeln!(out, "}}");
    out
}

struct GoTestFileContext<'a> {
    go_module_path: &'a str,
    import_alias: &'a str,
    e2e_config: &'a crate::e2e::config::E2eConfig,
    adapters: &'a [crate::core::config::AdapterConfig],
    data_enum_names: &'a std::collections::HashSet<&'a str>,
    config: &'a crate::core::config::ResolvedCrateConfig,
    type_defs: &'a [crate::core::ir::TypeDef],
    enums: &'a [crate::core::ir::EnumDef],
}

fn render_test_file(category: &str, fixtures: &[&Fixture], context: GoTestFileContext<'_>) -> String {
    let GoTestFileContext {
        go_module_path,
        import_alias,
        e2e_config,
        adapters,
        data_enum_names,
        config,
        type_defs,
        enums,
    } = context;
    let mut out = String::new();
    let emits_executable_test =
        |fixture: &Fixture| fixture.is_http_test() || fixture_has_go_callable(fixture, e2e_config);

    // Go convention: generated file marker must appear before the package declaration.
    out.push_str(&hash::header(CommentStyle::DoubleSlash));
    let _ = writeln!(out);

    // Determine if any fixture actually uses the pkg import.
    // Fixtures without mock_response are emitted as t.Skip() stubs and don't reference the
    // package — omit the import when no fixture needs it to avoid the Go "imported and not
    // used" compile error. Visitor fixtures reference the package types (NodeContext,
    // VisitResult, VisitResult* helpers) in struct method signatures emitted at file scope,
    // so they also require the import even when the test body itself is a Skip stub.
    // Direct-callable fixtures (non-HTTP, non-mock, with a resolved Go function) also
    // reference the package when a Go override function is configured.
    let needs_pkg = fixtures
        .iter()
        .any(|f| fixture_has_go_callable(f, e2e_config) || f.is_http_test() || f.visitor.is_some());

    // Determine if we need the "os" import (mock_url args, HTTP fixtures, or
    // client_factory fixtures that read MOCK_SERVER_URL via os.Getenv).
    let needs_os = fixtures.iter().any(|f| {
        if f.is_http_test() {
            return true;
        }
        if !emits_executable_test(f) {
            return false;
        }
        let call_config =
            e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
        let go_override = call_config
            .overrides
            .get("go")
            .or_else(|| e2e_config.call.overrides.get("go"));
        if go_override.and_then(|o| o.client_factory.as_deref()).is_some() {
            return true;
        }
        let call_args = f.resolved_args(call_config);
        // Need "os" for mock_url / mock_url_list args, or for bytes args with a string
        // fixture value (fixture-relative path loaded via os.ReadFile at test-run time).
        if call_args
            .iter()
            .any(|a| a.arg_type == "mock_url" || a.arg_type == "mock_url_list")
        {
            return true;
        }
        call_args.iter().any(|a| {
            if a.arg_type != "bytes" {
                return false;
            }
            // alef.toml field paths are dotted (e.g. "input.data"). The fixture's `input`
            // field already strips the "input." prefix, so we walk the remaining segments.
            let mut current = &f.input;
            let path = a.field.strip_prefix("input.").unwrap_or(&a.field);
            for segment in path.split('.') {
                match current.get(segment) {
                    Some(next) => current = next,
                    None => return false,
                }
            }
            current.is_string()
        })
    });

    // Note: file_path args are passed directly as relative strings — the e2e/go
    // TestMain in main_test.go already chdir's into the repo-root test_documents/.
    let needs_filepath = false;

    let _needs_json_stringify = fixtures.iter().any(|f| {
        emits_executable_test(f)
            && f.assertions.iter().any(|a| {
                matches!(
                    a.assertion_type.as_str(),
                    "contains" | "contains_all" | "contains_any" | "not_contains"
                ) && {
                    // Check if this assertion operates on an array field.
                    // If no field is specified, check if the result itself is an array.
                    if a.field.as_ref().is_none_or(|f| f.is_empty()) {
                        // No field specified: check if result is an array
                        e2e_config
                            .resolve_call_for_fixture(
                                f.call.as_deref(),
                                &f.id,
                                &f.resolved_category(),
                                &f.tags,
                                &f.input,
                            )
                            .result_is_array
                    } else {
                        // Field specified: check if that field is an array
                        let cc = e2e_config.resolve_call_for_fixture(
                            f.call.as_deref(),
                            &f.id,
                            &f.resolved_category(),
                            &f.tags,
                            &f.input,
                        );
                        let per_call_resolver = FieldResolver::new(
                            e2e_config.effective_fields(cc),
                            e2e_config.effective_fields_optional(cc),
                            e2e_config.effective_result_fields(cc),
                            e2e_config.effective_fields_array(cc),
                            &std::collections::HashSet::new(),
                        );
                        let resolved_name = per_call_resolver.resolve(a.field.as_deref().unwrap_or(""));
                        per_call_resolver.is_array(resolved_name)
                    }
                }
            })
    });

    // Determine if we need "encoding/json" (handle args with non-null config,
    // json_object args that will be unmarshalled into a typed struct, or HTTP
    // body/partial/validation-error assertions that use json.Unmarshal).
    let needs_json = fixtures.iter().any(|f| {
        // HTTP body assertions use json.Unmarshal for Object/Array bodies;
        // partial body and validation-error assertions always use json.Unmarshal.
        if let Some(http) = &f.http {
            let body_needs_json = http
                .expected_response
                .body
                .as_ref()
                .is_some_and(|b| matches!(b, serde_json::Value::Object(_) | serde_json::Value::Array(_)));
            let partial_needs_json = http.expected_response.body_partial.is_some();
            let ve_needs_json = http
                .expected_response
                .validation_errors
                .as_ref()
                .is_some_and(|v| !v.is_empty());
            if body_needs_json || partial_needs_json || ve_needs_json {
                return true;
            }
        }
        if !emits_executable_test(f) {
            return false;
        }

        let call =
            e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
        let call_args = &call.args;
        // handle args with non-null config value
        let has_handle = call_args.iter().any(|a| a.arg_type == "handle") && {
            call_args.iter().filter(|a| a.arg_type == "handle").any(|a| {
                let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
                let v = f.input.get(field).unwrap_or(&serde_json::Value::Null);
                !(v.is_null() || v.is_object() && v.as_object().is_some_and(|o| o.is_empty()))
            })
        };
        // json_object args with options_type or array values (will use JSON unmarshal)
        let go_override = call.overrides.get("go");
        let opts_type = go_override.and_then(|o| o.options_type.as_deref()).or_else(|| {
            e2e_config
                .call
                .overrides
                .get("go")
                .and_then(|o| o.options_type.as_deref())
        });
        let has_json_obj = call_args.iter().any(|a| {
            if a.arg_type != "json_object" {
                return false;
            }
            let v = if a.field == "input" {
                &f.input
            } else {
                let field = a.field.strip_prefix("input.").unwrap_or(&a.field);
                f.input.get(field).unwrap_or(&serde_json::Value::Null)
            };
            if v.is_array() {
                return true;
            } // array → []string unmarshal
            opts_type.is_some() && v.is_object() && !v.as_object().is_some_and(|o| o.is_empty())
        });
        has_handle || has_json_obj
    });

    // No runtime base64 calls remain in generated Go code. Bytes args with string values
    // are now loaded via os.ReadFile (see needs_os) and HTTP body byte arrays are
    // base64-encoded at codegen time and embedded as literal strings in the json.Unmarshal
    // call, which doesn't require the `encoding/base64` import in the test file.
    let needs_base64 = false;

    // Effective `result_is_simple` for a resolved call config — mirrors the per-test
    // body logic (Go override OR call-level OR rust override). When set, the assertion
    // renderer ignores assertion fields and operates on the result variable directly
    // (e.g. `fmt.Sprint(result)`), so import detection must treat such field-bearing
    // assertions as usable regardless of whether the field is valid on the result type.
    let call_result_is_simple = |cc: &crate::core::config::e2e::CallConfig| -> bool {
        cc.overrides.get("go").is_some_and(|o| o.result_is_simple)
            || cc.result_is_simple
            || cc.overrides.get("rust").map(|o| o.result_is_simple).unwrap_or(false)
    };

    // Determine if we need the "fmt" import (CustomTemplate visitor actions with placeholders).
    // Note: Assertion rendering uses string() for type conversions instead of fmt.Sprint/fmt.Sprintf,
    // and optional field variable bindings also use string() instead of fmt.Sprintf.
    // jsonString is in helpers_test.go (uses encoding/json, not fmt).
    let needs_fmt = fixtures.iter().any(|f| {
        // Check CustomTemplate visitor actions
        if f.visitor.as_ref().is_some_and(|v| {
            v.callbacks.values().any(|action| {
                if let CallbackAction::CustomTemplate { template, .. } = action {
                    template.contains('{')
                } else {
                    false
                }
            })
        }) {
            return true;
        }

        false
    });

    // Determine if we need the "strings" import.
    // Only count assertions whose fields are actually valid for the result type.
    // Also check if any fixture has mock_url_list args (needs strings.HasPrefix).
    let needs_strings = fixtures.iter().any(|f| {
        if !emits_executable_test(f) {
            return false;
        }
        // Check for mock_url_list in call args
        let cc =
            e2e_config.resolve_call_for_fixture(f.call.as_deref(), &f.id, &f.resolved_category(), &f.tags, &f.input);
        if cc.args.iter().any(|arg| arg.arg_type == "mock_url_list") {
            return true;
        }
        let per_call_resolver = FieldResolver::new(
            e2e_config.effective_fields(cc),
            e2e_config.effective_fields_optional(cc),
            e2e_config.effective_result_fields(cc),
            e2e_config.effective_fields_array(cc),
            &std::collections::HashSet::new(),
        );
        f.assertions.iter().any(|a| {
            let type_needs_strings = if a.assertion_type == "equals" {
                // equals with string values needs strings.TrimSpace
                a.value.as_ref().is_some_and(|v| v.is_string())
            } else {
                matches!(
                    a.assertion_type.as_str(),
                    "contains" | "contains_all" | "contains_any" | "not_contains" | "starts_with" | "ends_with"
                )
            };
            // For result_is_simple calls the renderer emits `strings.Contains(...)`
            // against the result variable directly, ignoring the assertion field.
            let simple_result = call_result_is_simple(cc);
            let field_valid = a
                .field
                .as_ref()
                .map(|f| f.is_empty() || simple_result || per_call_resolver.is_valid_for_result(f))
                .unwrap_or(true);
            type_needs_strings && field_valid
        })
    });

    // The testify `assert` import is decided purely empirically below: after the
    // file body is rendered into a buffer, we emit the import only if the body
    // actually contains an `assert.` reference. A predictive heuristic over
    // assertion shapes/categories produced false positives (importing testify in
    // files whose tests only use `t.Errorf`/`t.Fatalf`), which fails the Go
    // `imported and not used` compile check.

    // Determine if we need "net/http" and "io" (HTTP server tests via HTTP client).
    let has_http_fixtures = fixtures.iter().any(|f| f.is_http_test());
    let needs_http = has_http_fixtures;
    // io.ReadAll is emitted for every HTTP fixture (render_call always reads the body).
    let needs_io = has_http_fixtures;

    // Determine if we need "reflect" (for HTTP response body JSON comparison
    // and partial-body assertions, both of which use reflect.DeepEqual).
    let needs_reflect = fixtures.iter().any(|f| {
        if let Some(http) = &f.http {
            let body_needs_reflect = http
                .expected_response
                .body
                .as_ref()
                .is_some_and(|b| matches!(b, serde_json::Value::Object(_) | serde_json::Value::Array(_)));
            let partial_needs_reflect = http.expected_response.body_partial.is_some();
            body_needs_reflect || partial_needs_reflect
        } else {
            false
        }
    });

    // Render the file body (visitor structs + test functions) into a separate buffer
    // first so we can scan the actual emitted code for `assert.` usage. This avoids
    // false positives in the heuristic-based needs_assert detection: imports are emitted
    // only when the rendered code actually uses them.
    let mut body = String::new();
    for fixture in fixtures.iter() {
        if let Some(visitor_spec) = &fixture.visitor {
            let struct_name = visitor_struct_name(&fixture.id);
            emit_go_visitor_struct(&mut body, &struct_name, visitor_spec, import_alias);
            let _ = writeln!(body);
        }
    }
    for (i, fixture) in fixtures.iter().enumerate() {
        render_test_function(
            &mut body,
            fixture,
            GoTestFunctionContext {
                import_alias,
                e2e_config,
                adapters,
                data_enum_names,
                config,
                type_defs,
                enums,
            },
        );
        if i + 1 < fixtures.len() {
            let _ = writeln!(body);
        }
    }
    let needs_assert = body.contains("assert.");
    // `strings` is used by the rendered body in ways the up-front heuristic does not fully
    // capture — notably `strings.NewReader(...)` for every HTTP request body. Detect it
    // empirically from the rendered code (like `assert`) so the import always matches the
    // emitted usage and never goes missing (compile error) or unused (compile error).
    let needs_strings = needs_strings || body.contains("strings.");
    // The binding package is only referenced when the rendered body actually calls into it
    // (typed body deserialization, visitor structs, direct callable fixtures). Pure HTTP
    // status/header tests never touch it, so confirm usage empirically to avoid an
    // "imported and not used" compile error on those files.
    let needs_pkg = needs_pkg && body.contains(&format!("{import_alias}."));

    let _ = writeln!(out, "// E2e tests for category: {category}");
    let _ = writeln!(out, "package e2e_test");
    let _ = writeln!(out);
    let _ = writeln!(out, "import (");
    if needs_base64 {
        let _ = writeln!(out, "\t\"encoding/base64\"");
    }
    // Also check empirically: test-backend setup_block code may use json.RawMessage
    // or json.Unmarshal in package-level declarations that were not detected by the
    // up-front heuristic (which only checked handle/json_object call args).
    let needs_json = needs_json || body.contains("json.");
    if needs_json || needs_reflect {
        let _ = writeln!(out, "\t\"encoding/json\"");
    }
    if needs_fmt {
        let _ = writeln!(out, "\t\"fmt\"");
    }
    if needs_io {
        let _ = writeln!(out, "\t\"io\"");
    }
    if needs_http {
        let _ = writeln!(out, "\t\"net/http\"");
    }
    if needs_os {
        let _ = writeln!(out, "\t\"os\"");
    }
    let _ = needs_filepath; // reserved for future use; currently always false
    if needs_reflect {
        let _ = writeln!(out, "\t\"reflect\"");
    }
    if needs_strings {
        let _ = writeln!(out, "\t\"strings\"");
    }
    let _ = writeln!(out, "\t\"testing\"");
    if needs_assert {
        let _ = writeln!(out);
        let _ = writeln!(out, "\t\"github.com/stretchr/testify/assert\"");
    }

    if needs_pkg {
        let _ = writeln!(out);
        let _ = writeln!(out, "\t{import_alias} \"{go_module_path}\"");
    }
    let _ = writeln!(out, ")");
    let _ = writeln!(out);

    // Append the pre-rendered body (visitor structs + test functions).
    out.push_str(&body);

    // Clean up trailing newlines.
    while out.ends_with("\n\n") {
        out.pop();
    }
    if !out.ends_with('\n') {
        out.push('\n');
    }
    out
}

/// Return `true` when a non-HTTP fixture can be exercised by calling the Go
/// binding directly.
///
/// A fixture is Go-callable when the resolved call config provides a non-empty
/// function name — either via a Go-specific override (`[e2e.call.overrides.go]
/// function`) or via the base call `function` field.  The Go binding exposes all
/// public functions from the Rust core as PascalCase exports, so any non-empty
/// function name can be resolved to a valid Go symbol via `to_go_name`.
fn fixture_has_go_callable(fixture: &Fixture, e2e_config: &crate::e2e::config::E2eConfig) -> bool {
    // HTTP fixtures are handled by render_http_test_function — not our concern here.
    if fixture.is_http_test() {
        return false;
    }
    let call_config = e2e_config.resolve_call_for_fixture(
        fixture.call.as_deref(),
        &fixture.id,
        &fixture.resolved_category(),
        &fixture.tags,
        &fixture.input,
    );
    // Honor per-call `skip_languages`: when the resolved call's `skip_languages`
    // contains `"go"`, the Go binding doesn't expose this function.
    if call_config.skip_languages.iter().any(|l| l == "go") {
        return false;
    }
    let go_override = call_config
        .overrides
        .get("go")
        .or_else(|| e2e_config.call.overrides.get("go"));
    // When a client_factory is configured, the fixture is callable via the
    // client-method pattern even when the base function name is empty.
    if go_override.and_then(|o| o.client_factory.as_deref()).is_some() {
        return true;
    }
    // Prefer a Go-specific override function name; fall back to the base function name.
    // Any non-empty function name is callable: the Go binding exports all public
    // Rust functions as PascalCase symbols (snake_case → PascalCase via to_go_name).
    let fn_name = go_override
        .and_then(|o| o.function.as_deref())
        .filter(|s| !s.is_empty())
        .unwrap_or(call_config.function.as_str());
    !fn_name.is_empty()
}

struct GoTestFunctionContext<'a> {
    import_alias: &'a str,
    e2e_config: &'a crate::e2e::config::E2eConfig,
    adapters: &'a [crate::core::config::AdapterConfig],
    data_enum_names: &'a std::collections::HashSet<&'a str>,
    config: &'a crate::core::config::ResolvedCrateConfig,
    type_defs: &'a [crate::core::ir::TypeDef],
    enums: &'a [crate::core::ir::EnumDef],
}

fn render_test_function(out: &mut String, fixture: &Fixture, context: GoTestFunctionContext<'_>) {
    let GoTestFunctionContext {
        import_alias,
        e2e_config,
        adapters,
        data_enum_names,
        config,
        type_defs,
        enums,
    } = context;
    let fn_name = fixture.id.to_upper_camel_case();
    let description = &fixture.description;

    // Delegate HTTP fixtures to the shared driver via GoTestClientRenderer.
    if fixture.http.is_some() {
        render_http_test_function(out, fixture);
        return;
    }

    // Non-HTTP fixtures are tested directly when the call config provides a
    // callable Go function.  Emit a t.Skip() stub when:
    //   - No mock response and no callable (non-HTTP, non-mock, unreachable), or
    //   - The call's skip_languages includes "go" (e.g. streaming not supported).
    if !fixture_has_go_callable(fixture, e2e_config) {
        let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
        let _ = writeln!(out, "\t// {description}");
        let _ = writeln!(
            out,
            "\tt.Skip(\"non-HTTP fixture: Go binding does not expose a callable for the configured `[e2e.call]` function\")"
        );
        let _ = writeln!(out, "}}");
        return;
    }

    // Resolve call config per-fixture (supports named calls via fixture.call).
    let call_config = e2e_config.resolve_call_for_fixture(
        fixture.call.as_deref(),
        &fixture.id,
        &fixture.resolved_category(),
        &fixture.tags,
        &fixture.input,
    );
    // Per-call field resolver: uses effective fields/result_fields from the resolved call.
    let call_field_resolver = FieldResolver::new(
        e2e_config.effective_fields(call_config),
        e2e_config.effective_fields_optional(call_config),
        e2e_config.effective_result_fields(call_config),
        e2e_config.effective_fields_array(call_config),
        &std::collections::HashSet::new(),
    );
    let field_resolver = &call_field_resolver;
    let lang = "go";
    let overrides = call_config.overrides.get(lang);

    // Select the function name: Go bindings now integrate visitor support into
    // the main Convert() function via ConversionOptions.Visitor field.
    // (In other languages, there may be separate visitor_function overrides, but Go uses a single function.)
    let base_function_name = overrides
        .and_then(|o| o.function.as_deref())
        .unwrap_or(&call_config.function);
    let function_name = to_go_name(base_function_name);
    let result_var = &call_config.result_var;
    let args = fixture.resolved_args(call_config);

    // Whether the function returns (value, error) or just (error) or just (value).
    // Check Go override first, fall back to call-level returns_result.
    let returns_result = overrides
        .and_then(|o| o.returns_result)
        .unwrap_or(call_config.returns_result);

    // Whether the function returns only error (no value component), i.e. Result<(), E>.
    // When returns_result=true and returns_void=true, Go emits `err :=` not `_, err :=`.
    let returns_void = call_config.returns_void;

    // result_is_simple: result is a scalar (*string, *bool, etc.) not a struct.
    // Boolean OR with call-level — serde defaults `result_is_simple` to `false` on
    // CallOverride, so a Go override that only sets e.g. `returns_result = false`
    // would otherwise silently clobber a true call-level value. Falls back to the
    // rust override only when neither the Go override nor the call-level value is set.
    let result_is_simple = overrides.is_some_and(|o| o.result_is_simple)
        || call_config.result_is_simple
        || call_config
            .overrides
            .get("rust")
            .map(|o| o.result_is_simple)
            .unwrap_or(false);

    // result_is_array: the simple result is a slice/array type (e.g., []string).
    // Boolean OR with call-level — serde defaults `result_is_array` to `false` on
    // CallOverride, so a Go override that only sets `result_is_pointer` would
    // otherwise silently mask a true call-level value. Until the field becomes
    // Option<bool>, OR'ing is the only safe coalesce.
    let result_is_array = overrides.is_some_and(|o| o.result_is_array) || call_config.result_is_array;

    // Per-call Go options_type, falling back to the default call's Go override.
    let call_options_type = overrides.and_then(|o| o.options_type.as_deref()).or_else(|| {
        e2e_config
            .call
            .overrides
            .get("go")
            .and_then(|o| o.options_type.as_deref())
    });

    // Whether json_object options are passed as a pointer (*OptionsType).
    let call_options_ptr = overrides.map(|o| o.options_ptr).unwrap_or_else(|| {
        e2e_config
            .call
            .overrides
            .get("go")
            .map(|o| o.options_ptr)
            .unwrap_or(false)
    });

    let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
    // Validation-category fixtures expect engine *creation* to fail. Other expects_error
    // fixtures (error_*) construct a valid engine and expect the *operation* to fail —
    // engine creation should not be wrapped in assert.Error there.
    let validation_creation_failure = expects_error && fixture.resolved_category() == "validation";

    // Client factory: when set, the test creates a client via `pkg.Factory("test-key", baseURL)`
    // and calls methods on the instance rather than top-level package functions.
    let client_factory = overrides.and_then(|o| o.client_factory.as_deref()).or_else(|| {
        e2e_config
            .call
            .overrides
            .get(lang)
            .and_then(|o| o.client_factory.as_deref())
    });

    let (package_decls, mut setup_lines, args_str) = build_args_and_setup(
        &fixture.input,
        args,
        import_alias,
        call_options_type,
        fixture,
        call_options_ptr,
        validation_creation_failure,
        data_enum_names,
        config,
        type_defs,
        enums,
    );

    // Emit package-level declarations (test-backend struct + method receivers)
    // before the test function. Go disallows method declarations inside functions.
    for decl in &package_decls {
        out.push_str(decl);
        if !decl.ends_with('\n') {
            out.push('\n');
        }
        out.push('\n');
    }

    // Build visitor if present — integrate into options instead of separate parameter.
    // Go binding's Convert() checks options.Visitor and delegates to convertWithVisitorHelper when set.
    let mut visitor_opts_var: Option<String> = None;
    if fixture.visitor.is_some() {
        let struct_name = visitor_struct_name(&fixture.id);
        setup_lines.push(format!("visitor := &{struct_name}{{}}"));
        // Create a fresh opts variable with the visitor attached.
        let opts_type = call_options_type.unwrap_or("ConversionOptions");
        let opts_var = "opts".to_string();
        setup_lines.push(format!("opts := &{import_alias}.{opts_type}{{}}"));
        setup_lines.push("opts.Visitor = visitor".to_string());
        visitor_opts_var = Some(opts_var);
    }

    let go_extra_args = overrides.map(|o| o.extra_args.as_slice()).unwrap_or(&[]).to_vec();
    let final_args = {
        let mut parts: Vec<String> = Vec::new();
        if !args_str.is_empty() {
            // When visitor is present, replace trailing ", nil" with ", opts"
            let processed_args = if let Some(ref opts_var) = visitor_opts_var {
                args_str.trim_end_matches(", nil").to_string() + ", " + opts_var
            } else {
                args_str
            };
            parts.push(processed_args);
        }
        parts.extend(go_extra_args);
        parts.join(", ")
    };

    let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
    let _ = writeln!(out, "\t// {description}");

    // Live-API fixtures use `env.api_key_var` to mark the env var that
    // supplies the real API key. Skip the test when the env var is unset
    // (mirrors Python's pytest.skip and Node's early-return pattern).
    let has_mock = fixture.mock_response.is_some() || fixture.http.is_some();
    let api_key_var = fixture.env.as_ref().and_then(|e| e.api_key_var.as_deref());
    if let Some(var) = api_key_var {
        if has_mock {
            // Env-fallback branch: when the real API key is set use the live
            // provider; otherwise fall back to the mock server so the test
            // always runs in CI without credentials.
            let fixture_id = &fixture.id;
            let _ = writeln!(out, "\tapiKey := os.Getenv(\"{var}\")");
            let _ = writeln!(out, "\tvar baseURL *string");
            let _ = writeln!(out, "\tif apiKey != \"\" {{");
            let _ = writeln!(out, "\t\tt.Logf(\"{fixture_id}: using real API ({var} is set)\")");
            let _ = writeln!(out, "\t}} else {{");
            let _ = writeln!(out, "\t\tt.Logf(\"{fixture_id}: using mock server ({var} not set)\")");
            let _ = writeln!(
                out,
                "\t\tu := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
            );
            let _ = writeln!(out, "\t\tbaseURL = &u");
            let _ = writeln!(out, "\t\tapiKey = \"test-key\"");
            let _ = writeln!(out, "\t}}");
        } else {
            let _ = writeln!(out, "\tapiKey := os.Getenv(\"{var}\")");
            let _ = writeln!(out, "\tif apiKey == \"\" {{");
            let _ = writeln!(out, "\t\tt.Skipf(\"{var} not set\")");
            let _ = writeln!(out, "\t}}");
        }
    }

    for line in &setup_lines {
        let _ = writeln!(out, "\t{line}");
    }

    // Client factory: emit client creation before the call.
    // Each test creates a fresh client pointed at MOCK_SERVER_URL/fixtures/<id>
    // so the mock server can serve the fixture response via prefix routing.
    let call_prefix = if let Some(factory) = client_factory {
        let factory_name = to_go_name(factory);
        let fixture_id = &fixture.id;
        // Determine how to express the API key and base URL for the client
        // constructor call, depending on which code path was emitted above.
        let (api_key_expr, base_url_expr) = if has_mock && api_key_var.is_some() {
            // Env-fallback: local vars emitted above carry the right values.
            ("apiKey".to_string(), "baseURL".to_string())
        } else if api_key_var.is_some() {
            // Skip-unless-set: live API only, no mock fallback.
            ("apiKey".to_string(), "nil".to_string())
        } else if fixture.has_host_root_route() {
            let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
            let _ = writeln!(out, "\tmockURL := os.Getenv(\"{env_key}\")");
            let _ = writeln!(out, "\tif mockURL == \"\" {{");
            let _ = writeln!(
                out,
                "\t\tmockURL = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
            );
            let _ = writeln!(out, "\t}}");
            ("\"test-key\"".to_string(), "&mockURL".to_string())
        } else {
            let _ = writeln!(
                out,
                "\tmockURL := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\""
            );
            ("\"test-key\"".to_string(), "&mockURL".to_string())
        };
        let _ = writeln!(
            out,
            "\tclient, clientErr := {import_alias}.{factory_name}({api_key_expr}, {base_url_expr}, nil, nil, nil)"
        );
        let _ = writeln!(out, "\tif clientErr != nil {{");
        let _ = writeln!(out, "\t\tt.Fatalf(\"create client failed: %v\", clientErr)");
        let _ = writeln!(out, "\t}}");
        "client".to_string()
    } else {
        import_alias.to_string()
    };

    // The Go binding generator wraps the FFI call in `(T, error)` whenever any
    // param requires JSON marshalling, even when the underlying Rust function
    // does not return Result. Detect that so error-expecting tests emit `_, err :=`
    // instead of `err :=` when the binding has a value component.
    let binding_returns_error_pre = args
        .iter()
        .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
    let effective_returns_result_pre = returns_result || binding_returns_error_pre || client_factory.is_some();

    if expects_error {
        if effective_returns_result_pre && !returns_void {
            let _ = writeln!(out, "\t_, err := {call_prefix}.{function_name}({final_args})");
        } else {
            let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
        }
        let _ = writeln!(out, "\tif err == nil {{");
        let _ = writeln!(out, "\t\tt.Errorf(\"expected an error, but call succeeded\")");
        let _ = writeln!(out, "\t}}");
        let _ = writeln!(out, "}}");
        return;
    }

    // Detect streaming fixtures (call-level `streaming` opt-out is honored).
    let is_streaming = crate::e2e::codegen::streaming_assertions::resolve_is_streaming(fixture, call_config.streaming);

    // Determine the streaming item type for this call (used when draining the channel).
    // Find the first streaming adapter matching the function name (e.g., crawl_stream → CrawlEvent).
    // Match on snake_case adapter name vs. snake_case function name; e2e codegen sees
    // camelCase function names like `CrawlStream` but adapters declare `crawl_stream`.
    use heck::ToSnakeCase;
    let fn_snake = function_name.to_snake_case();
    let base_snake = base_function_name.to_snake_case();
    let streaming_item_type = if is_streaming {
        adapters
            .iter()
            .filter(|a| matches!(a.pattern, crate::core::config::extras::AdapterPattern::Streaming))
            .find(|a| a.name == fn_snake || a.name == base_snake)
            .and_then(|a| a.item_type.as_deref())
            .and_then(|t| t.rsplit("::").next())
            .unwrap_or("Item") // Fallback if no matching adapter is declared
    } else {
        "Item" // Unused, but needed for type consistency
    };

    // Check if any assertion actually uses the result variable.
    // If all assertions are skipped (field not on result type), use `_` to avoid
    // Go's "declared and not used" compile error.
    // For streaming fixtures: streaming virtual fields count as usable.
    let has_usable_assertion = fixture.assertions.iter().any(|a| {
        if a.assertion_type == "not_error" || a.assertion_type == "error" {
            return false;
        }
        // method_result assertions always use the result variable.
        if a.assertion_type == "method_result" {
            return true;
        }
        match &a.field {
            Some(f) if !f.is_empty() => {
                if is_streaming && crate::e2e::codegen::streaming_assertions::is_streaming_virtual_field(f) {
                    return true;
                }
                // When the call returns a plain scalar (result_is_simple), `render_assertion`
                // ignores the field entirely and operates on the result variable directly
                // (e.g. `fmt.Sprint(result)`). In that case the assertion always uses the
                // result variable regardless of whether `f` is a real field on the type, so
                // the call must bind to `result` and not `_`.
                if result_is_simple {
                    return true;
                }
                field_resolver.is_valid_for_result(f)
            }
            _ => true,
        }
    });

    // The Go binding generator (alef-backend-go) wraps the FFI call in `(T, error)`
    // whenever any param requires JSON marshalling (Vec, Map, Named struct), even when
    // the underlying Rust function does not return Result. So a result_is_simple call
    // like `generate_cache_key(parts: &[(String, String)]) -> String` still surfaces in
    // Go as `func GenerateCacheKey(parts [][]string) (*string, error)`. Detect that
    // here so the test emits `_, err :=` / `result, err :=` instead of `result :=`.
    let binding_returns_error = args
        .iter()
        .any(|a| matches!(a.arg_type.as_str(), "json_object" | "bytes"));
    // Client-factory methods always return (value, error) in the Go binding.
    let effective_returns_result = returns_result || binding_returns_error || client_factory.is_some();

    // For result_is_simple functions, the result variable IS the value (e.g. *string, *bool).
    // We create a local `value` that dereferences it so assertions can use a plain type.
    // For functions that return (value, error): emit `result, err :=`
    // For functions that return only error: emit `err :=`
    // For functions that return only a value (result_is_simple, no error): emit `result :=`
    if !effective_returns_result && result_is_simple {
        // Function returns a single value, no error (e.g. *string, *bool).
        let result_binding = if has_usable_assertion {
            result_var.to_string()
        } else {
            "_".to_string()
        };
        // In Go, `_ :=` is invalid — must use `_ =` for the blank identifier.
        let assign_op = if result_binding == "_" { "=" } else { ":=" };
        let _ = writeln!(
            out,
            "\t{result_binding} {assign_op} {call_prefix}.{function_name}({final_args})"
        );
        if has_usable_assertion && result_binding != "_" {
            if result_is_array {
                // Array results are slices (not pointers); assign directly without dereference.
                let _ = writeln!(out, "\tvalue := {result_var}");
            } else {
                // Check if ALL simple-result assertions are is_empty/is_null with no field.
                // If so, skip dereference — we'll use the pointer directly.
                let only_nil_assertions = fixture
                    .assertions
                    .iter()
                    .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
                    .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
                    .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));

                if !only_nil_assertions {
                    // Emit nil check and dereference for simple pointer results only if
                    // the result is actually a pointer (determined by result_is_pointer override).
                    let result_is_ptr = overrides.map(|o| o.result_is_pointer).unwrap_or(true);
                    if result_is_ptr {
                        let _ = writeln!(out, "\tif {result_var} == nil {{");
                        let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
                        let _ = writeln!(out, "\t}}");
                        let _ = writeln!(out, "\tvalue := *{result_var}");
                    } else {
                        // Result is a value type (not a pointer), use directly without dereference.
                        let _ = writeln!(out, "\tvalue := {result_var}");
                    }
                }
            }
        }
    } else if !effective_returns_result || returns_void {
        // Function returns only error (either returns_result=false, or returns_result=true
        // with returns_void=true meaning the Go function signature is `func(...) error`).
        let _ = writeln!(out, "\terr := {call_prefix}.{function_name}({final_args})");
        let _ = writeln!(out, "\tif err != nil {{");
        let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
        let _ = writeln!(out, "\t}}");
        // No result variable to use in assertions.
        let _ = writeln!(out, "}}");
        return;
    } else {
        // returns_result = true, returns_void = false: function returns (value, error).
        // For streaming fixtures, always capture the channel as `stream`.
        let result_binding = if is_streaming {
            "stream".to_string()
        } else if has_usable_assertion {
            result_var.to_string()
        } else {
            "_".to_string()
        };
        let _ = writeln!(
            out,
            "\t{result_binding}, err := {call_prefix}.{function_name}({final_args})"
        );
        let _ = writeln!(out, "\tif err != nil {{");
        let _ = writeln!(out, "\t\tt.Fatalf(\"call failed: %v\", err)");
        let _ = writeln!(out, "\t}}");
        // For streaming fixtures: drain the channel into a []T slice.
        if is_streaming {
            let _ = writeln!(out, "\tvar chunks []{import_alias}.{streaming_item_type}");
            let _ = writeln!(out, "\tfor chunk := range stream {{");
            let _ = writeln!(out, "\t\tchunks = append(chunks, chunk)");
            let _ = writeln!(out, "\t}}");
        }
        if result_is_simple && has_usable_assertion && result_binding != "_" {
            if result_is_array {
                // Array results are slices (not pointers); assign directly without dereference.
                let _ = writeln!(out, "\tvalue := {}", result_var);
            } else {
                // Check if ALL simple-result assertions are is_empty/is_null with no field.
                // If so, skip dereference — we'll use the pointer directly.
                let only_nil_assertions = fixture
                    .assertions
                    .iter()
                    .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
                    .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
                    .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));

                if !only_nil_assertions {
                    // Emit nil check and dereference for simple pointer results only if
                    // the result is actually a pointer (determined by result_is_pointer override).
                    let result_is_ptr = overrides.map(|o| o.result_is_pointer).unwrap_or(true);
                    if result_is_ptr {
                        let _ = writeln!(out, "\tif {} == nil {{", result_var);
                        let _ = writeln!(out, "\t\tt.Fatalf(\"expected non-nil result\")");
                        let _ = writeln!(out, "\t}}");
                        let _ = writeln!(out, "\tvalue := *{}", result_var);
                    } else {
                        // Result is a value type (not a pointer), use directly without dereference.
                        let _ = writeln!(out, "\tvalue := {}", result_var);
                    }
                }
            }
        }
    }

    // For result_is_simple functions, determine if we created a dereferenced `value` variable.
    // We skip dereferencing if all simple-result assertions are is_empty/is_null with no field,
    // or if the result is a value type (not a pointer).
    let result_is_ptr = overrides.map(|o| o.result_is_pointer).unwrap_or(true);
    let has_deref_value = if result_is_simple && has_usable_assertion && !result_is_array && result_is_ptr {
        let only_nil_assertions = fixture
            .assertions
            .iter()
            .filter(|a| a.field.as_ref().is_none_or(|f| f.is_empty()))
            .filter(|a| !matches!(a.assertion_type.as_str(), "not_error" | "error"))
            .all(|a| matches!(a.assertion_type.as_str(), "is_empty" | "is_null"));
        !only_nil_assertions
    } else if result_is_simple && has_usable_assertion && result_is_ptr {
        true
    } else {
        result_is_simple && has_usable_assertion
    };

    let effective_result_var = if has_deref_value {
        "value".to_string()
    } else {
        result_var.to_string()
    };

    // Collect optional fields referenced by assertions and emit nil-safe
    // dereference blocks so that assertions can use plain string locals.
    // Only dereference fields whose assertion values are strings (or that are
    // used in string-oriented assertions like equals/contains with string values).
    let mut optional_locals: std::collections::HashMap<String, String> = std::collections::HashMap::new();
    for assertion in &fixture.assertions {
        if let Some(f) = &assertion.field {
            if !f.is_empty() {
                // Only emit local vars for fields that are actually valid for the result type.
                // Skip fields that will be marked "skipped" later in render_assertion.
                if !result_is_simple && !field_resolver.is_valid_for_result(f) {
                    continue;
                }
                let resolved = field_resolver.resolve(f);
                if field_resolver.is_optional(resolved) && !optional_locals.contains_key(f.as_str()) {
                    // Only create deref locals for string-valued fields that are NOT arrays.
                    // Array fields (e.g., *[]string) must keep their pointer form so
                    // render_assertion can emit strings.Join(*field, " ") rather than
                    // treating them as plain strings.
                    let is_string_field = assertion.value.as_ref().is_some_and(|v| v.is_string());
                    let is_array_field = field_resolver.is_array(resolved);
                    if !is_string_field || is_array_field {
                        // Non-string optional fields (e.g., *uint64) and array optional
                        // fields (e.g., *[]string) are handled by nil guards in render_assertion.
                        continue;
                    }
                    let field_expr = field_resolver.accessor(f, "go", &effective_result_var);
                    let local_var = go_param_name(&resolved.replace(['.', '[', ']'], "_"));
                    if field_resolver.has_map_access(f) {
                        // Go map access returns a value type (string), not a pointer.
                        // Use the value directly — empty string means not present.
                        let _ = writeln!(out, "\t{local_var} := {field_expr}");
                    } else {
                        let _ = writeln!(out, "\tvar {local_var} string");
                        let _ = writeln!(out, "\tif {field_expr} != nil {{");
                        // Use string() to convert pointer fields (including json.RawMessage which is []byte)
                        // to string. This properly converts the []byte to a string representation instead
                        // of printing the byte array values with fmt.Sprintf.
                        let _ = writeln!(out, "\t\t{local_var} = string(*{field_expr})");
                        let _ = writeln!(out, "\t}}");
                    }
                    optional_locals.insert(f.clone(), local_var);
                }
            }
        }
    }

    // Emit assertions, wrapping in nil guards when an intermediate path segment is optional.
    for assertion in &fixture.assertions {
        if let Some(f) = &assertion.field {
            if !f.is_empty() && !optional_locals.contains_key(f.as_str()) {
                // Check if any prefix of the dotted path is optional (pointer in Go).
                // e.g., "document.nodes" — if "document" is optional, guard the whole block.
                let parts: Vec<&str> = f.split('.').collect();
                let mut guard_expr: Option<String> = None;
                for i in 1..parts.len() {
                    let prefix = parts[..i].join(".");
                    let resolved_prefix = field_resolver.resolve(&prefix);
                    if field_resolver.is_optional(resolved_prefix) {
                        // If the prefix ends with a numeric index (e.g. "segments[0]"),
                        // the element itself is a value type in Go — it cannot be nil.
                        // Use the array field without the index (e.g. "segments") as the
                        // nil guard instead, so we emit `result.Segments != nil` rather
                        // than the invalid `result.Segments[0] != nil`.
                        let guard_prefix = if let Some(bracket_pos) = resolved_prefix.rfind('[') {
                            let suffix = &resolved_prefix[bracket_pos + 1..];
                            let is_numeric_index = suffix.trim_end_matches(']').chars().all(|c| c.is_ascii_digit());
                            if is_numeric_index {
                                &resolved_prefix[..bracket_pos]
                            } else {
                                resolved_prefix
                            }
                        } else {
                            resolved_prefix
                        };
                        let accessor = field_resolver.accessor(guard_prefix, "go", &effective_result_var);
                        guard_expr = Some(accessor);
                        break;
                    }
                }
                if let Some(guard) = guard_expr {
                    // Only emit nil guard if the assertion will actually produce code
                    // (not just a skip comment), to avoid empty branches (SA9003).
                    if field_resolver.is_valid_for_result(f) {
                        // For Go, avoid emitting nil checks on struct value types.
                        // In Go, struct types that are not wrapped in Option<T> in Rust
                        // remain value types in the Go binding, so they cannot be nil.
                        // Skip the guard if the expression is a direct struct field access
                        // (no array indexing, no map keys, no function calls).
                        let is_struct_value = !guard.contains('[') && !guard.contains('(') && !guard.contains("map");
                        if is_struct_value {
                            // Guard refers to a struct value type — skip the nil check
                            // and render the assertion directly.
                            render_assertion(
                                out,
                                assertion,
                                &effective_result_var,
                                import_alias,
                                field_resolver,
                                &optional_locals,
                                result_is_simple,
                                result_is_array,
                                is_streaming,
                            );
                            continue;
                        }
                        let _ = writeln!(out, "\tif {guard} != nil {{");
                        // Render into a temporary buffer so we can re-indent by one
                        // tab level to sit inside the nil-guard block.
                        let mut nil_buf = String::new();
                        render_assertion(
                            &mut nil_buf,
                            assertion,
                            &effective_result_var,
                            import_alias,
                            field_resolver,
                            &optional_locals,
                            result_is_simple,
                            result_is_array,
                            is_streaming,
                        );
                        for line in nil_buf.lines() {
                            let _ = writeln!(out, "\t{line}");
                        }
                        let _ = writeln!(out, "\t}}");
                    } else {
                        render_assertion(
                            out,
                            assertion,
                            &effective_result_var,
                            import_alias,
                            field_resolver,
                            &optional_locals,
                            result_is_simple,
                            result_is_array,
                            is_streaming,
                        );
                    }
                    continue;
                }
            }
        }
        render_assertion(
            out,
            assertion,
            &effective_result_var,
            import_alias,
            field_resolver,
            &optional_locals,
            result_is_simple,
            result_is_array,
            is_streaming,
        );
    }

    let _ = writeln!(out, "}}");
}

/// Render an HTTP server test function using net/http against MOCK_SERVER_URL.
///
/// Delegates to the shared driver [`client::http_call::render_http_test`] via
/// [`GoTestClientRenderer`]. The emitted test shape is unchanged: `func Test_<Name>(t *testing.T)`
/// with a `net/http` client that hits `$MOCK_SERVER_URL/fixtures/<id>`.
fn render_http_test_function(out: &mut String, fixture: &Fixture) {
    client::http_call::render_http_test(out, &GoTestClientRenderer, fixture);
}

// ---------------------------------------------------------------------------
// HTTP test rendering — GoTestClientRenderer
// ---------------------------------------------------------------------------

/// Go `net/http` test renderer.
///
/// Go HTTP e2e tests send a request to `$MOCK_SERVER_URL/fixtures/<id>` using
/// the standard library `net/http` client. The trait primitives emit the
/// request-build, response-capture, and assertion code that the previous
/// monolithic renderer produced, so generated output is unchanged after the
/// migration.
struct GoTestClientRenderer;

impl client::TestClientRenderer for GoTestClientRenderer {
    fn language_name(&self) -> &'static str {
        "go"
    }

    /// Go test names use `UpperCamelCase` so they form valid exported identifiers
    /// (e.g. `Test_MyFixtureId`). Override the default `sanitize_ident` which
    /// produces `lower_snake_case`.
    fn sanitize_test_name(&self, id: &str) -> String {
        id.to_upper_camel_case()
    }

    /// Emit `func Test_<fn_name>(t *testing.T) {`, a description comment, and the
    /// `baseURL` / request scaffolding. Skipped fixtures get `t.Skip(...)` inline.
    fn render_test_open(&self, out: &mut String, fn_name: &str, description: &str, skip_reason: Option<&str>) {
        let _ = writeln!(out, "func Test_{fn_name}(t *testing.T) {{");
        let _ = writeln!(out, "\t// {description}");
        if let Some(reason) = skip_reason {
            let escaped = go_string_literal(reason);
            let _ = writeln!(out, "\tt.Skip({escaped})");
        }
    }

    fn render_test_close(&self, out: &mut String) {
        let _ = writeln!(out, "}}");
    }

    /// Emit the full `net/http` request scaffolding: URL construction, body,
    /// headers, cookies, a no-redirect client, and `io.ReadAll` for the body.
    ///
    /// `bodyBytes` is always declared (with `_ = bodyBytes` to avoid the Go
    /// "declared and not used" compile error on tests with no body assertion).
    fn render_call(&self, out: &mut String, ctx: &client::CallCtx<'_>) {
        let method = ctx.method.to_uppercase();
        let path = ctx.path;

        let _ = writeln!(out, "\tbaseURL := os.Getenv(\"MOCK_SERVER_URL\")");
        let _ = writeln!(out, "\tif baseURL == \"\" {{");
        let _ = writeln!(out, "\t\tbaseURL = \"http://localhost:8080\"");
        let _ = writeln!(out, "\t}}");

        // Build request body expression.
        let body_expr = if let Some(body) = ctx.body {
            let json = serde_json::to_string(body).unwrap_or_default();
            let escaped = go_string_literal(&json);
            format!("strings.NewReader({})", escaped)
        } else {
            "strings.NewReader(\"\")".to_string()
        };

        let _ = writeln!(out, "\tbody := {body_expr}");
        let _ = writeln!(
            out,
            "\treq, err := http.NewRequest(\"{method}\", baseURL+\"{path}\", body)"
        );
        let _ = writeln!(out, "\tif err != nil {{");
        let _ = writeln!(out, "\t\tt.Fatalf(\"new request failed: %v\", err)");
        let _ = writeln!(out, "\t}}");

        // Content-Type header (only when a body is present).
        if ctx.body.is_some() {
            let content_type = ctx.content_type.unwrap_or("application/json");
            let _ = writeln!(out, "\treq.Header.Set(\"Content-Type\", \"{content_type}\")");
        }

        // Explicit request headers (sorted for deterministic output).
        let mut header_names: Vec<&String> = ctx.headers.keys().collect();
        header_names.sort();
        for name in header_names {
            let value = &ctx.headers[name];
            let escaped_name = go_string_literal(name);
            let escaped_value = go_string_literal(value);
            let _ = writeln!(out, "\treq.Header.Set({escaped_name}, {escaped_value})");
        }

        // Cookies.
        if !ctx.cookies.is_empty() {
            let mut cookie_names: Vec<&String> = ctx.cookies.keys().collect();
            cookie_names.sort();
            for name in cookie_names {
                let value = &ctx.cookies[name];
                let escaped_name = go_string_literal(name);
                let escaped_value = go_string_literal(value);
                let _ = writeln!(
                    out,
                    "\treq.AddCookie(&http.Cookie{{Name: {escaped_name}, Value: {escaped_value}}})"
                );
            }
        }

        // No-redirect client so 3xx fixtures assert the redirect response itself.
        let _ = writeln!(out, "\tnoRedirectClient := &http.Client{{");
        let _ = writeln!(
            out,
            "\t\tCheckRedirect: func(req *http.Request, via []*http.Request) error {{"
        );
        let _ = writeln!(out, "\t\t\treturn http.ErrUseLastResponse");
        let _ = writeln!(out, "\t\t}},");
        let _ = writeln!(out, "\t}}");
        let _ = writeln!(out, "\tresp, err := noRedirectClient.Do(req)");
        let _ = writeln!(out, "\tif err != nil {{");
        let _ = writeln!(out, "\t\tt.Fatalf(\"request failed: %v\", err)");
        let _ = writeln!(out, "\t}}");
        let _ = writeln!(out, "\tdefer func() {{ _ = resp.Body.Close() }}()");

        // Always read the response body so body-assertion methods can reference
        // `bodyBytes`. Suppress the "declared and not used" compile error with
        // `_ = bodyBytes` for tests that have no body assertion.
        let _ = writeln!(out, "\tbodyBytes, err := io.ReadAll(resp.Body)");
        let _ = writeln!(out, "\tif err != nil {{");
        let _ = writeln!(out, "\t\tt.Fatalf(\"read body failed: %v\", err)");
        let _ = writeln!(out, "\t}}");
        let _ = writeln!(out, "\t_ = bodyBytes");
    }

    fn render_assert_status(&self, out: &mut String, _response_var: &str, status: u16) {
        let _ = writeln!(out, "\tif resp.StatusCode != {status} {{");
        let _ = writeln!(out, "\t\tt.Fatalf(\"status: got %d want {status}\", resp.StatusCode)");
        let _ = writeln!(out, "\t}}");
    }

    /// Emit a header assertion, skipping special tokens (`<<present>>`, `<<absent>>`,
    /// `<<uuid>>`) and hop-by-hop headers (`Connection`) that `net/http` strips.
    fn render_assert_header(&self, out: &mut String, _response_var: &str, name: &str, expected: &str) {
        // Skip special-token assertions.
        if matches!(expected, "<<absent>>" | "<<present>>" | "<<uuid>>") {
            return;
        }
        // Connection is a hop-by-hop header that Go's net/http strips.
        if name.eq_ignore_ascii_case("connection") {
            return;
        }
        let escaped_name = go_string_literal(name);
        let escaped_value = go_string_literal(expected);
        let _ = writeln!(
            out,
            "\tif !strings.Contains(resp.Header.Get({escaped_name}), {escaped_value}) {{"
        );
        let _ = writeln!(
            out,
            "\t\tt.Fatalf(\"header %s mismatch: got %q want to contain %q\", {escaped_name}, resp.Header.Get({escaped_name}), {escaped_value})"
        );
        let _ = writeln!(out, "\t}}");
    }

    /// Emit an exact-equality body assertion.
    ///
    /// JSON objects and arrays are round-tripped via `json.Unmarshal` + `reflect.DeepEqual`.
    /// Scalar values are compared as trimmed strings.
    fn render_assert_json_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
        match expected {
            serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
                let json_str = serde_json::to_string(expected).unwrap_or_default();
                let escaped = go_string_literal(&json_str);
                let _ = writeln!(out, "\tvar got any");
                let _ = writeln!(out, "\tvar want any");
                let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &got); err != nil {{");
                let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal got: %v\", err)");
                let _ = writeln!(out, "\t}}");
                let _ = writeln!(
                    out,
                    "\tif err := json.Unmarshal([]byte({escaped}), &want); err != nil {{"
                );
                let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal want: %v\", err)");
                let _ = writeln!(out, "\t}}");
                let _ = writeln!(out, "\tif !reflect.DeepEqual(got, want) {{");
                let _ = writeln!(out, "\t\tt.Fatalf(\"body mismatch: got %v want %v\", got, want)");
                let _ = writeln!(out, "\t}}");
            }
            serde_json::Value::String(s) => {
                let escaped = go_string_literal(s);
                let _ = writeln!(out, "\twant := {escaped}");
                let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
                let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
                let _ = writeln!(out, "\t}}");
            }
            other => {
                let escaped = go_string_literal(&other.to_string());
                let _ = writeln!(out, "\twant := {escaped}");
                let _ = writeln!(out, "\tif strings.TrimSpace(string(bodyBytes)) != want {{");
                let _ = writeln!(out, "\t\tt.Fatalf(\"body: got %q want %q\", string(bodyBytes), want)");
                let _ = writeln!(out, "\t}}");
            }
        }
    }

    /// Emit partial-body assertions: every key in `expected` must appear in the
    /// parsed JSON response with the matching value.
    fn render_assert_partial_body(&self, out: &mut String, _response_var: &str, expected: &serde_json::Value) {
        if let Some(obj) = expected.as_object() {
            let _ = writeln!(out, "\tvar _partialGot map[string]any");
            let _ = writeln!(
                out,
                "\tif err := json.Unmarshal(bodyBytes, &_partialGot); err != nil {{"
            );
            let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal partial: %v\", err)");
            let _ = writeln!(out, "\t}}");
            for (key, val) in obj {
                let escaped_key = go_string_literal(key);
                let json_val = serde_json::to_string(val).unwrap_or_default();
                let escaped_val = go_string_literal(&json_val);
                let _ = writeln!(out, "\t{{");
                let _ = writeln!(out, "\t\tvar _wantVal any");
                let _ = writeln!(
                    out,
                    "\t\tif err := json.Unmarshal([]byte({escaped_val}), &_wantVal); err != nil {{"
                );
                let _ = writeln!(out, "\t\t\tt.Fatalf(\"json unmarshal partial want: %v\", err)");
                let _ = writeln!(out, "\t\t}}");
                let _ = writeln!(
                    out,
                    "\t\tif !reflect.DeepEqual(_partialGot[{escaped_key}], _wantVal) {{"
                );
                let _ = writeln!(
                    out,
                    "\t\t\tt.Fatalf(\"partial body field {key}: got %v want %v\", _partialGot[{escaped_key}], _wantVal)"
                );
                let _ = writeln!(out, "\t\t}}");
                let _ = writeln!(out, "\t}}");
            }
        }
    }

    /// Emit validation-error assertions for 422 responses.
    ///
    /// Checks that each expected `msg` appears in at least one element of the
    /// parsed body's `"errors"` array.
    fn render_assert_validation_errors(
        &self,
        out: &mut String,
        _response_var: &str,
        errors: &[ValidationErrorExpectation],
    ) {
        let _ = writeln!(out, "\tvar _veBody map[string]any");
        let _ = writeln!(out, "\tif err := json.Unmarshal(bodyBytes, &_veBody); err != nil {{");
        let _ = writeln!(out, "\t\tt.Fatalf(\"json unmarshal validation errors: %v\", err)");
        let _ = writeln!(out, "\t}}");
        let _ = writeln!(out, "\t_veErrors, _ := _veBody[\"errors\"].([]any)");
        for ve in errors {
            let escaped_msg = go_string_literal(&ve.msg);
            let _ = writeln!(out, "\t{{");
            let _ = writeln!(out, "\t\t_found := false");
            let _ = writeln!(out, "\t\tfor _, _e := range _veErrors {{");
            let _ = writeln!(out, "\t\t\tif _em, ok := _e.(map[string]any); ok {{");
            let _ = writeln!(
                out,
                "\t\t\t\tif _msg, ok := _em[\"msg\"].(string); ok && strings.Contains(_msg, {escaped_msg}) {{"
            );
            let _ = writeln!(out, "\t\t\t\t\t_found = true");
            let _ = writeln!(out, "\t\t\t\t\tbreak");
            let _ = writeln!(out, "\t\t\t\t}}");
            let _ = writeln!(out, "\t\t\t}}");
            let _ = writeln!(out, "\t\t}}");
            let _ = writeln!(out, "\t\tif !_found {{");
            let _ = writeln!(
                out,
                "\t\t\tt.Fatalf(\"validation error with msg containing %q not found in errors\", {escaped_msg})"
            );
            let _ = writeln!(out, "\t\t}}");
            let _ = writeln!(out, "\t}}");
        }
    }
}

/// Build setup lines (e.g. handle creation) and the argument list for the function call.
///
/// Returns `(package_decls, setup_lines, args_string)`.
///
/// `package_decls` — Go declarations that must appear at the package (file) level,
/// outside any function body.  Test-backend stubs (struct type + method receivers)
/// are placed here because Go does not allow method declarations inside functions.
///
/// `options_ptr` — when `true`, `json_object` args with an `options_type` are
/// passed as a Go pointer (`*OptionsType`): absent/empty → `nil`, present →
/// `&varName` after JSON unmarshal.
#[allow(clippy::too_many_arguments)]
fn build_args_and_setup(
    input: &serde_json::Value,
    args: &[crate::e2e::config::ArgMapping],
    import_alias: &str,
    options_type: Option<&str>,
    fixture: &crate::e2e::fixture::Fixture,
    options_ptr: bool,
    expects_error: bool,
    data_enum_names: &std::collections::HashSet<&str>,
    config: &crate::core::config::ResolvedCrateConfig,
    type_defs: &[crate::core::ir::TypeDef],
    enums: &[crate::core::ir::EnumDef],
) -> (Vec<String>, Vec<String>, String) {
    let fixture_id = &fixture.id;
    use heck::ToUpperCamelCase;

    if args.is_empty() {
        return (Vec::new(), Vec::new(), String::new());
    }

    let mut package_decls: Vec<String> = Vec::new();
    let mut setup_lines: Vec<String> = Vec::new();
    let mut parts: Vec<String> = Vec::new();

    for arg in args {
        if arg.arg_type == "mock_url" {
            if fixture.has_host_root_route() {
                let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
                setup_lines.push(format!("{} := os.Getenv(\"{env_key}\")", arg.name));
                setup_lines.push(format!(
                    "if {} == \"\" {{ {} = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\" }}",
                    arg.name, arg.name
                ));
            } else {
                setup_lines.push(format!(
                    "{} := os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"",
                    arg.name,
                ));
            }
            parts.push(arg.name.clone());
            continue;
        }

        if arg.arg_type == "mock_url_list" {
            // []string of URLs: each element is either a bare path (`/seed1`) — prefixed
            // with the per-fixture mock-server URL at runtime — or an absolute URL kept as-is.
            // Mirrors the `mock_url` resolution: `MOCK_SERVER_<FIXTURE_ID>` first, then
            // `MOCK_SERVER_URL/fixtures/<id>`.
            let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
            let val = input.get(field).unwrap_or(&serde_json::Value::Null);

            let paths: Vec<String> = if let Some(arr) = val.as_array() {
                arr.iter().filter_map(|v| v.as_str().map(go_string_literal)).collect()
            } else {
                Vec::new()
            };

            let paths_literal = paths.join(", ");
            let var_name = &arg.name;

            setup_lines.push(format!(
                "{var_name}Base := os.Getenv(\"{env_key}\")\n\tif {var_name}Base == \"\" {{\n\t\t{var_name}Base = os.Getenv(\"MOCK_SERVER_URL\") + \"/fixtures/{fixture_id}\"\n\t}}"
            ));
            setup_lines.push(format!(
                "var {var_name} []string\n\tfor _, p := range []string{{{paths_literal}}} {{\n\t\tif strings.HasPrefix(p, \"http\") {{\n\t\t\t{var_name} = append({var_name}, p)\n\t\t}} else {{\n\t\t\t{var_name} = append({var_name}, {var_name}Base + p)\n\t\t}}\n\t}}"
            ));
            parts.push(var_name.to_string());
            continue;
        }

        if arg.arg_type == "test_backend" {
            if let Some(trait_name) = &arg.trait_name {
                if let Some(trait_bridge) = config.trait_bridges.iter().find(|tb| tb.trait_name == *trait_name) {
                    // Collect methods from both the main trait and its super-trait (if present).
                    // The super-trait methods are needed so stubs implement the full interface.
                    let mut methods: Vec<&crate::core::ir::MethodDef> = type_defs
                        .iter()
                        .find(|t| t.name == *trait_name)
                        .map(|t| t.methods.iter().collect())
                        .unwrap_or_default();

                    // If there's a super-trait, also collect its methods.
                    // The dispatcher (in mod.rs) uses trait_source to identify super-trait methods,
                    // so we need to account for methods that come from the super-trait.
                    if let Some(super_trait) = &trait_bridge.super_trait {
                        if let Some(super_type) = type_defs.iter().find(|t| &t.rust_path == super_trait) {
                            for method in &super_type.methods {
                                // Only add if not already present (avoid duplicates).
                                if !methods.iter().any(|m| m.name == method.name) {
                                    methods.push(method);
                                }
                            }
                        }
                    }

                    // Collect binding-excluded type names (e.g. InternalDocument) from IR.
                    // These types are never emitted as Go structs; the trait-bridge interface
                    // serialises them to JSON, so stubs must use json.RawMessage.
                    // Additionally, manually add Go-specific hardcoded excluded types like InternalDocument.
                    let mut excluded_named: std::collections::HashSet<&str> = type_defs
                        .iter()
                        .filter(|t| t.binding_excluded)
                        .map(|t| t.name.as_str())
                        .collect();
                    // InternalDocument and SyncExtractor are special cases: they're always excluded in Go trait bridges,
                    // even though they may not be marked as binding_excluded in the IR.
                    // SyncExtractor is marked alef(skip) in source but appears in DocumentExtractor.as_sync_extractor() return type.
                    excluded_named.insert("InternalDocument");
                    excluded_named.insert("SyncExtractor");
                    // Collect enum names for proper Go zero-value generation.
                    // Enums map to string types in Go, so their zero-value is "" not nil.
                    let enum_names: std::collections::HashSet<&str> = enums.iter().map(|e| e.name.as_str()).collect();
                    let emission = emit_test_backend_with_context(
                        trait_bridge,
                        &methods,
                        fixture,
                        &excluded_named,
                        import_alias,
                        &enum_names,
                    );
                    // Go does not allow method declarations inside function bodies.
                    // The setup_block (struct type + method receivers) must be emitted
                    // at the package level, outside any test function.
                    package_decls.push(emission.setup_block);
                    parts.push(emission.arg_expr);
                    continue;
                }
            }
            let emission = crate::e2e::codegen::TestBackendEmission::unimplemented("go");
            setup_lines.push(format!("// {}", emission.arg_expr));
            parts.push("nil".to_string());
            continue;
        }

        if arg.arg_type == "handle" {
            // Generate a CreateEngine (or equivalent) call and pass the variable.
            let constructor_name = format!("Create{}", arg.name.to_upper_camel_case());
            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
            let config_value = input.get(field).unwrap_or(&serde_json::Value::Null);
            // When the fixture expects an error (validation test), engine creation
            // is the error source. Assert the error and return so the test passes
            // without proceeding to the (unreachable) function call.
            let create_err_handler = if expects_error {
                "assert.Error(t, createErr)\n\t\treturn".to_string()
            } else {
                "t.Fatalf(\"create handle failed: %v\", createErr)".to_string()
            };
            if config_value.is_null()
                || config_value.is_object() && config_value.as_object().is_some_and(|o| o.is_empty())
            {
                setup_lines.push(format!(
                    "{name}, createErr := {import_alias}.{constructor_name}(nil)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}",
                    name = arg.name,
                ));
            } else {
                let json_str = serde_json::to_string(config_value).unwrap_or_default();
                let go_literal = go_string_literal(&json_str);
                let name = &arg.name;
                setup_lines.push(format!(
                    "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}}"
                ));
                setup_lines.push(format!(
                    "{name}, createErr := {import_alias}.{constructor_name}(&{name}Config)\n\tif createErr != nil {{\n\t\t{create_err_handler}\n\t}}"
                ));
            }
            parts.push(arg.name.clone());
            continue;
        }

        let val: Option<&serde_json::Value> = if arg.field == "input" {
            Some(input)
        } else {
            let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
            input.get(field)
        };

        // file_path args are fixture-relative paths under `test_documents/`. The Go test
        // runner's TestMain (in main_test.go) already does `os.Chdir(test_documents)` so
        // tests can pass these relative strings directly; no additional resolution needed.

        // Handle bytes type: fixture stores base64-encoded bytes.
        // Emit a Go base64.StdEncoding.DecodeString call to decode at runtime.
        if arg.arg_type == "bytes" {
            let var_name = format!("{}Bytes", arg.name);
            match val {
                None | Some(serde_json::Value::Null) => {
                    if arg.optional {
                        parts.push("nil".to_string());
                    } else {
                        parts.push("[]byte{}".to_string());
                    }
                }
                Some(serde_json::Value::String(s)) => {
                    // Bytes args whose fixture value is a string are fixture-relative paths into
                    // the repo-root `test_documents/` directory (matching the rust e2e codegen
                    // convention). The Go test runner's TestMain chdirs into test_documents/, so
                    // a bare relative path resolves correctly via os.ReadFile.
                    let go_path = go_string_literal(s);
                    setup_lines.push(format!(
                        "{var_name}, {var_name}Err := os.ReadFile({go_path})\n\tif {var_name}Err != nil {{\n\t\tt.Fatalf(\"read fixture {s}: %v\", {var_name}Err)\n\t}}"
                    ));
                    parts.push(var_name);
                }
                Some(other) => {
                    parts.push(format!("[]byte({})", json_to_go(other)));
                }
            }
            continue;
        }

        match val {
            None | Some(serde_json::Value::Null) if arg.optional => {
                // Optional arg absent: emit Go zero/nil for the type.
                match arg.arg_type.as_str() {
                    "string" => {
                        // Optional string in Go bindings is *string → nil.
                        parts.push("nil".to_string());
                    }
                    "json_object" => {
                        if options_ptr {
                            // Pointer options type (*OptionsType): absent → nil.
                            parts.push("nil".to_string());
                        } else if let Some(opts_type) = options_type {
                            // Value options type: zero-value struct.
                            parts.push(format!("{import_alias}.{opts_type}{{}}"));
                        } else {
                            parts.push("nil".to_string());
                        }
                    }
                    _ => {
                        parts.push("nil".to_string());
                    }
                }
            }
            None | Some(serde_json::Value::Null) => {
                // Required arg with no fixture value: pass a language-appropriate default.
                let default_val = match arg.arg_type.as_str() {
                    "string" => "\"\"".to_string(),
                    "int" | "integer" | "i64" => "0".to_string(),
                    "float" | "number" => "0.0".to_string(),
                    "bool" | "boolean" => "false".to_string(),
                    "json_object" => {
                        if options_ptr {
                            // Pointer options type (*OptionsType): absent → nil.
                            "nil".to_string()
                        } else if let Some(opts_type) = options_type {
                            format!("{import_alias}.{opts_type}{{}}")
                        } else {
                            "nil".to_string()
                        }
                    }
                    _ => "nil".to_string(),
                };
                parts.push(default_val);
            }
            Some(v) => {
                match arg.arg_type.as_str() {
                    "json_object" => {
                        // JSON arrays unmarshal into []string (Go slices).
                        // JSON objects with a known options_type unmarshal into that type.
                        let is_array = v.is_array();
                        let is_empty_obj = !is_array && v.is_object() && v.as_object().is_some_and(|o| o.is_empty());
                        if is_empty_obj {
                            if options_ptr {
                                // Pointer options type: empty object → nil.
                                parts.push("nil".to_string());
                            } else if let Some(opts_type) = options_type {
                                parts.push(format!("{import_alias}.{opts_type}{{}}"));
                            } else {
                                parts.push("nil".to_string());
                            }
                        } else if is_array {
                            // Array type — unmarshal into a Go slice. Honor `go_type` for a
                            // fully explicit Go type (e.g. `"sample_core.BatchBytesItem"`), fall
                            // back to deriving the slice type from `element_type`, defaulting
                            // to `[]string` for unknown types.
                            let go_slice_type = if let Some(go_t) = arg.go_type.as_deref() {
                                // go_type is the slice element type — wrap it in [].
                                // If it already starts with '[' the user specified the full
                                // slice type; use it verbatim.
                                if go_t.starts_with('[') {
                                    go_t.to_string()
                                } else {
                                    // Qualify unqualified types (e.g., "BatchBytesItem" → "sample_core.BatchBytesItem")
                                    let qualified = if go_t.contains('.') {
                                        go_t.to_string()
                                    } else {
                                        format!("{import_alias}.{go_t}")
                                    };
                                    format!("[]{qualified}")
                                }
                            } else {
                                element_type_to_go_slice(arg.element_type.as_deref(), import_alias)
                            };

                            // Extract the element type (unqualified) from go_type or element_type to check if it's a sum-type.
                            let element_type_name = if let Some(go_t) = arg.go_type.as_deref() {
                                // go_type is the element type — extract the unqualified name.
                                if go_t.starts_with('[') {
                                    None // Full slice type specified; can't extract element
                                } else if let Some(idx) = go_t.rfind('.') {
                                    Some(&go_t[idx + 1..]) // Strip package prefix
                                } else {
                                    Some(go_t)
                                }
                            } else {
                                arg.element_type.as_deref()
                            };

                            let is_sum_type = element_type_name.is_some_and(|et| data_enum_names.contains(et));

                            // Convert JSON for Go compatibility (e.g., byte arrays → base64 strings)
                            let converted_v = convert_json_for_go(v.clone());
                            let var_name = &arg.name;

                            if is_sum_type {
                                // For sum-types, unmarshal each element via Unmarshal<Type> discriminator.
                                let element_type = element_type_name.unwrap();
                                let json_str = serde_json::to_string(&converted_v).unwrap_or_default();
                                let go_literal = go_string_literal(&json_str);
                                setup_lines.push(format!(
                                    "var {var_name}Raw []json.RawMessage\n\tif err := json.Unmarshal([]byte({go_literal}), &{var_name}Raw); err != nil {{\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}}"
                                ));
                                setup_lines.push(format!(
                                    "var {var_name} {go_slice_type}\n\tfor _, raw := range {var_name}Raw {{\n\t\telem, err := {import_alias}.Unmarshal{element_type}(raw)\n\t\tif err != nil {{\n\t\t\tt.Fatalf(\"unmarshal {element_type} failed: %v\", err)\n\t\t}}\n\t\t{var_name} = append({var_name}, elem)\n\t}}"
                                ));
                            } else {
                                // For non-sum-type arrays, use standard json.Unmarshal.
                                let json_str = serde_json::to_string(&converted_v).unwrap_or_default();
                                let go_literal = go_string_literal(&json_str);
                                setup_lines.push(format!(
                                    "var {var_name} {go_slice_type}\n\tif err := json.Unmarshal([]byte({go_literal}), &{var_name}); err != nil {{\n\t\tt.Fatalf(\"config parse failed: %v\", err)\n\t}}"
                                ));
                            }
                            parts.push(var_name.to_string());
                        } else if let Some(opts_type) = options_type {
                            // Object with known type — unmarshal into typed struct.
                            // When options_ptr is set, the Go struct uses snake_case JSON
                            // field tags and lowercase/snake_case enum values.  Remap the
                            // fixture's camelCase keys and PascalCase enum string values.
                            let remapped_v = if options_ptr {
                                convert_json_for_go(v.clone())
                            } else {
                                v.clone()
                            };
                            let json_str = serde_json::to_string(&remapped_v).unwrap_or_default();
                            let go_literal = go_string_literal(&json_str);
                            let var_name = &arg.name;
                            setup_lines.push(format!(
                                "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}}"
                            ));
                            // Pass as pointer when options_ptr is set.
                            let arg_expr = if options_ptr {
                                format!("&{var_name}")
                            } else {
                                var_name.to_string()
                            };
                            parts.push(arg_expr);
                        } else {
                            parts.push(json_to_go(v));
                        }
                    }
                    "string" if arg.optional => {
                        // Optional string in Go is *string — take address of a local.
                        let var_name = format!("{}Val", arg.name);
                        let go_val = json_to_go(v);
                        setup_lines.push(format!("{var_name} := {go_val}"));
                        parts.push(format!("&{var_name}"));
                    }
                    _ => {
                        parts.push(json_to_go(v));
                    }
                }
            }
        }
    }

    (package_decls, setup_lines, parts.join(", "))
}

#[allow(clippy::too_many_arguments)]
fn render_assertion(
    out: &mut String,
    assertion: &Assertion,
    result_var: &str,
    import_alias: &str,
    field_resolver: &FieldResolver,
    optional_locals: &std::collections::HashMap<String, String>,
    result_is_simple: bool,
    result_is_array: bool,
    is_streaming: bool,
) {
    // Handle synthetic / derived fields before the is_valid_for_result check
    // so they are never treated as struct field accesses on the result.
    if !result_is_simple {
        if let Some(f) = &assertion.field {
            // embed_texts returns *[][]float32; the embedding matrix is *result_var.
            // We emit inline func() expressions so we don't need additional variables.
            let embed_deref = format!("(*{result_var})");
            match f.as_str() {
                "chunks_have_content" => {
                    let pred = format!(
                        "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range chunks {{ if c.Content == \"\" {{ return false }} }}; return true }}()"
                    );
                    match assertion.assertion_type.as_str() {
                        "is_true" => {
                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
                        }
                        "is_false" => {
                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
                        }
                        _ => {
                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
                        }
                    }
                    return;
                }
                "chunks_have_embeddings" => {
                    let pred = format!(
                        "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 }}()"
                    );
                    match assertion.assertion_type.as_str() {
                        "is_true" => {
                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
                        }
                        "is_false" => {
                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
                        }
                        _ => {
                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
                        }
                    }
                    return;
                }
                "chunks_have_heading_context" => {
                    let pred = format!(
                        "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil {{ return false }}; for _, c := range chunks {{ if c.Metadata.HeadingContext == nil {{ return false }} }}; return true }}()"
                    );
                    match assertion.assertion_type.as_str() {
                        "is_true" => {
                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
                        }
                        "is_false" => {
                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
                        }
                        _ => {
                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
                        }
                    }
                    return;
                }
                "first_chunk_starts_with_heading" => {
                    let pred = format!(
                        "func() bool {{ chunks := {result_var}.Chunks; if chunks == nil || len(chunks) == 0 {{ return false }}; return chunks[0].Metadata.HeadingContext != nil }}()"
                    );
                    match assertion.assertion_type.as_str() {
                        "is_true" => {
                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
                        }
                        "is_false" => {
                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
                        }
                        _ => {
                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
                        }
                    }
                    return;
                }
                "embeddings" => {
                    match assertion.assertion_type.as_str() {
                        "count_equals" => {
                            if let Some(val) = &assertion.value {
                                if let Some(n) = val.as_u64() {
                                    let _ = writeln!(
                                        out,
                                        "\tassert.Equal(t, {n}, len({embed_deref}), \"expected exactly {n} elements\")"
                                    );
                                }
                            }
                        }
                        "count_min" => {
                            if let Some(val) = &assertion.value {
                                if let Some(n) = val.as_u64() {
                                    let _ = writeln!(
                                        out,
                                        "\tassert.GreaterOrEqual(t, len({embed_deref}), {n}, \"expected at least {n} elements\")"
                                    );
                                }
                            }
                        }
                        "not_empty" => {
                            let _ = writeln!(
                                out,
                                "\tassert.NotEmpty(t, {embed_deref}, \"expected non-empty embeddings\")"
                            );
                        }
                        "is_empty" => {
                            let _ = writeln!(out, "\tassert.Empty(t, {embed_deref}, \"expected empty embeddings\")");
                        }
                        _ => {
                            let _ = writeln!(
                                out,
                                "\t// skipped: unsupported assertion type on synthetic field 'embeddings'"
                            );
                        }
                    }
                    return;
                }
                "embedding_dimensions" => {
                    let expr = format!(
                        "func() int {{ if len({embed_deref}) == 0 {{ return 0 }}; return len({embed_deref}[0]) }}()"
                    );
                    match assertion.assertion_type.as_str() {
                        "equals" => {
                            if let Some(val) = &assertion.value {
                                if let Some(n) = val.as_u64() {
                                    let _ = writeln!(
                                        out,
                                        "\tif {expr} != {n} {{\n\t\tt.Errorf(\"equals mismatch: got %v\", {expr})\n\t}}"
                                    );
                                }
                            }
                        }
                        "greater_than" => {
                            if let Some(val) = &assertion.value {
                                if let Some(n) = val.as_u64() {
                                    let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
                                }
                            }
                        }
                        _ => {
                            let _ = writeln!(
                                out,
                                "\t// skipped: unsupported assertion type on synthetic field 'embedding_dimensions'"
                            );
                        }
                    }
                    return;
                }
                "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
                    let pred = match f.as_str() {
                        "embeddings_valid" => {
                            format!(
                                "func() bool {{ for _, e := range {embed_deref} {{ if len(e) == 0 {{ return false }} }}; return true }}()"
                            )
                        }
                        "embeddings_finite" => {
                            format!(
                                "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 }}()"
                            )
                        }
                        "embeddings_non_zero" => {
                            format!(
                                "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 }}()"
                            )
                        }
                        "embeddings_normalized" => {
                            format!(
                                "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 }}()"
                            )
                        }
                        _ => unreachable!(),
                    };
                    match assertion.assertion_type.as_str() {
                        "is_true" => {
                            let _ = writeln!(out, "\tassert.True(t, {pred}, \"expected true\")");
                        }
                        "is_false" => {
                            let _ = writeln!(out, "\tassert.False(t, {pred}, \"expected false\")");
                        }
                        _ => {
                            let _ = writeln!(out, "\t// skipped: unsupported assertion type on synthetic field '{f}'");
                        }
                    }
                    return;
                }
                // ---- keywords / keywords_count ----
                // Go ExtractionResult does not expose extracted_keywords; skip.
                "keywords" | "keywords_count" => {
                    let _ = writeln!(out, "\t// skipped: field '{f}' not available on Go ExtractionResult");
                    return;
                }
                _ => {}
            }
        }
    }

    // Streaming virtual fields: intercept before is_valid_for_result so they are
    // never skipped.  These fields resolve against the `chunks` collected-list variable.
    // Skip the streaming interception entirely when the call has opted out
    // (`[e2e.calls.<name>] streaming = false`) — `chunks` then names a plain
    // field on the synchronous result struct and must flow through normal
    // accessor resolution (e.g. `result.Chunks`).
    if !result_is_simple && is_streaming {
        if let Some(f) = &assertion.field {
            if !f.is_empty() && crate::e2e::codegen::streaming_assertions::is_streaming_virtual_field(f) {
                if let Some(expr) =
                    crate::e2e::codegen::streaming_assertions::StreamingFieldResolver::accessor(f, "go", "chunks")
                {
                    match assertion.assertion_type.as_str() {
                        "count_min" => {
                            if let Some(val) = &assertion.value {
                                if let Some(n) = val.as_u64() {
                                    let _ = writeln!(
                                        out,
                                        "\tassert.GreaterOrEqual(t, len({expr}), {n}, \"expected >= {n} chunks\")"
                                    );
                                }
                            }
                        }
                        "count_equals" => {
                            if let Some(val) = &assertion.value {
                                if let Some(n) = val.as_u64() {
                                    let _ = writeln!(
                                        out,
                                        "\tassert.Equal(t, {n}, len({expr}), \"expected exactly {n} chunks\")"
                                    );
                                }
                            }
                        }
                        "equals" => {
                            if let Some(serde_json::Value::String(s)) = &assertion.value {
                                let escaped = crate::e2e::escape::go_string_literal(s);
                                // Deep-path streaming-virtual fields like `tool_calls[0].function.name`
                                // resolve to pointer-typed Go fields (`*string`). The flat virtual
                                // accessors `stream_content` / `finish_reason` already return `string`.
                                // Wrap only the deep-path case in a safe-deref IIFE.
                                let is_deep_path = f.contains('.') || f.contains('[');
                                let safe_expr = if is_deep_path {
                                    format!(
                                        "func() string {{ v := {expr}; if v == nil {{ return \"\" }}; return *v }}()"
                                    )
                                } else {
                                    expr.clone()
                                };
                                let _ = writeln!(out, "\tassert.Equal(t, {escaped}, {safe_expr})");
                            } else if let Some(val) = &assertion.value {
                                if let Some(n) = val.as_u64() {
                                    let _ = writeln!(out, "\tassert.Equal(t, {n}, {expr})");
                                }
                            }
                        }
                        "not_empty" => {
                            let _ = writeln!(out, "\tassert.NotEmpty(t, {expr}, \"expected non-empty\")");
                        }
                        "is_empty" => {
                            let _ = writeln!(out, "\tassert.Empty(t, {expr}, \"expected empty\")");
                        }
                        "is_true" => {
                            let _ = writeln!(out, "\tassert.True(t, {expr}, \"expected true\")");
                        }
                        "is_false" => {
                            let _ = writeln!(out, "\tassert.False(t, {expr}, \"expected false\")");
                        }
                        "greater_than" => {
                            if let Some(val) = &assertion.value {
                                if let Some(n) = val.as_u64() {
                                    let _ = writeln!(out, "\tassert.Greater(t, {expr}, {n}, \"expected > {n}\")");
                                }
                            }
                        }
                        "greater_than_or_equal" => {
                            if let Some(val) = &assertion.value {
                                if let Some(n) = val.as_u64() {
                                    let _ =
                                        writeln!(out, "\tassert.GreaterOrEqual(t, {expr}, {n}, \"expected >= {n}\")");
                                }
                            }
                        }
                        "contains" => {
                            if let Some(serde_json::Value::String(s)) = &assertion.value {
                                let escaped = crate::e2e::escape::go_string_literal(s);
                                let _ =
                                    writeln!(out, "\tassert.Contains(t, {expr}, {escaped}, \"expected to contain\")");
                            }
                        }
                        _ => {
                            let _ = writeln!(
                                out,
                                "\t// streaming field '{f}': assertion type '{}' not rendered",
                                assertion.assertion_type
                            );
                        }
                    }
                }
                return;
            }
        }
    }

    // Skip assertions on fields that don't exist on the result type.
    // When result_is_simple, all field assertions operate on the scalar result directly.
    if !result_is_simple {
        if let Some(f) = &assertion.field {
            if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
                let _ = writeln!(out, "\t// skipped: field '{f}' not available on result type");
                return;
            }
        }
    }

    let field_expr = if result_is_simple {
        // The result IS the value — field access is irrelevant.
        result_var.to_string()
    } else {
        match &assertion.field {
            Some(f) if !f.is_empty() => {
                // Use the local variable if the field was dereferenced above.
                if let Some(local_var) = optional_locals.get(f.as_str()) {
                    local_var.clone()
                } else {
                    field_resolver.accessor(f, "go", result_var)
                }
            }
            _ => result_var.to_string(),
        }
    };

    // Check if the field (after resolution) is optional, which means it's a pointer in Go.
    // Also check if a `.length` suffix's parent is optional (e.g., metadata.headings.length
    // where metadata.headings is optional → len() needs dereference).
    let is_optional = assertion
        .field
        .as_ref()
        .map(|f| {
            let resolved = field_resolver.resolve(f);
            let check_path = resolved
                .strip_suffix(".length")
                .or_else(|| resolved.strip_suffix(".count"))
                .or_else(|| resolved.strip_suffix(".size"))
                .unwrap_or(resolved);
            field_resolver.is_optional(check_path) && !optional_locals.contains_key(f.as_str())
        })
        .unwrap_or(false);

    // When field_expr is `len(X)` and X is an optional (pointer) field, rewrite to `len(*X)`
    // and we'll wrap with a nil guard in the assertion handlers.
    // However, slices are already nil-able and should not be dereferenced.
    let field_is_array_for_len = assertion
        .field
        .as_ref()
        .map(|f| {
            let resolved = field_resolver.resolve(f);
            let check_path = resolved
                .strip_suffix(".length")
                .or_else(|| resolved.strip_suffix(".count"))
                .or_else(|| resolved.strip_suffix(".size"))
                .unwrap_or(resolved);
            field_resolver.is_array(check_path)
        })
        .unwrap_or(false);
    let field_expr =
        if is_optional && field_expr.starts_with("len(") && field_expr.ends_with(')') && !field_is_array_for_len {
            let inner = &field_expr[4..field_expr.len() - 1];
            format!("len(*{inner})")
        } else {
            field_expr
        };
    // Build the nil-guard expression for the inner pointer (without len wrapper).
    let nil_guard_expr = if is_optional && field_expr.starts_with("len(*") {
        Some(field_expr[5..field_expr.len() - 1].to_string())
    } else {
        None
    };

    // For optional non-string fields that weren't dereferenced into locals,
    // we need to dereference the pointer in comparisons.
    // However, slices are already nil-able and should not be dereferenced.
    let field_is_slice = assertion
        .field
        .as_ref()
        .map(|f| field_resolver.is_array(field_resolver.resolve(f)))
        .unwrap_or(false);
    let deref_field_expr = if is_optional && !field_expr.starts_with("len(") && !field_is_slice {
        format!("*{field_expr}")
    } else {
        field_expr.clone()
    };

    // Detect array element access (e.g., `result.Assets[0].ContentHash`).
    // When the field_expr contains `[0]`, we must guard against an out-of-bounds
    // panic by checking that the array is non-empty first.
    // Extract the array slice expression (everything before `[0]`).
    let array_guard: Option<String> = if let Some(idx) = field_expr.find("[0]") {
        let mut array_expr = field_expr[..idx].to_string();
        if let Some(stripped) = array_expr.strip_prefix("len(") {
            array_expr = stripped.to_string();
        }
        Some(array_expr)
    } else {
        None
    };

    // Render the assertion into a temporary buffer first, then wrap with the array
    // bounds guard (if needed) by adding one extra level of indentation.
    let mut assertion_buf = String::new();
    let out_ref = &mut assertion_buf;

    match assertion.assertion_type.as_str() {
        "equals" => {
            if let Some(expected) = &assertion.value {
                let go_val = json_to_go(expected);
                // For string equality, trim whitespace to handle trailing newlines from the converter.
                if expected.is_string() {
                    // Wrap field expression with strings.TrimSpace() for string comparisons.
                    // Use string() cast to convert any pointer type to string (includes json.RawMessage,
                    // named string types, and struct types through their String() methods).
                    let trimmed_field = if is_optional && !field_expr.starts_with("len(") {
                        format!("strings.TrimSpace(string(*{field_expr}))")
                    } else {
                        format!("strings.TrimSpace(string({field_expr}))")
                    };
                    if is_optional && !field_expr.starts_with("len(") {
                        let _ = writeln!(out_ref, "\tif {field_expr} != nil && {trimmed_field} != {go_val} {{");
                    } else {
                        let _ = writeln!(out_ref, "\tif {trimmed_field} != {go_val} {{");
                    }
                } else if is_optional && !field_expr.starts_with("len(") {
                    let _ = writeln!(out_ref, "\tif {field_expr} != nil && {deref_field_expr} != {go_val} {{");
                } else {
                    let _ = writeln!(out_ref, "\tif {field_expr} != {go_val} {{");
                }
                let _ = writeln!(out_ref, "\t\tt.Errorf(\"equals mismatch: got %v\", {field_expr})");
                let _ = writeln!(out_ref, "\t}}");
            }
        }
        "contains" => {
            if let Some(expected) = &assertion.value {
                let go_val = json_to_go(expected);
                // Determine the "string view" of the field expression.
                // - []string (optional) → jsonString(field_expr) — Go slices are nil-able, no `*` needed
                // - *string → string(*field_expr)
                // - *[]byte/*json.RawMessage → string(*field_expr) — special case for binary types
                // - string → string(field_expr) (or just field_expr for plain strings)
                // - result_is_array (result_is_simple + array result) → jsonString(field_expr)
                let resolved_field = assertion.field.as_deref().unwrap_or("");
                let resolved_name = field_resolver.resolve(resolved_field);
                let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
                let is_opt =
                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
                let field_for_contains = if is_opt && field_is_array {
                    // Go slices are nil-able directly — no pointer dereference needed.
                    format!("jsonString({field_expr})")
                } else if is_opt {
                    // For optional pointer fields, use string() cast instead of fmt.Sprint()
                    // to handle json.RawMessage ([]byte alias) correctly. fmt.Sprint(*rawmsg)
                    // prints the byte array as [34 74...], whereas string(*rawmsg) treats
                    // it as a JSON string value like "JS Test Page".
                    format!("string(*{field_expr})")
                } else if field_is_array {
                    format!("jsonString({field_expr})")
                } else {
                    format!("string({field_expr})")
                };
                if is_opt {
                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
                    let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
                    let _ = writeln!(
                        out_ref,
                        "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
                    );
                    let _ = writeln!(out_ref, "\t}}");
                    let _ = writeln!(out_ref, "\t}}");
                } else {
                    let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
                    let _ = writeln!(
                        out_ref,
                        "\t\tt.Errorf(\"expected to contain %s, got %v\", {go_val}, {field_expr})"
                    );
                    let _ = writeln!(out_ref, "\t}}");
                }
            }
        }
        "contains_all" => {
            if let Some(values) = &assertion.values {
                let resolved_field = assertion.field.as_deref().unwrap_or("");
                let resolved_name = field_resolver.resolve(resolved_field);
                let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
                let is_opt =
                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
                for val in values {
                    let go_val = json_to_go(val);
                    let field_for_contains = if is_opt && field_is_array {
                        // Go slices are nil-able directly — no pointer dereference needed.
                        format!("jsonString({field_expr})")
                    } else if is_opt {
                        format!("string(*{field_expr})")
                    } else if field_is_array {
                        format!("jsonString({field_expr})")
                    } else {
                        format!("string({field_expr})")
                    };
                    if is_opt {
                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
                        let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
                        let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
                        let _ = writeln!(out_ref, "\t}}");
                        let _ = writeln!(out_ref, "\t}}");
                    } else {
                        let _ = writeln!(out_ref, "\tif !strings.Contains({field_for_contains}, {go_val}) {{");
                        let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected to contain %s\", {go_val})");
                        let _ = writeln!(out_ref, "\t}}");
                    }
                }
            }
        }
        "not_contains" => {
            if let Some(expected) = &assertion.value {
                let go_val = json_to_go(expected);
                let resolved_field = assertion.field.as_deref().unwrap_or("");
                let resolved_name = field_resolver.resolve(resolved_field);
                let field_is_array = result_is_array || field_resolver.is_array(resolved_name);
                let is_opt =
                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
                let field_for_contains = if is_opt && field_is_array {
                    // Go slices are nil-able directly — no pointer dereference needed.
                    format!("jsonString({field_expr})")
                } else if is_opt {
                    format!("string(*{field_expr})")
                } else if field_is_array {
                    format!("jsonString({field_expr})")
                } else {
                    format!("string({field_expr})")
                };
                let _ = writeln!(out_ref, "\tif strings.Contains({field_for_contains}, {go_val}) {{");
                let _ = writeln!(
                    out_ref,
                    "\t\tt.Errorf(\"expected NOT to contain %s, got %v\", {go_val}, {field_expr})"
                );
                let _ = writeln!(out_ref, "\t}}");
            }
        }
        "not_empty" => {
            // For optional struct pointers (not arrays), just check != nil.
            // For optional slice/string pointers, check nil and len.
            let field_is_array = {
                let rf = assertion.field.as_deref().unwrap_or("");
                let rn = field_resolver.resolve(rf);
                field_resolver.is_array(rn)
            };
            if is_optional && !field_is_array {
                // Struct pointer: non-empty means not nil.
                let _ = writeln!(out_ref, "\tif {field_expr} == nil {{");
            } else if is_optional && field_is_slice {
                // Slice optional: Go slices are already nil-able — no dereference needed.
                let _ = writeln!(out_ref, "\tif {field_expr} == nil || len({field_expr}) == 0 {{");
            } else if is_optional {
                // Pointer-to-slice (*[]T): dereference then len.
                let _ = writeln!(out_ref, "\tif {field_expr} == nil || len(*{field_expr}) == 0 {{");
            } else if result_is_simple && result_is_array {
                // Simple array result ([]T) — direct slice, not a pointer; check length only.
                let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
            } else {
                let _ = writeln!(out_ref, "\tif len({field_expr}) == 0 {{");
            }
            let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected non-empty value\")");
            let _ = writeln!(out_ref, "\t}}");
        }
        "is_empty" => {
            let field_is_array = {
                let rf = assertion.field.as_deref().unwrap_or("");
                let rn = field_resolver.resolve(rf);
                field_resolver.is_array(rn)
            };
            // Special case: result_is_simple && !result_is_array && no field means the result is a pointer.
            // Empty means nil.
            if result_is_simple && !result_is_array && assertion.field.as_ref().is_none_or(|f| f.is_empty()) {
                // Pointer result (not dereferenced): empty means nil.
                let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
            } else if is_optional && !field_is_array {
                // Struct pointer: empty means nil.
                let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
            } else if is_optional && field_is_slice {
                // Slice optional: Go slices are already nil-able — no dereference needed.
                let _ = writeln!(out_ref, "\tif {field_expr} != nil && len({field_expr}) != 0 {{");
            } else if is_optional {
                // Pointer-to-slice (*[]T): dereference then len.
                let _ = writeln!(out_ref, "\tif {field_expr} != nil && len(*{field_expr}) != 0 {{");
            } else {
                let _ = writeln!(out_ref, "\tif len({field_expr}) != 0 {{");
            }
            let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected empty value, got %v\", {field_expr})");
            let _ = writeln!(out_ref, "\t}}");
        }
        "contains_any" => {
            if let Some(values) = &assertion.values {
                let resolved_field = assertion.field.as_deref().unwrap_or("");
                let resolved_name = field_resolver.resolve(resolved_field);
                let field_is_array = field_resolver.is_array(resolved_name);
                let is_opt =
                    is_optional && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()));
                let field_for_contains = if is_opt && field_is_array {
                    // Go slices are nil-able directly — no pointer dereference needed.
                    format!("jsonString({field_expr})")
                } else if is_opt {
                    format!("string(*{field_expr})")
                } else if field_is_array {
                    format!("jsonString({field_expr})")
                } else {
                    format!("string({field_expr})")
                };
                let _ = writeln!(out_ref, "\t{{");
                let _ = writeln!(out_ref, "\t\tfound := false");
                for val in values {
                    let go_val = json_to_go(val);
                    let _ = writeln!(
                        out_ref,
                        "\t\tif strings.Contains({field_for_contains}, {go_val}) {{ found = true }}"
                    );
                }
                let _ = writeln!(out_ref, "\t\tif !found {{");
                let _ = writeln!(
                    out_ref,
                    "\t\t\tt.Errorf(\"expected to contain at least one of the specified values\")"
                );
                let _ = writeln!(out_ref, "\t\t}}");
                let _ = writeln!(out_ref, "\t}}");
            }
        }
        "greater_than" => {
            if let Some(val) = &assertion.value {
                let go_val = json_to_go(val);
                // Use `< N+1` instead of `<= N` to avoid golangci-lint sloppyLen
                // warning when N is 0 (len(x) <= 0 → len(x) < 1).
                // For optional (pointer) fields, dereference and guard with nil check.
                if is_optional {
                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
                    if let Some(n) = val.as_u64() {
                        let next = n + 1;
                        let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {next} {{");
                    } else {
                        let _ = writeln!(out_ref, "\t\tif {deref_field_expr} <= {go_val} {{");
                    }
                    let _ = writeln!(
                        out_ref,
                        "\t\t\tt.Errorf(\"expected > {go_val}, got %v\", {deref_field_expr})"
                    );
                    let _ = writeln!(out_ref, "\t\t}}");
                    let _ = writeln!(out_ref, "\t}}");
                } else if let Some(n) = val.as_u64() {
                    let next = n + 1;
                    let _ = writeln!(out_ref, "\tif {field_expr} < {next} {{");
                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
                    let _ = writeln!(out_ref, "\t}}");
                } else {
                    let _ = writeln!(out_ref, "\tif {field_expr} <= {go_val} {{");
                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected > {go_val}, got %v\", {field_expr})");
                    let _ = writeln!(out_ref, "\t}}");
                }
            }
        }
        "less_than" => {
            if let Some(val) = &assertion.value {
                let go_val = json_to_go(val);
                if let Some(ref guard) = nil_guard_expr {
                    let _ = writeln!(out_ref, "\tif {guard} != nil {{");
                    let _ = writeln!(out_ref, "\t\tif {field_expr} >= {go_val} {{");
                    let _ = writeln!(out_ref, "\t\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
                    let _ = writeln!(out_ref, "\t\t}}");
                    let _ = writeln!(out_ref, "\t}}");
                } else if is_optional && !field_expr.starts_with("len(") {
                    // Optional pointer field: nil-guard and dereference before comparison.
                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
                    let _ = writeln!(out_ref, "\t\tif {deref_field_expr} >= {go_val} {{");
                    let _ = writeln!(
                        out_ref,
                        "\t\t\tt.Errorf(\"expected < {go_val}, got %v\", {deref_field_expr})"
                    );
                    let _ = writeln!(out_ref, "\t\t}}");
                    let _ = writeln!(out_ref, "\t}}");
                } else {
                    let _ = writeln!(out_ref, "\tif {field_expr} >= {go_val} {{");
                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected < {go_val}, got %v\", {field_expr})");
                    let _ = writeln!(out_ref, "\t}}");
                }
            }
        }
        "greater_than_or_equal" => {
            if let Some(val) = &assertion.value {
                let go_val = json_to_go(val);
                if let Some(ref guard) = nil_guard_expr {
                    let _ = writeln!(out_ref, "\tif {guard} != nil {{");
                    let _ = writeln!(out_ref, "\t\tif {field_expr} < {go_val} {{");
                    let _ = writeln!(
                        out_ref,
                        "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})"
                    );
                    let _ = writeln!(out_ref, "\t\t}}");
                    let _ = writeln!(out_ref, "\t}}");
                } else if is_optional && !field_expr.starts_with("len(") {
                    // Optional pointer field: nil-guard and dereference before comparison.
                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
                    let _ = writeln!(out_ref, "\t\tif {deref_field_expr} < {go_val} {{");
                    let _ = writeln!(
                        out_ref,
                        "\t\t\tt.Errorf(\"expected >= {go_val}, got %v\", {deref_field_expr})"
                    );
                    let _ = writeln!(out_ref, "\t\t}}");
                    let _ = writeln!(out_ref, "\t}}");
                } else {
                    let _ = writeln!(out_ref, "\tif {field_expr} < {go_val} {{");
                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected >= {go_val}, got %v\", {field_expr})");
                    let _ = writeln!(out_ref, "\t}}");
                }
            }
        }
        "less_than_or_equal" => {
            if let Some(val) = &assertion.value {
                let go_val = json_to_go(val);
                if is_optional && !field_expr.starts_with("len(") {
                    // Optional pointer field: nil-guard and dereference before comparison.
                    let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
                    let _ = writeln!(out_ref, "\t\tif {deref_field_expr} > {go_val} {{");
                    let _ = writeln!(
                        out_ref,
                        "\t\t\tt.Errorf(\"expected <= {go_val}, got %v\", {deref_field_expr})"
                    );
                    let _ = writeln!(out_ref, "\t\t}}");
                    let _ = writeln!(out_ref, "\t}}");
                } else {
                    let _ = writeln!(out_ref, "\tif {field_expr} > {go_val} {{");
                    let _ = writeln!(out_ref, "\t\tt.Errorf(\"expected <= {go_val}, got %v\", {field_expr})");
                    let _ = writeln!(out_ref, "\t}}");
                }
            }
        }
        "starts_with" => {
            if let Some(expected) = &assertion.value {
                let go_val = json_to_go(expected);
                let field_for_prefix = if is_optional
                    && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
                {
                    format!("string(*{field_expr})")
                } else {
                    format!("string({field_expr})")
                };
                let _ = writeln!(out_ref, "\tif !strings.HasPrefix({field_for_prefix}, {go_val}) {{");
                let _ = writeln!(
                    out_ref,
                    "\t\tt.Errorf(\"expected to start with %s, got %v\", {go_val}, {field_expr})"
                );
                let _ = writeln!(out_ref, "\t}}");
            }
        }
        "count_min" => {
            if let Some(val) = &assertion.value {
                if let Some(n) = val.as_u64() {
                    if is_optional {
                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
                        // Slices are value types in Go — use len(slice) not len(*slice).
                        let len_expr = if field_is_slice {
                            format!("len({field_expr})")
                        } else {
                            format!("len(*{field_expr})")
                        };
                        let _ = writeln!(
                            out_ref,
                            "\t\tassert.GreaterOrEqual(t, {len_expr}, {n}, \"expected at least {n} elements\")"
                        );
                        let _ = writeln!(out_ref, "\t}}");
                    } else {
                        let _ = writeln!(
                            out_ref,
                            "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected at least {n} elements\")"
                        );
                    }
                }
            }
        }
        "count_equals" => {
            if let Some(val) = &assertion.value {
                if let Some(n) = val.as_u64() {
                    if is_optional {
                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
                        // Slices are value types in Go — use len(slice) not len(*slice).
                        let len_expr = if field_is_slice {
                            format!("len({field_expr})")
                        } else {
                            format!("len(*{field_expr})")
                        };
                        let _ = writeln!(
                            out_ref,
                            "\t\tassert.Equal(t, {len_expr}, {n}, \"expected exactly {n} elements\")"
                        );
                        let _ = writeln!(out_ref, "\t}}");
                    } else {
                        let _ = writeln!(
                            out_ref,
                            "\tassert.Equal(t, len({field_expr}), {n}, \"expected exactly {n} elements\")"
                        );
                    }
                }
            }
        }
        "is_true" => {
            if is_optional {
                let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
                let _ = writeln!(out_ref, "\t\tassert.True(t, *{field_expr}, \"expected true\")");
                let _ = writeln!(out_ref, "\t}}");
            } else {
                let _ = writeln!(out_ref, "\tassert.True(t, {field_expr}, \"expected true\")");
            }
        }
        "is_false" => {
            if is_optional {
                let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
                let _ = writeln!(out_ref, "\t\tassert.False(t, *{field_expr}, \"expected false\")");
                let _ = writeln!(out_ref, "\t}}");
            } else {
                let _ = writeln!(out_ref, "\tassert.False(t, {field_expr}, \"expected false\")");
            }
        }
        "method_result" => {
            if let Some(method_name) = &assertion.method {
                let info = build_go_method_call(result_var, method_name, assertion.args.as_ref(), import_alias);
                let check = assertion.check.as_deref().unwrap_or("is_true");
                // For pointer-returning functions, dereference with `*`. Value-returning
                // functions (e.g., NodeInfo field access) are used directly.
                let deref_expr = if info.is_pointer {
                    format!("*{}", info.call_expr)
                } else {
                    info.call_expr.clone()
                };
                match check {
                    "equals" => {
                        if let Some(val) = &assertion.value {
                            if val.is_boolean() {
                                if val.as_bool() == Some(true) {
                                    let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
                                } else {
                                    let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
                                }
                            } else {
                                // Apply type cast to numeric literals when the method returns
                                // a typed uint (e.g., *uint) to avoid reflect.DeepEqual
                                // mismatches between int and uint in testify's assert.Equal.
                                let go_val = if let Some(cast) = info.value_cast {
                                    if val.is_number() {
                                        format!("{cast}({})", json_to_go(val))
                                    } else {
                                        json_to_go(val)
                                    }
                                } else {
                                    json_to_go(val)
                                };
                                let _ = writeln!(
                                    out_ref,
                                    "\tassert.Equal(t, {go_val}, {deref_expr}, \"method_result equals assertion failed\")"
                                );
                            }
                        }
                    }
                    "is_true" => {
                        let _ = writeln!(out_ref, "\tassert.True(t, {deref_expr}, \"expected true\")");
                    }
                    "is_false" => {
                        let _ = writeln!(out_ref, "\tassert.False(t, {deref_expr}, \"expected false\")");
                    }
                    "greater_than_or_equal" => {
                        if let Some(val) = &assertion.value {
                            let n = val.as_u64().unwrap_or(0);
                            // Use the value_cast type if available (e.g., uint for named_children_count).
                            let cast = info.value_cast.unwrap_or("uint");
                            let _ = writeln!(
                                out_ref,
                                "\tassert.GreaterOrEqual(t, {deref_expr}, {cast}({n}), \"expected >= {n}\")"
                            );
                        }
                    }
                    "count_min" => {
                        if let Some(val) = &assertion.value {
                            let n = val.as_u64().unwrap_or(0);
                            let _ = writeln!(
                                out_ref,
                                "\tassert.GreaterOrEqual(t, len({deref_expr}), {n}, \"expected at least {n} elements\")"
                            );
                        }
                    }
                    "contains" => {
                        if let Some(val) = &assertion.value {
                            let go_val = json_to_go(val);
                            let _ = writeln!(
                                out_ref,
                                "\tassert.Contains(t, {deref_expr}, {go_val}, \"expected result to contain value\")"
                            );
                        }
                    }
                    "is_error" => {
                        let _ = writeln!(out_ref, "\t{{");
                        let _ = writeln!(out_ref, "\t\t_, methodErr := {}", info.call_expr);
                        let _ = writeln!(out_ref, "\t\tassert.Error(t, methodErr)");
                        let _ = writeln!(out_ref, "\t}}");
                    }
                    other_check => {
                        panic!("Go e2e generator: unsupported method_result check type: {other_check}");
                    }
                }
            } else {
                panic!("Go e2e generator: method_result assertion missing 'method' field");
            }
        }
        "min_length" => {
            if let Some(val) = &assertion.value {
                if let Some(n) = val.as_u64() {
                    if is_optional {
                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
                        let _ = writeln!(
                            out_ref,
                            "\t\tassert.GreaterOrEqual(t, len(*{field_expr}), {n}, \"expected length >= {n}\")"
                        );
                        let _ = writeln!(out_ref, "\t}}");
                    } else if field_expr.starts_with("len(") {
                        let _ = writeln!(
                            out_ref,
                            "\tassert.GreaterOrEqual(t, {field_expr}, {n}, \"expected length >= {n}\")"
                        );
                    } else {
                        let _ = writeln!(
                            out_ref,
                            "\tassert.GreaterOrEqual(t, len({field_expr}), {n}, \"expected length >= {n}\")"
                        );
                    }
                }
            }
        }
        "max_length" => {
            if let Some(val) = &assertion.value {
                if let Some(n) = val.as_u64() {
                    if is_optional {
                        let _ = writeln!(out_ref, "\tif {field_expr} != nil {{");
                        let _ = writeln!(
                            out_ref,
                            "\t\tassert.LessOrEqual(t, len(*{field_expr}), {n}, \"expected length <= {n}\")"
                        );
                        let _ = writeln!(out_ref, "\t}}");
                    } else if field_expr.starts_with("len(") {
                        let _ = writeln!(
                            out_ref,
                            "\tassert.LessOrEqual(t, {field_expr}, {n}, \"expected length <= {n}\")"
                        );
                    } else {
                        let _ = writeln!(
                            out_ref,
                            "\tassert.LessOrEqual(t, len({field_expr}), {n}, \"expected length <= {n}\")"
                        );
                    }
                }
            }
        }
        "ends_with" => {
            if let Some(expected) = &assertion.value {
                let go_val = json_to_go(expected);
                let field_for_suffix = if is_optional
                    && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
                {
                    format!("string(*{field_expr})")
                } else {
                    format!("string({field_expr})")
                };
                let _ = writeln!(out_ref, "\tif !strings.HasSuffix({field_for_suffix}, {go_val}) {{");
                let _ = writeln!(
                    out_ref,
                    "\t\tt.Errorf(\"expected to end with %s, got %v\", {go_val}, {field_expr})"
                );
                let _ = writeln!(out_ref, "\t}}");
            }
        }
        "matches_regex" => {
            if let Some(expected) = &assertion.value {
                let go_val = json_to_go(expected);
                let field_for_regex = if is_optional
                    && !optional_locals.contains_key(assertion.field.as_ref().unwrap_or(&String::new()))
                {
                    format!("*{field_expr}")
                } else {
                    field_expr.clone()
                };
                let _ = writeln!(
                    out_ref,
                    "\tassert.Regexp(t, {go_val}, {field_for_regex}, \"expected value to match regex\")"
                );
            }
        }
        "not_error" => {
            // Already handled by the `if err != nil` check above.
        }
        "error" => {
            // Handled at the test function level.
        }
        other => {
            panic!("Go e2e generator: unsupported assertion type: {other}");
        }
    }

    // If the assertion accesses an array element via [0], wrap the generated code in a
    // bounds check to prevent an index-out-of-range panic when the array is empty.
    if let Some(ref arr) = array_guard {
        if !assertion_buf.is_empty() {
            let _ = writeln!(out, "\tif len({arr}) > 0 {{");
            // Re-indent each line by one additional tab level.
            for line in assertion_buf.lines() {
                let _ = writeln!(out, "\t{line}");
            }
            let _ = writeln!(out, "\t}}");
        }
    } else {
        out.push_str(&assertion_buf);
    }
}

/// Metadata about the return type of a Go method call for `method_result` assertions.
struct GoMethodCallInfo {
    /// The call expression string.
    call_expr: String,
    /// Whether the return type is a pointer (needs `*` dereference for value comparison).
    is_pointer: bool,
    /// Optional Go type cast to apply to numeric literal values in `equals` assertions
    /// (e.g., `"uint"` so that `0` becomes `uint(0)` to match `*uint` deref type).
    value_cast: Option<&'static str>,
}

/// Build a Go call expression for a `method_result` assertion on a sample_language Tree.
///
/// Maps method names to the appropriate Go function calls, matching the Go binding API
/// in `packages/go/binding.go`. Returns a [`GoMethodCallInfo`] describing the call and
/// its return type characteristics.
///
/// Return types by method:
/// - `has_error_nodes`, `contains_node_type` → `*bool` (pointer)
/// - `error_count` → `*uint` (pointer, value_cast = "uint")
/// - `tree_to_sexp` → `*string` (pointer)
/// - `root_node_type` → `string` via `RootNodeInfo(tree).Kind` (value)
/// - `named_children_count` → `uint` via `RootNodeInfo(tree).NamedChildCount` (value, value_cast = "uint")
/// - `find_nodes_by_type` → `*[]NodeInfo` (pointer to slice)
/// - `run_query` → `(*[]QueryMatch, error)` (pointer + error; use `is_error` check type)
fn build_go_method_call(
    result_var: &str,
    method_name: &str,
    args: Option<&serde_json::Value>,
    import_alias: &str,
) -> GoMethodCallInfo {
    match method_name {
        "root_node_type" => GoMethodCallInfo {
            call_expr: format!("{import_alias}.RootNodeInfo({result_var}).Kind"),
            is_pointer: false,
            value_cast: None,
        },
        "named_children_count" => GoMethodCallInfo {
            call_expr: format!("{import_alias}.RootNodeInfo({result_var}).NamedChildCount"),
            is_pointer: false,
            value_cast: Some("uint"),
        },
        "has_error_nodes" => GoMethodCallInfo {
            call_expr: format!("{import_alias}.TreeHasErrorNodes({result_var})"),
            is_pointer: true,
            value_cast: None,
        },
        "error_count" | "tree_error_count" => GoMethodCallInfo {
            call_expr: format!("{import_alias}.TreeErrorCount({result_var})"),
            is_pointer: true,
            value_cast: Some("uint"),
        },
        "tree_to_sexp" => GoMethodCallInfo {
            call_expr: format!("{import_alias}.TreeToSexp({result_var})"),
            is_pointer: true,
            value_cast: None,
        },
        "contains_node_type" => {
            let node_type = args
                .and_then(|a| a.get("node_type"))
                .and_then(|v| v.as_str())
                .unwrap_or("");
            GoMethodCallInfo {
                call_expr: format!("{import_alias}.TreeContainsNodeType({result_var}, \"{node_type}\")"),
                is_pointer: true,
                value_cast: None,
            }
        }
        "find_nodes_by_type" => {
            let node_type = args
                .and_then(|a| a.get("node_type"))
                .and_then(|v| v.as_str())
                .unwrap_or("");
            GoMethodCallInfo {
                call_expr: format!("{import_alias}.FindNodesByType({result_var}, \"{node_type}\")"),
                is_pointer: true,
                value_cast: None,
            }
        }
        "run_query" => {
            let query_source = args
                .and_then(|a| a.get("query_source"))
                .and_then(|v| v.as_str())
                .unwrap_or("");
            let language = args
                .and_then(|a| a.get("language"))
                .and_then(|v| v.as_str())
                .unwrap_or("");
            let query_lit = go_string_literal(query_source);
            let lang_lit = go_string_literal(language);
            // RunQuery returns (*[]QueryMatch, error) — use is_error check type.
            GoMethodCallInfo {
                call_expr: format!("{import_alias}.RunQuery({result_var}, {lang_lit}, {query_lit}, []byte(source))"),
                is_pointer: false,
                value_cast: None,
            }
        }
        other => {
            let method_pascal = other.to_upper_camel_case();
            GoMethodCallInfo {
                call_expr: format!("{result_var}.{method_pascal}()"),
                is_pointer: false,
                value_cast: None,
            }
        }
    }
}

/// Convert a `serde_json::Value` to a Go literal string.
/// Recursively convert a JSON value for Go struct unmarshalling.
///
/// The Go binding's `ConversionOptions` struct uses:
/// - `snake_case` JSON field tags (e.g. `"code_block_style"` not `"codeBlockStyle"`)
/// - lowercase/snake_case string values for enums (e.g. `"indented"`, `"atx_closed"`)
///
/// Fixture JSON uses camelCase keys and PascalCase enum values (Python/TS conventions).
/// This function remaps both so the generated Go tests can unmarshal correctly.
fn convert_json_for_go(value: serde_json::Value) -> serde_json::Value {
    match value {
        serde_json::Value::Object(map) => {
            let new_map: serde_json::Map<String, serde_json::Value> = map
                .into_iter()
                .map(|(k, v)| (camel_to_snake_case(&k), convert_json_for_go(v)))
                .collect();
            serde_json::Value::Object(new_map)
        }
        serde_json::Value::Array(arr) => {
            // Check if this is a byte array (array of integers 0-255).
            // If so, encode as base64 string for Go json.Unmarshal compatibility.
            if is_byte_array(&arr) {
                let bytes: Vec<u8> = arr
                    .iter()
                    .filter_map(|v| v.as_u64().and_then(|n| if n <= 255 { Some(n as u8) } else { None }))
                    .collect();
                // Encode bytes as base64 for Go json.Unmarshal (Go expects []byte as base64 strings)
                let encoded = base64_encode(&bytes);
                serde_json::Value::String(encoded)
            } else {
                serde_json::Value::Array(arr.into_iter().map(convert_json_for_go).collect())
            }
        }
        serde_json::Value::String(s) => {
            // Convert PascalCase enum values to snake_case.
            // Only convert values that look like PascalCase (start with uppercase, no spaces).
            serde_json::Value::String(pascal_to_snake_case(&s))
        }
        other => other,
    }
}

/// Check if an array looks like a byte array (all elements are integers 0-255).
fn is_byte_array(arr: &[serde_json::Value]) -> bool {
    if arr.is_empty() {
        return false;
    }
    arr.iter().all(|v| {
        if let serde_json::Value::Number(n) = v {
            n.is_u64() && n.as_u64().is_some_and(|u| u <= 255)
        } else {
            false
        }
    })
}

/// Encode bytes as base64 string (standard alphabet without padding in this output,
/// though Go's json.Unmarshal handles both).
fn base64_encode(bytes: &[u8]) -> String {
    const TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    let mut result = String::new();
    let mut i = 0;

    while i + 2 < bytes.len() {
        let b1 = bytes[i];
        let b2 = bytes[i + 1];
        let b3 = bytes[i + 2];

        result.push(TABLE[(b1 >> 2) as usize] as char);
        result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
        result.push(TABLE[(((b2 & 0x0f) << 2) | (b3 >> 6)) as usize] as char);
        result.push(TABLE[(b3 & 0x3f) as usize] as char);

        i += 3;
    }

    // Handle remaining bytes
    if i < bytes.len() {
        let b1 = bytes[i];
        result.push(TABLE[(b1 >> 2) as usize] as char);

        if i + 1 < bytes.len() {
            let b2 = bytes[i + 1];
            result.push(TABLE[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
            result.push(TABLE[((b2 & 0x0f) << 2) as usize] as char);
            result.push('=');
        } else {
            result.push(TABLE[((b1 & 0x03) << 4) as usize] as char);
            result.push_str("==");
        }
    }

    result
}

/// Convert a camelCase or PascalCase string to snake_case.
fn camel_to_snake_case(s: &str) -> String {
    let mut result = String::new();
    let mut prev_upper = false;
    for (i, c) in s.char_indices() {
        if c.is_uppercase() {
            if i > 0 && !prev_upper {
                result.push('_');
            }
            result.push(c.to_lowercase().next().unwrap_or(c));
            prev_upper = true;
        } else {
            if prev_upper && i > 1 {
                // Handles sequences like "URLPath" → "url_path": insert _ before last uppercase
                // when transitioning from a run of uppercase back to lowercase.
                // This is tricky — use simple approach: detect Aa pattern.
            }
            result.push(c);
            prev_upper = false;
        }
    }
    result
}

/// Convert a PascalCase string to snake_case (for enum values).
///
/// Only converts if the string looks like PascalCase (starts uppercase, no spaces/underscores).
/// Values that are already lowercase/snake_case are returned unchanged.
fn pascal_to_snake_case(s: &str) -> String {
    // Skip conversion for strings that already contain underscores, spaces, or start lowercase.
    let first_char = s.chars().next();
    if first_char.is_none() || !first_char.unwrap().is_uppercase() || s.contains('_') || s.contains(' ') {
        return s.to_string();
    }
    camel_to_snake_case(s)
}

/// Map an `ArgMapping.element_type` to a Go slice type. Used for `json_object` args
/// whose fixture value is a JSON array. The element type is wrapped in `[]…` so an
/// element of `String` becomes `[]string` and `Vec<String>` becomes `[][]string`.
fn element_type_to_go_slice(element_type: Option<&str>, import_alias: &str) -> String {
    let elem = element_type.unwrap_or("String").trim();
    let go_elem = rust_type_to_go(elem, import_alias);
    format!("[]{go_elem}")
}

/// Map a small subset of Rust scalar / `Vec<T>` types to their Go equivalents.
/// For unknown types, qualify with the import alias (e.g., "sample_core.BatchBytesItem").
fn rust_type_to_go(rust: &str, import_alias: &str) -> String {
    let trimmed = rust.trim();
    if let Some(inner) = trimmed.strip_prefix("Vec<").and_then(|s| s.strip_suffix('>')) {
        return format!("[]{}", rust_type_to_go(inner, import_alias));
    }
    match trimmed {
        "String" | "&str" | "str" => "string".to_string(),
        "bool" => "bool".to_string(),
        "f32" => "float32".to_string(),
        "f64" => "float64".to_string(),
        "i8" => "int8".to_string(),
        "i16" => "int16".to_string(),
        "i32" => "int32".to_string(),
        "i64" | "isize" => "int64".to_string(),
        "u8" => "uint8".to_string(),
        "u16" => "uint16".to_string(),
        "u32" => "uint32".to_string(),
        "u64" | "usize" => "uint64".to_string(),
        _ => format!("{import_alias}.{trimmed}"),
    }
}

fn json_to_go(value: &serde_json::Value) -> String {
    match value {
        serde_json::Value::String(s) => go_string_literal(s),
        serde_json::Value::Bool(b) => b.to_string(),
        serde_json::Value::Number(n) => n.to_string(),
        serde_json::Value::Null => "nil".to_string(),
        // For complex types, serialize to JSON string and pass as literal.
        other => go_string_literal(&other.to_string()),
    }
}

// ---------------------------------------------------------------------------
// Visitor generation
// ---------------------------------------------------------------------------

/// Derive a unique, exported Go struct name for a visitor from a fixture ID.
///
/// E.g. `visitor_continue_default` → `visitorContinueDefault` (unexported, avoids
/// polluting the exported API of the test package while still being package-level).
fn visitor_struct_name(fixture_id: &str) -> String {
    use heck::ToUpperCamelCase;
    // Use UpperCamelCase so Go treats it as exported — required for method sets.
    format!("testVisitor{}", fixture_id.to_upper_camel_case())
}

/// Emit a package-level Go struct declaration and all its visitor methods.
///
/// The struct embeds `BaseVisitor` to satisfy all interface methods not
/// explicitly overridden by the fixture callbacks.
fn emit_go_visitor_struct(
    out: &mut String,
    struct_name: &str,
    visitor_spec: &crate::e2e::fixture::VisitorSpec,
    import_alias: &str,
) {
    let _ = writeln!(out, "type {struct_name} struct{{");
    let _ = writeln!(out, "\t{import_alias}.BaseVisitor");
    let _ = writeln!(out, "}}");
    for (method_name, action) in &visitor_spec.callbacks {
        emit_go_visitor_method(out, struct_name, method_name, action, import_alias);
    }
}

/// Emit a Go visitor method for a callback action on the named struct.
fn emit_go_visitor_method(
    out: &mut String,
    struct_name: &str,
    method_name: &str,
    action: &CallbackAction,
    import_alias: &str,
) {
    let camel_method = method_to_camel(method_name);
    // Parameter signatures must exactly match the samplemarkdown.Visitor interface.
    // Optional fields use pointer types (*string, *uint32, etc.) to indicate nil-ability.
    let params = match method_name {
        "visit_link" => format!("_ {import_alias}.NodeContext, href string, text string, title *string"),
        "visit_image" => format!("_ {import_alias}.NodeContext, src string, alt string, title *string"),
        "visit_heading" => format!("_ {import_alias}.NodeContext, level uint32, text string, id *string"),
        "visit_code_block" => format!("_ {import_alias}.NodeContext, lang *string, code string"),
        "visit_code_inline"
        | "visit_strong"
        | "visit_emphasis"
        | "visit_strikethrough"
        | "visit_underline"
        | "visit_subscript"
        | "visit_superscript"
        | "visit_mark"
        | "visit_button"
        | "visit_summary"
        | "visit_figcaption"
        | "visit_definition_term"
        | "visit_definition_description" => format!("_ {import_alias}.NodeContext, text string"),
        "visit_text" => format!("_ {import_alias}.NodeContext, text string"),
        "visit_list_item" => {
            format!("_ {import_alias}.NodeContext, ordered bool, marker string, text string")
        }
        "visit_blockquote" => format!("_ {import_alias}.NodeContext, content string, depth uint"),
        "visit_table_row" => format!("_ {import_alias}.NodeContext, cells []string, isHeader bool"),
        "visit_custom_element" => format!("_ {import_alias}.NodeContext, tagName string, html string"),
        "visit_form" => format!("_ {import_alias}.NodeContext, action *string, method *string"),
        "visit_input" => {
            format!("_ {import_alias}.NodeContext, inputType string, name *string, value *string")
        }
        "visit_audio" | "visit_video" | "visit_iframe" => {
            format!("_ {import_alias}.NodeContext, src *string")
        }
        "visit_details" => format!("_ {import_alias}.NodeContext, open bool"),
        "visit_element_end" | "visit_table_end" | "visit_definition_list_end" | "visit_figure_end" => {
            format!("_ {import_alias}.NodeContext, output string")
        }
        "visit_list_start" => format!("_ {import_alias}.NodeContext, ordered bool"),
        "visit_list_end" => format!("_ {import_alias}.NodeContext, ordered bool, output string"),
        _ => format!("_ {import_alias}.NodeContext"),
    };

    let _ = writeln!(
        out,
        "func (v *{struct_name}) {camel_method}({params}) {import_alias}.VisitResult {{"
    );
    match action {
        CallbackAction::Skip => {
            let _ = writeln!(out, "\treturn {import_alias}.VisitResultSkip()");
        }
        CallbackAction::Continue => {
            let _ = writeln!(out, "\treturn {import_alias}.VisitResultContinue()");
        }
        CallbackAction::PreserveHtml => {
            let _ = writeln!(out, "\treturn {import_alias}.VisitResultPreserveHTML()");
        }
        CallbackAction::Custom { output } => {
            let escaped = go_string_literal(output);
            let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped})");
        }
        CallbackAction::CustomTemplate { template, .. } => {
            // Convert {var} placeholders to %s format verbs and collect arg names.
            // E.g. `QUOTE: "{text}"` → fmt.Sprintf("QUOTE: \"%s\"", text)
            //
            // For pointer-typed params (e.g. `src *string`), dereference with `*`
            // — the test fixtures always supply a non-nil value for methods that
            // fire a custom template, so this is safe in practice.
            let ptr_params = go_visitor_ptr_params(method_name);
            let (fmt_str, fmt_args) = template_to_sprintf(template, &ptr_params);
            let escaped_fmt = go_string_literal(&fmt_str);
            if fmt_args.is_empty() {
                let _ = writeln!(out, "\treturn {import_alias}.VisitResultCustom({escaped_fmt})");
            } else {
                let args_str = fmt_args.join(", ");
                let _ = writeln!(
                    out,
                    "\treturn {import_alias}.VisitResultCustom(fmt.Sprintf({escaped_fmt}, {args_str}))"
                );
            }
        }
    }
    let _ = writeln!(out, "}}");
}

/// Return the set of camelCase parameter names that are pointer types (`*string`) for a
/// given visitor method name.  Used to dereference pointers in template `fmt.Sprintf` calls.
fn go_visitor_ptr_params(method_name: &str) -> std::collections::HashSet<&'static str> {
    match method_name {
        "visit_link" => ["title"].into(),
        "visit_image" => ["title"].into(),
        "visit_heading" => ["id"].into(),
        "visit_code_block" => ["lang"].into(),
        "visit_form" => ["action", "method"].into(),
        "visit_input" => ["name", "value"].into(),
        "visit_audio" | "visit_video" | "visit_iframe" => ["src"].into(),
        _ => std::collections::HashSet::new(),
    }
}

/// Convert a `{var}` template string into a `fmt.Sprintf` format string and argument list.
///
/// For example, `QUOTE: "{text}"` becomes `("QUOTE: \"%s\"", vec!["text"])`.
///
/// Placeholder names in the template use snake_case (matching fixture field names); they
/// are converted to Go camelCase parameter names using `go_param_name` so they match the
/// generated visitor method signatures (e.g. `{input_type}` → `inputType`).
///
/// `ptr_params` — camelCase names of parameters that are `*string`; these are
/// dereferenced with `*` when used as `fmt.Sprintf` arguments.  The fixtures that
/// use `custom_template` on pointer-param methods always supply a non-nil value.
fn template_to_sprintf(template: &str, ptr_params: &std::collections::HashSet<&str>) -> (String, Vec<String>) {
    let mut fmt_str = String::new();
    let mut args: Vec<String> = Vec::new();
    let mut chars = template.chars().peekable();
    while let Some(c) = chars.next() {
        if c == '{' {
            // Collect placeholder name until '}'.
            let mut name = String::new();
            for inner in chars.by_ref() {
                if inner == '}' {
                    break;
                }
                name.push(inner);
            }
            fmt_str.push_str("%s");
            // Convert snake_case placeholder to Go camelCase to match method param names.
            let go_name = go_param_name(&name);
            // Dereference pointer params so fmt.Sprintf receives a string value.
            let arg_expr = if ptr_params.contains(go_name.as_str()) {
                format!("*{go_name}")
            } else {
                go_name
            };
            args.push(arg_expr);
        } else {
            fmt_str.push(c);
        }
    }
    (fmt_str, args)
}

/// Convert snake_case method names to Go camelCase.
fn method_to_camel(snake: &str) -> String {
    use heck::ToUpperCamelCase;
    snake.to_upper_camel_case()
}

/// Emit a Go test backend stub.
///
/// Go is interface-based: define a package-level struct type + methods that satisfy
/// the trait's Go interface. The Plugin super-trait `Name()` method returns the fixture id.
///
/// Check if a type maps to json.RawMessage (only TypeRef::Json).
/// Named types now use their proper Go types, so we only need json import for
/// the Json type itself.
fn uses_json_type(ty: &crate::core::ir::TypeRef) -> bool {
    use crate::core::ir::TypeRef;
    match ty {
        TypeRef::Json => true,
        TypeRef::Optional(inner) => uses_json_type(inner),
        TypeRef::Vec(inner) => uses_json_type(inner),
        TypeRef::Map(k, v) => uses_json_type(k) || uses_json_type(v),
        _ => false,
    }
}

/// Because Go does not allow method declarations inside function bodies, the `setup_block`
/// contains package-level type and method declarations. The `arg_expr` is the struct
/// literal `testStub_<id>{}` that callers pass to `Register<Trait>`.
///
/// Call [`emit_test_backend_with_context`] from e2e test-file renderers that have the
/// `excluded_types` set (binding-excluded types → `json.RawMessage`) and `import_alias`
/// (qualifies named types for an external test package).
pub fn emit_test_backend(
    trait_bridge: &crate::core::config::TraitBridgeConfig,
    methods: &[&crate::core::ir::MethodDef],
    fixture: &crate::e2e::fixture::Fixture,
) -> super::TestBackendEmission {
    emit_test_backend_with_context(
        trait_bridge,
        methods,
        fixture,
        &std::collections::HashSet::new(),
        "",
        &std::collections::HashSet::new(),
    )
}

/// Like [`emit_test_backend`] but with type-qualification context.
///
/// `excluded_types` — names of binding-excluded types (e.g. `InternalDocument`) that should
/// be substituted with `json.RawMessage` in method signatures.  These types exist in the Rust
/// IR but are never emitted as Go structs; the trait-bridge interface serialises them to JSON.
///
/// `import_alias` — the import alias used for the binding package in the generated test file
/// (e.g. `"myproject"`).  When non-empty, `Named` types are qualified as `{alias}.{GoName}`
/// so the stub compiles from `package e2e_test` which imports the binding under that alias.
///
/// `enum_names` — set of type names that are enums in the IR (used to determine zero-values
/// for stub returns; enums map to string types in Go, so their zero-value is `""` not `nil`).
pub fn emit_test_backend_with_context(
    trait_bridge: &crate::core::config::TraitBridgeConfig,
    methods: &[&crate::core::ir::MethodDef],
    fixture: &crate::e2e::fixture::Fixture,
    excluded_types: &std::collections::HashSet<&str>,
    import_alias: &str,
    enum_names: &std::collections::HashSet<&str>,
) -> super::TestBackendEmission {
    use crate::codegen::defaults::language_defaults;
    use crate::e2e::escape::sanitize_ident;

    let defaults = language_defaults("go");
    let safe_id = sanitize_ident(&fixture.id);
    let struct_name = format!("testStub_{safe_id}");

    let mut setup = String::new();

    // Package-level struct declaration.
    let _ = writeln!(setup, "type {struct_name} struct{{}}");
    setup.push('\n');

    // Super-trait methods: filter by trait_source matching the configured super_trait.
    // Driven from IR — no method names are hardcoded. The `name` method returns the
    // fixture id; all other super-trait methods use the standard per-method logic.
    if let Some(super_trait) = trait_bridge.super_trait.as_deref() {
        let super_methods: Vec<_> = methods
            .iter()
            .filter(|m| m.trait_source.as_deref() == Some(super_trait))
            .collect();
        for method in &super_methods {
            let go_method = method_to_camel(&method.name);
            if method.name == "name" {
                let _ = writeln!(
                    setup,
                    "func ({struct_name}) {go_method}() string {{ return \"{safe_id}\" }}"
                );
            } else {
                emit_go_stub_method_body(
                    &mut setup,
                    &struct_name,
                    &go_method,
                    method,
                    &*defaults,
                    excluded_types,
                    import_alias,
                    enum_names,
                );
            }
        }
        if !super_methods.is_empty() {
            setup.push('\n');
        }
    }

    // Emit method stubs for all required methods.
    // Go interfaces require ALL abstract methods to be implemented, even if they have
    // default implementations in the Rust trait.
    // Skip: (1) super-trait methods already emitted above, (2) methods using excluded types
    // (which are not exported in the binding), and (3) name() when hardcoded by super_trait.
    for method in methods.iter() {
        // Skip super-trait methods already emitted above.
        if trait_bridge
            .super_trait
            .as_deref()
            .is_some_and(|st| method.trait_source.as_deref() == Some(st))
        {
            continue;
        }
        // Skip methods whose return type or parameters are excluded types
        // in ways that exclude them from the binding interface.
        // For return types: skip if directly excluded OR Optional<excluded>.
        // Don't skip Result<excluded> because binding generation converts those.
        // Skip methods whose return type is excluded in a way that excludes them
        // from the binding interface (directly excluded or Optional<excluded>).
        // Don't skip Result<excluded> because binding generation converts those.
        // Parameters with excluded types are OK - binding generation converts those.
        if should_skip_method_with_type(&method.return_type, excluded_types, method.error_type.is_some()) {
            continue;
        }
        let go_method = method_to_camel(&method.name);
        emit_go_stub_method_body(
            &mut setup,
            &struct_name,
            &go_method,
            method,
            &*defaults,
            excluded_types,
            import_alias,
            enum_names,
        );
    }

    // Determine if encoding/json is needed by checking if any method uses json.RawMessage.
    // This includes both TypeRef::Json variants and excluded Named types (substituted to json.RawMessage).
    let uses_json_with_context = |ty: &crate::core::ir::TypeRef| -> bool {
        uses_json_type(ty) || {
            use crate::core::ir::TypeRef;
            matches!(ty, TypeRef::Named(n) if excluded_types.contains(n.as_str()))
        }
    };
    let needs_json = methods
        .iter()
        .any(|m| uses_json_with_context(&m.return_type) || m.params.iter().any(|p| uses_json_with_context(&p.ty)));

    let mut type_imports = Vec::new();
    if needs_json {
        type_imports.push("encoding/json".to_string());
    }

    super::TestBackendEmission {
        setup_block: setup,
        arg_expr: format!("{struct_name}{{}}"),
        type_imports,
        teardown_block: String::new(),
    }
}

/// Returns the Go zero-value expression for a stub method return statement.
///
/// Uses go_zero_value from the type_map to ensure consistency with actual
/// Go binding signatures. Named types check enum_names to determine if they're
/// enums (zero-value `""`) or structs (zero-value `nil`). Primitives produce
/// their standard zero values (0, false, ""), and Vec produces a nil slice.
///
/// Use `go_stub_default_with_context` with the same excluded/import-alias substitution as
/// `stub_go_type_with_context` so the emitted zero-value matches the rendered return
/// type. Excluded types become `json.RawMessage(nil)`, struct types qualified via
/// `import_alias` use `alias.Type{}` (Go's struct zero-value), enums stay as `""`,
/// and primitives/maps/slices/optionals fall back to `go_zero_value`.
fn go_stub_default_with_context(
    ty: &crate::core::ir::TypeRef,
    enum_names: &std::collections::HashSet<&str>,
    excluded_types: &std::collections::HashSet<&str>,
    import_alias: &str,
) -> String {
    use crate::backends::go::type_map::go_zero_value;
    use crate::core::ir::TypeRef;

    match ty {
        TypeRef::Named(name) if excluded_types.contains(name.as_str()) => "nil".to_string(),
        TypeRef::Named(name) if enum_names.contains(name.as_str()) => "\"\"".to_string(),
        TypeRef::Named(name) if !import_alias.is_empty() => {
            let go_name = crate::codegen::naming::go_type_name(name);
            format!("{import_alias}.{go_name}{{}}")
        }
        TypeRef::Named(name) => {
            let go_name = crate::codegen::naming::go_type_name(name);
            format!("{go_name}{{}}")
        }
        _ => go_zero_value(ty),
    }
}

/// Check if a type (or its top-level structure) is an excluded type in a way that would
/// exclude the entire method from the binding interface.
///
/// For return types with error handling (has_error_type=true):
/// - Excludes if Optional<ExcludedType> (e.g., Option<SyncExtractor>)
/// - Does NOT exclude directly excluded types (Result<ExcludedType> is handled by binding)
///
/// For return types without error handling (has_error_type=false) or parameter types:
/// - Excludes if the type IS an excluded type
/// - Excludes if Optional<ExcludedType>
fn should_skip_method_with_type(
    ty: &crate::core::ir::TypeRef,
    excluded_types: &std::collections::HashSet<&str>,
    is_result_return: bool,
) -> bool {
    use crate::core::ir::TypeRef;
    match ty {
        // Optional<ExcludedType> is always skipped (e.g., Option<SyncExtractor>)
        TypeRef::Optional(inner) => {
            matches!(inner.as_ref(), TypeRef::Named(name) if excluded_types.contains(name.as_str()))
        }
        // Directly excluded types: skip unless it's a Result return (binding handles conversion)
        TypeRef::Named(name) if excluded_types.contains(name.as_str()) => !is_result_return,
        // Other types are not skipped
        _ => false,
    }
}

/// Maps a type reference to its Go representation in stub method signatures, with context.
///
/// When `excluded_types` is non-empty, any `TypeRef::Named` whose name appears in the set
/// is substituted with `json.RawMessage` (matching the actual trait-bridge interface which
/// serialises excluded/internal types to JSON). When `import_alias` is non-empty, remaining
/// `TypeRef::Named` types are qualified as `{import_alias}.{GoName}` so the stub compiles
/// from an external test package (e.g. `package e2e_test`) that imports the binding package
/// under an alias.
fn stub_go_type_with_context(
    ty: &crate::core::ir::TypeRef,
    excluded_types: &std::collections::HashSet<&str>,
    import_alias: &str,
) -> String {
    use crate::backends::go::type_map::go_type;
    use crate::core::ir::TypeRef;
    match ty {
        TypeRef::Named(name) if !excluded_types.is_empty() && excluded_types.contains(name.as_str()) => {
            "json.RawMessage".to_string()
        }
        TypeRef::Named(name) if !import_alias.is_empty() => {
            let go_name = crate::codegen::naming::go_type_name(name);
            format!("{import_alias}.{go_name}")
        }
        TypeRef::Optional(inner) => {
            let inner_str = stub_go_type_with_context(inner, excluded_types, import_alias);
            // Excluded types become json.RawMessage which is a slice — don't add pointer
            if inner_str == "json.RawMessage" {
                inner_str
            } else {
                format!("*{inner_str}")
            }
        }
        TypeRef::Vec(inner) => {
            let inner_str = stub_go_type_with_context(inner, excluded_types, import_alias);
            format!("[]{inner_str}")
        }
        TypeRef::Map(k, v) => {
            let k_str = stub_go_type_with_context(k, excluded_types, import_alias);
            let v_str = stub_go_type_with_context(v, excluded_types, import_alias);
            format!("map[{k_str}]{v_str}")
        }
        _ => go_type(ty).into_owned(),
    }
}

/// Emit a single Go stub method receiver function into `out`.
///
/// Used by both the main method loop and the super-trait method section of
/// `emit_test_backend` so both paths share the same formatting logic.
/// `go_method` is the already-PascalCased method name (caller's responsibility).
///
/// `excluded_types` — names of binding-excluded types substituted with `json.RawMessage`.
/// `import_alias` — binding package import alias; qualifies Named types for external packages.
/// `enum_names` — set of type names that are enums (map to string types, zero-value is `""`).
#[allow(clippy::too_many_arguments)]
fn emit_go_stub_method_body(
    out: &mut String,
    struct_name: &str,
    go_method: &str,
    method: &crate::core::ir::MethodDef,
    defaults: &dyn crate::codegen::defaults::LanguageDefaults,
    excluded_types: &std::collections::HashSet<&str>,
    import_alias: &str,
    enum_names: &std::collections::HashSet<&str>,
) {
    use crate::core::ir::TypeRef;

    // Build parameter list: `name GoType` pairs, substituting opaque Named types
    // with json.RawMessage (matches the generated Go interface signatures).
    let params: Vec<String> = method
        .params
        .iter()
        .map(|p| {
            let go_param = go_param_name(&p.name);
            let type_str = stub_go_type_with_context(&p.ty, excluded_types, import_alias);
            format!("{go_param} {type_str}")
        })
        .collect();
    let param_str = params.join(", ");

    let ret_ty = stub_go_type_with_context(&method.return_type, excluded_types, import_alias);

    // Build return type.
    let return_type_str = if method.error_type.is_some() {
        match &method.return_type {
            TypeRef::Unit => "error".to_string(),
            _ => format!("({ret_ty}, error)"),
        }
    } else {
        ret_ty.clone()
    };

    // Build return expression.
    let return_expr = if method.error_type.is_some() {
        match &method.return_type {
            TypeRef::Unit => "return nil".to_string(),
            _ => {
                let default_val =
                    go_stub_default_with_context(&method.return_type, enum_names, excluded_types, import_alias);
                format!("return {default_val}, nil")
            }
        }
    } else if matches!(method.return_type, TypeRef::Unit) {
        String::new()
    } else {
        let default_val = go_stub_default_with_context(&method.return_type, enum_names, excluded_types, import_alias);
        format!("return {default_val}")
    };

    // Drop the `defaults` parameter — the stub uses go_stub_default directly.
    let _ = defaults; // suppress unused-variable warning

    let _ = writeln!(
        out,
        "func ({struct_name}) {go_method}({param_str}) {return_type_str} {{ {return_expr} }}"
    );
}

#[cfg(test)]
mod trait_bridge_tests {
    use super::{emit_test_backend, emit_test_backend_with_context};
    use crate::core::config::TraitBridgeConfig;
    use crate::core::ir::{MethodDef, ParamDef, TypeRef};
    use crate::e2e::fixture::Fixture;

    fn make_fixture(id: &str) -> Fixture {
        Fixture {
            id: id.to_string(),
            category: None,
            description: "test".to_string(),
            tags: vec![],
            skip: None,
            env: None,
            call: None,
            input: serde_json::Value::Null,
            mock_response: Some(crate::e2e::fixture::MockResponse {
                status: 200,
                body: Some(serde_json::Value::Null),
                stream_chunks: None,
                headers: std::collections::BTreeMap::new(),
            }),
            source: String::new(),
            http: None,
            assertions: vec![],
            visitor: None,
            args: vec![],
        }
    }

    fn make_param(name: &str, ty: TypeRef) -> ParamDef {
        ParamDef {
            name: name.to_string(),
            ty,
            optional: false,
            default: None,
            sanitized: false,
            typed_default: None,
            is_ref: false,
            is_mut: false,
            newtype_wrapper: None,
            original_type: None,
            map_is_ahash: false,
            map_key_is_cow: false,
        }
    }

    fn make_method(name: &str, params: Vec<(&str, TypeRef)>, ret: TypeRef, is_async: bool) -> MethodDef {
        MethodDef {
            name: name.to_string(),
            params: params.into_iter().map(|(n, ty)| make_param(n, ty)).collect(),
            return_type: ret,
            is_async,
            is_static: false,
            error_type: Some("Error".to_string()),
            doc: String::new(),
            receiver: Some(crate::core::ir::ReceiverKind::Ref),
            sanitized: false,
            trait_source: None,
            returns_ref: false,
            returns_cow: false,
            return_newtype_wrapper: None,
            has_default_impl: false,
            binding_excluded: false,
            binding_exclusion_reason: None,
        }
    }

    /// Genericity test: a synthetic TestTrait with one sync method and Plugin super-trait
    /// must not reference any sample_core-domain names in setup_block or arg_expr.
    #[test]
    fn test_backend_emission_is_generic() {
        let trait_bridge = TraitBridgeConfig {
            trait_name: "TestTrait".to_string(),
            super_trait: Some("SomeSuperTrait".to_string()),
            register_fn: Some("register_test_trait".to_string()),
            ..TraitBridgeConfig::default()
        };

        let do_thing = make_method(
            "do_thing",
            vec![("x", TypeRef::Primitive(crate::core::ir::PrimitiveType::I32))],
            TypeRef::String,
            false,
        );

        let fixture = make_fixture("my_test_fixture");
        let methods = vec![&do_thing];
        let emission = emit_test_backend(&trait_bridge, &methods, &fixture);

        // setup_block must not reference any sample_core-domain trait or method names.
        assert!(
            !emission.setup_block.contains("OcrBackend"),
            "setup_block must not hardcode domain trait names, got:\n{}",
            emission.setup_block
        );
        assert!(
            !emission.setup_block.contains("ProcessImage"),
            "setup_block must not hardcode domain method names, got:\n{}",
            emission.setup_block
        );
        // Must emit the method name from MethodDef (Go PascalCase).
        assert!(
            emission.setup_block.contains("DoThing"),
            "setup_block must contain Go PascalCase method 'DoThing', got:\n{}",
            emission.setup_block
        );
        // Must emit struct declaration.
        assert!(
            emission.setup_block.contains("type testStub_my_test_fixture struct"),
            "setup_block must contain struct declaration, got:\n{}",
            emission.setup_block
        );
        // With trait_source: None, super-trait methods are NOT emitted — no hardcoded lifecycle names.
        assert!(
            !emission.setup_block.contains("Initialize"),
            "setup_block must not contain hardcoded 'Initialize', got:\n{}",
            emission.setup_block
        );
        assert!(
            !emission.setup_block.contains("Shutdown"),
            "setup_block must not contain hardcoded 'Shutdown', got:\n{}",
            emission.setup_block
        );
        // arg_expr is the struct literal.
        assert!(
            emission.arg_expr.contains("testStub_my_test_fixture"),
            "arg_expr must reference struct name, got: {}",
            emission.arg_expr
        );
        assert!(
            emission.arg_expr.ends_with("{}"),
            "arg_expr must be a struct literal, got: {}",
            emission.arg_expr
        );
    }

    /// Verify that super-trait methods with `trait_source` set are driven from
    /// the IR slice rather than a hardcoded list of method names.
    ///
    /// A synthetic `Plugin` super-trait with methods `name`, `version`, `init`
    /// (note: `init`, NOT `Initialize`) is passed via `trait_source`. The emitter
    /// must emit `Init` (PascalCase of `init`), NOT the previously-hardcoded
    /// `Initialize` string, proving the method names come from IR.
    #[test]
    fn test_go_super_trait_methods_driven_from_ir_not_hardcoded() {
        let make_super_method = |name: &str, ret: TypeRef| -> MethodDef {
            MethodDef {
                name: name.to_string(),
                params: vec![],
                return_type: ret,
                is_async: false,
                is_static: false,
                error_type: None,
                doc: String::new(),
                receiver: Some(crate::core::ir::ReceiverKind::Ref),
                sanitized: false,
                // trait_source matches the super_trait configured on the bridge.
                trait_source: Some("Plugin".to_string()),
                returns_ref: false,
                returns_cow: false,
                return_newtype_wrapper: None,
                has_default_impl: false,
                binding_excluded: false,
                binding_exclusion_reason: None,
            }
        };

        let name_method = make_super_method("name", TypeRef::String);
        let version_method = make_super_method("version", TypeRef::String);
        let init_method = make_super_method("init", TypeRef::Unit);

        let trait_bridge = TraitBridgeConfig {
            trait_name: "TestPlugin".to_string(),
            super_trait: Some("Plugin".to_string()),
            register_fn: Some("register_test_plugin".to_string()),
            ..TraitBridgeConfig::default()
        };

        let fixture = make_fixture("my_plugin_fixture");
        let methods = vec![&name_method, &version_method, &init_method];
        let emission = emit_test_backend(&trait_bridge, &methods, &fixture);

        // Must emit `Init` (PascalCase of "init"), not the old hardcoded "Initialize".
        assert!(
            emission.setup_block.contains("Init("),
            "setup_block must contain 'Init(' (from IR), got:\n{}",
            emission.setup_block
        );
        assert!(
            !emission.setup_block.contains("Initialize"),
            "setup_block must NOT contain hardcoded 'Initialize', got:\n{}",
            emission.setup_block
        );
        // `Version` comes from IR method name "version".
        assert!(
            emission.setup_block.contains("Version("),
            "setup_block must contain 'Version(' (from IR), got:\n{}",
            emission.setup_block
        );
        // Must not contain old hardcoded `Shutdown`.
        assert!(
            !emission.setup_block.contains("Shutdown"),
            "setup_block must NOT contain hardcoded 'Shutdown', got:\n{}",
            emission.setup_block
        );
        // `Name()` is emitted and returns the fixture id.
        assert!(
            emission.setup_block.contains("Name()"),
            "setup_block must contain Name() from IR name method, got:\n{}",
            emission.setup_block
        );
    }

    /// Verify that Named types (like OcrBackendType) use their proper Go type names
    /// in stubs, matching the actual trait-bridge interface signatures.
    #[test]
    fn test_go_stub_named_types_use_proper_go_names() {
        let backend_type_method = make_method(
            "backend_type",
            vec![],
            TypeRef::Named("OcrBackendType".to_string()),
            false,
        );

        let trait_bridge = TraitBridgeConfig {
            trait_name: "OcrBackend".to_string(),
            super_trait: Some("Plugin".to_string()),
            register_fn: Some("register_ocr_backend".to_string()),
            ..TraitBridgeConfig::default()
        };

        let fixture = make_fixture("backend_type_test");
        let methods = vec![&backend_type_method];
        let emission = emit_test_backend(&trait_bridge, &methods, &fixture);

        // The method signature should use OcrBackendType (proper Go name), not json.RawMessage.
        assert!(
            emission.setup_block.contains("BackendType()") && emission.setup_block.contains("OcrBackendType"),
            "setup_block must use OcrBackendType in BackendType() method signature, got:\n{}",
            emission.setup_block
        );

        // Return value must match go_zero_value for OcrBackendType (which is nil for Named types).
        assert!(
            !emission.setup_block.contains("json.RawMessage(nil)"),
            "setup_block must not use json.RawMessage for OcrBackendType, got:\n{}",
            emission.setup_block
        );
    }

    /// Verify that methods with binding-excluded types are handled correctly:
    /// - Methods returning directly excluded types are skipped
    /// - Methods returning Optional<ExcludedType> are skipped
    /// - Methods with wrapped returns (Result<ExcludedType>, Vec<ExcludedType>) are emitted
    ///   (binding generation converts these appropriately)
    /// - Normal methods are emitted with proper type qualification
    #[test]
    fn test_go_stub_skips_excluded_return_types() {
        // Method 1: returns InternalDocument directly → should be SKIPPED
        let excluded_return_method = make_method(
            "get_internal_doc",
            vec![],
            TypeRef::Named("InternalDocument".to_string()),
            false,
        );

        // Method 2: returns Result<InternalDocument> → should be EMITTED
        // (Result wrapping is handled by binding generation)
        let result_return_method = make_method(
            "extract_bytes",
            vec![("content", TypeRef::Bytes)],
            TypeRef::Named("InternalDocument".to_string()), // In IR; becomes json.RawMessage in binding
            true,                                           // has_error_type = true
        );

        // Method 3: normal method with non-excluded types → should be EMITTED
        let normal_method = make_method(
            "get_config",
            vec![],
            TypeRef::Named("ExtractionConfig".to_string()),
            false,
        );

        let trait_bridge = TraitBridgeConfig {
            trait_name: "DocumentExtractor".to_string(),
            super_trait: None,
            register_fn: Some("register_document_extractor".to_string()),
            ..TraitBridgeConfig::default()
        };

        let fixture = make_fixture("extractor_test");
        let methods = vec![&excluded_return_method, &result_return_method, &normal_method];

        let mut excluded = std::collections::HashSet::new();
        excluded.insert("InternalDocument");

        let enum_names = std::collections::HashSet::new();
        let emission =
            emit_test_backend_with_context(&trait_bridge, &methods, &fixture, &excluded, "myproject", &enum_names);

        // Method returning directly excluded type must NOT appear in stub.
        assert!(
            !emission.setup_block.contains("get_internal_doc"),
            "method with directly excluded return type must be skipped, got:\n{}",
            emission.setup_block
        );

        // Method with Result-wrapped excluded type should appear (binding generation handles conversion).
        assert!(
            emission.setup_block.contains("ExtractBytes"),
            "method with Result<ExcludedType> should be emitted (binding handles conversion), got:\n{}",
            emission.setup_block
        );

        // Normal method with non-excluded types must appear (in PascalCase).
        assert!(
            emission.setup_block.contains("GetConfig"),
            "normal method must be emitted, got:\n{}",
            emission.setup_block
        );

        // Normal method's return type must be qualified with import alias.
        assert!(
            emission.setup_block.contains("myproject.ExtractionConfig"),
            "named type ExtractionConfig must be qualified as myproject.ExtractionConfig, got:\n{}",
            emission.setup_block
        );
    }

    /// Verify that methods returning Optional<ExcludedType> are skipped
    /// (e.g., as_sync_extractor() -> Option<&dyn SyncExtractor>).
    #[test]
    fn test_go_stub_skips_optional_excluded_return_types() {
        // Method returning Option<SyncExtractor> → should be SKIPPED
        // (SyncExtractor is not exported in the binding)
        let optional_excluded_method = make_method(
            "as_sync_extractor",
            vec![],
            TypeRef::Optional(Box::new(TypeRef::Named("SyncExtractor".to_string()))),
            false,
        );

        let trait_bridge = TraitBridgeConfig {
            trait_name: "DocumentExtractor".to_string(),
            super_trait: None,
            register_fn: Some("register_document_extractor".to_string()),
            ..TraitBridgeConfig::default()
        };

        let fixture = make_fixture("extractor_test");
        let methods = vec![&optional_excluded_method];

        let mut excluded = std::collections::HashSet::new();
        excluded.insert("SyncExtractor");

        let enum_names = std::collections::HashSet::new();
        let emission =
            emit_test_backend_with_context(&trait_bridge, &methods, &fixture, &excluded, "kreuzberg", &enum_names);

        // Method returning Optional<ExcludedType> must NOT appear in stub.
        assert!(
            !emission.setup_block.contains("as_sync_extractor") && !emission.setup_block.contains("AsSyncExtractor"),
            "method with Option<ExcludedType> return must be skipped, got:\n{}",
            emission.setup_block
        );

        // SyncExtractor must not appear anywhere in the stub.
        assert!(
            !emission.setup_block.contains("SyncExtractor"),
            "excluded type SyncExtractor must not appear in stub, got:\n{}",
            emission.setup_block
        );
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::e2e::config::{CallConfig, E2eConfig};
    use crate::e2e::fixture::{Assertion, Fixture};

    fn make_fixture(id: &str) -> Fixture {
        Fixture {
            id: id.to_string(),
            category: None,
            description: "test fixture".to_string(),
            tags: vec![],
            skip: None,
            env: None,
            call: None,
            input: serde_json::Value::Null,
            mock_response: Some(crate::e2e::fixture::MockResponse {
                status: 200,
                body: Some(serde_json::Value::Null),
                stream_chunks: None,
                headers: std::collections::BTreeMap::new(),
            }),
            source: String::new(),
            http: None,
            assertions: vec![Assertion {
                assertion_type: "not_error".to_string(),
                ..Default::default()
            }],
            visitor: None,
            args: vec![],
        }
    }

    /// snake_case function names in `[e2e.call]` must be routed through `to_go_name`
    /// so the emitted Go call uses the idiomatic CamelCase (e.g. `CleanExtractedText`
    /// instead of `clean_extracted_text`).
    #[test]
    fn test_go_method_name_uses_go_casing() {
        let e2e_config = E2eConfig {
            call: CallConfig {
                function: "clean_extracted_text".to_string(),
                module: "github.com/example/mylib".to_string(),
                result_var: "result".to_string(),
                returns_result: true,
                ..CallConfig::default()
            },
            ..E2eConfig::default()
        };

        let fixture = make_fixture("basic_text");
        let mut out = String::new();
        let config = crate::core::config::ResolvedCrateConfig::default();
        let type_defs: Vec<crate::core::ir::TypeDef> = Vec::new();
        let enums: Vec<crate::core::ir::EnumDef> = Vec::new();
        render_test_function(
            &mut out,
            &fixture,
            GoTestFunctionContext {
                import_alias: "sample_crate",
                e2e_config: &e2e_config,
                adapters: &[],
                data_enum_names: &std::collections::HashSet::new(),
                config: &config,
                type_defs: &type_defs,
                enums: &enums,
            },
        );

        assert!(
            out.contains("sample_crate.CleanExtractedText("),
            "expected Go-cased method name 'CleanExtractedText', got:\n{out}"
        );
        assert!(
            !out.contains("sample_crate.clean_extracted_text("),
            "must not emit raw snake_case method name, got:\n{out}"
        );
    }

    #[test]
    fn test_streaming_fixture_emits_collect_snippet() {
        // A streaming fixture should emit `stream, err :=` and the collect loop.
        let streaming_fixture_json = r#"{
            "id": "basic_stream",
            "description": "basic streaming test",
            "call": "chat_stream",
            "input": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
            "mock_response": {
                "status": 200,
                "stream_chunks": [{"delta": "hello"}]
            },
            "assertions": [
                {"type": "count_min", "field": "chunks", "value": 1}
            ]
        }"#;
        let fixture: Fixture = serde_json::from_str(streaming_fixture_json).unwrap();
        assert!(fixture.is_streaming_mock(), "fixture should be detected as streaming");

        let e2e_config = E2eConfig {
            call: CallConfig {
                function: "chat_stream".to_string(),
                module: "github.com/example/mylib".to_string(),
                result_var: "result".to_string(),
                returns_result: true,
                r#async: true,
                ..CallConfig::default()
            },
            ..E2eConfig::default()
        };

        let mut out = String::new();
        let config = crate::core::config::ResolvedCrateConfig::default();
        let type_defs: Vec<crate::core::ir::TypeDef> = Vec::new();
        let enums: Vec<crate::core::ir::EnumDef> = Vec::new();
        render_test_function(
            &mut out,
            &fixture,
            GoTestFunctionContext {
                import_alias: "pkg",
                e2e_config: &e2e_config,
                adapters: &[],
                data_enum_names: &std::collections::HashSet::new(),
                config: &config,
                type_defs: &type_defs,
                enums: &enums,
            },
        );

        assert!(out.contains("stream, err :="), "should use stream binding, got:\n{out}");
        assert!(
            out.contains("for chunk := range stream"),
            "should emit collect loop, got:\n{out}"
        );
    }

    #[test]
    fn test_streaming_with_client_factory_and_json_arg() {
        // Mimics the real sample-llm setup: no returns_result on the call,
        // json_object arg (binding_returns_error=true), and client_factory from
        // the default Go call override.
        use crate::core::config::e2e::{ArgMapping, CallOverride};
        let streaming_fixture_json = r#"{
            "id": "basic_stream_client",
            "description": "basic streaming test with client",
            "call": "chat_stream",
            "input": {"model": "gpt-4", "messages": [{"role": "user", "content": "hello"}]},
            "mock_response": {
                "status": 200,
                "stream_chunks": [{"delta": "hello"}]
            },
            "assertions": [
                {"type": "count_min", "field": "chunks", "value": 1}
            ]
        }"#;
        let fixture: Fixture = serde_json::from_str(streaming_fixture_json).unwrap();
        assert!(fixture.is_streaming_mock(), "fixture should be detected as streaming");

        let go_override = CallOverride {
            client_factory: Some("CreateClient".to_string()),
            ..Default::default()
        };

        let mut call_overrides = std::collections::HashMap::new();
        call_overrides.insert("go".to_string(), go_override);

        let e2e_config = E2eConfig {
            call: CallConfig {
                function: "chat_stream".to_string(),
                module: "github.com/example/mylib".to_string(),
                result_var: "result".to_string(),
                returns_result: false, // NOT true — like real sample-llm
                r#async: true,
                args: vec![ArgMapping {
                    name: "request".to_string(),
                    field: "input".to_string(),
                    arg_type: "json_object".to_string(),
                    optional: false,
                    owned: true,
                    element_type: None,
                    go_type: None,
                    trait_name: None,
                }],
                overrides: call_overrides,
                ..CallConfig::default()
            },
            ..E2eConfig::default()
        };

        let mut out = String::new();
        render_test_function(
            &mut out,
            &fixture,
            GoTestFunctionContext {
                import_alias: "pkg",
                e2e_config: &e2e_config,
                adapters: &[],
                data_enum_names: &std::collections::HashSet::new(),
                config: &crate::core::config::ResolvedCrateConfig::default(),
                type_defs: &[],
                enums: &[],
            },
        );

        eprintln!("generated:\n{out}");
        assert!(out.contains("stream, err :="), "should use stream binding, got:\n{out}");
        assert!(
            out.contains("for chunk := range stream"),
            "should emit collect loop, got:\n{out}"
        );
    }

    /// When `segments` is an optional field (Option<Vec<T>>) and a fixture asserts on
    /// `segments[0].id`, the prefix guard must be `result.Segments != nil` — NOT
    /// `result.Segments[0] != nil`, which is a compile error for a value-typed element.
    #[test]
    fn test_indexed_element_prefix_guard_uses_array_not_element() {
        let mut optional_fields = std::collections::HashSet::new();
        optional_fields.insert("segments".to_string());
        let mut array_fields = std::collections::HashSet::new();
        array_fields.insert("segments".to_string());

        let e2e_config = E2eConfig {
            call: CallConfig {
                function: "transcribe".to_string(),
                module: "github.com/example/mylib".to_string(),
                result_var: "result".to_string(),
                returns_result: true,
                ..CallConfig::default()
            },
            fields_optional: optional_fields,
            fields_array: array_fields,
            ..E2eConfig::default()
        };

        let fixture = Fixture {
            id: "edge_transcribe_with_timestamps".to_string(),
            category: None,
            description: "Transcription with timestamp segments".to_string(),
            tags: vec![],
            skip: None,
            env: None,
            call: None,
            input: serde_json::Value::Null,
            mock_response: Some(crate::e2e::fixture::MockResponse {
                status: 200,
                body: Some(serde_json::Value::Null),
                stream_chunks: None,
                headers: std::collections::BTreeMap::new(),
            }),
            source: String::new(),
            http: None,
            assertions: vec![
                Assertion {
                    assertion_type: "not_error".to_string(),
                    ..Default::default()
                },
                Assertion {
                    assertion_type: "equals".to_string(),
                    field: Some("segments[0].id".to_string()),
                    value: Some(serde_json::Value::Number(serde_json::Number::from(0u64))),
                    ..Default::default()
                },
            ],
            visitor: None,
            args: vec![],
        };

        let mut out = String::new();
        let config = crate::core::config::ResolvedCrateConfig::default();
        let type_defs: Vec<crate::core::ir::TypeDef> = Vec::new();
        let enums: Vec<crate::core::ir::EnumDef> = Vec::new();
        render_test_function(
            &mut out,
            &fixture,
            GoTestFunctionContext {
                import_alias: "pkg",
                e2e_config: &e2e_config,
                adapters: &[],
                data_enum_names: &std::collections::HashSet::new(),
                config: &config,
                type_defs: &type_defs,
                enums: &enums,
            },
        );

        eprintln!("generated:\n{out}");

        // Must guard on the slice itself — not on the element.
        // Accepts either `Segments != nil` or `len(Segments) > 0`; both are
        // valid Go guards for the slice and avoid the invalid element nil
        // check.
        assert!(
            out.contains("result.Segments != nil") || out.contains("len(result.Segments) > 0"),
            "guard must be on Segments (the slice), not an element; got:\n{out}"
        );
        // Must NOT emit the invalid element nil check.
        assert!(
            !out.contains("result.Segments[0] != nil"),
            "must not emit Segments[0] != nil for a value-type element; got:\n{out}"
        );
    }

    /// Regression test: a `result_is_simple` call with a `contains` assertion whose
    /// `field` ("result") is not a struct field must still bind the call to the result
    /// variable AND emit the `fmt`/`strings` imports.  The assertion renderer ignores
    /// the field for `result_is_simple` calls and emits `strings.Contains(fmt.Sprint(result), …)`,
    /// so binding to `_` (or omitting the imports) produces uncompilable Go.
    #[test]
    fn test_result_is_simple_contains_binds_result_and_emits_imports() {
        use crate::core::config::e2e::ArgMapping;

        let e2e_config = E2eConfig {
            call: CallConfig {
                function: "detect_mime_type_from_bytes".to_string(),
                module: "github.com/example/mylib".to_string(),
                result_var: "result".to_string(),
                returns_result: true,
                result_is_simple: true,
                args: vec![ArgMapping {
                    name: "content".to_string(),
                    field: "input.data".to_string(),
                    arg_type: "bytes".to_string(),
                    optional: false,
                    owned: false,
                    element_type: None,
                    go_type: None,
                    trait_name: None,
                }],
                ..CallConfig::default()
            },
            ..E2eConfig::default()
        };

        let fixture = Fixture {
            id: "mime_detect_bytes".to_string(),
            category: None,
            description: "Detect MIME type from file bytes".to_string(),
            tags: vec![],
            skip: None,
            env: None,
            call: None,
            input: serde_json::json!({"data": "pdf/fake_memo.pdf"}),
            mock_response: None,
            source: String::new(),
            http: None,
            assertions: vec![Assertion {
                assertion_type: "contains".to_string(),
                field: Some("result".to_string()),
                value: Some(serde_json::Value::String("pdf".to_string())),
                ..Default::default()
            }],
            visitor: None,
            args: vec![],
        };

        let config = crate::core::config::ResolvedCrateConfig::default();
        let type_defs: Vec<crate::core::ir::TypeDef> = Vec::new();
        let enums: Vec<crate::core::ir::EnumDef> = Vec::new();
        let out = render_test_file(
            "mime_utilities",
            &[&fixture],
            GoTestFileContext {
                go_module_path: "github.com/example/mylib",
                import_alias: "sample_crate",
                e2e_config: &e2e_config,
                adapters: &[],
                data_enum_names: &std::collections::HashSet::new(),
                config: &config,
                type_defs: &type_defs,
                enums: &enums,
            },
        );

        assert!(
            out.contains("result, err := sample_crate.DetectMimeTypeFromBytes("),
            "expected the call to bind to `result`, not `_`; got:\n{out}"
        );
        assert!(
            out.contains("strings.Contains(") && out.contains("string("),
            "expected `strings.Contains(string(...))` cast rendering; got:\n{out}"
        );
        assert!(
            !out.contains("\t\"fmt\""),
            "expected fmt import to NOT be emitted (uses string cast not fmt.Sprint); got:\n{out}"
        );
        assert!(
            out.contains("\t\"strings\""),
            "expected the `strings` import to be emitted; got:\n{out}"
        );
    }
}
