// ***************************************************************************************************************************
// * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements.  See the NOTICE file *
// * distributed with this work for additional information regarding copyright ownership.  The ASF licenses this file        *
// * to you under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance            *
// * with the License.  You may obtain a copy of the License at                                                              *
// *                                                                                                                         *
// *  http://www.apache.org/licenses/LICENSE-2.0                                                                             *
// *                                                                                                                         *
// * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an  *
// * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the        *
// * specific language governing permissions and limitations under the License.                                              *
// ***************************************************************************************************************************
package org.apache.juneau.assertions;

import static org.apache.juneau.internal.ThrowableUtils.*;
import static org.apache.juneau.internal.StringUtils.*;

import java.io.*;
import java.lang.reflect.*;

import org.apache.juneau.*;
import org.apache.juneau.cp.*;
import org.apache.juneau.internal.*;

/**
 * Base class for all assertion objects.
 *
 * <h5 class='topic'>Test Methods</h5>
 * <p>
 * <ul class='javatree'>
 * 	<li>None
 * </ul>
 *
 * <h5 class='topic'>Transform Methods</h5>
 * <p>
 * <ul class='javatree'>
 * 	<li>None
 * </ul>
 *
 * <h5 class='topic'>Configuration Methods</h5>
 * <p>
 * <ul class='javatree'>
 * 	<li class='jc'>{@link Assertion}
 * 	<ul class='javatreec'>
 * 		<li class='jm'>{@link Assertion#setMsg(String, Object...) setMsg(String, Object...)}
 * 		<li class='jm'>{@link Assertion#setOut(PrintStream) setOut(PrintStream)}
 * 		<li class='jm'>{@link Assertion#setSilent() setSilent()}
 * 		<li class='jm'>{@link Assertion#setStdOut() setStdOut()}
 * 		<li class='jm'>{@link Assertion#setThrowable(Class) setThrowable(Class)}
 * 	</ul>
 * </ul>
 *
 * <ul class='seealso'>
 * 	<li class='link'>{@doc jm.FluentAssertions}
 * 	<li class='extlink'>{@source}
 * </ul>
 */
@FluentSetters
public class Assertion {

	//-----------------------------------------------------------------------------------------------------------------
	// Static
	//-----------------------------------------------------------------------------------------------------------------

	private static final Messages MESSAGES = Messages.of(Assertion.class, "Messages");

	static final String
		MSG_parameterCannotBeNull = MESSAGES.getString("parameterCannotBeNull"),
		MSG_causedBy = MESSAGES.getString("causedBy");

	//-----------------------------------------------------------------------------------------------------------------
	// Instance
	//-----------------------------------------------------------------------------------------------------------------

	private String msg;
	private Object[] msgArgs;
	private PrintStream out = System.err;
	private Class<? extends RuntimeException> throwable;

	/**
	 * Constructor used when this assertion is being created from within another assertion.
	 *
	 * @param creator The creator of this assertion.
	 */
	protected Assertion(Assertion creator) {
		if (creator != null) {
			this.msg = creator.msg;
			this.msgArgs = creator.msgArgs;
			this.out = creator.out;
			this.throwable = creator.throwable;
		}
	}

	//-----------------------------------------------------------------------------------------------------------------
	// Config
	//-----------------------------------------------------------------------------------------------------------------

	/**
	 * Allows you to override the assertion failure message.
	 *
	 * <p>
	 * String can contain <js>"{msg}"</js> to represent the original message.
	 *
	 * <h5 class='section'>Example:</h5>
	 * <p class='bjava'>
	 * 	<jk>import static</jk> org.apache.juneau.assertions.Assertions.*;
	 *
	 *	<jc>// Throws an assertion with a custom message instead of the default "Value was null."</jc>
	 * 	<jsm>assertString</jsm>(<jv>myString</jv>)
	 * 		.setMsg(<js>"My string was bad:  {msg}"</js>)
	 * 		.isNotNull();
	 * </p>
	 *
	 * @param msg The assertion failure message.
	 * @param args Optional message arguments.
	 * @return This object.
	 */
	@FluentSetter
	public Assertion setMsg(String msg, Object...args) {
		this.msg = msg.replace("{msg}", "<<<MSG>>>");
		this.msgArgs = args;
		return this;
	}

	/**
	 * If an error occurs, send the error message to STDOUT instead of STDERR.
	 *
	 * @return This object.
	 */
	@FluentSetter
	public Assertion setStdOut() {
		return setOut(System.out);
	}

	/**
	 * If an error occurs, send the error message to the specified stream instead of STDERR.
	 *
	 * @param value
	 * 	The output stream.
	 * 	Can be <jk>null</jk> to suppress output.
	 * @return This object.
	 */
	@FluentSetter
	public Assertion setOut(PrintStream value) {
		this.out = value;
		return this;
	}

	/**
	 * Suppresses output to STDERR.
	 *
	 * <p>
	 * This is the equivalent to calling <c>out(<jk>null</jk>)</c>.
	 *
	 * @return This object.
	 */
	@FluentSetter
	public Assertion setSilent() {
		return setOut(null);
	}

	/**
	 * If an error occurs, throw this exception instead of the standard {@link AssertionError}.
	 *
	 * <p>
	 * The throwable class must have a public constructor that takes in any of the following parameters:
	 * <ul>
	 * 	<li>{@link Throwable} - The caused-by exception (if there is one).
	 * 	<li>{@link String} - The assertion failure message.
	 * </ul>
	 *
	 * <p>
	 * If the throwable cannot be instantiated, a {@link RuntimeException} is thrown instead.
	 *
	 * <h5 class='section'>Example:</h5>
	 * <p class='bjava'>
	 * 	<jk>import static</jk> org.apache.juneau.assertions.Assertions.*;
	 *
	 *	<jc>// Throws a BadRequest instead of an AssertionError if the string is null.</jc>
	 * 	<jsm>assertString</jsm>(<jv>myString</jv>)
	 * 		.setThrowable(BadRequest.<jk>class</jk>)
	 * 		.isNotNull();
	 * </p>
	 *
	 * @param value The new value for this setting.
	 * @return This object.
	 */
	@FluentSetter
	public Assertion setThrowable(Class<? extends RuntimeException> value) {
		this.throwable = value;
		return this;
	}

	//-----------------------------------------------------------------------------------------------------------------
	// Fluent setters
	//-----------------------------------------------------------------------------------------------------------------

	// <FluentSetters>

	// </FluentSetters>

	//-----------------------------------------------------------------------------------------------------------------
	// Utility methods
	//-----------------------------------------------------------------------------------------------------------------


	/**
	 * Creates a new {@link BasicAssertionError}.
	 *
	 * @param msg The message.
	 * @param args The message arguments.
	 * @return A new {@link BasicAssertionError}.
	 */
	protected BasicAssertionError error(String msg, Object...args) {
		return error(null, msg, args);
	}

	/**
	 * Creates a new {@link BasicAssertionError}.
	 *
	 * @param cause Optional caused-by throwable.
	 * @param msg The message.
	 * @param args The message arguments.
	 * @return A new {@link BasicAssertionError}.
	 */
	protected BasicAssertionError error(Throwable cause, String msg, Object...args) {
		msg = format(msg, args);
		if (this.msg != null)
			msg = format(this.msg, this.msgArgs).replace("<<<MSG>>>", msg);
		if (out != null)
			out.println(msg);
		if (throwable != null) {
			try {
				throw BeanStore
					.create()
					.build()
					.addBean(Throwable.class, cause)
					.addBean(String.class, msg)
					.addBean(Object[].class,new Object[0])
					.createBean(throwable)
					.run();
			} catch (ExecutableException e) {
				// If we couldn't create requested exception, just throw a RuntimeException.
				throw runtimeException(cause, msg);
			}
		}
		return new BasicAssertionError(cause, msg);
	}

	/**
	 * Convenience method for getting the class name for an object.
	 *
	 * @param o The object to get the class name for.
	 * @return The class name for an object.
	 */
	protected static String className(Object o) {
		return ClassUtils.className(o);
	}

	/**
	 * Convenience method for getting the array class of the specified element type.
	 *
	 * @param <E> The element type.
	 * @param c The object to get the class name for.
	 * @return The class name for an object.
	 */
	@SuppressWarnings("unchecked")
	protected static <E> Class<E[]> arrayClass(Class<E> c) {
		return (Class<E[]>)Array.newInstance(c,0).getClass();
	}
}
