/**
 * Copyright 2007-2015, Kaazing Corporation. All rights reserved.
 *
 * 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.kaazing.k3po.junit.rules;

import static java.lang.String.format;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

import org.junit.AssumptionViolatedException;
import org.junit.ComparisonFailure;
import org.junit.runners.model.Statement;
import org.kaazing.k3po.junit.rules.internal.ScriptPair;

final class SpecificationStatement extends Statement {

    private final Statement statement;
    private final Latch latch;
    private final ScriptRunner scriptRunner;

    SpecificationStatement(Statement statement, URL controlURL, List<String> scriptNames, Latch latch,
            List<String> overridenScriptProperties) {
        this.statement = statement;
        this.latch = latch;
        this.scriptRunner = new ScriptRunner(controlURL, scriptNames, latch, overridenScriptProperties);
    }

    @Override
    public void evaluate() throws Throwable {

        latch.setInterruptOnException(Thread.currentThread());

        FutureTask<ScriptPair> scriptFuture = new FutureTask<>(scriptRunner);

        try {
            // start the script execution
            new Thread(scriptFuture).start();

            try {
                // wait for script to be prepared (all binds ready for incoming connections from statement)
                latch.awaitPrepared();

                // note: JUnit timeout will trigger an exception
                statement.evaluate();
            } catch (AssumptionViolatedException e) {

                if (!latch.isFinished()) {
                    scriptRunner.abort();
                }

                throw e;
            } catch (Throwable cause) {
                // any exception aborts the script (including timeout)
                if (latch.hasException()) {
                    // propagate exception if the latch has an exception
                	if (cause instanceof InterruptedException) {
                		// take the error from the latch, it is more meaningful
                		throw latch.getException();
                	}
                    throw cause;
                } else {
                    // It is possible that the script is finished even if we get an exception
                    // in particular a timeout may occur but the script may finish before we actually
                    // process it the timeout, send the abort and get the result back.

                    // No reason to send the abort if we are already finished.
                    // Note that there is a race in that the script may be finished before
                    // we actually send the abort. But that is ok.
                    if (!latch.isFinished()) {
                        scriptRunner.abort();
                    }

                    try {
                        // wait at most 5sec for the observed script (due to the abort case)
                        // should take less than a second for K3PO to complete
                        ScriptPair scripts = scriptFuture.get(5, SECONDS);

                        try {
                            assertEquals("Specified behavior did not match", scripts.getExpectedScript(),
                                    scripts.getObservedScript());
                            // Throw the original exception if we are equal
                            throw cause;
                        } catch (ComparisonFailure f) {
                            // throw an exception that highlights the difference in behavior, but caused by the timeout
                            // (or
                            // original exception)
                            f.initCause(cause);
                            throw f;
                        }

                    } catch (ExecutionException ee) {
                        throw ee.getCause().initCause(cause);
                    } catch (Exception e) {
                        // Note that ComparisonFailure are not an Exception we do want those to bubble up.
                        throw cause;
                    }
                }
            }

            // note: statement MUST call join() to ensure wrapped Rule(s) do not complete early
            // and to allow Specification script(s) to make progress
            String k3poSimpleName = K3poRule.class.getSimpleName();
            assertTrue(format("Did you instantiate %s with a @Rule and call %s.join()?", k3poSimpleName, k3poSimpleName),
                    latch.isStartable());

            ScriptPair scripts = scriptFuture.get();

            assertEquals("Specified behavior did not match", scripts.getExpectedScript(), scripts.getObservedScript());
        } finally {
            try {
                scriptRunner.dispose();
            } finally {
                // clean up the task if it is still running
                scriptFuture.cancel(true);
            }
        }
    }

    public void awaitBarrier(String barrierName) throws InterruptedException {
        scriptRunner.awaitBarrier(barrierName);

    }

    public void notifyBarrier(String barrierName) throws InterruptedException {
        scriptRunner.notifyBarrier(barrierName);
    }
}

@@ -889,30 +889,8 @@
 st;%0A
-import java.util.Map;%0A
 impo
@@ -2129,24 +2129,320 @@
 Prepared();%0A
+            %7D catch (InterruptedException e) %7B%0A                if (latch.hasException()) %7B%0A                    // propagate exception if the latch has an exception%0A                    throw latch.getException();%0A                %7D else%0A                    throw e;%0A            %7D%0A%0A            try %7B
 %0A           
@@ -4609,16 +4609,17 @@
  timeout
+ 
 %0A       
@@ -4649,39 +4649,8 @@
  (or
-%0A                            //
  ori
@@ -5129,32 +5129,157 @@
 %0A            %7D%0A%0A
+            // Stefan - not sure what the comment bellow is about. There is no join() method in the statement or rule class.%0A
             // n
@@ -5578,16 +5578,48 @@
  %25s.
-join()?%22
+start() or %25s.finish()?%22, k3poSimpleName
 , k3
@@ -6229,17 +6229,16 @@
 rName);%0A
-%0A
     %7D%0A%0A 

Throw more meaningfull exception in case of failed prepare plus some small comments and texts modifications.