package org.gridkit.sjk.test.console.junit4;

import java.util.Arrays;
import java.util.concurrent.TimeUnit;

import org.gridkit.jvmtool.cli.CommandLauncher;
import org.gridkit.sjk.test.console.StopCommandAfter;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;

/**
 * JUnit4 {@link Rule} for {@link CommandLauncher} based commands.
 * Command execution happens in process so debug is supported.
 * <p>
 * Supports {@link StopCommandAfter} annotation forcing command to be interrupted
 * after timeout. Which is useful for continuous monitoring commands.
 *
 * @author Alexey Ragozin (alexey.ragozin@gmail.com)
 */
public class CliTestRule implements TestRule {

    private final Class<?> mainClass;

    public final ConsoleRule out = ConsoleRule.out();
    public final ConsoleRule err = ConsoleRule.err();

    public CliTestRule() {
        this.mainClass = null;
    }

    public CliTestRule(Class<?> mainClass) {
        this.mainClass = mainClass;
    }

    @Override
    public Statement apply(Statement base, Description description) {
        StopCommandAfter duration = description.getAnnotation(StopCommandAfter.class);

        if (duration != null) {
            base = new Autoshutdown(base, TimeUnit.SECONDS.toMillis(duration.value()));
        }

        base = out.apply(base, description);
        base = err.apply(base, description);

        return base;
    };

    static class Autoshutdown extends Statement {

        private final Statement fOriginalStatement;

        private final long fTimeout;

        public Autoshutdown(Statement originalStatement, long timeout) {
            fOriginalStatement = originalStatement;
            fTimeout = timeout;
        }

        @Override
        public void evaluate() throws Throwable {
            StatementThread thread = evaluateStatement();
            if (!thread.fFinished) {
                throwExceptionForUnfinishedThread(thread);
            }
        }

        private StatementThread evaluateStatement() throws InterruptedException {
            ThreadGroup tg = new ThreadGroup("TestGroup");
            StatementThread thread = new StatementThread(tg, fOriginalStatement);
            thread.start();
            thread.join(fTimeout);
            if (thread.isAlive()) {
                System.err.println("Command interrupted - @StopCommandAfter");
                thread.silentShutdown = true;
                shutdown(tg);
                thread.join(1000);
                shutdown(tg);
            }
            join(tg);
            return thread;
        }

        private void shutdown(ThreadGroup tg) {
            Thread[] threads = listThreads(tg);
            for (Thread th: threads) {
                if (th != null) {
                    th.interrupt();
                }
            }
        }

        private void join(ThreadGroup tg) {
            Thread[] threads = listThreads(tg);
            for (Thread th: threads) {
                if (th != null) {
                    try {
                        th.join(1000);
                    } catch (InterruptedException e) {
                        // ignore
                    }
                }
            }
        }

        private Thread[] listThreads(ThreadGroup tg) {
            Thread[] threads = new Thread[2 * tg.activeCount()];
            while(true) {
                int n = tg.enumerate(threads, true);
                if (n == 0) {
                    return new Thread[0];
                } else if (n < threads.length) {
                    return Arrays.copyOf(threads, n);
                } else {
                    // not enough space to enlist all threads
                    threads = new Thread[2 * threads.length];
                }
            }
        }

        private void throwExceptionForUnfinishedThread(StatementThread thread)
                throws Throwable {
            if (thread.fExceptionThrownByOriginalStatement != null) {
                throw thread.fExceptionThrownByOriginalStatement;
            }
        }

        private static class StatementThread extends Thread {

            private final Statement fStatement;

            private boolean fFinished = false;

            private Throwable fExceptionThrownByOriginalStatement = null;

            private boolean silentShutdown;

            public StatementThread(ThreadGroup group, Statement statement) {
                super(group, "TestThread");
                fStatement = statement;
            }

            @Override
            public void run() {
                try {
                    fStatement.evaluate();
                    fFinished = true;
                } catch (InterruptedException e) {
                    // don't log the InterruptedException
                } catch (Throwable e) {
                    if (!silentShutdown) {
                        fExceptionThrownByOriginalStatement = e;
                    }
                }
            }
        }
    }

    public void exec(String... cmd) {
        try {
            CommandLauncher cl = (CommandLauncher) mainClass.newInstance();
            cl.suppressSystemExit();
            StringBuilder sb = new StringBuilder();
            sb.append(mainClass.getSimpleName());
            for(String c: cmd) {
                sb.append(' ').append(escape(c));
            }
            System.out.println(sb);
            out.line(sb.toString());
            out.verify();
            Assert.assertTrue(cl.start(cmd));

        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public void fail(String... cmd) {
        try {
            CommandLauncher cl = (CommandLauncher) mainClass.newInstance();
            cl.suppressSystemExit();
            StringBuilder sb = new StringBuilder();
            sb.append(mainClass.getSimpleName());
            for(String c: cmd) {
                sb.append(' ').append(escape(c));
            }
            System.out.println(sb);
            out.line(sb.toString());
            out.verify();
            Assert.assertFalse(cl.start(cmd));

        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private Object escape(String c) {
        if (c.split("\\s").length > 1) {
            return '\"' + c + '\"';
        }
        else {
            return c;
        }
    }
}
