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.Optional.*;
020import static org.apache.juneau.junit.bct.Utils.*;
021
022import java.text.*;
023import java.util.*;
024import java.util.function.*;
025
026/**
027 * Configuration and context object for advanced assertion operations.
028 *
029 * <p>This class encapsulates additional arguments and configuration options for assertion methods
030 * in the Bean-Centric Testing (BCT) framework. It provides a fluent API for customizing assertion
031 * behavior including custom converters and enhanced error messaging.</p>
032 *
033 * <p>The primary purposes of this class are:</p>
034 * <ul>
035 *     <li><b>Custom Bean Conversion:</b> Override the default {@link BeanConverter} for specialized object introspection</li>
036 *     <li><b>Enhanced Error Messages:</b> Add context-specific error messages with parameter substitution</li>
037 *     <li><b>Fluent Configuration:</b> Chain configuration calls for readable test setup</li>
038 *     <li><b>Assertion Context:</b> Provide additional context for complex assertion scenarios</li>
039 * </ul>
040 *
041 * <h5 class='section'>Basic Usage:</h5>
042 * <p class='bjava'>
043 *     <jc>// Simple usage with default settings</jc>
044 *     <jsm>assertBean</jsm>(<jsm>args</jsm>(), <jv>myBean</jv>, <js>"name,age"</js>, <js>"John,30"</js>);
045 *
046 *     <jc>// Custom error message</jc>
047 *     <jsm>assertBean</jsm>(<jsm>args</jsm>().setMessage(<js>"User validation failed"</js>),
048 *         <jv>user</jv>, <js>"email,active"</js>, <js>"john@example.com,true"</js>);
049 * </p>
050 *
051 * <h5 class='section'>Custom Bean Converter:</h5>
052 * <p class='bjava'>
053 *     <jc>// Use custom converter for specialized object handling</jc>
054 *     <jk>var</jk> <jv>customConverter</jv> = BasicBeanConverter.<jsm>builder</jsm>()
055 *         .defaultSettings()
056 *         .addStringifier(MyClass.<jk>class</jk>, <jp>obj</jp> -> <jp>obj</jp>.getDisplayName())
057 *         .build();
058 *
059 *     <jsm>assertBean</jsm>(<jsm>args</jsm>().setBeanConverter(<jv>customConverter</jv>),
060 *         <jv>myCustomObject</jv>, <js>"property"</js>, <js>"expectedValue"</js>);
061 * </p>
062 *
063 * <h5 class='section'>Advanced Error Messages:</h5>
064 * <p class='bjava'>
065 *     <jc>// Parameterized error messages</jc>
066 *     <jsm>assertBean</jsm>(<jsm>args</jsm>().setMessage(<js>"Validation failed for user {0}"</js>, <jv>userId</jv>),
067 *         <jv>user</jv>, <js>"status"</js>, <js>"ACTIVE"</js>);
068 *
069 *     <jc>// Dynamic error message with supplier</jc>
070 *     <jsm>assertBean</jsm>(<jsm>args</jsm>().setMessage(() -> <js>"Test failed at "</js> + Instant.<jsm>now</jsm>()),
071 *         <jv>result</jv>, <js>"success"</js>, <js>"true"</js>);
072 * </p>
073 *
074 * <h5 class='section'>Fluent Configuration:</h5>
075 * <p class='bjava'>
076 *     <jc>// Chain multiple configuration options</jc>
077 *     <jk>var</jk> <jv>testArgs</jv> = args()
078 *         .setBeanConverter(<jv>customConverter</jv>)
079 *         .setMessage(<js>"Integration test failed for module {0}"</js>, <jv>moduleName</jv>);
080 *
081 *     <jsm>assertBean</jsm>(<jv>testArgs</jv>, <jv>moduleConfig</jv>, <js>"enabled,version"</js>, <js>"true,2.1.0"</js>);
082 *     <jsm>assertBeans</jsm>(<jv>testArgs</jv>, <jv>moduleList</jv>, <js>"name,status"</js>,
083 *         <js>"ModuleA,ACTIVE"</js>, <js>"ModuleB,ACTIVE"</js>);
084 * </p>
085 *
086 * <h5 class='section'>Error Message Composition:</h5>
087 * <p>When assertion failures occur, error messages are intelligently composed:</p>
088 * <ul>
089 *     <li><b>Base Message:</b> Custom message set via {@link #setMessage(String, Object...)} or {@link #setMessage(Supplier)}</li>
090 *     <li><b>Assertion Context:</b> Specific context provided by individual assertion methods</li>
091 *     <li><b>Composite Format:</b> <js>"{base message}, Caused by: {assertion context}"</js></li>
092 * </ul>
093 *
094 * <p class='bjava'>
095 *     <jc>// Example error message composition:</jc>
096 *     <jc>// Base: "User validation failed for user 123"</jc>
097 *     <jc>// Context: "Bean assertion failed."</jc>
098 *     <jc>// Result: "User validation failed for user 123, Caused by: Bean assertion failed."</jc>
099 * </p>
100 *
101 * <h5 class='section'>Thread Safety:</h5>
102 * <p>This class is <b>not thread-safe</b> and is intended for single-threaded test execution.
103 * Each test method should create its own instance using {@link BctAssertions#args()} or create
104 * a new instance directly with {@code new AssertionArgs()}.</p>
105 *
106 * <h5 class='section'>Immutability Considerations:</h5>
107 * <p>While this class uses fluent setters that return {@code this} for chaining, the instance
108 * is mutable. For reusable configurations across multiple tests, consider creating a factory
109 * method that returns pre-configured instances.</p>
110 *
111 * @see BctAssertions#args()
112 * @see BeanConverter
113 * @see BasicBeanConverter
114 */
115public class AssertionArgs {
116
117   private BeanConverter beanConverter;
118   private Supplier<String> messageSupplier;
119
120   /**
121    * Creates a new instance with default settings.
122    *
123    * <p>Instances start with no custom bean converter and no custom error message.
124    * All assertion methods will use default behavior until configured otherwise.</p>
125    */
126   public AssertionArgs() { /* no-op */ }
127
128   /**
129    * Sets a custom {@link BeanConverter} for object introspection and property access.
130    *
131    * <p>The custom converter allows fine-tuned control over how objects are converted to strings,
132    * how collections are listified, and how nested properties are accessed. This is particularly
133    * useful for:</p>
134    * <ul>
135    *     <li><b>Custom Object Types:</b> Objects that don't follow standard JavaBean patterns</li>
136    *     <li><b>Specialized Formatting:</b> Custom string representations for assertion comparisons</li>
137    *     <li><b>Performance Optimization:</b> Cached or optimized property access strategies</li>
138    *     <li><b>Domain-Specific Logic:</b> Business-specific property resolution rules</li>
139    * </ul>
140    *
141    * <h5 class='section'>Example:</h5>
142    * <p class='bjava'>
143    *     <jc>// Create converter with custom stringifiers</jc>
144    *     <jk>var</jk> <jv>converter</jv> = BasicBeanConverter.<jsm>builder</jsm>()
145    *         .defaultSettings()
146    *         .addStringifier(LocalDate.<jk>class</jk>, <jp>date</jp> -> <jp>date</jp>.format(DateTimeFormatter.<jsf>ISO_LOCAL_DATE</jsf>))
147    *         .addStringifier(Money.<jk>class</jk>, <jp>money</jp> -> <jp>money</jp>.getAmount().toPlainString())
148    *         .build();
149    *
150    *     <jc>// Use in assertions</jc>
151    *     <jsm>assertBean</jsm>(<jsm>args</jsm>().setBeanConverter(<jv>converter</jv>),
152    *         <jv>order</jv>, <js>"date,total"</js>, <js>"2023-12-01,99.99"</js>);
153    * </p>
154    *
155    * @param value The custom bean converter to use. If null, assertions will fall back to the default converter.
156    * @return This instance for method chaining.
157    */
158   public AssertionArgs setBeanConverter(BeanConverter value) {
159      beanConverter = value;
160      return this;
161   }
162
163   /**
164    * Gets the configured bean converter, if any.
165    *
166    * @return An Optional containing the custom converter, or empty if using default behavior.
167    */
168   protected Optional<BeanConverter> getBeanConverter() {
169      return ofNullable(beanConverter);
170   }
171
172   /**
173    * Sets a custom error message supplier for assertion failures.
174    *
175    * <p>The supplier allows for dynamic message generation, including context that may only
176    * be available at the time of assertion failure. This is useful for:</p>
177    * <ul>
178    *     <li><b>Timestamps:</b> Including the exact time of failure</li>
179    *     <li><b>Test State:</b> Including runtime state information</li>
180    *     <li><b>Expensive Operations:</b> Deferring costly string operations until needed</li>
181    *     <li><b>Conditional Messages:</b> Different messages based on runtime conditions</li>
182    * </ul>
183    *
184    * <h5 class='section'>Example:</h5>
185    * <p class='bjava'>
186    *     <jc>// Dynamic message with timestamp</jc>
187    *     <jsm>assertBean</jsm>(<jsm>args</jsm>().setMessage(() -> <js>"Test failed at "</js> + Instant.<jsm>now</jsm>()),
188    *         <jv>result</jv>, <js>"status"</js>, <js>"SUCCESS"</js>);
189    *
190    *     <jc>// Message with expensive computation</jc>
191    *     <jsm>assertBean</jsm>(<jsm>args</jsm>().setMessage(() -> <js>"Failed after "</js> + computeTestDuration() + <js>" ms"</js>),
192    *         <jv>response</jv>, <js>"error"</js>, <js>"null"</js>);
193    * </p>
194    *
195    * @param value The message supplier. Called only when an assertion fails.
196    * @return This instance for method chaining.
197    */
198   public AssertionArgs setMessage(Supplier<String> value) {
199      messageSupplier = value;
200      return this;
201   }
202
203   /**
204    * Sets a parameterized error message for assertion failures.
205    *
206    * <p>This method uses {@link MessageFormat} to substitute parameters into the message template.
207    * The formatting occurs immediately when this method is called, not when the assertion fails.</p>
208    *
209    * <h5 class='section'>Parameter Substitution:</h5>
210    * <p>Uses standard MessageFormat patterns:</p>
211    * <ul>
212    *     <li><code>{0}</code> - First parameter</li>
213    *     <li><code>{1}</code> - Second parameter</li>
214    *     <li><code>{0,number,#}</code> - Formatted number</li>
215    *     <li><code>{0,date,short}</code> - Formatted date</li>
216    * </ul>
217    *
218    * <h5 class='section'>Examples:</h5>
219    * <p class='bjava'>
220    *     <jc>// Simple parameter substitution</jc>
221    *     <jsm>assertBean</jsm>(<jsm>args</jsm>().setMessage(<js>"User {0} validation failed"</js>, <jv>userId</jv>),
222    *         <jv>user</jv>, <js>"active"</js>, <js>"true"</js>);
223    *
224    *     <jc>// Multiple parameters</jc>
225    *     <jsm>assertBean</jsm>(<jsm>args</jsm>().setMessage(<js>"Test {0} failed on iteration {1}"</js>, <jv>testName</jv>, <jv>iteration</jv>),
226    *         <jv>result</jv>, <js>"success"</js>, <js>"true"</js>);
227    *
228    *     <jc>// Number formatting</jc>
229    *     <jsm>assertBean</jsm>(<jsm>args</jsm>().setMessage(<js>"Expected {0,number,#.##} but got different value"</js>, <jv>expectedValue</jv>),
230    *         <jv>actual</jv>, <js>"value"</js>, <js>"123.45"</js>);
231    * </p>
232    *
233    * @param message The message template with MessageFormat placeholders.
234    * @param args The parameters to substitute into the message template.
235    * @return This instance for method chaining.
236    */
237   public AssertionArgs setMessage(String message, Object... args) {
238      messageSupplier = fs(message, args);
239      return this;
240   }
241
242   /**
243    * Gets the base message supplier for composition with assertion-specific messages.
244    *
245    * @return The configured message supplier, or null if no custom message was set.
246    */
247   protected Supplier<String> getMessage() {
248      return messageSupplier;
249   }
250
251   /**
252    * Composes the final error message by combining custom and assertion-specific messages.
253    *
254    * <p>This method implements the message composition strategy used throughout the assertion framework:</p>
255    * <ul>
256    *     <li><b>No Custom Message:</b> Returns the assertion-specific message as-is</li>
257    *     <li><b>With Custom Message:</b> Returns <code>"{custom}, Caused by: {assertion}"</code></li>
258    * </ul>
259    *
260    * <p>This allows tests to provide high-level context while preserving the specific
261    * technical details about what assertion failed.</p>
262    *
263    * @param msg The assertion-specific message template.
264    * @param args Parameters for the assertion-specific message.
265    * @return A supplier that produces the composed error message.
266    */
267   protected Supplier<String> getMessage(String msg, Object...args) {
268      return messageSupplier == null ? fs(msg, args) : fs("{0}, Caused by: {1}", messageSupplier.get(), f(msg, args));
269   }
270}