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