use crate::e2e::codegen::transform_json_keys_for_language;
use crate::e2e::escape::escape_c;
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::Fixture;
use heck::ToSnakeCase;
use std::collections::{HashMap, HashSet};
use std::fmt::Write as FmtWrite;
use super::{
emit_nested_accessor, infer_opaque_handle_type, is_primitive_c_type, is_skipped_c_field, render_assertion,
try_emit_enum_accessor,
};
#[allow(clippy::too_many_arguments)]
pub(super) fn render_engine_factory_test_function(
out: &mut String,
fixture: &Fixture,
prefix: &str,
function_name: &str,
result_var: &str,
field_resolver: &FieldResolver,
fields_c_types: &HashMap<String, String>,
fields_enum: &HashSet<String>,
result_type_name: &str,
config_type: &str,
expects_error: bool,
raw_c_result_type: Option<&str>,
) {
let prefix_upper = prefix.to_uppercase();
let config_snake = config_type.to_snake_case();
let config_val = fixture.input.get("config");
let config_json = match config_val {
Some(v) if !v.is_null() => {
let normalized = transform_json_keys_for_language(v, "snake_case");
serde_json::to_string(&normalized).unwrap_or_else(|_| "{}".to_string())
}
_ => "{}".to_string(),
};
let config_escaped = escape_c(&config_json);
let fixture_id = &fixture.id;
let has_active_assertions = fixture.assertions.iter().any(|a| {
if let Some(f) = &a.field {
!f.is_empty() && field_resolver.is_valid_for_result(f)
} else {
false
}
});
let _ = writeln!(
out,
" {prefix_upper}{config_type}* config_handle = \
{prefix}_{config_snake}_from_json(\"{config_escaped}\");"
);
if expects_error {
let _ = writeln!(out, " if (config_handle == NULL) {{ return; }}");
} else {
let _ = writeln!(out, " assert(config_handle != NULL && \"failed to parse config\");");
}
let _ = writeln!(
out,
" {prefix_upper}CrawlEngineHandle* engine = {prefix}_create_engine(config_handle);"
);
let _ = writeln!(out, " {prefix}_{config_snake}_free(config_handle);");
if expects_error {
let _ = writeln!(out, " if (engine == NULL) {{ return; }}");
} else {
let _ = writeln!(out, " assert(engine != NULL && \"failed to create engine\");");
}
let fixture_env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
let _ = writeln!(out, " const char* mock_per_fixture = getenv(\"{fixture_env_key}\");");
let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
let _ = writeln!(out, " char url[2048];");
let _ = writeln!(out, " if (mock_per_fixture && mock_per_fixture[0] != '\\0') {{");
let _ = writeln!(out, " snprintf(url, sizeof(url), \"%s\", mock_per_fixture);");
let _ = writeln!(out, " }} else {{");
let _ = writeln!(
out,
" assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");"
);
let _ = writeln!(
out,
" snprintf(url, sizeof(url), \"%s/fixtures/{fixture_id}\", mock_base);"
);
let _ = writeln!(out, " }}");
let actions_arg = fixture.input.get("actions").and_then(|v| {
if v.is_null() {
None
} else {
let normalized = transform_json_keys_for_language(v, "snake_case");
let json = serde_json::to_string(&normalized).ok()?;
let escaped = escape_c(&json);
Some(escaped)
}
});
if let Some(ref escaped_actions) = actions_arg {
let _ = writeln!(out, " const char* actions_json = \"{escaped_actions}\";");
}
let extra_call_args = if actions_arg.is_some() {
", actions_json".to_string()
} else {
String::new()
};
if let Some(raw_type) = raw_c_result_type {
if raw_type == "char*" {
let _ = writeln!(
out,
" char* {result_var} = {prefix}_{function_name}(engine, url{extra_call_args});"
);
let _ = writeln!(out, " if ({result_var} != NULL) {prefix}_free_string({result_var});");
let _ = writeln!(out, " {prefix}_crawl_engine_handle_free(engine);");
let _ = writeln!(out, "}}");
return;
} else {
let raw_snake = raw_type.to_snake_case();
let _ = writeln!(
out,
" {prefix_upper}{raw_type}* {result_var} = {prefix}_{function_name}(engine, url{extra_call_args});"
);
let _ = writeln!(
out,
" if ({result_var} != NULL) {prefix}_{raw_snake}_free({result_var});"
);
let _ = writeln!(out, " {prefix}_crawl_engine_handle_free(engine);");
let _ = writeln!(out, "}}");
return;
}
}
let _ = writeln!(
out,
" {prefix_upper}{result_type_name}* {result_var} = {prefix}_{function_name}(engine, url{extra_call_args});"
);
if !has_active_assertions {
let result_type_snake = result_type_name.to_snake_case();
let _ = writeln!(
out,
" if ({result_var} != NULL) {prefix}_{result_type_snake}_free({result_var});"
);
let _ = writeln!(out, " {prefix}_crawl_engine_handle_free(engine);");
let _ = writeln!(out, "}}");
return;
}
let _ = writeln!(out, " assert({result_var} != NULL && \"expected call to succeed\");");
let mut intermediate_handles: Vec<(String, String)> = Vec::new();
let mut accessed_fields: Vec<(String, String, bool)> = Vec::new();
let mut primitive_locals: HashMap<String, String> = HashMap::new();
let mut opaque_handle_locals: HashMap<String, String> = HashMap::new();
for assertion in &fixture.assertions {
if let Some(f) = &assertion.field
&& !f.is_empty()
&& field_resolver.is_valid_for_result(f)
&& !accessed_fields.iter().any(|(k, _, _)| k == f)
{
let resolved_raw = field_resolver.resolve(f);
let resolved = if let Some(stripped) = field_resolver.namespace_stripped_path(resolved_raw) {
let stripped_first = stripped.split('.').next().unwrap_or(stripped);
let stripped_first = stripped_first.split('[').next().unwrap_or(stripped_first);
if field_resolver.is_valid_for_result(stripped_first) {
stripped
} else {
resolved_raw
}
} else {
resolved_raw
};
let local_var = f.replace(['.', '['], "_").replace(']', "");
let has_map_access = resolved.contains('[');
if resolved.contains('.') {
let leaf_result = emit_nested_accessor(
out,
prefix,
resolved,
&local_var,
result_var,
fields_c_types,
fields_enum,
&mut intermediate_handles,
result_type_name,
f,
);
if let Some(returned_type) = leaf_result {
if is_primitive_c_type(&returned_type) {
primitive_locals.insert(local_var.clone(), returned_type);
} else {
opaque_handle_locals.insert(local_var.clone(), returned_type);
}
}
} else {
let result_type_snake = result_type_name.to_snake_case();
let accessor_fn = format!("{prefix}_{result_type_snake}_{resolved}");
let lookup_key = format!("{result_type_snake}.{resolved}");
if is_skipped_c_field(fields_c_types, &result_type_snake, resolved) {
primitive_locals.insert(local_var.clone(), "__skip__".to_string());
} else if let Some(t) = fields_c_types.get(&lookup_key).filter(|t| is_primitive_c_type(t)) {
let _ = writeln!(out, " {t} {local_var} = {accessor_fn}({result_var});");
primitive_locals.insert(local_var.clone(), t.clone());
} else if try_emit_enum_accessor(
out,
prefix,
&prefix_upper,
f,
resolved,
&result_type_snake,
&accessor_fn,
result_var,
&local_var,
fields_c_types,
fields_enum,
&mut intermediate_handles,
) {
} else if let Some(handle_pascal) =
infer_opaque_handle_type(fields_c_types, &result_type_snake, resolved)
{
let _ = writeln!(
out,
" {prefix_upper}{handle_pascal}* {local_var} = {accessor_fn}({result_var});"
);
opaque_handle_locals.insert(local_var.clone(), handle_pascal.to_snake_case());
} else {
let _ = writeln!(out, " char* {local_var} = {accessor_fn}({result_var});");
}
}
accessed_fields.push((f.clone(), local_var, has_map_access));
}
}
for assertion in &fixture.assertions {
render_assertion(
out,
assertion,
result_var,
prefix,
field_resolver,
&accessed_fields,
&primitive_locals,
&opaque_handle_locals,
);
}
for (_f, local_var, from_json) in &accessed_fields {
if primitive_locals.contains_key(local_var) {
continue;
}
if let Some(snake_type) = opaque_handle_locals.get(local_var) {
let _ = writeln!(out, " {prefix}_{snake_type}_free({local_var});");
continue;
}
if *from_json {
let _ = writeln!(out, " free({local_var});");
} else {
let _ = writeln!(out, " {prefix}_free_string({local_var});");
}
}
for (handle_var, snake_type) in intermediate_handles.iter().rev() {
if snake_type == "free_string" {
let _ = writeln!(out, " {prefix}_free_string({handle_var});");
} else if snake_type == "free" {
let _ = writeln!(out, " free({handle_var});");
} else {
let _ = writeln!(out, " {prefix}_{snake_type}_free({handle_var});");
}
}
let result_type_snake = result_type_name.to_snake_case();
let _ = writeln!(out, " {prefix}_{result_type_snake}_free({result_var});");
let _ = writeln!(out, " {prefix}_crawl_engine_handle_free(engine);");
let _ = writeln!(out, "}}");
}
#[allow(clippy::too_many_arguments)]
pub(super) fn render_bytes_test_function(
out: &mut String,
fixture: &Fixture,
prefix: &str,
function_name: &str,
_result_var: &str,
args: &[crate::e2e::config::ArgMapping],
options_type_name: &str,
result_type_name: &str,
factory: &str,
client_owner_type: &str,
expects_error: bool,
) {
let prefix_upper = prefix.to_uppercase();
let mut request_handle_vars: Vec<(String, String)> = Vec::new();
let mut string_arg_exprs: Vec<String> = Vec::new();
for arg in args {
match arg.arg_type.as_str() {
"json_object" => {
let request_type_pascal = if !options_type_name.is_empty() {
options_type_name.to_string()
} else if let Some(stripped) = result_type_name.strip_suffix("Response") {
format!("{}Request", stripped)
} else {
format!("{result_type_name}Request")
};
let request_type_snake = request_type_pascal.to_snake_case();
let var_name = format!("{request_type_snake}_handle");
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
let json_val = if field.is_empty() || field == "input" {
Some(&fixture.input)
} else {
fixture.input.get(field)
};
if let Some(val) = json_val
&& !val.is_null()
{
let normalized = transform_json_keys_for_language(val, "snake_case");
let json_str = serde_json::to_string(&normalized).unwrap_or_default();
let escaped = escape_c(&json_str);
let _ = writeln!(
out,
" {prefix_upper}{request_type_pascal}* {var_name} = \
{prefix}_{request_type_snake}_from_json(\"{escaped}\");"
);
if expects_error {
let _ = writeln!(out, " if ({var_name} == NULL) {{ return; }}");
} else {
let _ = writeln!(out, " assert({var_name} != NULL && \"failed to build request\");");
}
request_handle_vars.push((arg.name.clone(), var_name));
}
}
"string" => {
let field = arg.field.strip_prefix("input.").unwrap_or(&arg.field);
let val = fixture.input.get(field);
let expr = match val {
Some(serde_json::Value::String(s)) => format!("\"{}\"", escape_c(s)),
Some(serde_json::Value::Null) | None if arg.optional => "NULL".to_string(),
Some(v) => serde_json::to_string(v).unwrap_or_else(|_| "NULL".to_string()),
None => "NULL".to_string(),
};
string_arg_exprs.push(expr);
}
_ => {
string_arg_exprs.push("NULL".to_string());
}
}
}
let fixture_id = &fixture.id;
if fixture.needs_mock_server() {
let _ = writeln!(out, " const char* mock_base = getenv(\"MOCK_SERVER_URL\");");
let _ = writeln!(out, " assert(mock_base != NULL && \"MOCK_SERVER_URL must be set\");");
let _ = writeln!(out, " char base_url[1024];");
let _ = writeln!(
out,
" snprintf(base_url, sizeof(base_url), \"%s/fixtures/{fixture_id}\", mock_base);"
);
let _ = writeln!(
out,
" {prefix_upper}{client_owner_type}* client = {prefix}_{factory}(\"test-key\", base_url, (uint64_t)-1, (uint32_t)-1, NULL);"
);
} else {
let _ = writeln!(
out,
" {prefix_upper}{client_owner_type}* client = {prefix}_{factory}(\"test-key\", NULL, (uint64_t)-1, (uint32_t)-1, NULL);"
);
}
let _ = writeln!(out, " assert(client != NULL && \"failed to create client\");");
let _ = writeln!(out, " uint8_t* out_ptr = NULL;");
let _ = writeln!(out, " uintptr_t out_len = 0;");
let _ = writeln!(out, " uintptr_t out_cap = 0;");
let mut method_args: Vec<String> = Vec::new();
for (_, v) in &request_handle_vars {
method_args.push(v.clone());
}
method_args.extend(string_arg_exprs.iter().cloned());
let extra_args = if method_args.is_empty() {
String::new()
} else {
format!(", {}", method_args.join(", "))
};
let call_fn = format!("{prefix}_default_client_{function_name}");
let _ = writeln!(
out,
" int32_t status = {call_fn}(client{extra_args}, &out_ptr, &out_len, &out_cap);"
);
if expects_error {
for (_, var_name) in &request_handle_vars {
let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
}
let _ = writeln!(out, " {prefix}_default_client_free(client);");
let _ = writeln!(out, " assert(status != 0 && \"expected call to fail\");");
let _ = writeln!(out, " {prefix}_free_bytes(out_ptr, out_len, out_cap);");
let _ = writeln!(out, "}}");
return;
}
let _ = writeln!(out, " assert(status == 0 && \"expected call to succeed\");");
let mut emitted_len_check = false;
for assertion in &fixture.assertions {
match assertion.assertion_type.as_str() {
"not_error" => {
}
"not_empty" | "not_null" => {
if !emitted_len_check {
let _ = writeln!(out, " assert(out_len > 0 && \"expected non-empty value\");");
emitted_len_check = true;
}
}
_ => {
let _ = writeln!(
out,
" /* skipped: assertion '{}' not meaningful on raw byte buffer */",
assertion.assertion_type
);
}
}
}
let _ = writeln!(out, " {prefix}_free_bytes(out_ptr, out_len, out_cap);");
for (_, var_name) in &request_handle_vars {
let req_snake = var_name.strip_suffix("_handle").unwrap_or(var_name);
let _ = writeln!(out, " {prefix}_{req_snake}_free({var_name});");
}
let _ = writeln!(out, " {prefix}_default_client_free(client);");
let _ = writeln!(out, "}}");
}