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