package com.xxx.amp.magenta.livecodebench;

import com.xxx.amp.magenta.livecodebench.model.TestRun;
import com.xxx.amp.magenta.livecodebench.model.TestStatus;
import org.json.JSONObject;

import java.lang.reflect.AnnotatedType;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Parameter;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.*;

/**
 * A test runner calling the solution method through Java API.
 *
 * @author Alexey Kharlamov <aih1013@xxx.com>
 */
public class FuncCallSuite extends ProblemTestSuite {
    private final List<List<Object>> parsedInputArgs = new ArrayList<>();
    private final List<Object> parsedOutput = new ArrayList<>();
    private final LiteralParser parser = new LiteralParser();

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

    @Override
    public void load(JSONObject serialized) throws LiteralSyntaxException {
        super.load(serialized);

        for (String in : this.inputs) {
            Parameter[] parameters = getMethod().getParameters();
            Type[] paramTypes = Arrays.stream(parameters)
                    .map(Parameter::getAnnotatedType)
                    .map(AnnotatedType::getType)
                    .toArray(Type[]::new);
            this.parsedInputArgs.add(parseInput(in, paramTypes));
        }

        Type returnType = getMethod().getGenericReturnType();
        for (String out : this.outputs) {
            this.parsedOutput.add(parser.parse(returnType, out.trim()));
        }
    }

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

        for (int i = 0; i < this.parsedInputArgs.size(); i++) {
            List<Object> input = this.parsedInputArgs.get(i);
            Object expectedOutput = this.parsedOutput.get(i);

            TestRun testRun = new TestRun();
            testRun.setInput(this.inputs.get(i));
            testRun.setExpectedOutput(this.outputs.get(i));
            testRun.setStatus(TestStatus.PASSED);

            Future<Object> testFuture = startTestRun(input, testRun);

            waitAndCheck(testFuture, testRun, expectedOutput, testTimeoutMills);

            results.add(testRun);
        }

        return results;
    }

    private Future<Object> startTestRun(List<Object> input, TestRun testRun) {
        // This is far from optimal. Unfortunately, there are no reliable
        // methods to terminate CPU hogging thread.
        //
        // If some test takes too long, we cannot terminate it. So we create
        // a new executor and hope the rest will be fine. Worst case scenario
        // the parent process will kill us.
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        Future<Object> future = executorService.submit(() -> {
            Object result = null;
            try {
                Object solutionObj = this.getSolutionClass()
                        .getDeclaredConstructors()[0].newInstance();
                result = getMethod().invoke(solutionObj, input.toArray());
                testRun.setOutput(result != null ? result.toString() : null);
            } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
                e.printStackTrace();
                testRun.setStatus(TestStatus.FAILED);
            } catch (Throwable t) {
                t.printStackTrace();
                testRun.setStatus(TestStatus.ERROR);
            }
            return result;
        });
        executorService.shutdownNow();
        return future;
    }

    private void waitAndCheck(Future<Object> testFuture, TestRun testRun, Object expectedOutput, long testTimeoutMills) {
        try {
            Object output = testFuture.get(testTimeoutMills, TimeUnit.MILLISECONDS);
            if (testRun.getStatus() == TestStatus.PASSED && expectedOutput.equals(output)) {
                testRun.setStatus(TestStatus.PASSED);
            } else {
                testRun.setStatus(TestStatus.FAILED);
            }
        } catch (InterruptedException | TimeoutException | ExecutionException e) {
            testRun.setStatus(TestStatus.FAILED);
        }
    }

    private List<Object> parseInput(String in, Type[] parameterTypes) throws LiteralSyntaxException {
        String[] lines = in.split("\n");
        if (lines.length != parameterTypes.length) {
            throw new LiteralSyntaxException(
                    "Incorrect input: expected %d arguments, got %d".formatted(parameterTypes.length, lines.length)
            );
        }

        List<Object> result = new ArrayList<>();
        for (int i = 0; i < lines.length; i++) {
            String trimmed = lines[i].trim();
            Type cls = parameterTypes[i];

            result.add(parser.parse(cls, trimmed));
        }
        return result;
    }

    public List<List<Object>> getParsedInputArgs() {
        return parsedInputArgs;
    }

    public List<Object> getParsedOutput() {
        return parsedOutput;
    }
}
