//! Assertion rendering for TypeScript e2e tests.

use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::Assertion;

use super::json::json_to_js;

/// Render a single assertion into the test body.
#[allow(clippy::too_many_arguments)]
pub(super) fn render_assertion(
    out: &mut String,
    assertion: &Assertion,
    result_var: &str,
    field_resolver: &FieldResolver,
    result_is_simple: bool,
    result_enum_fields: &std::collections::HashMap<String, String>,
    lang: &str,
    is_streaming: bool,
) {
    // For simple-result methods (e.g., `speech` returning bytes/Buffer), every
    // field-based assertion targets the result itself — there is no struct to
    // access. Drop length-only assertions onto the result directly and skip
    // anything that requires a real struct sub-field.
    if result_is_simple {
        if let Some(f) = &assertion.field {
            if !f.is_empty() {
                match assertion.assertion_type.as_str() {
                    "not_empty" => {
                        out.push_str(&format!("    expect({result_var}.length).toBeGreaterThan(0);\n"));
                        return;
                    }
                    "is_empty" => {
                        out.push_str(&format!("    expect({result_var}.length).toBe(0);\n"));
                        return;
                    }
                    "count_equals" => {
                        if let Some(val) = &assertion.value {
                            let js_val = json_to_js(val);
                            out.push_str(&format!("    expect({result_var}.length).toBe({js_val});\n"));
                        }
                        return;
                    }
                    "count_min" => {
                        if let Some(val) = &assertion.value {
                            let js_val = json_to_js(val);
                            out.push_str(&format!(
                                "    expect({result_var}.length).toBeGreaterThanOrEqual({js_val});\n"
                            ));
                        }
                        return;
                    }
                    _ => {
                        out.push_str(&format!(
                            "    // skipped: field '{f}' not applicable for simple result type\n"
                        ));
                        return;
                    }
                }
            }
        }
    }

    // Handle synthetic / derived fields before the is_valid_for_result check
    // so they are never treated as struct property accesses on the result.
    if let Some(f) = &assertion.field {
        if render_synthetic_field_assertion(out, assertion, result_var, f, is_streaming) {
            return;
        }
    }

    // Skip assertions on fields that don't exist on the result type.
    if let Some(f) = &assertion.field {
        if !f.is_empty() && !field_resolver.is_valid_for_result(f) {
            out.push_str(&format!("    // skipped: field '{f}' not available on result type\n"));
            return;
        }
    }

    let mut field_expr = match &assertion.field {
        Some(f) if !f.is_empty() => field_resolver.accessor(f, "typescript", result_var),
        _ => result_var.to_string(),
    };

    // Check if this field is an enum type that needs coercion
    let mut is_enum_field = false;
    if let Some(f) = assertion.field.as_deref() {
        if !f.is_empty()
            && result_enum_fields
                .get(f)
                .or_else(|| result_enum_fields.get(field_resolver.resolve(f)))
                .is_some()
        {
            is_enum_field = true;
        }
    }

    // Check if this is metadata.format field (FormatMetadata tagged enum)
    let is_format_metadata = assertion.field.as_deref().is_some_and(|f| f == "metadata.format");

    // WASM: enum-typed result fields are exposed as numeric discriminants by
    // wasm-bindgen, not strings. Compare against `EnumClass.Variant` instead of
    // running `.trim()` on a number.
    if lang == "wasm" {
        if let Some(enum_class) = assertion.field.as_deref().and_then(|f| {
            result_enum_fields
                .get(f)
                .or_else(|| result_enum_fields.get(field_resolver.resolve(f)))
        }) {
            if render_wasm_enum_assertion(out, assertion, &field_expr, enum_class) {
                return;
            }
        }

        // wasm-bindgen maps Rust `u64`/`i64` to JS `BigInt`. Numeric assertions
        // would otherwise fail with `expected 15n to be 15`. Wrap the field
        // expression in `Number(...)` for numeric `equals` / inequality
        // comparisons so both BigInt and Number values compare correctly.
        if assertion_value_is_numeric(assertion) {
            field_expr = format!("Number({field_expr})");
        }
    }

    let field_is_array = assertion
        .field
        .as_deref()
        .is_some_and(|f| field_resolver.is_array(field_resolver.resolve(f)));

    render_standard_assertion(
        out,
        assertion,
        result_var,
        &field_expr,
        field_resolver,
        field_is_array,
        is_enum_field,
        is_format_metadata,
    );
}

/// Return `true` when the assertion compares the field against a numeric value.
fn assertion_value_is_numeric(assertion: &Assertion) -> bool {
    match assertion.assertion_type.as_str() {
        "equals" | "greater_than" | "less_than" | "greater_than_or_equal" | "less_than_or_equal" => {
            assertion.value.as_ref().is_some_and(|v| v.is_number())
        }
        _ => false,
    }
}

/// Render an enum-typed result assertion. WASM optional-enum getters return
/// `Option<String>` (the serde wire format) rather than the wasm-bindgen enum
/// discriminant, so we compare against the original snake_case string value
/// rather than the `EnumClass.Variant` reference. The `enum_class` is no longer
/// referenced in the emitted code but is retained in the signature for clarity
/// at the call site.
/// Returns `true` if the assertion was rendered.
fn render_wasm_enum_assertion(out: &mut String, assertion: &Assertion, field_expr: &str, _enum_class: &str) -> bool {
    match assertion.assertion_type.as_str() {
        "equals" => {
            if let Some(serde_json::Value::String(s)) = &assertion.value {
                out.push_str(&format!("    expect({field_expr}).toBe(\"{s}\");\n"));
                return true;
            }
        }
        "not_empty" | "is_not_empty" => {
            out.push_str(&format!("    expect({field_expr}).toBeDefined();\n"));
            return true;
        }
        _ => {}
    }
    false
}

/// Try to render a synthetic/virtual field assertion. Returns `true` when the field was handled.
fn render_synthetic_field_assertion(
    out: &mut String,
    assertion: &Assertion,
    result_var: &str,
    field: &str,
    is_streaming: bool,
) -> bool {
    match field {
        "chunks_have_content" => {
            let pred = format!("({result_var}.chunks ?? []).every((c: {{ content?: string }}) => !!c.content)");
            emit_bool_assertion(out, &pred, assertion.assertion_type.as_str(), field);
            true
        }
        "chunks_have_embeddings" => {
            let pred = format!(
                "({result_var}.chunks ?? []).every((c: {{ embedding?: number[] }}) => c.embedding != null && c.embedding.length > 0)"
            );
            emit_bool_assertion(out, &pred, assertion.assertion_type.as_str(), field);
            true
        }
        "chunks_have_heading_context" => {
            let pred = format!(
                "({result_var}.chunks ?? []).every((c: {{ metadata?: {{ headingContext?: string }} }}) => c.metadata?.headingContext != null)"
            );
            emit_bool_assertion(out, &pred, assertion.assertion_type.as_str(), field);
            true
        }
        "first_chunk_starts_with_heading" => {
            let pred = format!("({result_var}.chunks ?? []).at(0)?.metadata?.headingContext != null");
            emit_bool_assertion(out, &pred, assertion.assertion_type.as_str(), field);
            true
        }
        "embeddings" => {
            render_embeddings_assertion(out, assertion, result_var);
            true
        }
        "embedding_dimensions" => {
            render_embedding_dimensions(out, assertion, result_var);
            true
        }
        "embeddings_valid" | "embeddings_finite" | "embeddings_non_zero" | "embeddings_normalized" => {
            let pred = match field {
                "embeddings_valid" => {
                    format!("{result_var}.every((e: number[]) => e.length > 0)")
                }
                "embeddings_finite" => {
                    format!("{result_var}.every((e: number[]) => e.every((v: number) => isFinite(v)))")
                }
                "embeddings_non_zero" => {
                    format!("{result_var}.every((e: number[]) => e.some((v: number) => v !== 0))")
                }
                "embeddings_normalized" => {
                    format!(
                        "{result_var}.every((e: number[]) => {{ const n = e.reduce((s: number, v: number) => s + v * v, 0); return Math.abs(n - 1.0) < 1e-3; }})"
                    )
                }
                _ => unreachable!(),
            };
            emit_bool_assertion(out, &pred, assertion.assertion_type.as_str(), field);
            true
        }
        "keywords" | "keywords_count" => {
            out.push_str(&format!(
                "    // skipped: field '{field}' not available on Node JsExtractionResult\n"
            ));
            true
        }
        // Streaming virtual 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`).
        f if is_streaming && crate::e2e::codegen::streaming_assertions::is_streaming_virtual_field(f) => {
            // lang is always "node" or "wasm" here; both use the same JS expressions.
            if let Some(expr) =
                crate::e2e::codegen::streaming_assertions::StreamingFieldResolver::accessor(f, "node", "chunks")
            {
                match assertion.assertion_type.as_str() {
                    "count_min" => {
                        if let Some(val) = &assertion.value {
                            let js_val = json_to_js(val);
                            out.push_str(&format!(
                                "    expect({expr}.length).toBeGreaterThanOrEqual({js_val});\n"
                            ));
                        }
                    }
                    "count_equals" => {
                        if let Some(val) = &assertion.value {
                            let js_val = json_to_js(val);
                            out.push_str(&format!("    expect({expr}.length).toBe({js_val});\n"));
                        }
                    }
                    "equals" => {
                        if let Some(val) = &assertion.value {
                            let js_val = json_to_js(val);
                            out.push_str(&format!("    expect({expr}).toBe({js_val});\n"));
                        }
                    }
                    "not_empty" => {
                        out.push_str(&format!("    expect({expr}).toBeTruthy();\n"));
                    }
                    "is_empty" => {
                        out.push_str(&format!("    expect({expr}).toBeFalsy();\n"));
                    }
                    "is_true" => {
                        out.push_str(&format!("    expect({expr}).toBe(true);\n"));
                    }
                    "is_false" => {
                        out.push_str(&format!("    expect({expr}).toBe(false);\n"));
                    }
                    "greater_than" => {
                        if let Some(val) = &assertion.value {
                            let js_val = json_to_js(val);
                            out.push_str(&format!("    expect({expr}).toBeGreaterThan({js_val});\n"));
                        }
                    }
                    "greater_than_or_equal" => {
                        if let Some(val) = &assertion.value {
                            let js_val = json_to_js(val);
                            out.push_str(&format!("    expect({expr}).toBeGreaterThanOrEqual({js_val});\n"));
                        }
                    }
                    "contains" => {
                        if let Some(val) = &assertion.value {
                            let js_val = json_to_js(val);
                            out.push_str(&format!("    expect({expr}).toContain({js_val});\n"));
                        }
                    }
                    _ => {
                        out.push_str(&format!(
                            "    // streaming field '{field}': assertion type '{}' not rendered\n",
                            assertion.assertion_type
                        ));
                    }
                }
            }
            true
        }
        _ => false,
    }
}

fn emit_bool_assertion(out: &mut String, pred: &str, assertion_type: &str, field: &str) {
    let pred = pred.to_string();
    let assertion_type = assertion_type.to_string();
    let field_name = field.to_string();
    let rendered = crate::e2e::template_env::render(
        "typescript/synthetic_assertion.jinja",
        minijinja::context! {
            assertion_type,
            pred,
            field_name,
        },
    );
    out.push_str(&rendered);
}

fn render_embeddings_assertion(out: &mut String, assertion: &Assertion, result_var: &str) {
    let assertion_type = assertion.assertion_type.as_str();
    let result_var = result_var.to_string();
    let field_name = "embeddings".to_string();

    match assertion_type {
        "count_equals" | "count_min" => {
            if let Some(val) = &assertion.value {
                let js_val = json_to_js(val);
                let rendered = crate::e2e::template_env::render(
                    "typescript/synthetic_assertion.jinja",
                    minijinja::context! {
                        assertion_type,
                        result_var,
                        field_name,
                        js_val,
                    },
                );
                out.push_str(&rendered);
            }
        }
        "not_empty" | "is_empty" => {
            let rendered = crate::e2e::template_env::render(
                "typescript/synthetic_assertion.jinja",
                minijinja::context! {
                    assertion_type,
                    result_var,
                    field_name,
                },
            );
            out.push_str(&rendered);
        }
        _ => {
            let rendered = crate::e2e::template_env::render(
                "typescript/synthetic_assertion.jinja",
                minijinja::context! {
                    assertion_type,
                    field_name,
                },
            );
            out.push_str(&rendered);
        }
    }
}

fn render_embedding_dimensions(out: &mut String, assertion: &Assertion, result_var: &str) {
    let expr = format!("({result_var}.length > 0 ? {result_var}[0].length : 0)");
    let assertion_type = assertion.assertion_type.as_str();
    let expr = expr.clone();
    let field_name = "embedding_dimensions".to_string();

    match assertion_type {
        "equals" | "greater_than" => {
            if let Some(val) = &assertion.value {
                let js_val = json_to_js(val);
                let rendered = crate::e2e::template_env::render(
                    "typescript/synthetic_assertion.jinja",
                    minijinja::context! {
                        assertion_type,
                        expr,
                        field_name,
                        js_val,
                    },
                );
                out.push_str(&rendered);
            }
        }
        _ => {
            let rendered = crate::e2e::template_env::render(
                "typescript/synthetic_assertion.jinja",
                minijinja::context! {
                    assertion_type,
                    field_name,
                },
            );
            out.push_str(&rendered);
        }
    }
}

#[allow(clippy::too_many_arguments)]
fn render_standard_assertion(
    out: &mut String,
    assertion: &Assertion,
    result_var: &str,
    field_expr: &str,
    field_resolver: &FieldResolver,
    field_is_array: bool,
    is_enum_field: bool,
    is_format_metadata: bool,
) {
    let assertion_type = assertion.assertion_type.as_str();
    let resolved_field = assertion.field.as_deref().unwrap_or("");
    let field_is_optional =
        !resolved_field.is_empty() && field_resolver.is_optional(field_resolver.resolve(resolved_field));

    // Handle different assertion types
    match assertion_type {
        "equals" => {
            if let Some(expected) = &assertion.value {
                let js_val = json_to_js(expected);
                let rendered = crate::e2e::template_env::render(
                    "typescript/assertion.jinja",
                    minijinja::context! {
                        assertion_type => assertion_type,
                        field_expr => field_expr,
                        field_is_optional => field_is_optional,
                        is_string_val => expected.is_string(),
                        is_enum_field => is_enum_field && expected.is_string(),
                        is_format_metadata => is_format_metadata && expected.is_string(),
                        js_val => js_val,
                        has_js_val => true,
                    },
                );
                out.push_str(&rendered);
            }
        }
        "contains" => {
            if let Some(expected) = &assertion.value {
                let js_val = json_to_js(expected);
                let rendered = crate::e2e::template_env::render(
                    "typescript/assertion.jinja",
                    minijinja::context! {
                        assertion_type => assertion_type,
                        field_expr => field_expr,
                        field_is_optional => field_is_optional,
                        field_is_array => field_is_array,
                        is_string_val => expected.is_string(),
                        js_val => js_val,
                        has_js_val => true,
                    },
                );
                out.push_str(&rendered);
            }
        }
        "contains_all" => {
            if let Some(values) = &assertion.values {
                let items: Vec<String> = values.iter().map(json_to_js).collect();
                let rendered = crate::e2e::template_env::render(
                    "typescript/assertion.jinja",
                    minijinja::context! {
                        assertion_type => assertion_type,
                        field_expr => field_expr,
                        field_is_optional => field_is_optional,
                        field_is_array => field_is_array,
                        is_string_val => values.iter().all(|v| v.is_string()),
                        values_js => items,
                    },
                );
                out.push_str(&rendered);
            }
        }
        "not_contains" => {
            if let Some(expected) = &assertion.value {
                let js_val = json_to_js(expected);
                let rendered = crate::e2e::template_env::render(
                    "typescript/assertion.jinja",
                    minijinja::context! {
                        assertion_type => assertion_type,
                        field_expr => field_expr,
                        field_is_array => field_is_array,
                        is_string_val => expected.is_string(),
                        js_val => js_val,
                        has_js_val => true,
                    },
                );
                out.push_str(&rendered);
            }
        }
        "not_empty" => {
            let rendered = crate::e2e::template_env::render(
                "typescript/assertion.jinja",
                minijinja::context! {
                    assertion_type => assertion_type,
                    field_expr => field_expr,
                    field_is_optional => field_is_optional,
                },
            );
            out.push_str(&rendered);
        }
        "is_empty" => {
            let rendered = crate::e2e::template_env::render(
                "typescript/assertion.jinja",
                minijinja::context! {
                    assertion_type => assertion_type,
                    field_expr => field_expr,
                },
            );
            out.push_str(&rendered);
        }
        "contains_any" => {
            if let Some(values) = &assertion.values {
                let items: Vec<String> = values.iter().map(json_to_js).collect();
                let rendered = crate::e2e::template_env::render(
                    "typescript/assertion.jinja",
                    minijinja::context! {
                        assertion_type => assertion_type,
                        field_expr => field_expr,
                        field_is_array => field_is_array,
                        is_string_val => values.iter().all(|v| v.is_string()),
                        values_js => items,
                    },
                );
                out.push_str(&rendered);
            }
        }
        "greater_than" | "less_than" | "greater_than_or_equal" | "less_than_or_equal" => {
            if let Some(val) = &assertion.value {
                let js_val = json_to_js(val);
                let rendered = crate::e2e::template_env::render(
                    "typescript/assertion.jinja",
                    minijinja::context! {
                        assertion_type => assertion_type,
                        field_expr => field_expr,
                        js_val => js_val,
                        has_js_val => true,
                    },
                );
                out.push_str(&rendered);
            }
        }
        "starts_with" => {
            if let Some(expected) = &assertion.value {
                let js_val = json_to_js(expected);
                let rendered = crate::e2e::template_env::render(
                    "typescript/assertion.jinja",
                    minijinja::context! {
                        assertion_type => assertion_type,
                        field_expr => field_expr,
                        field_is_optional => field_is_optional,
                        js_val => js_val,
                        has_js_val => true,
                    },
                );
                out.push_str(&rendered);
            }
        }
        "count_min" | "count_equals" | "min_length" | "max_length" => {
            if let Some(val) = &assertion.value {
                if let Some(n) = val.as_u64() {
                    let rendered = crate::e2e::template_env::render(
                        "typescript/assertion.jinja",
                        minijinja::context! {
                            assertion_type => assertion_type,
                            field_expr => field_expr,
                            n => n,
                        },
                    );
                    out.push_str(&rendered);
                }
            }
        }
        "is_true" => {
            let rendered = crate::e2e::template_env::render(
                "typescript/assertion.jinja",
                minijinja::context! {
                    assertion_type => assertion_type,
                    field_expr => field_expr,
                },
            );
            out.push_str(&rendered);
        }
        "is_false" => {
            let rendered = crate::e2e::template_env::render(
                "typescript/assertion.jinja",
                minijinja::context! {
                    assertion_type => assertion_type,
                    field_expr => field_expr,
                },
            );
            out.push_str(&rendered);
        }
        "method_result" => {
            render_method_result_assertion(out, assertion, result_var);
        }
        "ends_with" => {
            if let Some(expected) = &assertion.value {
                let js_val = json_to_js(expected);
                let rendered = crate::e2e::template_env::render(
                    "typescript/assertion.jinja",
                    minijinja::context! {
                        assertion_type => assertion_type,
                        field_expr => field_expr,
                        js_val => js_val,
                        has_js_val => true,
                    },
                );
                out.push_str(&rendered);
            }
        }
        "matches_regex" => {
            if let Some(expected) = &assertion.value {
                if let Some(pattern) = expected.as_str() {
                    let rendered = crate::e2e::template_env::render(
                        "typescript/assertion.jinja",
                        minijinja::context! {
                            assertion_type => assertion_type,
                            field_expr => field_expr,
                            expected_pattern => pattern,
                        },
                    );
                    out.push_str(&rendered);
                }
            }
        }
        "not_error" => {
            // No-op — if we got here, the call succeeded (it would have thrown).
        }
        "error" => {
            // Handled at the test level (early return above).
        }
        other => {
            panic!("TypeScript e2e generator: unsupported assertion type: {other}");
        }
    }
}

fn render_method_result_assertion(out: &mut String, assertion: &Assertion, result_var: &str) {
    if let Some(method_name) = &assertion.method {
        let call_expr = build_ts_method_call(result_var, method_name, assertion.args.as_ref());
        let check = assertion.check.as_deref().unwrap_or("is_true");
        match check {
            "equals" => {
                if let Some(val) = &assertion.value {
                    let js_val = json_to_js(val);
                    let rendered = crate::e2e::template_env::render(
                        "typescript/assertion.jinja",
                        minijinja::context! {
                            assertion_type => "method_result",
                            check => check,
                            call_expr => call_expr,
                            check_js_val => js_val,
                            has_check_js_val => true,
                        },
                    );
                    out.push_str(&rendered);
                }
            }
            "is_true" => {
                let rendered = crate::e2e::template_env::render(
                    "typescript/assertion.jinja",
                    minijinja::context! {
                        assertion_type => "method_result",
                        check => check,
                        call_expr => call_expr,
                    },
                );
                out.push_str(&rendered);
            }
            "is_false" => {
                let rendered = crate::e2e::template_env::render(
                    "typescript/assertion.jinja",
                    minijinja::context! {
                        assertion_type => "method_result",
                        check => check,
                        call_expr => call_expr,
                    },
                );
                out.push_str(&rendered);
            }
            "greater_than_or_equal" => {
                if let Some(val) = &assertion.value {
                    let n = val.as_u64().unwrap_or(0);
                    let rendered = crate::e2e::template_env::render(
                        "typescript/assertion.jinja",
                        minijinja::context! {
                            assertion_type => "method_result",
                            check => check,
                            call_expr => call_expr,
                            check_n => n,
                        },
                    );
                    out.push_str(&rendered);
                }
            }
            "count_min" => {
                if let Some(val) = &assertion.value {
                    let n = val.as_u64().unwrap_or(0);
                    let rendered = crate::e2e::template_env::render(
                        "typescript/assertion.jinja",
                        minijinja::context! {
                            assertion_type => "method_result",
                            check => check,
                            call_expr => call_expr,
                            check_n => n,
                        },
                    );
                    out.push_str(&rendered);
                }
            }
            "contains" => {
                if let Some(val) = &assertion.value {
                    let js_val = json_to_js(val);
                    let rendered = crate::e2e::template_env::render(
                        "typescript/assertion.jinja",
                        minijinja::context! {
                            assertion_type => "method_result",
                            check => check,
                            call_expr => call_expr,
                            check_js_val => js_val,
                            has_check_js_val => true,
                        },
                    );
                    out.push_str(&rendered);
                }
            }
            "is_error" => {
                let rendered = crate::e2e::template_env::render(
                    "typescript/assertion.jinja",
                    minijinja::context! {
                        assertion_type => "method_result",
                        check => check,
                        call_expr => call_expr,
                    },
                );
                out.push_str(&rendered);
            }
            other_check => {
                panic!("TypeScript e2e generator: unsupported method_result check type: {other_check}");
            }
        }
    } else {
        panic!("TypeScript e2e generator: method_result assertion missing 'method' field");
    }
}

/// Build a TypeScript call expression for a method_result assertion on a tree-sitter Tree.
pub(super) fn build_ts_method_call(result_var: &str, method_name: &str, args: Option<&serde_json::Value>) -> String {
    match method_name {
        "root_child_count" => format!("{result_var}.rootNode.childCount"),
        "root_node_type" => format!("{result_var}.rootNode.type"),
        "named_children_count" => format!("{result_var}.rootNode.namedChildCount"),
        "has_error_nodes" => format!("treeHasErrorNodes({result_var})"),
        "error_count" | "tree_error_count" => format!("treeErrorCount({result_var})"),
        "tree_to_sexp" => format!("treeToSexp({result_var})"),
        "contains_node_type" => {
            let node_type = args
                .and_then(|a| a.get("node_type"))
                .and_then(|v| v.as_str())
                .unwrap_or("");
            format!("treeContainsNodeType({result_var}, \"{node_type}\")")
        }
        "find_nodes_by_type" => {
            let node_type = args
                .and_then(|a| a.get("node_type"))
                .and_then(|v| v.as_str())
                .unwrap_or("");
            format!("findNodesByType({result_var}, \"{node_type}\")")
        }
        "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("");
            format!("runQuery({result_var}, \"{language}\", \"{query_source}\", source)")
        }
        _ => {
            if let Some(args_val) = args {
                let arg_str = args_val
                    .as_object()
                    .map(|obj| {
                        obj.iter()
                            .map(|(k, v)| format!("{}: {}", k, json_to_js(v)))
                            .collect::<Vec<_>>()
                            .join(", ")
                    })
                    .unwrap_or_default();
                format!("{result_var}.{method_name}({arg_str})")
            } else {
                format!("{result_var}.{method_name}()")
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use std::collections::{HashMap, HashSet};

    use super::*;
    use crate::e2e::field_access::FieldResolver;
    use crate::e2e::fixture::Assertion;

    fn empty_resolver() -> FieldResolver {
        FieldResolver::new(
            &HashMap::new(),
            &HashSet::new(),
            &HashSet::new(),
            &HashSet::new(),
            &HashSet::new(),
        )
    }

    fn array_resolver(field: &str) -> FieldResolver {
        let result_fields = HashSet::from([field.to_string()]);
        let array_fields = HashSet::from([field.to_string()]);
        FieldResolver::new(
            &HashMap::new(),
            &HashSet::new(),
            &result_fields,
            &array_fields,
            &HashSet::new(),
        )
    }

    fn make_assertion(assertion_type: &str, field: Option<&str>, value: Option<serde_json::Value>) -> Assertion {
        Assertion {
            assertion_type: assertion_type.to_string(),
            field: field.map(|s| s.to_string()),
            value,
            ..Default::default()
        }
    }

    #[test]
    fn render_assertion_not_empty_emits_length_check() {
        let resolver = empty_resolver();
        let assertion = make_assertion("not_empty", None, None);
        let mut out = String::new();
        render_assertion(
            &mut out,
            &assertion,
            "result",
            &resolver,
            false,
            &std::collections::HashMap::new(),
            "node",
            false,
        );
        assert!(out.contains(".length"), "got: {out}");
    }

    #[test]
    fn render_assertion_equals_string_trims() {
        let resolver = empty_resolver();
        let assertion = make_assertion("equals", None, Some(serde_json::Value::String("hello".into())));
        let mut out = String::new();
        render_assertion(
            &mut out,
            &assertion,
            "result",
            &resolver,
            false,
            &std::collections::HashMap::new(),
            "node",
            false,
        );
        assert!(out.contains(".trim()"), "got: {out}");
    }

    #[test]
    fn render_assertion_is_empty_allows_nullish_simple_results() {
        let resolver = empty_resolver();
        let assertion = make_assertion("is_empty", None, None);
        let mut out = String::new();
        render_assertion(
            &mut out,
            &assertion,
            "result",
            &resolver,
            false,
            &std::collections::HashMap::new(),
            "node",
            false,
        );
        assert!(out.contains("(result ?? \"\").length"), "got: {out}");
    }

    #[test]
    fn render_assertion_contains_string_array_uses_item_texts() {
        let resolver = array_resolver("structure");
        let assertion = make_assertion(
            "contains",
            Some("structure"),
            Some(serde_json::Value::String("Function".into())),
        );
        let mut out = String::new();
        render_assertion(
            &mut out,
            &assertion,
            "result",
            &resolver,
            false,
            &std::collections::HashMap::new(),
            "node",
            false,
        );
        assert!(out.contains("_alefE2eItemTexts(item)"), "got: {out}");
    }

    #[test]
    fn build_ts_method_call_root_child_count() {
        let expr = build_ts_method_call("tree", "root_child_count", None);
        assert_eq!(expr, "tree.rootNode.childCount");
    }
}
