/*
 * JBoss, Home of Professional Open Source
 * Copyright 2009 Red Hat Inc. and/or its affiliates and other contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * 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.jboss.arquillian.test.spi;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * A test result which may be serialized for communicate between client and
 * server
 *
 * @author Pete Muir
 * @author <a href="mailto:aknutsen@redhat.com">Aslak Knutsen</a>
 */
public final class TestResult implements Serializable {
    private static final long serialVersionUID = 1L;
    private Status status;
    private String description = "";
    private transient Throwable throwable;
    private ExceptionProxy exceptionProxy;
    private long start;
    private long end;

    @Deprecated
    public TestResult(Status status, String description) {
        this(status);
        this.description = description;
    }

    /**
     * Create a empty result.<br/>
     * <br/>
     * Start time is set to Current Milliseconds.
     */
    @Deprecated
    public TestResult() {
        this(null);
    }

    /**
     * Create a new TestResult.<br/>
     * <br/>
     * Start time is set to Current Milliseconds.
     *
     * @param status
     *     The result status.
     */
    @Deprecated
    public TestResult(Status status) {
        this(status, (Throwable) null);
    }

    /**
     * Create a new TestResult.<br/>
     * <br/>
     * Start time is set to Current Milliseconds.
     *
     * @param status
     *     The result status.
     * @param throwable
     *     thrown exception if any
     */
    @Deprecated
    public TestResult(Status status, Throwable throwable) {
        this.status = status;
        setThrowable(throwable);

        this.start = System.currentTimeMillis();
    }

    public static TestResult passed() {
        return new TestResult(Status.PASSED);
    }

    public static TestResult passed(String description) {
        return new TestResult(Status.PASSED, description);
    }

    public static TestResult skipped(Throwable cause) {
        return new TestResult(Status.SKIPPED, cause);
    }

    public static TestResult skipped(String description) {
        return new TestResult(Status.SKIPPED, description);
    }

    public static TestResult skipped() {
        return new TestResult(Status.SKIPPED);
    }

    public static TestResult failed(Throwable cause) {
        return new TestResult(Status.FAILED, cause);
    }

    public static TestResult flatten(Collection<TestResult> results) {
        final TestResult combinedResult = new TestResult(Status.PASSED);
        final Map<Status, TestResult> resultsPerStatus = new HashMap<Status, TestResult>();
        final List<Throwable> allExceptions = new ArrayList<Throwable>();

        for (TestResult result : results) {
            if (result == null) {
                continue;  // For some strange reason sometimes results contain null objects
            }
            resultsPerStatus.put(result.getStatus(), result);
            if (result.getThrowable() != null) {
                allExceptions.add(result.getThrowable());
            }
            combinedResult.addDescription(
                String.format("%s: '%s'%n", result.getStatus().name(), result.getDescription()));
        }

        propagateTestResultStatus(combinedResult, resultsPerStatus);
        propagateExceptions(combinedResult, allExceptions);

        return combinedResult;
    }

    private static void propagateExceptions(TestResult combinedResult, List<Throwable> allExceptions) {
        if (!allExceptions.isEmpty()) {
            switch (allExceptions.size()) {
                case 1:
                    combinedResult.setThrowable(allExceptions.get(0));
                    break;
                default:
                    combinedResult.setThrowable(new CombinedException("Combined test result exceptions", allExceptions));
            }
        }
    }

    private static void propagateTestResultStatus(TestResult combinedResult, Map<Status, TestResult> resultsPerStatus) {
        if (resultsPerStatus.containsKey(Status.FAILED)) {
            combinedResult.status = Status.FAILED;
        } else if (resultsPerStatus.containsKey(Status.PASSED)) {
            combinedResult.status = Status.PASSED;
        } else if (resultsPerStatus.containsKey(Status.SKIPPED)) {
            combinedResult.status = Status.SKIPPED;
        }
    }

    /**
     * Get the status of this test
     */
    public Status getStatus() {
        return status;
    }

    @Deprecated
    public TestResult setStatus(Status status) {
        this.status = status;
        return this;
    }

    public String getDescription() {
        return description;
    }

    @Deprecated
    public void setDescription(String description) {
        this.description = description;
    }

    public void addDescription(String description) {
        this.description += description;
    }

    /**
     * If the test failed, the exception that was thrown. It does not need to be
     * the root cause.
     */
    public Throwable getThrowable() {
        if (throwable == null) {
            if (exceptionProxy != null) {
                throwable = exceptionProxy.createException();
            }
        }
        return throwable;
    }

    public TestResult setThrowable(Throwable throwable) {
        this.throwable = throwable;
        this.exceptionProxy = ExceptionProxy.createForException(throwable);
        return this;
    }

    /**
     * Get the start time.
     *
     * @return Start time in milliseconds
     */
    public long getStart() {
        return start;
    }

    /**
     * Set the start time of the test.
     *
     * @param start
     *     Start time in milliseconds
     */
    public TestResult setStart(long start) {
        this.start = start;
        return this;
    }

    /**
     * Get the end time.
     *
     * @return End time in milliseconds
     */
    public long getEnd() {
        return end;
    }

    /**
     * Set the end time of the test.
     *
     * @param end
     *     time in milliseconds
     */
    public TestResult setEnd(long end) {
        this.end = end;
        return this;
    }

    public ExceptionProxy getExceptionProxy() {

        return exceptionProxy;
    }

    @Override
    public String toString() {
        long time = (end > 0 ? end - start : System.currentTimeMillis() - start);
        return "TestResult[status=" + status + ",time=" + time + "ms]";
    }

    /**
     * The test status
     *
     * @author Pete Muir
     */
    public enum Status {
        /**
         * The test passed
         */
        PASSED,
        /**
         * The test failed
         */
        FAILED,
        /**
         * The test was skipped due to some deployment problem
         */
        SKIPPED;
    }
}
