use crate::core::config::ResolvedCrateConfig;
use crate::e2e::config::E2eConfig;
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::Fixture;
use heck::ToLowerCamelCase;
use std::collections::{HashMap, HashSet};
use super::{args, assertions, stubs, types, visitor};
#[allow(clippy::too_many_arguments)]
pub(super) fn render_test_method(
out: &mut String,
fixture: &Fixture,
e2e_config: &E2eConfig,
lang: &str,
namespace: &str,
class_name: &str,
type_defs: &[crate::core::ir::TypeDef],
php_enum_names: &HashSet<String>,
enum_fields: &HashMap<String, String>,
result_is_simple: bool,
php_client_factory: Option<&str>,
options_via: &str,
adapters: &[crate::core::config::extras::AdapterConfig],
php_lang_rename_all: &str,
config: &ResolvedCrateConfig,
trait_bridge_imports: &mut Vec<String>,
) {
let mut call_config = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
call_config = crate::e2e::codegen::select_best_matching_call(call_config, e2e_config, fixture);
let per_call_getter_map = types::build_php_getter_map(
type_defs,
php_enum_names,
call_config,
e2e_config.effective_result_fields(call_config),
);
let call_field_resolver = FieldResolver::new_with_php_getters(
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),
&HashSet::new(),
&HashMap::new(),
per_call_getter_map,
);
let field_resolver = &call_field_resolver;
let call_overrides = call_config.overrides.get(lang);
let has_override = call_overrides.is_some_and(|o| o.function.is_some());
let result_is_simple =
call_config.result_is_simple || call_overrides.is_some_and(|o| o.result_is_simple) || result_is_simple;
let mut function_name = call_overrides
.and_then(|o| o.function.as_ref())
.cloned()
.unwrap_or_else(|| call_config.function.clone());
if !has_override {
function_name = function_name.to_lower_camel_case();
}
let result_var = &call_config.result_var;
let recipe = crate::e2e::codegen::recipe::ResolvedE2eCallRecipe::resolve(lang, fixture, call_config, type_defs);
let args = recipe.args;
let method_name = crate::e2e::escape::sanitize_filename(&fixture.id);
let description = &fixture.description;
let expects_error = fixture.assertions.iter().any(|a| a.assertion_type == "error");
let call_options_type = recipe.options_type.or_else(|| {
e2e_config
.call
.overrides
.get(lang)
.and_then(|o| o.options_type.as_deref())
});
let call_adapter = adapters.iter().find(|a| a.name == call_config.function.as_str());
let adapter_request_type: Option<String> = call_adapter
.and_then(|a| a.request_type.as_deref())
.map(|rt| rt.rsplit("::").next().unwrap_or(rt).to_string());
let streaming_owner_handle: Option<String> = if call_adapter.is_some_and(|a| {
matches!(a.pattern, crate::core::config::extras::AdapterPattern::Streaming) && a.owner_type.is_some()
}) {
args.iter().find(|a| a.arg_type == "handle").map(|a| a.name.clone())
} else {
None
};
let (mut setup_lines, args_str, teardown_block) = args::build_args_and_setup(
&fixture.input,
args,
class_name,
enum_fields,
fixture,
options_via,
call_options_type,
adapter_request_type.as_deref(),
namespace,
streaming_owner_handle.is_some(),
type_defs,
php_lang_rename_all,
config,
trait_bridge_imports,
);
let skip_test = call_config.skip_languages.iter().any(|l| l == "php");
if skip_test {
let rendered = crate::e2e::template_env::render(
"php/test_method.jinja",
minijinja::context! {
method_name => method_name,
description => description,
client_factory => String::new(),
setup_lines => Vec::<String>::new(),
expects_error => false,
skip_test => true,
has_usable_assertions => false,
call_expr => String::new(),
result_var => result_var,
assertions_body => String::new(),
},
);
out.push_str(&rendered);
return;
}
let mut options_already_created = !args_str.is_empty() && args_str == "$options";
if let Some(visitor_spec) = &fixture.visitor {
visitor::build_php_visitor(&mut setup_lines, visitor_spec);
if !options_already_created {
let Some(options_type) = call_options_type.or_else(|| stubs::trait_bridge_options_type(config)) else {
let rendered = crate::e2e::template_env::render(
"php/test_method.jinja",
minijinja::context! {
method_name => method_name,
description => description,
client_factory => String::new(),
setup_lines => Vec::<String>::new(),
expects_error => false,
skip_test => true,
has_usable_assertions => false,
call_expr => String::new(),
result_var => result_var,
assertions_body => String::new(),
},
);
out.push_str(&rendered);
return;
};
if options_via == "from_json" {
setup_lines.push(format!("$options = \\{namespace}\\{options_type}::from_json('{{}}');"));
setup_lines.push(format!(
"$visitorHandle = \\{namespace}\\VisitorHandle::from_php_object($visitor);"
));
setup_lines.push("$options = $options->withVisitor($visitorHandle);".to_string());
} else {
setup_lines.push(format!("$builder = \\{namespace}\\{options_type}::builder();"));
setup_lines.push("$options = $builder->visitor($visitor)->build();".to_string());
}
options_already_created = true;
}
}
let final_args = if options_already_created {
if args_str.is_empty() || args_str == "$options" {
"$options".to_string()
} else {
format!("{args_str}, $options")
}
} else {
args_str
};
let call_expr = if php_client_factory.is_some() {
format!("$client->{function_name}({final_args})")
} else if let Some(ref handle_var) = streaming_owner_handle {
format!("${handle_var}->{function_name}({final_args})")
} else {
format!("{class_name}::{function_name}({final_args})")
};
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());
let client_factory = if let Some(factory) = php_client_factory {
let fixture_id = &fixture.id;
if let Some(var) = api_key_var.filter(|_| has_mock) {
format!(
"$apiKey = getenv('{var}');\n $baseUrl = ($apiKey !== false && $apiKey !== '') ? null : getenv('MOCK_SERVER_URL') . '/fixtures/{fixture_id}';\n fwrite(STDERR, \"{fixture_id}: \" . ($baseUrl === null ? 'using real API ({var} is set)' : 'using mock server ({var} not set)') . \"\\n\");\n $client = \\{namespace}\\{class_name}::{factory}($baseUrl === null ? $apiKey : 'test-key', $baseUrl);"
)
} else if has_mock {
let base_url_expr = if fixture.has_host_root_route() {
let env_key = format!("MOCK_SERVER_{}", fixture_id.to_uppercase());
format!("(getenv('{env_key}') ?: getenv('MOCK_SERVER_URL') . '/fixtures/{fixture_id}')")
} else {
format!("getenv('MOCK_SERVER_URL') . '/fixtures/{fixture_id}'")
};
format!("$client = \\{namespace}\\{class_name}::{factory}('test-key', {base_url_expr});")
} else if let Some(var) = api_key_var {
format!(
"$apiKey = getenv('{var}');\n if (!$apiKey) {{ $this->markTestSkipped('{var} not set'); return; }}\n $client = \\{namespace}\\{class_name}::{factory}($apiKey);"
)
} else {
format!("$client = \\{namespace}\\{class_name}::{factory}('test-key');")
}
} else {
String::new()
};
let is_streaming =
crate::e2e::codegen::streaming_assertions::resolve_is_streaming(fixture, call_config.streaming_enabled());
let has_usable_assertions = fixture.assertions.iter().any(|a| {
if a.assertion_type == "error" || a.assertion_type == "not_error" {
return false;
}
match &a.field {
Some(f) if !f.is_empty() => {
if is_streaming && crate::e2e::codegen::streaming_assertions::is_streaming_virtual_field(f) {
return true;
}
let is_synthetic_field = matches!(
f.as_str(),
"chunks_have_content"
| "chunks_have_embeddings"
| "chunks_have_heading_context"
| "first_chunk_starts_with_heading"
| "embeddings"
| "embedding_dimensions"
| "embeddings_valid"
| "embeddings_finite"
| "embeddings_non_zero"
| "embeddings_normalized"
);
is_synthetic_field || field_resolver.is_valid_for_result(f)
}
_ => true,
}
});
let collect_snippet = if is_streaming {
crate::e2e::codegen::streaming_assertions::StreamingFieldResolver::collect_snippet("php", result_var, "chunks")
.unwrap_or_default()
} else {
String::new()
};
let mut fields_array_bindings: std::collections::BTreeMap<String, (String, String)> =
std::collections::BTreeMap::new();
for assertion in &fixture.assertions {
if let Some(f) = &assertion.field {
let is_enum_variant_accessor = f.contains("metadata.format.") && f.matches('.').count() >= 2;
if !f.is_empty()
&& !is_enum_variant_accessor
&& field_resolver.is_array(f)
&& field_resolver.is_valid_for_result(f)
{
if !fields_array_bindings.contains_key(f.as_str()) {
let accessor = field_resolver.accessor(f, "php", &format!("${result_var}"));
let var_name = f.to_lower_camel_case();
fields_array_bindings.insert(f.clone(), (var_name, accessor));
}
}
}
}
let mut field_bindings = String::new();
for (var_name, accessor) in fields_array_bindings.values() {
field_bindings.push_str(&format!(" ${} = {};\n", var_name, accessor));
}
let mut assertions_body = String::new();
for assertion in &fixture.assertions {
assertions::render_assertion(
&mut assertions_body,
assertion,
result_var,
field_resolver,
result_is_simple,
call_config.result_is_array,
&fields_array_bindings,
is_streaming,
);
}
if is_streaming && !expects_error && assertions_body.trim().is_empty() {
assertions_body.push_str(" $this->assertTrue(is_array($chunks), 'expected drained chunks list');\n");
}
let rendered = crate::e2e::template_env::render(
"php/test_method.jinja",
minijinja::context! {
method_name => method_name,
description => description,
client_factory => client_factory,
setup_lines => setup_lines,
expects_error => expects_error,
skip_test => fixture.assertions.is_empty(),
has_usable_assertions => has_usable_assertions || is_streaming,
call_expr => call_expr,
result_var => result_var,
collect_snippet => collect_snippet,
field_bindings => field_bindings,
assertions_body => assertions_body,
teardown_block => teardown_block,
},
);
out.push_str(&rendered);
}