package com.xxx.amp.magenta.livecodebench;

import com.xxx.amp.magenta.livecodebench.model.TestRun;
import com.xxx.amp.magenta.livecodebench.model.TestStatus;
import org.junit.contrib.java.lang.system.SystemOutRule;
import org.junit.contrib.java.lang.system.TextFromStandardInputStream;
import org.junit.runners.model.Statement;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;

import static org.junit.contrib.java.lang.system.TextFromStandardInputStream.emptyStandardInputStream;

/**
 * A test runner capable to interact with a solution through
 * stdin/stdout streams.
 *
 * @author Alexey Kharlamov <aih1013@xxx.com>
 */
public class StdioSuite extends ProblemTestSuite {

    public StdioSuite(Class<?> solutionClass) {
        super(solutionClass);
    }

    public List<TestRun> run(long testTimeoutMills) {
        List<TestRun> results = new ArrayList<>();

        for (int i = 0; i < inputs.size(); i++) {
            String input = this.inputs.get(i)
                    .trim();
            String expectedOutput = this.outputs.get(i)
                    .trim();

            TestRun result = TestRun.newFrom(input, expectedOutput);

            executeWithTimeout(result, testTimeoutMills);

            results.add(result);
        }

        return results;
    }

    private void executeWithTimeout(TestRun theRun, long testTimeoutMills) {
        ExecutorService executorService = Executors.newSingleThreadExecutor();

        Future<?> future = executorService.submit(() -> {
            runTest(theRun);
        });

        try {
            future.get(testTimeoutMills, TimeUnit.MILLISECONDS);
        } catch (InterruptedException | ExecutionException | TimeoutException e) {
            theRun.setStatus(TestStatus.FAILED);
        }

        executorService.shutdownNow();
    }

    private void runTest(TestRun theRun) {
        TextFromStandardInputStream systemInMock = emptyStandardInputStream();
        systemInMock.provideLines(theRun.getInput());
        SystemOutRule outRule = new SystemOutRule().enableLog();

        Statement assertion = new Statement() {
            @Override
            public void evaluate() throws Throwable {
                getMethod().invoke(null, (Object) new String[]{});
                String output = outRule.getLog()
                        .trim();
                theRun.setOutput(output);

                if (theRun.getExpectedOutput()
                        .equals(output)) {
                    theRun.setStatus(TestStatus.PASSED);
                } else {
                    theRun.setStatus(TestStatus.FAILED);
                }
            }
        };

        Statement mockedInput = systemInMock.apply(
                outRule.apply(assertion, null),
                null);

        try {
            mockedInput.evaluate();
        } catch (Throwable e) {
            theRun.setStatus(TestStatus.FAILED);
        }
    }
}
