/*
 * Copyright 2015-2023 the original author or authors.
 *
 * Licensed 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.opentest4j;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * {@link MultipleFailuresError} is an {@link AssertionError} that aggregates
 * multiple failures thrown in a given context (i.e., typically within the
 * invocation of a single test).
 *
 * @author Johannes Link
 * @author Sam Brannen
 * @author Marc Philipp
 * @since 1.0
 */
public class MultipleFailuresError extends AssertionError {

	private static final long serialVersionUID = 1L;

	private static final String EOL = System.getProperty("line.separator");

	private final String heading;
	private final List<Throwable> failures;

	/**
	 * Constructs an {@code MultipleFailuresError} with the supplied heading and
	 * failures.
	 *
	 * @param heading the message heading; a default value will be used if
	 * {@code null} or blank
	 * @param failures the list of failures; must not be {@code null} or contain
	 * {@code null} elements
	 */
	public MultipleFailuresError(String heading, List<? extends Throwable> failures) {
		if (failures == null) {
			throw new NullPointerException("failures must not be null");
		}
		this.heading = isBlank(heading) ? "Multiple Failures" : heading.trim();

		this.failures = new ArrayList<Throwable>();
		for (Throwable failure : failures) {
			if (failure == null) {
				throw new NullPointerException("failures must not contain null elements");
			}
			this.failures.add(failure);
		}
	}

	@Override
	public String getMessage() {
		int failureCount = this.failures.size();

		if (failureCount == 0) {
			return this.heading;
		}

		// @formatter:off
		StringBuilder builder = new StringBuilder(this.heading)
				.append(" (")
				.append(failureCount).append(" ")
				.append(pluralize(failureCount, "failure", "failures"))
				.append(")")
				.append(EOL);
		// @formatter:on

		int lastIndex = failureCount - 1;
		for (Throwable failure : this.failures.subList(0, lastIndex)) {
			builder.append("\t").append(nullSafeMessage(failure)).append(EOL);
		}
		builder.append('\t').append(nullSafeMessage(this.failures.get(lastIndex)));

		return builder.toString();
	}

	/**
	 * Returns the list of failures contained in this error.
	 */
	public List<Throwable> getFailures() {
		return Collections.unmodifiableList(this.failures);
	}

	/**
	 * Returns whether this error contains any failures.
	 */
	public boolean hasFailures() {
		return !this.failures.isEmpty();
	}

	private static boolean isBlank(String str) {
		return (str == null || str.trim().length() == 0);
	}

	private static String pluralize(int count, String singular, String plural) {
		return count == 1 ? singular : plural;
	}

	private static String nullSafeMessage(Throwable failure) {
		if (isBlank(failure.getMessage())) {
			return failure.getClass().getName() + ": <no message>";
		}
		return failure.getClass().getName() + ": " + failure.getMessage();
	}

}
