/*
 * Licensed to The OpenNMS Group, Inc (TOG) under one or more
 * contributor license agreements.  See the LICENSE.md file
 * distributed with this work for additional information
 * regarding copyright ownership.
 *
 * TOG licenses this file to You under the GNU Affero General
 * Public License Version 3 (the "License") or (at your option)
 * any later version.  You may not use this file except in
 * compliance with the License.  You may obtain a copy of the
 * License at:
 *
 *      https://www.gnu.org/licenses/agpl-3.0.txt
 *
 * 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.opennms.test;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;

import org.junit.Assert;
import junit.framework.AssertionFailedError;

/**
 * <p>ThrowableAnticipator class.</p>
 *
 * @author ranger
 * @version $Id: $
 */
public class ThrowableAnticipator extends Assert {
    /** Constant <code>IGNORE_MESSAGE="*** ThrowableAnticipator ignore Throwab"{trunked}</code> */
    public final static String IGNORE_MESSAGE =
        "*** ThrowableAnticipator ignore Throwable.getMessage() ***";
    
    private List<Throwable> m_anticipated;
    private List<Throwable> m_unanticipated;
    private boolean m_failFast;

    /**
     * Create a new anticipator with default failFast setting of true.
     */
    public ThrowableAnticipator() {
        this(true);
    }
    
    /**
     * Create a new anticipator with the specified failFast setting.
     *
     * @param failFast whether unanticipated exceptions will cause an immediate
     *        junit.framework.Assert#fail(String).  See #setFailFast(boolean)
     *        for details.
     */
    public ThrowableAnticipator(boolean failFast) {
        init();
        m_failFast = failFast;
    }
    
    private void init() {
        m_anticipated = new ArrayList<>();
        m_unanticipated = new ArrayList<>();
        m_failFast = true;
    }
   
    /**
     * Anticipate a specific java.lang.Throwable.
     *
     * @param t The java.lang.Throwable to anticipate.  Both the class and the
     *          message will be matched.
     */
    public void anticipate(Throwable t) {
        m_anticipated.add(t);
    }
    
    /**
     * Process a received throwable.  See #setFailFast(boolean) for the effects
     * of the failFast setting on when errors are signaled.
     *
     * @param t The received throwable.
     * @throws AssertionFailedError if failFast is set to true and an
     *         unanticipated java.lang.Throwable is received.
     */
    public void throwableReceived(Throwable t) {
        if (t == null) {
            throw new IllegalArgumentException("Throwable must not be null");
        }
        
        boolean foundMatch = false;

        for (Throwable our : m_anticipated) {
            if (t.getClass().isAssignableFrom(our.getClass())) {
                if (IGNORE_MESSAGE.equals(our.getMessage())
                        || (t.getMessage() == null && our.getMessage() == null)
                        || (t.getMessage() != null
                                && t.getMessage().equals(our.getMessage()))) {
                    m_anticipated.remove(our);
                    foundMatch = true;
                    break;
                }
            }
        }
        
        if (!foundMatch) {
            throwableFailedMatch(t);
        }
    }
    
    private void throwableFailedMatch(Throwable t) {
        if (m_failFast) {
            fail("Received an unexpected Exception: " + t.toString(), t);
        } else {
            m_unanticipated.add(t);
        }
    }

    /**
     * Set the failFast value for this anticipator.  This controls what happens
     * when an unanticipation throwable is received #receiveThrowable(Throwable).
     *
     * @param failFast when set to true (the default), an unanticipated throwable
     *        will cause #receiveThrowable(Throwable) to call
     *        junit.framework.Assert#fail().  When set to false, the error will
     *        be delayed until #verifyAnticipated() is called.
     * @throws junit.framework.AssertionFailedError if failFast is being changed from false to
     *         true and one or more unanticipated exceptions have been received.
     */
    public void setFailFast(boolean failFast) throws AssertionFailedError {
        if (m_failFast == false && failFast == true
                && m_unanticipated.size() > 0) {
            fail("failFast is being changed from false to true and unanticipated "
                 + "exceptions have been received:\n" + listUnanticipated());
        }
        m_failFast = failFast;
    }

    /**
     * Returns the failFast value for this anticipator.
     *
     * @return failFast value.  See #setFailFast(boolean) for details.
     */
    public boolean isFailFast() {
        return m_failFast;
    }
    
    /**
     * Reset the class back to its initial state as if it had just been
     * instantiated.
     */
    public void reset() {
        init();
    }
    
    /**
     * Perform after-test verification that all anticipated java.lang.Throwable's
     * have been seen and that no unanticipated java.lang.Throwable's have been
     * seen.
     *
     * @throws junit.framework.AssertionFailedError if one or more anticipated Throwables were
     *         not received or one or more unanticipated Throwables were received.
     */
    public void verifyAnticipated() throws AssertionFailedError {
        StringBuffer error = new StringBuffer();
        
        if (m_anticipated.size() != 0) {
            error.append("Anticipated list is non-zero (has "
                         + m_anticipated.size() + " entries):\n");
            error.append(listAnticipated());
        }
        
        if (m_unanticipated.size() != 0) {
            error.append("Unanticipated list is non-zero (has "
                         + m_unanticipated.size() + " entries):\n");
            error.append(listUnanticipated());
        }
        
        if (error.length() > 0) {
            fail(error.toString());
        }
    }
    
    private StringBuffer listAnticipated() {
        return makeList(m_anticipated,
                        "Anticipated but unreceived Throwable: ", "\n");
    }
    
    private StringBuffer listUnanticipated() {
        return makeList(m_unanticipated, "Unanticipated Throwable: ", "\n");
    }
    
    private StringBuffer makeList(List<Throwable> list, String before, String after) {
        StringBuffer output = new StringBuffer();
        
        for (Throwable t : list) {
            output.append(before + t.toString() + after);
            StringWriter w = new StringWriter();
            PrintWriter pw = new PrintWriter(w);
            t.printStackTrace(pw);
            output.append(w.getBuffer());
            output.append("\n");
        }
        
        return output;
    }
    
    /**
     * Generate an AssertionFailedError with the specified cause.
     *
     * @param message error message
     * @param t java.lang.Throwable that caused this error to be thrown
     * @throws junit.framework.AssertionFailedError always thrown.  Generated based on the
     * parameters passed.
     */
    public void fail(String message, Throwable t) throws AssertionFailedError {
        AssertionFailedError error = new AssertionFailedError(message);
        error.initCause(t);
        throw error;
    }

}
