/*
 *
 * 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.qpid.junit.extensions.listeners;

import junit.framework.AssertionFailedError;
import junit.framework.Test;
import junit.framework.TestCase;

import org.apache.log4j.Logger;

import org.apache.qpid.junit.extensions.ShutdownHookable;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.*;

/**
 * Listens for test results for a named test and outputs these in the standard JUnit XML format to the specified
 * writer.
 *
 * <p/>The API for this listener accepts notifications about different aspects of a tests results through different
 * methods, so some assumption needs to be made as to which test result a notification refers to. For example
 * {@link #startTest} will be called, then possibly {@link #timing} will be called, even though the test instance is
 * passed in both cases, it is not enough to distinguish a particular run of the test, as the test case instance may
 * be being shared between multiple threads, or being run a repeated number of times, and can therfore be re-used
 * between calls. The listeners make the assumption that, for every test, a unique thread will call {@link #startTest}
 * and {@link #endTest} to delimit each test. All calls to set test parameters, timings, state and so on, will occur
 * between the start and end and will be given with the same thread id as the start and end, so the thread id provides
 * a unqiue value to identify a particular test run against.
 *
 * <p><table id="crc"><caption>CRC Card</caption>
 * <tr><th> Responsibilities <th> Collaborations
 * <tr><td> Listen to test lifecycle notifications.
 * <tr><td> Listen to test errors and failures.
 * <tr><td> Listen to test timings.
 * <tr><td> Listen to test memory usages.
 * <tr><td> Listen to parameterized test parameters.
 * <tr><th> Responsibilities
 * </table>
 *
 * @todo Merge this class with CSV test listener, making the collection of results common to both, and only factoring
 *       out the results printing code into sub-classes. Provide a simple XML results formatter with the same format as
 *       the ant XML formatter, and a more structured one for outputing results with timings and summaries from
 *       performance tests.
 *
 * @author Rupert Smith
 */
public class XMLTestListener implements TKTestListener, ShutdownHookable
{
    /** Used for debugging. */
    private static final Logger log = Logger.getLogger(XMLTestListener.class);

    /** The results file writer. */
    protected Writer writer;

    /** Holds the results for individual tests. */
    // protected Map<Result, Result> results = new LinkedHashMap<Result, Result>();
    // protected List<Result> results = new ArrayList<Result>();

    /**
     * Map for holding results on a per thread basis as they come in. A ThreadLocal is not used as sometimes an
     * explicit thread id must be used, where notifications come from different threads than the ones that called
     * the test method.
     */
    Map<Long, Result> threadLocalResults = Collections.synchronizedMap(new LinkedHashMap<Long, Result>());

    /**
     * Holds results for tests that have ended. Transferring these results here from the per-thread results map, means
     * that the thread id is freed for the thread to generate more results.
     */
    List<Result> results = new ArrayList<Result>();

    /** Holds the overall error count. */
    protected int errors = 0;

    /** Holds the overall failure count. */
    protected int failures = 0;

    /** Holds the overall tests run count. */
    protected int runs = 0;

    /** Holds the name of the class that tests are being run for. */
    String testClassName;

    /**
     * Creates a new XML results output listener that writes to the specified location.
     *
     * @param writer        The location to write results to.
     * @param testClassName The name of the test class to include in the test results.
     */
    public XMLTestListener(Writer writer, String testClassName)
    {
        log.debug("public XMLTestListener(Writer writer, String testClassName = " + testClassName + "): called");

        this.writer = writer;
        this.testClassName = testClassName;
    }

    /**
     * Resets the test results to the default state of time zero, memory usage zero, parameter zero, test passed.
     *
     * @param test     The test to resest any results for.
     * @param threadId Optional thread id if not calling from thread that started the test method. May be null.
     */
    public void reset(Test test, Long threadId)
    {
        log.debug("public void reset(Test test = " + test + ", Long threadId = " + threadId + "): called");

        XMLTestListener.Result r =
            (threadId == null) ? threadLocalResults.get(Thread.currentThread().getId()) : threadLocalResults.get(threadId);

        r.error = null;
        r.failure = null;

    }

    /**
     * Notification that a test started.
     *
     * @param test The test that started.
     */
    public void startTest(Test test)
    {
        log.debug("public void startTest(Test test = " + test + "): called");

        Result newResult = new Result(test.getClass().getName(), ((TestCase) test).getName());

        // Initialize the thread local test results.
        threadLocalResults.put(Thread.currentThread().getId(), newResult);
        runs++;
    }

    /**
     * Should be called every time a test completes with the run time of that test.
     *
     * @param test     The name of the test.
     * @param nanos    The run time of the test in nanoseconds.
     * @param threadId Optional thread id if not calling from thread that started the test method. May be null.
     */
    public void timing(Test test, long nanos, Long threadId)
    { }

    /**
     * Optionally called every time a test completes with the second timing test.
     *
     * @param test    The name of the test.
     * @param nanos   The second timing information of the test in nanoseconds.
     * @param threadId Optional thread id if not calling from thread that started the test method. May be null.
     */
    public void timing2(Test test, Long nanos, Long threadId)
    { }

    /**
     * Should be called every time a test completed with the amount of memory used before and after the test was run.
     *
     * @param test     The test which memory was measured for.
     * @param memStart The total JVM memory used before the test was run.
     * @param memEnd   The total JVM memory used after the test was run.
     * @param threadId Optional thread id if not calling from thread that started the test method. May be null.
     */
    public void memoryUsed(Test test, long memStart, long memEnd, Long threadId)
    { }

    /**
     * Should be called every time a parameterized test completed with the int value of its test parameter.
     *
     * @param test      The test which memory was measured for.
     * @param parameter The int parameter value.
     * @param threadId  Optional thread id if not calling from thread that started the test method. May be null.
     */
    public void parameterValue(Test test, int parameter, Long threadId)
    { }

    /**
     * Should be called every time a test completes with the current number of test threads running.
     *
     * @param test     The test for which the measurement is being generated.
     * @param threads  The number of tests being run concurrently.
     * @param threadId Optional thread id if not calling from thread that started the test method. May be null.
     */
    public void concurrencyLevel(Test test, int threads, Long threadId)
    { }

    /**
     * Notifies listeners of the tests read/set properties.
     *
     * @param properties The tests read/set properties.
     */
    public void properties(Properties properties)
    { }

    /**
     * Notification that a test ended.
     *
     * @param test The test that ended.
     */
    public void endTest(Test test)
    {
        log.debug("public void endTest(Test test = " + test + "): called");

        // Move complete test results into the completed tests list.
        Result r = threadLocalResults.get(Thread.currentThread().getId());
        results.add(r);

        // Clear all the test results for the thread.
        threadLocalResults.remove(Thread.currentThread().getId());
    }

    /**
     * Called when a test completes. Success, failure and errors. This method should be used when registering an
     * end test from a different thread than the one that started the test.
     *
     * @param test     The test which completed.
     * @param threadId Optional thread id if not calling from thread that started the test method. May be null.
     */
    public void endTest(Test test, Long threadId)
    {
        log.debug("public void endTest(Test test = " + test + ", Long threadId = " + threadId + "): called");

        // Move complete test results into the completed tests list.
        Result r =
            (threadId == null) ? threadLocalResults.get(Thread.currentThread().getId()) : threadLocalResults.get(threadId);
        results.add(r);

        // Clear all the test results for the thread.
        threadLocalResults.remove(Thread.currentThread().getId());
    }

    /**
     * An error occurred.
     *
     * @param test The test in which the error occurred.
     * @param t    The throwable that resulted from the error.
     */
    public void addError(Test test, Throwable t)
    {
        log.debug("public void addError(Test test = " + test + ", Throwable t = " + t + "): called");

        Result r = threadLocalResults.get(Thread.currentThread().getId());
        r.error = t;
        errors++;
    }

    /**
     * A failure occurred.
     *
     * @param test The test in which the failure occurred.
     * @param t    The JUnit assertions that led to the failure.
     */
    public void addFailure(Test test, AssertionFailedError t)
    {
        log.debug("public void addFailure(Test test = " + test + ", AssertionFailedError t = " + t + "): called");

        Result r = threadLocalResults.get(Thread.currentThread().getId());
        r.failure = t;
        failures++;
    }

    /**
     * Called when a test completes to mark it as a test fail. This method should be used when registering a
     * failure from a different thread than the one that started the test.
     *
     * @param test     The test which failed.
     * @param e        The assertion that failed the test.
     * @param threadId Optional thread id if not calling from thread that started the test method. May be null.
     */
    public void addFailure(Test test, AssertionFailedError e, Long threadId)
    {
        log.debug("public void addFailure(Test test, AssertionFailedError e, Long threadId): called");

        Result r =
            (threadId == null) ? threadLocalResults.get(Thread.currentThread().getId()) : threadLocalResults.get(threadId);
        r.failure = e;
        failures++;
    }

    /**
     * Notifies listeners of the start of a complete run of tests.
     */
    public void startBatch()
    {
        log.debug("public void startBatch(): called");

        // Reset all results counts.
        threadLocalResults = Collections.synchronizedMap(new HashMap<Long, Result>());
        errors = 0;
        failures = 0;
        runs = 0;

        // Write out the file header.
        try
        {
            writer.write("<?xml version=\"1.0\" ?>\n");
        }
        catch (IOException e)
        {
            throw new RuntimeException("Unable to write the test results.", e);
        }
    }

    /**
     * Notifies listeners of the end of a complete run of tests.
     *
     * @param parameters The optional test parameters to log out with the batch results.
     */
    public void endBatch(Properties parameters)
    {
        log.debug("public void endBatch(Properties parameters = " + parameters + "): called");

        // Write out the results.
        try
        {
            // writer.write("<?xml version=\"1.0\" ?>\n");
            writer.write("<testsuite errors=\"" + errors + "\" failures=\"" + failures + "\" tests=\"" + runs + "\" name=\""
                + testClassName + "\">\n");

            for (Result result : results)
            {
                writer.write("  <testcase classname=\"" + result.testClass + "\" name=\"" + result.testName + "\">\n");

                if (result.error != null)
                {
                    writer.write("    <error type=\"" + result.error.getClass() + "\">");
                    result.error.printStackTrace(new PrintWriter(writer));
                    writer.write("    </error>");
                }
                else if (result.failure != null)
                {
                    writer.write("    <failure type=\"" + result.failure.getClass() + "\">");
                    result.failure.printStackTrace(new PrintWriter(writer));
                    writer.write("    </failure>");
                }

                writer.write("  </testcase>\n");
            }

            writer.write("</testsuite>\n");
            writer.flush();
        }
        catch (IOException e)
        {
            throw new RuntimeException("Unable to write the test results.", e);
        }
    }

    /**
     * Supplies the shutdown hook.
     *
     * @return The shut down hook.
     */
    public Thread getShutdownHook()
    {
        return new Thread(new Runnable()
                {
                    public void run()
                    {
                        log.debug("XMLTestListener::ShutdownHook: called");
                    }
                });
    }

    /**
     * Used to capture the results of a particular test run.
     */
    protected static class Result
    {
        /** Holds the name of the test class. */
        public String testClass;

        /** Holds the name of the test method. */
        public String testName;

        /** Holds the exception that caused error in this test. */
        public Throwable error;

        /** Holds the assertion exception that caused failure in this test. */
        public AssertionFailedError failure;

        /**
         * Creates a placeholder for the results of a test.
         *
         * @param testClass The test class.
         * @param testName  The name of the test that was run.
         */
        public Result(String testClass, String testName)
        {
            this.testClass = testClass;
            this.testName = testName;
        }
    }
}
