001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.junit.bct;
018
019import static java.util.stream.Collectors.*;
020import static org.apache.juneau.junit.bct.Utils.*;
021import static org.junit.jupiter.api.Assertions.*;
022
023import java.util.*;
024import java.util.function.*;
025import java.util.stream.*;
026
027import org.opentest4j.*;
028
029/**
030 * Comprehensive utility class for Bean-Centric Tests (BCT) and general testing operations.
031 *
032 * <p>This class extends the functionality provided by the JUnit Assertions class, with particular emphasis
033 * on the Bean-Centric Testing (BCT) framework. BCT enables sophisticated assertion patterns for
034 * testing object properties, collections, maps, and complex nested structures with minimal code.</p>
035 *
036 * <h5 class='section'>Bean-Centric Testing (BCT) Framework:</h5>
037 * <p>The BCT framework consists of several key components:</p>
038 * <ul>
039 *    <li><b>{@link BeanConverter}:</b> Core interface for object conversion and property access</li>
040 *    <li><b>{@link BasicBeanConverter}:</b> Default implementation with extensible type handlers</li>
041 *    <li><b>Assertion Methods:</b> High-level testing methods that leverage the converter framework</li>
042 * </ul>
043 *
044 * <h5 class='section'>Primary BCT Assertion Methods:</h5>
045 * <dl>
046 *    <dt><b>{@link #assertBean(Object, String, String)}</b></dt>
047 *    <dd>Tests object properties with nested syntax support and collection iteration</dd>
048 *
049 *    <dt><b>{@link #assertBeans(Collection, String, String...)}</b></dt>
050 *    <dd>Tests collections of objects by extracting and comparing specific fields</dd>
051 *
052 *    <dt><b>{@link #assertMapped(Object, java.util.function.BiFunction, String, String)}</b></dt>
053 *    <dd>Tests custom property access using BiFunction for non-standard objects</dd>
054 *
055 *    <dt><b>{@link #assertList(List, Object...)}</b></dt>
056 *    <dd>Tests list/collection elements with varargs for expected values</dd>
057 * </dl>
058 *
059 * <h5 class='section'>BCT Advanced Features:</h5>
060 * <ul>
061 *    <li><b>Nested Property Syntax:</b> "address{street,city}" for testing nested objects</li>
062 *    <li><b>Collection Iteration:</b> "#{address{street,city}}" syntax for testing all elements</li>
063 *    <li><b>Universal Size Properties:</b> "length" and "size" work on all collection types</li>
064 *    <li><b>Array/List Access:</b> Numeric indices for element-specific testing</li>
065 *    <li><b>Method Chaining:</b> Fluent setters can be tested directly</li>
066 *    <li><b>Direct Field Access:</b> Public fields accessed without getters</li>
067 *    <li><b>Map Key Access:</b> Including special <js>"&lt;null&gt;"</js> syntax for null keys</li>
068 * </ul>
069 *
070 * <h5 class='section'>Converter Extensibility:</h5>
071 * <p>The BCT framework is built on the extensible {@link BasicBeanConverter} which allows:</p>
072 * <ul>
073 *    <li><b>Custom Stringifiers:</b> Type-specific string conversion logic</li>
074 *    <li><b>Custom Listifiers:</b> Collection-type conversion for iteration</li>
075 *    <li><b>Custom Swappers:</b> Object transformation before conversion</li>
076 *    <li><b>Custom PropertyExtractors:</b> Property extraction</li>
077 *    <li><b>Configurable Settings:</b> Formatting, delimiters, and display options</li>
078 * </ul>
079 *
080 * <h5 class='section'>Usage Examples:</h5>
081 *
082 * <p><b>Basic Property Testing:</b></p>
083 * <p class='bjava'>
084 *    <jc>// Test multiple properties</jc>
085 *    <jsm>assertBean</jsm>(<jv>user</jv>, <js>"name,age,active"</js>, <js>"John,30,true"</js>);
086 *
087 *    <jc>// Test nested properties - user has getAddress() returning Address with getStreet() and getCity()</jc>
088 *    <jsm>assertBean</jsm>(<jv>user</jv>, <js>"name,address{street,city}"</js>, <js>"John,{123 Main St,Springfield}"</js>);
089 * </p>
090 *
091 * <p><b>Collection and Array Testing:</b></p>
092 * <p class='bjava'>
093 *    <jc>// Test collection size and iterate over all elements - order has getItems() returning List&lt;Product&gt; where Product has getName()</jc>
094 *    <jsm>assertBean</jsm>(<jv>order</jv>, <js>"items{length,#{name}}"</js>, <js>"{3,[{Laptop},{Phone},{Tablet}]}"</js>);
095 *
096 *    <jc>// Test specific array elements - listOfData is a List&lt;DataObject&gt; where DataObject has getData()</jc>
097 *    <jsm>assertBean</jsm>(<jv>listOfData</jv>, <js>"0{data},1{data}"</js>, <js>"{100},{200}"</js>);
098 * </p>
099 *
100 * <p><b>Collection Testing:</b></p>
101 * <p class='bjava'>
102 *    <jc>// Test list elements</jc>
103 *    <jsm>assertList</jsm>(tags, <js>"red"</js>, <js>"green"</js>, <js>"blue"</js>);
104 *
105 *    <jc>// Test map entries using assertBean</jc>
106 *    <jsm>assertBean</jsm>(<jv>config</jv>, <js>"timeout,retries"</js>, <js>"30000,3"</js>);
107 * </p>
108 *
109 * <p><b>Custom Property Access:</b></p>
110 * <p class='bjava'>
111 *    <jc>// Test with custom accessor function</jc>
112 *    <jsm>assertMapped</jsm>(<jv>myObject</jv>, (<jp>obj</jp>, <jp>prop</jp>) -> <jp>obj</jp>.getProperty(<jp>prop</jp>),
113 *       <js>"prop1,prop2"</js>, <js>"value1,value2"</js>);
114 * </p>
115 *
116 * <h5 class='section'>Performance and Thread Safety:</h5>
117 * <p>The BCT framework is designed for high performance with:</p>
118 * <ul>
119 *    <li><b>Caching:</b> Type-to-handler mappings cached for fast lookup</li>
120 *    <li><b>Thread Safety:</b> All operations are thread-safe for concurrent testing</li>
121 *    <li><b>Minimal Allocation:</b> Efficient object reuse and minimal temporary objects</li>
122 * </ul>
123 *
124 * @see BeanConverter
125 * @see BasicBeanConverter
126 */
127public class BctAssertions {
128
129   private static final BeanConverter DEFAULT_CONVERTER = BasicBeanConverter.DEFAULT;
130
131   private BctAssertions() {}
132
133   /**
134    * Creates a new {@link AssertionArgs} instance for configuring assertion behavior.
135    *
136    * <p>AssertionArgs provides fluent configuration for customizing assertion behavior, including:</p>
137    * <ul>
138    *    <li><b>Custom Messages:</b> Static strings, parameterized with <code>MessageFormat</code>, or dynamic suppliers</li>
139    *    <li><b>Custom Bean Converters:</b> Override default object-to-string conversion behavior</li>
140    *    <li><b>Timeout Configuration:</b> Set timeouts for operations that may take time</li>
141    * </ul>
142    *
143    * <h5 class='section'>Usage Examples:</h5>
144    * <p class='bjava'>
145    *    <jc>// Static message</jc>
146    *    <jsm>assertBean</jsm>(<jsm>args</jsm>().setMessage(<js>"User validation failed"</js>),
147    *       <jv>user</jv>, <js>"name,age"</js>, <js>"John,30"</js>);
148    *
149    *    <jc>// Parameterized message</jc>
150    *    <jsm>assertBean</jsm>(<jsm>args</jsm>().setMessage(<js>"Test failed for user {0}"</js>, <jv>userId</jv>),
151    *       <jv>user</jv>, <js>"status"</js>, <js>"ACTIVE"</js>);
152    *
153    *    <jc>// Dynamic message with supplier</jc>
154    *    <jsm>assertBean</jsm>(<jsm>args</jsm>().setMessage(() -> <js>"Test failed at "</js> + Instant.<jsm>now</jsm>()),
155    *       <jv>result</jv>, <js>"success"</js>, <js>"true"</js>);
156    *
157    *    <jc>// Custom bean converter</jc>
158    *    <jk>var</jk> <jv>converter</jv> = BasicBeanConverter.<jsm>builder</jsm>()
159    *       .defaultSettings()
160    *       .addStringifier(LocalDate.<jk>class</jk>, <jp>date</jp> -> <jp>date</jp>.format(DateTimeFormatter.<jsf>ISO_LOCAL_DATE</jsf>))
161    *       .build();
162    *    <jsm>assertBean</jsm>(<jsm>args</jsm>().setBeanConverter(<jv>converter</jv>),
163    *       <jv>event</jv>, <js>"date"</js>, <js>"2023-12-01"</js>);
164    * </p>
165    *
166    * @return A new AssertionArgs instance for fluent configuration
167    * @see AssertionArgs
168    */
169   public static AssertionArgs args() {
170      return new AssertionArgs();
171   }
172
173   /**
174    * Asserts that the fields/properties on the specified bean are the specified values after being converted to strings.
175    *
176    * <p>This is the primary method for Bean-Centric Tests (BCT), supporting extensive property validation
177    * patterns including nested objects, collections, arrays, method chaining, direct field access, collection iteration
178    * with <js>"#{property}"</js> syntax, and universal <js>"length"</js>/<js>"size"</js> properties for all collection types.</p>
179    *
180    * <p>The method uses the {@link BasicBeanConverter#DEFAULT} converter internally for object introspection
181    * and value extraction. The converter provides sophisticated property access through the {@link BeanConverter}
182    * interface, supporting multiple fallback mechanisms for accessing object properties and values.</p>
183    *
184    * <h5 class='section'>Basic Usage:</h5>
185    * <p class='bjava'>
186    *    <jc>// Test multiple properties</jc>
187    *    <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"prop1,prop2,prop3"</js>, <js>"val1,val2,val3"</js>);
188    *
189    *    <jc>// Test single property</jc>
190    *    <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"name"</js>, <js>"John"</js>);
191    * </p>
192    *
193    * <h5 class='section'>Nested Property Testing:</h5>
194    * <p class='bjava'>
195    *    <jc>// Test nested bean properties</jc>
196    *    <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"name,address{street,city,state}"</js>, <js>"John,{123 Main St,Springfield,IL}"</js>);
197    *
198    *    <jc>// Test arbitrarily deep nesting</jc>
199    *    <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"name,person{address{geo{lat,lon}}}"</js>, <js>"John,{{{40.7,-74.0}}}"</js>);
200    * </p>
201    *
202    * <h5 class='section'>Array, List, and Stream Testing:</h5>
203    * <p class='bjava'>
204    *    <jc>// Test array/list elements by index - items is a String[] or List&lt;String&gt;</jc>
205    *    <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"items{0,1,2}"</js>, <js>"{item1,item2,item3}"</js>);
206    *
207    *    <jc>// Test nested properties within array elements - orders is a List&lt;Order&gt; where Order has getId() and getTotal()</jc>
208    *    <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"orders{0{id,total}}"</js>, <js>"{{123,99.95}}"</js>);
209    *
210    *    <jc>// Test array length property - items can be any array or collection type</jc>
211    *    <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"items{length}"</js>, <js>"{5}"</js>);
212    *
213    *    <jc>// Works with any iterable type including Streams - userStream returns a Stream&lt;User&gt; where User has getName()</jc>
214    *    <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"userStream{#{name}}"</js>, <js>"[{Alice},{Bob}]"</js>);
215    * </p>
216    *
217    * <h5 class='section'>Collection Iteration Syntax:</h5>
218    * <p class='bjava'>
219    *    <jc>// Test properties across ALL elements in a collection using #{...} syntax - userList is a List&lt;User&gt; where User has getName()</jc>
220    *    <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"userList{#{name}}"</js>, <js>"[{John},{Jane},{Bob}]"</js>);
221    *
222    *    <jc>// Test multiple properties from each element - orderList is a List&lt;Order&gt; where Order has getId() and getStatus()</jc>
223    *    <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"orderList{#{id,status}}"</js>, <js>"[{123,ACTIVE},{124,PENDING}]"</js>);
224    *
225    *    <jc>// Works with nested properties within each element - customers is a List&lt;Customer&gt; where Customer has getAddress() returning Address with getCity()</jc>
226    *    <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"customers{#{address{city}}}"</js>, <js>"[{{New York}},{{Los Angeles}}]"</js>);
227    *
228    *    <jc>// Works with arrays and any iterable collection type (including Streams)</jc>
229    *    <jsm>assertBean</jsm>(<jv>config</jv>, <js>"itemArray{#{type}}"</js>, <js>"[{String},{Integer},{Boolean}]"</js>);
230    *    <jsm>assertBean</jsm>(<jv>data</jv>, <js>"statusSet{#{name}}"</js>, <js>"[{ACTIVE},{PENDING},{CANCELLED}]"</js>);
231    *    <jsm>assertBean</jsm>(<jv>processor</jv>, <js>"dataStream{#{value}}"</js>, <js>"[{A},{B},{C}]"</js>);
232    * </p>
233    *
234    * <h5 class='section'>Universal Collection Size Properties:</h5>
235    * <p class='bjava'>
236    *    <jc>// Both 'length' and 'size' work universally across all collection types</jc>
237    *    <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"myArray{length}"</js>, <js>"{5}"</js>);        <jc>// Arrays</jc>
238    *    <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"myArray{size}"</js>, <js>"{5}"</js>);          <jc>// Also works for arrays</jc>
239    *
240    *    <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"myList{size}"</js>, <js>"{3}"</js>);           <jc>// Collections</jc>
241    *    <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"myList{length}"</js>, <js>"{3}"</js>);         <jc>// Also works for collections</jc>
242    *
243    *    <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"myMap{size}"</js>, <js>"{7}"</js>);            <jc>// Maps</jc>
244    *    <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"myMap{length}"</js>, <js>"{7}"</js>);          <jc>// Also works for maps</jc>
245    * </p>
246    *
247    * <h5 class='section'>Class Name Testing:</h5>
248    * <p class='bjava'>
249    *    <jc>// Test class properties (prefer simple names for maintainability)</jc>
250    *    <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"obj{class{simpleName}}"</js>, <js>"{{MyClass}}"</js>);
251    *
252    *    <jc>// Test full class names when needed</jc>
253    *    <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"obj{class{name}}"</js>, <js>"{{com.example.MyClass}}"</js>);
254    * </p>
255    *
256    * <h5 class='section'>Method Chaining Support:</h5>
257    * <p class='bjava'>
258    *    <jc>// Test fluent setter chains (returns same object)</jc>
259    *    <jsm>assertBean</jsm>(
260    *       <jv>item</jv>.setType(<js>"foo"</js>).setFormat(<js>"bar"</js>).setDefault(<js>"baz"</js>),
261    *       <js>"type,format,default"</js>,
262    *       <js>"foo,bar,baz"</js>
263    *    );
264    * </p>
265    *
266    * <h5 class='section'>Advanced Collection Analysis:</h5>
267    * <p class='bjava'>
268    *    <jc>// Combine size/length, metadata, and content iteration in single assertions - users is a List&lt;User&gt;</jc>
269    *    <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"users{length,class{simpleName},#{name}}"</js>,
270    *       <js>"{3,{ArrayList},[{John},{Jane},{Bob}]}"</js>);
271    *
272    *    <jc>// Comprehensive collection validation with multiple iteration patterns - items is a List&lt;Product&gt; where Product has getName() and getPrice()</jc>
273    *    <jsm>assertBean</jsm>(<jv>order</jv>, <js>"items{size,#{name},#{price}}"</js>,
274    *       <js>"{3,[{Laptop},{Phone},{Tablet}],[{999.99},{599.99},{399.99}]}"</js>);
275    *
276    *    <jc>// Perfect for validation testing - verify error count and details; errors is a List&lt;ValidationError&gt; where ValidationError has getField() and getCode()</jc>
277    *    <jsm>assertBean</jsm>(<jv>result</jv>, <js>"errors{length,#{field},#{code}}"</js>,
278    *       <js>"{2,[{email},{password}],[{E001},{E002}]}"</js>);
279    *
280    *    <jc>// Mixed collection types with consistent syntax - results and metadata are different collection types</jc>
281    *    <jsm>assertBean</jsm>(<jv>response</jv>, <js>"results{size},metadata{length}"</js>, <js>"{25},{4}"</js>);
282    * </p>
283    *
284    * <h5 class='section'>Direct Field Access:</h5>
285    * <p class='bjava'>
286    *    <jc>// Test public fields directly (no getters required)</jc>
287    *    <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"f1,f2,f3"</js>, <js>"val1,val2,val3"</js>);
288    *
289    *    <jc>// Test field properties with chaining</jc>
290    *    <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"f1{length},f2{class{simpleName}}"</js>, <js>"{5},{{String}}"</js>);
291    * </p>
292    *
293    * <h5 class='section'>Map Testing:</h5>
294    * <p class='bjava'>
295    *    <jc>// Test map values by key</jc>
296    *    <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"configMap{timeout,retries}"</js>, <js>"{30000,3}"</js>);
297    *
298    *    <jc>// Test map size</jc>
299    *    <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"settings{size}"</js>, <js>"{5}"</js>);
300    *
301    *    <jc>// Test null keys using special &lt;null&gt; syntax</jc>
302    *    <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"mapWithNullKey{&lt;null&gt;}"</js>, <js>"{nullKeyValue}"</js>);
303    * </p>
304    *
305    * <h5 class='section'>Collection and Boolean Values:</h5>
306    * <p class='bjava'>
307    *    <jc>// Test boolean values</jc>
308    *    <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"enabled,visible"</js>, <js>"true,false"</js>);
309    *
310    *    <jc>// Test enum collections</jc>
311    *    <jsm>assertBean</jsm>(<jv>myBean</jv>, <js>"statuses"</js>, <js>"[ACTIVE,PENDING]"</js>);
312    * </p>
313    *
314    * <h5 class='section'>Value Syntax Rules:</h5>
315    * <ul>
316    *    <li><b>Simple values:</b> <js>"value"</js> for direct property values</li>
317    *    <li><b>Nested values:</b> <js>"{value}"</js> for single-level nested properties</li>
318    *    <li><b>Deep nested values:</b> <js>"{{value}}"</js>, <js>"{{{value}}}"</js> for multiple nesting levels</li>
319    *    <li><b>Array/Collection values:</b> <js>"[item1,item2]"</js> for collections</li>
320    *    <li><b>Collection iteration:</b> <js>"#{property}"</js> iterates over ALL collection elements, returns <js>"[{val1},{val2}]"</js></li>
321    *    <li><b>Universal size properties:</b> <js>"length"</js> and <js>"size"</js> work on arrays, collections, and maps</li>
322    *    <li><b>Boolean values:</b> <js>"true"</js>, <js>"false"</js></li>
323    *    <li><b>Null values:</b> <js>"null"</js></li>
324    * </ul>
325    *
326    * <h5 class='section'>Property Access Priority:</h5>
327    * <ol>
328    *    <li><b>Collection/Array access:</b> Numeric indices for arrays/lists (e.g., <js>"0"</js>, <js>"1"</js>)</li>
329    *    <li><b>Universal size properties:</b> <js>"length"</js> and <js>"size"</js> for arrays, collections, and maps</li>
330    *    <li><b>Map key access:</b> Direct key lookup for Map objects (including <js>"&lt;null&gt;"</js> for null keys)</li>
331    *    <li><b>is{Property}()</b> methods (for boolean properties)</li>
332    *    <li><b>get{Property}()</b> methods</li>
333    *    <li><b>Public fields</b> (direct field access)</li>
334    * </ol>
335    *
336    * @param actual The bean object to test. Must not be null.
337    * @param fields Comma-delimited list of property names to test. Supports nested syntax with {}.
338    * @param expected Comma-delimited list of expected values. Must match the order of fields.
339    * @throws NullPointerException if the bean is null
340    * @throws AssertionError if any property values don't match expected values
341    * @see BeanConverter
342    * @see BasicBeanConverter
343    */
344   public static void assertBean(Object actual, String fields, String expected) {
345      assertBean(args(), actual, fields, expected);
346   }
347
348   /**
349    * Same as {@link #assertBean(Object, String, String)} but with configurable assertion behavior.
350    *
351    * @param args Assertion configuration. See {@link #args()} for usage examples.
352    * @param actual The bean to test. Must not be null.
353    * @param fields A comma-delimited list of bean property names (supports nested syntax).
354    * @param expected The expected property values as a comma-delimited string.
355    * @see #assertBean(Object, String, String)
356    * @see #args()
357    */
358   public static void assertBean(AssertionArgs args, Object actual, String fields, String expected) {
359      assertNotNull(actual, "Actual was null.");
360      assertArgNotNull("args", args);
361      assertArgNotNull("fields", fields);
362      assertArgNotNull("expected", expected);
363      assertEquals(
364         expected,
365         tokenize(fields).stream().map(x -> args.getBeanConverter().orElse(DEFAULT_CONVERTER).getNested(actual, x)).collect(joining(",")),
366         args.getMessage("Bean assertion failed.")
367         );
368   }
369
370   /**
371    * Asserts that multiple beans in a collection have the expected property values.
372    *
373    * <p>This method validates that each bean in a collection has the specified property values,
374    * using the same property access logic as {@link #assertBean(Object, String, String)}.
375    * It's perfect for testing collections of similar objects or validation results.</p>
376    *
377    * <h5 class='section'>Basic Usage:</h5>
378    * <p class='bjava'>
379    *    <jc>// Test list of user beans</jc>
380    *    <jsm>assertBeans</jsm>(<jv>userList</jv>, <js>"name,age"</js>,
381    *       <js>"John,25"</js>, <js>"Jane,30"</js>, <js>"Bob,35"</js>);
382    * </p>
383    *
384    * <h5 class='section'>Complex Property Testing:</h5>
385    * <p class='bjava'>
386    *    <jc>// Test nested properties across multiple beans - orderList is a List&lt;Order&gt; where Order has getId() and getCustomer() returning Customer with getName() and getEmail()</jc>
387    *    <jsm>assertBeans</jsm>(<jv>orderList</jv>, <js>"id,customer{name,email}"</js>,
388    *       <js>"1,{John,john@example.com}"</js>,
389    *       <js>"2,{Jane,jane@example.com}"</js>);
390    *
391    *    <jc>// Test collection properties within beans - cartList is a List&lt;ShoppingCart&gt; where ShoppingCart has getItems() returning List&lt;Product&gt; and getTotal()</jc>
392    *    <jsm>assertBeans</jsm>(<jv>cartList</jv>, <js>"items{0{name}},total"</js>,
393    *       <js>"{{Laptop}},999.99"</js>,
394    *       <js>"{{Phone}},599.99"</js>);
395    * </p>
396    *
397    * <h5 class='section'>Validation Testing:</h5>
398    * <p class='bjava'>
399    *    <jc>// Test validation results</jc>
400    *    <jsm>assertBeans</jsm>(<jv>validationErrors</jv>, <js>"field,message,code"</js>,
401    *       <js>"email,Invalid email format,E001"</js>,
402    *       <js>"age,Must be 18 or older,E002"</js>);
403    * </p>
404    *
405    * <h5 class='section'>Collection Iteration Testing:</h5>
406    * <p class='bjava'>
407    *    <jc>// Test collection iteration within beans (#{...} syntax)</jc>
408    *    <jsm>assertBeans</jsm>(<jv>departmentList</jv>, <js>"name,employees{#{name}}"</js>,
409    *       <js>"Engineering,[{Alice},{Bob},{Charlie}]"</js>,
410    *       <js>"Marketing,[{David},{Eve}]"</js>);
411    * </p>
412    *
413    * <h5 class='section'>Parser Result Testing:</h5>
414    * <p class='bjava'>
415    *    <jc>// Test parsed object collections</jc>
416    *    <jk>var</jk> <jv>parsed</jv> = JsonParser.<jsf>DEFAULT</jsf>.parse(<jv>jsonArray</jv>, MyBean[].class);
417    *    <jsm>assertBeans</jsm>(<jsm>Arrays.asList</jsm>(<jv>parsed</jv>), <js>"prop1,prop2"</js>,
418    *       <js>"val1,val2"</js>, <js>"val3,val4"</js>);
419    * </p>
420    *
421    * @param actual The collection of beans to check. Must not be null.
422    * @param fields A comma-delimited list of bean property names (supports nested syntax).
423    * @param expected Array of expected value strings, one per bean. Each string contains comma-delimited values matching the fields.
424    * @throws AssertionError if the collection size doesn't match values array length or if any bean properties don't match
425    * @see #assertBean(Object, String, String)
426    */
427   public static void assertBeans(Object actual, String fields, String...expected) {
428      assertBeans(args(), actual, fields, expected);
429   }
430
431   /**
432    * Same as {@link #assertBeans(Object, String, String...)} but with configurable assertion behavior.
433    *
434    * @param args Assertion configuration. See {@link #args()} for usage examples.
435    * @param actual The collection of beans to test. Must not be null.
436    * @param fields A comma-delimited list of bean property names (supports nested syntax).
437    * @param expected Array of expected value strings, one per bean.
438    * @see #assertBeans(Object, String, String...)
439    * @see #args()
440    */
441   public static void assertBeans(AssertionArgs args, Object actual, String fields, String...expected) {
442      assertNotNull(actual, "Value was null.");
443      assertArgNotNull("args", args);
444      assertArgNotNull("fields", fields);
445      assertArgNotNull("expected", expected);
446
447      var converter = args.getBeanConverter().orElse(DEFAULT_CONVERTER);
448      var tokens = tokenize(fields);
449      var errors = new ArrayList<AssertionFailedError>();
450      var actualList = converter.listify(actual);
451
452      if (ne(expected.length, actualList.size())) {
453         errors.add(assertEqualsFailed(expected.length, actualList.size(), args.getMessage("Wrong number of beans.")));
454      } else {
455         for (var i = 0; i < actualList.size(); i++) {
456            var i2 = i;
457            var e = converter.stringify(expected[i]);
458            var a = tokens.stream().map(x -> converter.getNested(actualList.get(i2), x)).collect(joining(","));
459            if (ne(e, a)) {
460               errors.add(assertEqualsFailed(e, a, args.getMessage("Bean at row <{0}> did not match.", i)));
461            }
462         }
463      }
464
465      if (errors.isEmpty()) return;
466
467      var actualStrings = new ArrayList<String>();
468      for (var o : actualList) {
469         actualStrings.add(tokens.stream().map(x -> converter.getNested(o, x)).collect(joining(",")));
470      }
471
472      throw assertEqualsFailed(
473         Stream.of(expected).map(Utils::escapeForJava).collect(joining("\", \"", "\"", "\"")),
474         actualStrings.stream().map(Utils::escapeForJava).collect(joining("\", \"", "\"", "\"")),
475         args.getMessage("{0} bean assertions failed:\n{1}", errors.size(), errors.stream().map(x -> x.getMessage()).collect(joining("\n")))
476      );
477   }
478
479   /**
480    * Asserts that mapped property access on an object returns expected values using a custom BiFunction.
481    *
482    * <p>This is designed for testing objects that don't follow
483    * standard JavaBean patterns or require custom property access logic. The BiFunction allows complete
484    * control over how properties are retrieved from the target object.</p>
485    *
486    * <p>This method creates an intermediate LinkedHashMap to collect all property values before
487    * using the same logic as assertBean for comparison. This ensures consistent ordering
488    * and supports the full nested property syntax. The {@link BasicBeanConverter#DEFAULT} is used
489    * for value stringification and nested property access.</p>
490    *
491    * @param <T> The type of object being tested
492    * @param actual The object to test properties on
493    * @param function The BiFunction that extracts property values. Receives (<jp>object</jp>, <jp>propertyName</jp>) and returns the property value.
494    * @param properties Comma-delimited list of property names to test
495    * @param expected Comma-delimited list of expected values (exceptions become simple class names)
496    * @throws AssertionError if any mapped property values don't match expected values
497    * @see #assertBean(Object, String, String)
498    * @see BeanConverter
499    * @see BasicBeanConverter
500    */
501   public static <T> void assertMapped(T actual, BiFunction<T,String,Object> function, String properties, String expected) {
502      assertMapped(args(), actual, function, properties, expected);
503   }
504
505   /**
506    * Same as {@link #assertMapped(Object, BiFunction, String, String)} but with configurable assertion behavior.
507    *
508    * @param <T> The object type being tested.
509    * @param args Assertion configuration. See {@link #args()} for usage examples.
510    * @param actual The object to test. Must not be null.
511    * @param function Custom property access function.
512    * @param properties A comma-delimited list of property names.
513    * @param expected The expected property values as a comma-delimited string.
514    * @see #assertMapped(Object, BiFunction, String, String)
515    * @see #args()
516    */
517   public static <T> void assertMapped(AssertionArgs args, T actual, BiFunction<T,String,Object> function, String properties, String expected) {
518      assertNotNull(actual, "Value was null.");
519      assertArgNotNull("args", args);
520      assertArgNotNull("function", function);
521      assertArgNotNull("properties", properties);
522      assertArgNotNull("expected", expected);
523
524      var m = new LinkedHashMap<String,Object>();
525      for (var p : tokenize(properties)) {
526         var pv = p.getValue();
527         m.put(pv, safe(() -> function.apply(actual, pv)));
528      }
529
530      assertBean(args, m, properties, expected);
531   }
532
533   /**
534    * Asserts that the string representation of an object contains the expected substring.
535    *
536    * <p>This method converts the actual object to its string representation using the current
537    * {@link BeanConverter} and then checks if it contains the expected substring. This is useful
538    * for testing partial content matches without requiring exact string equality.</p>
539    *
540    * <h5 class='section'>Usage Examples:</h5>
541    * <p class='bjava'>
542    *    <jc>// Test that error message contains key information</jc>
543    *    <jsm>assertContains</jsm>(<js>"FileNotFoundException"</js>, <jv>exception</jv>);
544    *
545    *    <jc>// Test that object string representation contains expected data</jc>
546    *    <jsm>assertContains</jsm>(<js>"status=ACTIVE"</js>, <jv>user</jv>);
547    *
548    *    <jc>// Test partial JSON/XML content</jc>
549    *    <jsm>assertContains</jsm>(<js>"\"name\":\"John\""</js>, <jv>jsonResponse</jv>);
550    * </p>
551    *
552    * @param expected The substring that must be present in the actual object's string representation
553    * @param actual The object to test. Must not be null.
554    * @throws AssertionError if the actual object is null or its string representation doesn't contain the expected substring
555    * @see #assertContainsAll(Object, String...) for multiple substring assertions
556    * @see #assertString(String, Object) for exact string matching
557    */
558   public static void assertContains(String expected, Object actual) {
559      assertContains(args(), expected, actual);
560   }
561
562   /**
563    * Same as {@link #assertContains(String, Object)} but with configurable assertion behavior.
564    *
565    * @param args Assertion configuration. See {@link #args()} for usage examples.
566    * @param expected The substring that must be present.
567    * @param actual The object to test. Must not be null.
568    * @see #assertContains(String, Object)
569    * @see #args()
570    */
571   public static void assertContains(AssertionArgs args, String expected, Object actual) {
572      assertArgNotNull("args", args);
573      assertArgNotNull("expected", expected);
574      assertArgNotNull("actual", actual);
575      assertNotNull(actual, "Value was null.");
576
577      var a = args.getBeanConverter().orElse(DEFAULT_CONVERTER).stringify(actual);
578      assertTrue(a.contains(expected), args.getMessage("String did not contain expected substring.  ==> expected: <{0}> but was: <{1}>", expected, a));
579   }
580
581   /**
582    * Asserts that the string representation of an object contains all specified substrings.
583    *
584    * <p>This method is similar to {@link #assertContains(String, Object)} but tests for multiple
585    * required substrings. All provided substrings must be present in the actual object's string
586    * representation for the assertion to pass.</p>
587    *
588    * <h5 class='section'>Usage Examples:</h5>
589    * <p class='bjava'>
590    *    <jc>// Test that error contains multiple pieces of information</jc>
591    *    <jsm>assertContainsAll</jsm>(<jv>exception</jv>, <js>"FileNotFoundException"</js>, <js>"config.xml"</js>, <js>"/etc"</js>);
592    *
593    *    <jc>// Test that user object contains expected fields</jc>
594    *    <jsm>assertContainsAll</jsm>(<jv>user</jv>, <js>"name=John"</js>, <js>"age=30"</js>, <js>"status=ACTIVE"</js>);
595    *
596    *    <jc>// Test log output contains all required entries</jc>
597    *    <jsm>assertContainsAll</jsm>(<jv>logOutput</jv>, <js>"INFO"</js>, <js>"Started"</js>, <js>"Successfully"</js>);
598    * </p>
599    *
600    * @param actual The object to test. Must not be null.
601    * @param expected Multiple substrings that must all be present in the actual object's string representation
602    * @throws AssertionError if the actual object is null or its string representation doesn't contain all expected substrings
603    * @see #assertContains(String, Object) for single substring assertions
604    */
605   public static void assertContainsAll(Object actual, String...expected) {
606      assertContainsAll(args(), actual, expected);
607   }
608
609   /**
610    * Same as {@link #assertContainsAll(Object, String...)} but with configurable assertion behavior.
611    *
612    * @param args Assertion configuration. See {@link #args()} for usage examples.
613    * @param actual The object to test. Must not be null.
614    * @param expected Multiple substrings that must all be present.
615    * @see #assertContainsAll(Object, String...)
616    * @see #args()
617    */
618   public static void assertContainsAll(AssertionArgs args, Object actual, String...expected) {
619      assertArgNotNull("args", args);
620      assertArgNotNull("expected", expected);
621      assertNotNull(actual, "Value was null.");
622
623      var a = args.getBeanConverter().orElse(DEFAULT_CONVERTER).stringify(actual);
624      var errors = new ArrayList<AssertionFailedError>();
625
626      for (var e : expected) {
627         if (!a.contains(e)) {
628            errors.add(assertEqualsFailed(true, false, args.getMessage("String did not contain expected substring.  ==> expected: <{0}> but was: <{1}>", e, a)));
629         }
630      }
631
632      if (errors.isEmpty()) return;
633
634      if (errors.size() == 1) throw errors.get(0);
635
636      var missingSubstrings = new ArrayList<String>();
637      for (var e : expected) {
638         if (!a.contains(e)) {
639            missingSubstrings.add(e);
640         }
641      }
642
643      throw assertEqualsFailed(
644         missingSubstrings.stream().map(Utils::escapeForJava).collect(joining("\", \"", "\"", "\"")),
645         Utils.escapeForJava(a),
646         args.getMessage("{0} substring assertions failed:\n{1}", errors.size(), errors.stream().map(x -> x.getMessage()).collect(joining("\n")))
647      );
648   }
649
650   /**
651    * Asserts that a collection-like object or Optional is not null and empty.
652    *
653    * <p>This method validates that the provided object is empty according to its type:</p>
654    * <ul>
655    *    <li><b>Optional:</b> Must be empty (not present)</li>
656    *    <li><b>Map:</b> Must have no entries</li>
657    *    <li><b>Collection-like objects:</b> Must be convertible to an empty List via {@link BeanConverter#listify(Object)}</li>
658    * </ul>
659    *
660    * <h5 class='section'>Supported Types:</h5>
661    * <p>Any object that can be converted to a List, including:</p>
662    * <ul>
663    *    <li>Collections (List, Set, Queue, etc.)</li>
664    *    <li>Arrays (primitive and object arrays)</li>
665    *    <li>Iterables, Iterators, Streams</li>
666    *    <li>Maps (converted to list of entries)</li>
667    *    <li>Optional objects</li>
668    * </ul>
669    *
670    * <h5 class='section'>Usage Examples:</h5>
671    * <p class='bjava'>
672    *    <jc>// Test empty collections</jc>
673    *    <jsm>assertEmpty</jsm>(Collections.<jsm>emptyList</jsm>());
674    *    <jsm>assertEmpty</jsm>(<jk>new</jk> ArrayList&lt;&gt;());
675    *
676    *    <jc>// Test empty arrays</jc>
677    *    <jsm>assertEmpty</jsm>(<jk>new</jk> String[0]);
678    *
679    *    <jc>// Test empty Optional</jc>
680    *    <jsm>assertEmpty</jsm>(Optional.<jsm>empty</jsm>());
681    *
682    *    <jc>// Test empty Map</jc>
683    *    <jsm>assertEmpty</jsm>(<jk>new</jk> HashMap&lt;&gt;());
684    * </p>
685    *
686    * @param value The object to test. Must not be null.
687    * @throws AssertionError if the object is null or not empty
688    * @see #assertNotEmpty(Object) for testing non-empty collections
689    * @see #assertSize(int, Object) for testing specific sizes
690    */
691   public static void assertEmpty(Object value) {
692      assertEmpty(args(), value);
693   }
694
695   /**
696    * Same as {@link #assertEmpty(Object)} but with configurable assertion behavior.
697    *
698    * @param args Assertion configuration. See {@link #args()} for usage examples.
699    * @param value The object to test. Must not be null.
700    * @see #assertEmpty(Object)
701    * @see #args()
702    */
703   public static void assertEmpty(AssertionArgs args, Object value) {
704      assertArgNotNull("args", args);
705      assertNotNull(value, "Value was null.");
706
707      if (value instanceof Optional<?> v2) {
708         assertTrue(v2.isEmpty(), "Optional was not empty");
709         return;
710      }
711
712      if (value instanceof Map<?,?> v2) {
713         assertTrue(v2.isEmpty(), "Map was not empty");
714         return;
715      }
716
717      var converter = args.getBeanConverter().orElse(DEFAULT_CONVERTER);
718
719      assertTrue(converter.canListify(value), args.getMessage("Value cannot be converted to a list.  Class=<{0}>", value.getClass().getSimpleName()));
720      assertTrue(converter.listify(value).isEmpty(), args.getMessage("Value was not empty."));
721   }
722
723   /**
724    * Asserts that a List or List-like object contains the expected values using flexible comparison logic.
725    *
726    * <h5 class='section'>Testing Non-List Collections:</h5>
727    * <p class='bjava'>
728    *    <jc>// Test a Set using l() conversion</jc>
729    *    Set&lt;String&gt; <jv>mySet</jv> = <jk>new</jk> TreeSet&lt;&gt;(Arrays.<jsm>asList</jsm>(<js>"a"</js>, <js>"b"</js>, <js>"c"</js>));
730    *    <jsm>assertList</jsm>(<jsm>l</jsm>(<jv>mySet</jv>), <js>"a"</js>, <js>"b"</js>, <js>"c"</js>);
731    *
732    *    <jc>// Test an array using l() conversion</jc>
733    *    String[] <jv>myArray</jv> = {<js>"x"</js>, <js>"y"</js>, <js>"z"</js>};
734    *    <jsm>assertList</jsm>(<jsm>l</jsm>(<jv>myArray</jv>), <js>"x"</js>, <js>"y"</js>, <js>"z"</js>);
735    *
736    *    <jc>// Test a Stream using l() conversion</jc>
737    *    Stream&lt;String&gt; <jv>myStream</jv> = Stream.<jsm>of</jsm>(<js>"foo"</js>, <js>"bar"</js>);
738    *    <jsm>assertList</jsm>(<jsm>l</jsm>(<jv>myStream</jv>), <js>"foo"</js>, <js>"bar"</js>);
739    * </p>
740    *
741    * <h5 class='section'>Comparison Modes:</h5>
742    * <p>The method supports three different ways to compare expected vs actual values:</p>
743    *
744    * <h6 class='section'>1. String Comparison (Readable Format):</h6>
745    * <p class='bjava'>
746    *    <jc>// Elements are converted to strings using the bean converter and compared as strings</jc>
747    *    <jsm>assertList</jsm>(List.<jsm>of</jsm>(1, 2, 3), <js>"1"</js>, <js>"2"</js>, <js>"3"</js>);
748    *    <jsm>assertList</jsm>(List.<jsm>of</jsm>(<js>"a"</js>, <js>"b"</js>), <js>"a"</js>, <js>"b"</js>);
749    * </p>
750    *
751    * <h6 class='section'>2. Predicate Testing (Functional Validation):</h6>
752    * <p class='bjava'>
753    *    <jc>// Use Predicate&lt;T&gt; for functional testing</jc>
754    *    Predicate&lt;Integer&gt; <jv>greaterThanOne</jv> = <jv>x</jv> -&gt; <jv>x</jv> &gt; 1;
755    *    <jsm>assertList</jsm>(List.<jsm>of</jsm>(2, 3, 4), <jv>greaterThanOne</jv>, <jv>greaterThanOne</jv>, <jv>greaterThanOne</jv>);
756    *
757    *    <jc>// Mix predicates with other comparison types</jc>
758    *    Predicate&lt;String&gt; <jv>startsWithA</jv> = <jv>s</jv> -&gt; <jv>s</jv>.startsWith(<js>"a"</js>);
759    *    <jsm>assertList</jsm>(List.<jsm>of</jsm>(<js>"apple"</js>, <js>"banana"</js>), <jv>startsWithA</jv>, <js>"banana"</js>);
760    * </p>
761    *
762    * <h6 class='section'>3. Object Equality (Direct Comparison):</h6>
763    * <p class='bjava'>
764    *    <jc>// Non-String, non-Predicate objects use <jsm>Objects.equals</jsm>() comparison</jc>
765    *    <jsm>assertList</jsm>(List.<jsm>of</jsm>(1, 2, 3), 1, 2, 3); <jc>// Integer objects</jc>
766    *    <jsm>assertList</jsm>(List.<jsm>of</jsm>(<jv>myBean1</jv>, <jv>myBean2</jv>), <jv>myBean1</jv>, <jv>myBean2</jv>); <jc>// Custom objects</jc>
767    * </p>
768    *
769    * @param actual The List to test. Must not be null.
770    * @param expected Multiple arguments of expected values.
771    *                 Can be Strings (readable format comparison), Predicates (functional testing), or Objects (direct equality).
772    * @throws AssertionError if the List size or contents don't match expected values
773    */
774   public static void assertList(Object actual, Object...expected) {
775      assertList(args(), actual, expected);
776   }
777
778   /**
779    * Asserts that a Map contains the expected key/value pairs using flexible comparison logic.
780    *
781    * <h5 class='section'>Map Entry Serialization:</h5>
782    * <p>Map entries are serialized to strings as key/value pairs in the format <js>"key=value"</js>.
783    * Nested maps and collections are supported with appropriate formatting.</p>
784    *
785    * <h5 class='section'>Testing Nested Maps and Collections:</h5>
786    * <p class='bjava'>
787    *    <jc>// Test simple map entries</jc>
788    *    Map&lt;String,String&gt; <jv>simpleMap</jv> = Map.<jsm>of</jsm>(<js>"a"</js>, <js>"1"</js>, <js>"b"</js>, <js>"2"</js>);
789    *    <jsm>assertMap</jsm>(<jv>simpleMap</jv>, <js>"a=1"</js>, <js>"b=2"</js>);
790    *
791    *    <jc>// Test nested maps</jc>
792    *    Map&lt;String,Map&lt;String,Integer&gt;&gt; <jv>nestedMap</jv> = Map.<jsm>of</jsm>(<js>"a"</js>, Map.<jsm>of</jsm>(<js>"b"</js>, 1));
793    *    <jsm>assertMap</jsm>(<jv>nestedMap</jv>, <js>"a={b=1}"</js>);
794    *
795    *    <jc>// Test maps with arrays/collections</jc>
796    *    Map&lt;String,Map&lt;String,Integer[]&gt;&gt; <jv>mapWithArrays</jv> = Map.<jsm>of</jsm>(<js>"a"</js>, Map.<jsm>of</jsm>(<js>"b"</js>, <jk>new</jk> Integer[]{1,2}));
797    *    <jsm>assertMap</jsm>(<jv>mapWithArrays</jv>, <js>"a={b=[1,2]}"</js>);
798    * </p>
799    *
800    * <h5 class='section'>Comparison Modes:</h5>
801    * <p>The method supports the same comparison modes as {@link #assertList(Object, Object...)}:</p>
802    *
803    * <h6 class='section'>1. String Comparison (Readable Format):</h6>
804    * <p class='bjava'>
805    *    <jc>// Map entries are converted to strings and compared as strings</jc>
806    *    <jsm>assertMap</jsm>(Map.<jsm>of</jsm>(<js>"key1"</js>, <js>"value1"</js>), <js>"key1=value1"</js>);
807    *    <jsm>assertMap</jsm>(Map.<jsm>of</jsm>(<js>"count"</js>, 42), <js>"count=42"</js>);
808    * </p>
809    *
810    * <h6 class='section'>2. Predicate Testing (Functional Validation):</h6>
811    * <p class='bjava'>
812    *    <jc>// Use Predicate&lt;Map.Entry&lt;K,V&gt;&gt; for functional testing</jc>
813    *    Predicate&lt;Map.Entry&lt;String,Integer&gt;&gt; <jv>valueGreaterThanTen</jv> = <jv>entry</jv> -&gt; <jv>entry</jv>.getValue() &gt; 10;
814    *    <jsm>assertMap</jsm>(Map.<jsm>of</jsm>(<js>"count"</js>, 42), <jv>valueGreaterThanTen</jv>);
815    * </p>
816    *
817    * <h6 class='section'>3. Object Equality (Direct Comparison):</h6>
818    * <p class='bjava'>
819    *    <jc>// Non-String, non-Predicate objects use <jsm>Objects.equals</jsm>() comparison</jc>
820    *    <jsm>assertMap</jsm>(Map.<jsm>of</jsm>(<js>"key"</js>, <jv>myObject</jv>), <jv>expectedEntry</jv>);
821    * </p>
822    *
823    * <h5 class='section'>Map Ordering Behavior:</h5>
824    * <p>The {@link Listifiers#mapListifier()} method ensures deterministic ordering for map entries:</p>
825    * <ul>
826    *    <li><b>{@link SortedMap} (TreeMap, etc.):</b> Preserves existing sort order</li>
827    *    <li><b>{@link LinkedHashMap}:</b> Preserves insertion order</li>
828    *    <li><b>{@link HashMap} and other unordered Maps:</b> Converts to {@link TreeMap} for natural key ordering</li>
829    * </ul>
830    * <p>This ensures predictable test results regardless of the original map implementation.</p>
831    *
832    * @param actual The Map to test. Must not be null.
833    * @param expected Multiple arguments of expected map entries.
834    *                 Can be Strings (readable format comparison), Predicates (functional testing), or Objects (direct equality).
835    * @throws AssertionError if the Map size or contents don't match expected values
836    * @see #assertList(Object, Object...)
837    */
838   public static void assertMap(Map<?,?> actual, Object...expected) {
839      assertList(args(), actual, expected);
840   }
841
842   /**
843    * Same as {@link #assertMap(Map, Object...)} but with configurable assertion behavior.
844    *
845    * @param args Assertion configuration. See {@link #args()} for usage examples.
846    * @param actual The Map to test. Must not be null.
847    * @param expected Multiple arguments of expected map entries.
848    * @see #assertMap(Map, Object...)
849    * @see #args()
850    */
851   public static void assertMap(AssertionArgs args, Map<?,?> actual, Object...expected) {
852      assertList(args, actual, expected);
853   }
854
855   /**
856    * Same as {@link #assertList(Object, Object...)} but with configurable assertion behavior.
857    *
858    * @param args Assertion configuration. See {@link #args()} for usage examples.
859    * @param actual The List to test. Must not be null.
860    * @param expected Multiple arguments of expected values.
861    * @see #assertList(Object, Object...)
862    * @see #args()
863    */
864   public static void assertList(AssertionArgs args, Object actual, Object...expected) {
865      assertArgNotNull("args", args);
866      assertArgNotNull("expected", expected);
867      assertNotNull(actual, "Value was null.");
868
869      var converter = args.getBeanConverter().orElse(DEFAULT_CONVERTER);
870      var list = converter.listify(actual);
871      var errors = new ArrayList<AssertionFailedError>();
872
873      if (ne(expected.length, list.size())) {
874         errors.add(assertEqualsFailed(expected.length, list.size(), args.getMessage("Wrong list length.")));
875      } else {
876         for (var i = 0; i < expected.length; i++) {
877            var x = list.get(i);
878            var e = expected[i];
879            if (e instanceof String e2) {
880               if (ne(e2, converter.stringify(x))) {
881                  errors.add(assertEqualsFailed(e2, converter.stringify(x), args.getMessage("Element at index {0} did not match.", i)));
882               }
883            } else if (e instanceof Predicate e2) {  // NOSONAR
884               if (!e2.test(x)) {
885                  errors.add(new AssertionFailedError(args.getMessage("Element at index {0} did not pass predicate.  ==> actual: <{1}>", i, converter.stringify(x)).get()));
886               }
887            } else {
888               if (ne(e, x)) {
889                  errors.add(assertEqualsFailed(e, x, args.getMessage("Element at index {0} did not match.  ==> expected: <{1}({2})> but was: <{3}({4})>", i, e, t(e), x, t(x))));
890               }
891            }
892         }
893      }
894
895      if (errors.isEmpty()) return;
896
897      var actualStrings = new ArrayList<String>();
898      for (var o : list) {
899         actualStrings.add(converter.stringify(o));
900      }
901
902      if (errors.size() == 1) throw errors.get(0);
903
904      throw assertEqualsFailed(
905         Stream.of(expected).map(converter::stringify).map(Utils::escapeForJava).collect(joining("\", \"", "[\"", "\"]")),
906         actualStrings.stream().map(Utils::escapeForJava).collect(joining("\", \"", "[\"", "\"]")),
907         args.getMessage("{0} list assertions failed:\n{1}", errors.size(), errors.stream().map(x -> x.getMessage()).collect(joining("\n")))
908      );
909   }
910
911   /**
912    * Asserts that a collection-like object or Optional is not null and not empty.
913    *
914    * <p>This method validates that the provided object is not empty according to its type:</p>
915    * <ul>
916    *    <li><b>Optional:</b> Must be present (not empty)</li>
917    *    <li><b>Map:</b> Must have at least one entry</li>
918    *    <li><b>Collection-like objects:</b> Must convert to a non-empty List via {@link BeanConverter#listify(Object)}</li>
919    * </ul>
920    *
921    * <h5 class='section'>Supported Types:</h5>
922    * <p>Any object that can be converted to a List, including:</p>
923    * <ul>
924    *    <li>Collections (List, Set, Queue, etc.)</li>
925    *    <li>Arrays (primitive and object arrays)</li>
926    *    <li>Iterables, Iterators, Streams</li>
927    *    <li>Maps (converted to list of entries)</li>
928    *    <li>Optional objects</li>
929    * </ul>
930    *
931    * <h5 class='section'>Usage Examples:</h5>
932    * <p class='bjava'>
933    *    <jc>// Test non-empty collections</jc>
934    *    <jsm>assertNotEmpty</jsm>(List.<jsm>of</jsm>(<js>"item1"</js>, <js>"item2"</js>));
935    *    <jsm>assertNotEmpty</jsm>(<jk>new</jk> ArrayList&lt;&gt;(Arrays.<jsm>asList</jsm>(<js>"a"</js>)));
936    *
937    *    <jc>// Test non-empty arrays</jc>
938    *    <jsm>assertNotEmpty</jsm>(<jk>new</jk> String[]{<js>"value"</js>});
939    *
940    *    <jc>// Test present Optional</jc>
941    *    <jsm>assertNotEmpty</jsm>(Optional.<jsm>of</jsm>(<js>"value"</js>));
942    *
943    *    <jc>// Test non-empty Map</jc>
944    *    <jsm>assertNotEmpty</jsm>(Map.<jsm>of</jsm>(<js>"key"</js>, <js>"value"</js>));
945    * </p>
946    *
947    * @param value The object to test. Must not be null.
948    * @throws AssertionError if the object is null or empty
949    * @see #assertEmpty(Object) for testing empty collections
950    * @see #assertSize(int, Object) for testing specific sizes
951    */
952   public static void assertNotEmpty(Object value) {
953      assertNotEmpty(args(), value);
954   }
955
956   /**
957    * Same as {@link #assertNotEmpty(Object)} but with configurable assertion behavior.
958    *
959    * @param args Assertion configuration. See {@link #args()} for usage examples.
960    * @param value The object to test. Must not be null.
961    * @see #assertNotEmpty(Object)
962    * @see #args()
963    */
964   public static void assertNotEmpty(AssertionArgs args, Object value) {
965      assertArgNotNull("args", args);
966      assertNotNull(value, "Value was null.");
967
968      if (value instanceof Optional<?> v2) {
969         assertFalse(v2.isEmpty(), "Optional was empty");
970         return;
971      }
972
973      if (value instanceof Map<?,?> v2) {
974         assertFalse(v2.isEmpty(), "Map was empty");
975         return;
976      }
977
978      assertFalse(args.getBeanConverter().orElse(DEFAULT_CONVERTER).listify(value).isEmpty(), args.getMessage("Value was empty."));
979   }
980
981   /**
982    * Asserts that a collection-like object or string is not null and of the specified size.
983    *
984    * <p>This method can validate the size of various types of objects:</p>
985    * <ul>
986    *    <li><b>String:</b> Validates character length</li>
987    *    <li><b>Collection-like objects:</b> Any object that can be converted to a List via the underlying converter</li>
988    * </ul>
989    *
990    * <h5 class='section'>Usage Examples:</h5>
991    * <p class='bjava'>
992    *    <jc>// Test string length</jc>
993    *    <jsm>assertSize</jsm>(5, <js>"hello"</js>);
994    *
995    *    <jc>// Test collection size</jc>
996    *    <jsm>assertSize</jsm>(3, List.<jsm>of</jsm>(<js>"a"</js>, <js>"b"</js>, <js>"c"</js>));
997    *
998    *    <jc>// Test array size</jc>
999    *    <jsm>assertSize</jsm>(2, <jk>new</jk> String[]{<js>"x"</js>, <js>"y"</js>});
1000    * </p>
1001    *
1002    * @param expected The expected size/length.
1003    * @param actual The object to test. Must not be null.
1004    * @throws AssertionError if the object is null or not the expected size.
1005    */
1006   public static void assertSize(int expected, Object actual) {
1007      assertSize(args(), expected, actual);
1008   }
1009
1010   /**
1011    * Same as {@link #assertSize(int, Object)} but with configurable assertion behavior.
1012    *
1013    * @param args Assertion configuration. See {@link #args()} for usage examples.
1014    * @param expected The expected size/length.
1015    * @param actual The object to test. Must not be null.
1016    * @see #assertSize(int, Object)
1017    * @see #args()
1018    */
1019   public static void assertSize(AssertionArgs args, int expected, Object actual) {
1020      assertArgNotNull("args", args);
1021      assertNotNull(actual, "Value was null.");
1022
1023      if (actual instanceof String a) {
1024         assertEquals(expected, a.length(), args.getMessage("Value not expected size.  value: <{0}>", a));
1025         return;
1026      }
1027
1028      var size = args.getBeanConverter().orElse(DEFAULT_CONVERTER).listify(actual).size();
1029      assertEquals(expected, size, args.getMessage("Value not expected size."));
1030   }
1031
1032   /**
1033    * Asserts that an object's string representation exactly matches the expected value.
1034    *
1035    * <p>This method converts the actual object to its string representation using the current
1036    * {@link BeanConverter} and performs an exact equality comparison with the expected string.
1037    * This is useful for testing complete string output, formatted objects, or converted values.</p>
1038    *
1039    * <h5 class='section'>Usage Examples:</h5>
1040    * <p class='bjava'>
1041    *    <jc>// Test exact string conversion</jc>
1042    *    <jsm>assertString</jsm>(<js>"John,30,true"</js>, <jv>user</jv>); <jc>// Assuming user converts to this format</jc>
1043    *
1044    *    <jc>// Test formatted dates or numbers</jc>
1045    *    <jsm>assertString</jsm>(<js>"2023-12-01"</js>, <jv>localDate</jv>);
1046    *
1047    *    <jc>// Test complex object serialization</jc>
1048    *    <jsm>assertString</jsm>(<js>"{name=John,age=30}"</js>, <jv>userMap</jv>);
1049    *
1050    *    <jc>// Test array/collection formatting</jc>
1051    *    <jsm>assertString</jsm>(<js>"[red,green,blue]"</js>, <jv>colors</jv>);
1052    * </p>
1053    *
1054    * @param expected The exact string that the actual object should convert to
1055    * @param actual The object to test. Must not be null.
1056    * @throws AssertionError if the actual object is null or its string representation doesn't exactly match expected
1057    * @see #assertContains(String, Object) for partial string matching
1058    * @see #assertMatchesGlob(String, Object) for pattern-based matching
1059    */
1060   public static void assertString(String expected, Object actual) {
1061      assertString(args(), expected, actual);
1062   }
1063
1064   /**
1065    * Same as {@link #assertString(String, Object)} but with configurable assertion behavior.
1066    *
1067    * @param args Assertion configuration. See {@link #args()} for usage examples.
1068    * @param expected The expected string value.
1069    * @param actual The object to test. Must not be null.
1070    * @see #assertString(String, Object)
1071    * @see #args()
1072    */
1073   public static void assertString(AssertionArgs args, String expected, Object actual) {
1074      assertArgNotNull("args", args);
1075      assertNotNull(actual, "Value was null.");
1076
1077      assertEquals(expected, args.getBeanConverter().orElse(DEFAULT_CONVERTER).stringify(actual), args.getMessage());
1078   }
1079
1080   /**
1081    * Asserts that an object's string representation matches the specified glob-style pattern.
1082    *
1083    * <p>This method converts the actual object to its string representation using the current
1084    * {@link BeanConverter} and then tests it against the provided glob-style pattern.
1085    * This is useful for testing string formats with simple wildcard patterns.</p>
1086    *
1087    * <h5 class='section'>Pattern Syntax:</h5>
1088    * <p>The pattern uses glob-style wildcards:</p>
1089    * <ul>
1090    *    <li><b>{@code *}</b> matches any sequence of characters (including none)</li>
1091    *    <li><b>{@code ?}</b> matches exactly one character</li>
1092    *    <li><b>All other characters</b> are treated literally</li>
1093    * </ul>
1094    *
1095    * <h5 class='section'>Usage Examples:</h5>
1096    * <p class='bjava'>
1097    *    <jc>// Test filename patterns</jc>
1098    *    <jsm>assertMatchesGlob</jsm>(<js>"user_*_temp"</js>, <jv>filename</jv>);
1099    *
1100    *    <jc>// Test single character wildcards</jc>
1101    *    <jsm>assertMatchesGlob</jsm>(<js>"file?.txt"</js>, <jv>fileName</jv>);
1102    *
1103    *    <jc>// Test combined patterns</jc>
1104    *    <jsm>assertMatchesGlob</jsm>(<js>"log_*_?.txt"</js>, <jv>logFile</jv>);
1105    * </p>
1106    *
1107    * @param pattern The glob-style pattern to match against.
1108    * @param value The object to test. Must not be null.
1109    * @throws AssertionError if the value is null or its string representation doesn't match the pattern
1110    * @see #assertString(String, Object) for exact string matching
1111    * @see #assertContains(String, Object) for substring matching
1112    * @see Utils#getGlobMatchPattern(String) for pattern compilation details
1113    */
1114   public static void assertMatchesGlob(String pattern, Object value) {
1115      assertMatchesGlob(args(), pattern, value);
1116   }
1117
1118   /**
1119    * Same as {@link #assertMatchesGlob(String, Object)} but with configurable assertion behavior.
1120    *
1121    * @param args Assertion configuration. See {@link #args()} for usage examples.
1122    * @param pattern The glob-style pattern to match against.
1123    * @param value The object to test. Must not be null.
1124    * @see #assertMatchesGlob(String, Object)
1125    * @see #args()
1126    */
1127   public static void assertMatchesGlob(AssertionArgs args, String pattern, Object value) {
1128      assertArgNotNull("args", args);
1129      assertArgNotNull("pattern", pattern);
1130      assertNotNull(value, "Value was null.");
1131
1132      var v = args.getBeanConverter().orElse(DEFAULT_CONVERTER).stringify(value);
1133      var m = getGlobMatchPattern(pattern).matcher(v);
1134      assertTrue(m.matches(), args.getMessage("Pattern didn''t match. ==> pattern: <{0}> but was: <{1}>", pattern, v));
1135   }
1136}