use heck::ToLowerCamelCase;
use std::fmt::Write as FmtWrite;
use crate::e2e::codegen::assertion_type_skip::{
streaming_assertion_type_skip_line, streaming_assertion_value_skip_line,
};
use crate::e2e::codegen::field_skip::{FieldSkip, nested_wildcard_skip_line};
use crate::e2e::codegen::payload_union_skip::{UnionLoweringTarget, payload_union_skip_line};
use crate::e2e::escape::escape_kotlin;
use crate::e2e::field_access::FieldResolver;
use crate::e2e::fixture::Assertion;
#[allow(clippy::too_many_arguments)]
pub(super) fn try_render_field_shape_gates(
out: &mut String,
assertion: &Assertion,
field_resolver: &FieldResolver,
result_var: &str,
result_is_simple: bool,
is_streaming: bool,
kotlin_android_style: bool,
fields_c_types: &std::collections::HashMap<String, String>,
) -> bool {
if try_render_streaming_usage_field_assertion(out, assertion, is_streaming, kotlin_android_style, fields_c_types) {
return true;
}
if try_render_streaming_virtual_field_assertion(out, assertion, is_streaming, kotlin_android_style) {
return true;
}
if try_skip_field_not_available_on_result_type(out, assertion, field_resolver) {
return true;
}
if try_render_discriminated_union_android_assertion(
out,
assertion,
field_resolver,
result_var,
kotlin_android_style,
) {
return true;
}
if try_render_generic_union_fallback(out, assertion, field_resolver, result_var, kotlin_android_style) {
return true;
}
let accessor_lang = if kotlin_android_style {
"kotlin_android"
} else {
"kotlin"
};
if try_render_wildcard_traversal_assertion(
out,
assertion,
result_var,
field_resolver,
result_is_simple,
accessor_lang,
) {
return true;
}
try_skip_payload_union_scalar_lowering(out, assertion, field_resolver, kotlin_android_style)
}
fn try_skip_payload_union_scalar_lowering(
out: &mut String,
assertion: &Assertion,
field_resolver: &FieldResolver,
kotlin_android_style: bool,
) -> bool {
let target = if kotlin_android_style {
UnionLoweringTarget::KotlinAndroid
} else {
UnionLoweringTarget::KotlinJvm
};
let Some(line) = payload_union_skip_line(" ", "//", field_resolver, assertion.field.as_deref(), target)
else {
return false;
};
let _ = writeln!(out, "{line}");
true
}
pub(super) fn try_render_streaming_usage_field_assertion(
out: &mut String,
assertion: &Assertion,
is_streaming: bool,
kotlin_android_style: bool,
fields_c_types: &std::collections::HashMap<String, String>,
) -> bool {
if is_streaming
&& let Some(f) = &assertion.field
&& (f == "usage" || f.starts_with("usage."))
{
let expr = resolve_streaming_usage_expr(f, kotlin_android_style);
let field_is_long = fields_c_types
.get(f.as_str())
.is_some_and(|t| matches!(t.as_str(), "uint64_t" | "int64_t"));
let line = render_streaming_usage_line(assertion, f, &expr, field_is_long);
out.push_str(&line);
return true;
}
false
}
fn resolve_streaming_usage_expr(f: &str, kotlin_android_style: bool) -> String {
let stream_lang = if kotlin_android_style {
"kotlin_android"
} else {
"kotlin"
};
let base_expr =
crate::e2e::codegen::streaming_assertions::StreamingFieldResolver::accessor("usage", stream_lang, "chunks")
.unwrap_or_else(|| {
if kotlin_android_style {
"(if (chunks.isEmpty()) null else chunks.last().usage)".to_string()
} else {
"(if (chunks.isEmpty()) null else chunks.last().usage())".to_string()
}
});
if let Some(tail) = f.strip_prefix("usage.") {
if kotlin_android_style {
tail.split('.')
.fold(base_expr, |acc, seg| format!("{acc}?.{}", seg.to_lower_camel_case()))
} else {
tail.split('.')
.fold(base_expr, |acc, seg| format!("{acc}?.{}()", seg.to_lower_camel_case()))
}
} else {
base_expr
}
}
fn render_streaming_usage_line(assertion: &Assertion, f: &str, expr: &str, field_is_long: bool) -> String {
match assertion.assertion_type.as_str() {
"equals" => {
if let Some(expected) = &assertion.value {
let kotlin_val = if field_is_long && expected.is_number() && !expected.is_f64() {
format!("{}L", expected)
} else {
super::values::json_to_kotlin(expected)
};
format!(" assertEquals({kotlin_val}, {expr}!!)\n")
} else {
streaming_assertion_value_skip_line(" ", "//", f, &assertion.assertion_type) + "\n"
}
}
_ => streaming_assertion_type_skip_line(" ", "//", f, &assertion.assertion_type) + "\n",
}
}
pub(super) fn try_render_streaming_virtual_field_assertion(
out: &mut String,
assertion: &Assertion,
is_streaming: bool,
kotlin_android_style: bool,
) -> bool {
if let Some(f) = &assertion.field
&& is_streaming
&& !f.is_empty()
&& crate::e2e::codegen::streaming_assertions::is_streaming_virtual_field(f)
{
let stream_lang = if kotlin_android_style {
"kotlin_android"
} else {
"kotlin"
};
if let Some(expr) =
crate::e2e::codegen::streaming_assertions::StreamingFieldResolver::accessor(f, stream_lang, "chunks")
{
let line = render_streaming_virtual_field_line(assertion, f, &expr);
out.push_str(&line);
} else {
let _ = writeln!(
out,
" // skipped: {}",
FieldSkip::StreamingAssertionOnUnsupportedField.message(f)
);
}
return true;
}
false
}
fn render_streaming_virtual_field_line(assertion: &Assertion, f: &str, expr: &str) -> String {
match assertion.assertion_type.as_str() {
"count_min" | "count_equals" => render_streaming_virtual_field_count_line(assertion, f, expr),
"equals" => {
if let Some(serde_json::Value::String(s)) = &assertion.value {
let literal = super::values::kotlin_string_literal(s);
format!(" assertEquals({literal}, {expr})\n")
} else if let Some(b) = assertion.value.as_ref().and_then(|v| v.as_bool()) {
format!(" assertEquals({b}, {expr})\n")
} else {
streaming_assertion_value_skip_line(" ", "//", f, &assertion.assertion_type) + "\n"
}
}
"not_empty" => {
format!(" assertFalse({expr}.isEmpty(), \"expected non-empty\")\n")
}
"is_empty" => {
format!(" assertTrue({expr}.isEmpty(), \"expected empty\")\n")
}
"is_true" => {
format!(" assertTrue({expr} == true, \"expected true\")\n")
}
"is_false" => {
format!(" assertTrue({expr} == false, \"expected false\")\n")
}
"greater_than" => {
if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
format!(" assertTrue({expr} > {n}, \"expected > {n}\")\n")
} else {
streaming_assertion_value_skip_line(" ", "//", f, &assertion.assertion_type) + "\n"
}
}
"contains" => render_streaming_virtual_field_contains_line(assertion, f, expr),
_ => format!(
"{}\n",
streaming_assertion_type_skip_line(" ", "//", f, &assertion.assertion_type)
),
}
}
fn render_streaming_virtual_field_count_line(assertion: &Assertion, f: &str, expr: &str) -> String {
match assertion.assertion_type.as_str() {
"count_min" => {
if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
format!(" assertTrue({expr}.size >= {n}, \"expected >= {n} chunks\")\n")
} else {
streaming_assertion_value_skip_line(" ", "//", f, &assertion.assertion_type) + "\n"
}
}
"count_equals" => {
if let Some(n) = assertion.value.as_ref().and_then(|v| v.as_u64()) {
format!(" assertEquals({n}.toLong(), {expr}.size.toLong(), \"expected exactly {n} elements\")\n")
} else {
streaming_assertion_value_skip_line(" ", "//", f, &assertion.assertion_type) + "\n"
}
}
_ => unreachable!("only called for count_min/count_equals"),
}
}
fn render_streaming_virtual_field_contains_line(assertion: &Assertion, f: &str, expr: &str) -> String {
if let Some(serde_json::Value::String(s)) = &assertion.value {
let escaped = escape_kotlin(s);
format!(
" assertTrue({expr}.toString().lowercase().contains(\"{escaped}\".lowercase()), \"expected to contain: {escaped}\")\n"
)
} else {
streaming_assertion_value_skip_line(" ", "//", f, &assertion.assertion_type) + "\n"
}
}
pub(super) fn try_skip_field_not_available_on_result_type(
out: &mut String,
assertion: &Assertion,
field_resolver: &FieldResolver,
) -> bool {
if let Some(f) = &assertion.field
&& !f.is_empty()
&& !field_resolver.is_valid_for_result(f)
{
let _ = writeln!(
out,
" // skipped: {}",
FieldSkip::NotAvailableOnResultType.message(f)
);
return true;
}
false
}
pub(super) fn try_render_discriminated_union_android_assertion(
out: &mut String,
assertion: &Assertion,
field_resolver: &FieldResolver,
result_var: &str,
kotlin_android_style: bool,
) -> bool {
if kotlin_android_style
&& let Some(f) = assertion.field.as_deref().filter(|f| !f.is_empty())
&& let Some((variant_pascal, inner_field)) = super::discriminated::parse_discriminated_union_access(f)
{
let variant_var = format!("format{variant_pascal}");
let (container, field_is_collection) =
resolve_discriminated_union_android_binding(field_resolver, f, result_var, &variant_pascal, &inner_field);
let _ = writeln!(out, " when (val {variant_var} = {container}) {{");
let _ = writeln!(out, " is FormatMetadata.{variant_pascal} -> {{");
super::discriminated::render_discriminated_union_assertion(
out,
assertion,
&variant_var,
"metadata",
&inner_field,
field_is_collection,
);
let _ = writeln!(out, " }}");
let _ = writeln!(
out,
" else -> kotlin.test.assertTrue(false, \"Expected {variant_pascal} variant\")"
);
let _ = writeln!(out, " }}");
return true;
}
false
}
fn resolve_discriminated_union_android_binding(
field_resolver: &FieldResolver,
f: &str,
result_var: &str,
variant_pascal: &str,
inner_field: &str,
) -> (String, bool) {
let format_path = match f.find(".format") {
Some(idx) => &f[..idx + ".format".len()],
None => f,
};
let container = field_resolver.accessor(format_path, "kotlin_android", result_var);
let field_is_collection = if inner_field.is_empty() {
field_resolver.union_variant_payload_is_collection("FormatMetadata", variant_pascal)
} else {
field_resolver.union_variant_field_is_collection(format_path, variant_pascal, inner_field)
};
(container, field_is_collection)
}
pub(super) fn try_render_generic_union_fallback(
out: &mut String,
assertion: &Assertion,
field_resolver: &FieldResolver,
result_var: &str,
kotlin_android_style: bool,
) -> bool {
if let Some(f) = assertion.field.as_deref().filter(|f| !f.is_empty())
&& super::discriminated::try_render_generic_union_assertion(
out,
assertion,
field_resolver,
result_var,
kotlin_android_style,
f,
)
{
return true;
}
false
}
pub(super) fn try_render_wildcard_traversal_assertion(
out: &mut String,
assertion: &Assertion,
result_var: &str,
field_resolver: &FieldResolver,
result_is_simple: bool,
accessor_lang: &str,
) -> bool {
if !result_is_simple
&& let Some(f) = assertion.field.as_deref().filter(|f| !f.is_empty())
&& let Some((array_part, elem_part)) = field_resolver.wildcard_split(f)
{
if let Some(line) = nested_wildcard_skip_line(" ", "//", f, &elem_part) {
let _ = writeln!(out, "{line}");
return true;
}
let array_accessor = resolve_wildcard_array_accessor(field_resolver, result_var, &array_part, accessor_lang);
let elem_accessor = field_resolver.element_accessor(&elem_part, accessor_lang, "e");
render_wildcard_traversal_match(out, assertion, f, &array_accessor, &elem_accessor);
return true;
}
false
}
fn resolve_wildcard_array_accessor(
field_resolver: &FieldResolver,
result_var: &str,
array_part: &str,
accessor_lang: &str,
) -> String {
let raw_array_accessor = if array_part.is_empty() {
result_var.to_string()
} else {
field_resolver.accessor(array_part, accessor_lang, result_var)
};
let array_is_nullable =
raw_array_accessor.contains("?.") || (!array_part.is_empty() && field_resolver.is_optional(array_part));
if array_is_nullable {
format!("{raw_array_accessor}.orEmpty()")
} else {
raw_array_accessor
}
}
fn render_wildcard_traversal_match(
out: &mut String,
assertion: &Assertion,
f: &str,
array_accessor: &str,
elem_accessor: &str,
) {
match assertion.assertion_type.as_str() {
"contains" | "contains_all" | "not_contains" => {
let negated = assertion.assertion_type == "not_contains";
let assert_fn = if negated { "assertFalse" } else { "assertTrue" };
let expectation = if negated {
"expected NOT to contain: "
} else {
"expected to contain: "
};
for expected in assertion.expected_values() {
let kotlin_val = super::values::json_to_kotlin(expected);
let _ = writeln!(
out,
" {assert_fn}({array_accessor}.any {{ e -> {elem_accessor}.toString().contains({kotlin_val}) }}, \"{expectation}\" + {kotlin_val})"
);
}
}
"not_empty" => {
let _ = writeln!(
out,
" assertTrue({array_accessor}.any {{ e -> {elem_accessor}.toString().isNotEmpty() }}, \"expected a non-empty element in '{f}'\")"
);
}
other => {
let _ = writeln!(
out,
" // skipped: unsupported traversal assertion '{other}' on '{f}'"
);
}
}
}