package com.alibaba.qlexpress4;

import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;

import com.alibaba.fastjson2.JSONException;
import com.alibaba.qlexpress4.aparser.ImportManager;
import com.alibaba.qlexpress4.exception.QLException;
import com.alibaba.qlexpress4.exception.QLRuntimeException;
import com.alibaba.qlexpress4.exception.UserDefineException;
import com.alibaba.qlexpress4.runtime.*;
import com.alibaba.qlexpress4.runtime.function.CustomFunction;

import com.alibaba.qlexpress4.security.QLSecurityStrategy;

import org.junit.Assert;
import org.junit.Test;

import static org.junit.Assert.assertEquals;

/**
 * Author: DQinYuan
 */
public class TestSuiteRunner {
    private static final String ASSERT_FUNCTION_NAME = "assert";
    
    private static final String ASSERT_FALSE_FUNCTION_NAME = "assertFalse";
    
    private static final String ASSERT_ERROR_CODE_FUNCTION_NAME = "assertErrorCode";
    
    private static final String PRINT_FUNCTION_NAME = "println";
    
    private static final String TEST_PATH_ATT = "TEST_PATH";
    
    private static final Express4Runner CONFIG_RUNNER = new Express4Runner(InitOptions.builder()
        .securityStrategy(QLSecurityStrategy.open())
        .addDefaultImport(Arrays.asList(ImportManager.importCls("com.alibaba.qlexpress4.QLOptions"),
            ImportManager.importCls("com.alibaba.qlexpress4.InitOptions")))
        .build());
    
    private Express4Runner prepareRunner(InitOptions initOptions) {
        Express4Runner testRunner = new Express4Runner(initOptions);
        testRunner.addFunction(ASSERT_FUNCTION_NAME, new AssertFunction());
        testRunner.addFunction(ASSERT_FALSE_FUNCTION_NAME, new AssertFalseFunction());
        testRunner.addFunction(ASSERT_ERROR_CODE_FUNCTION_NAME, new AssertErrorCodeFunction());
        testRunner.addFunction(PRINT_FUNCTION_NAME, new PrintFunction());
        return testRunner;
    }
    
    @Test
    public void suiteTest()
        throws URISyntaxException, IOException {
        Path testSuiteRoot = getTestSuiteRoot();
        handleDirectory(testSuiteRoot, "");
    }
    
    @Test
    public void featureDebug()
        throws URISyntaxException, IOException {
        Path filePath = getTestSuiteRoot().resolve("independent/function/nested_function_calls.ql");
        handleFile(filePath, filePath.toString(), true);
    }
    
    private void handleDirectory(Path dir, String pathPrefix)
        throws IOException {
        Files.list(dir).forEach(path -> {
            try {
                String newPrefix = pathPrefix + "/" + path.getFileName();
                if (Files.isDirectory(path)) {
                    handleDirectory(path, newPrefix);
                }
                else {
                    handleFile(path, newPrefix, false);
                }
            }
            catch (IOException e) {
                throw new RuntimeException(e);
            }
        });
    }
    
    public void handleFile(Path qlFile, String path, boolean debug)
        throws IOException {
        printRunning(path);
        Map<String, Object> attachments = new HashMap<>();
        attachments.put(TEST_PATH_ATT, path);
        
        String qlScript = new String(Files.readAllBytes(qlFile));
        // parse testsuite option first
        Optional<Map<String, Object>> scriptOptionOp = parseOption(qlScript);
        Optional<String> errCodeOp = scriptOptionOp.map(scriptOption -> (String)scriptOption.get("errCode"));
        Optional<InitOptions.Builder> initOptionsBuilder =
            scriptOptionOp.map(scriptOption -> (InitOptions.Builder)scriptOption.get("initOptions"));
        InitOptions initOptions = initOptionsBuilder.isPresent()
            ? initOptionsBuilder.get().securityStrategy(QLSecurityStrategy.open()).debug(debug).build()
            : InitOptions.builder().securityStrategy(QLSecurityStrategy.open()).debug(debug).build();
        Express4Runner express4Runner = prepareRunner(initOptions);
        Optional<QLOptions.Builder> optionsBuilder =
            scriptOptionOp.map(scriptOption -> (QLOptions.Builder)scriptOption.get("qlOptions"));
        QLOptions qlOptions = optionsBuilder.isPresent() ? optionsBuilder.get().attachments(attachments).build()
            : QLOptions.builder().attachments(attachments).build();
        if (errCodeOp.isPresent()) {
            long start = System.currentTimeMillis();
            assertErrCode(express4Runner, path, qlScript, qlOptions, errCodeOp.get(), debug);
            printOk(path, System.currentTimeMillis() - start);
            return;
        }
        
        try {
            long start = System.currentTimeMillis();
            express4Runner.execute(qlScript, Collections.emptyMap(), qlOptions);
            printOk(path, System.currentTimeMillis() - start);
        }
        catch (Exception e) {
            System.out.printf("%1$-95s %2$s\n", path, "error");
            throw e;
        }
    }
    
    private void printRunning(String path) {
        System.out.printf("%1$-98s %2$s\n", path, "running");
    }
    
    private void printOk(String path, long consumeTime) {
        System.out.printf("%1$-98s %2$s consume %3$dms\n", path, "ok", consumeTime);
    }
    
    private void assertErrCode(Express4Runner runner, String path, String qlScript, QLOptions qlOptions,
        String expectErrCode, boolean printE) {
        try {
            runner.execute(qlScript, Collections.emptyMap(), qlOptions);
            Assert.fail("expect error codes:" + expectErrCode + ", but end normally");
        }
        catch (QLException qlException) {
            if (printE) {
                qlException.printStackTrace();
            }
            assertEquals(path + " error code assert fail", expectErrCode, qlException.getErrorCode());
        }
        catch (Exception e) {
            throw new RuntimeException(path + " unknown error", e);
        }
    }
    
    private Optional<Map<String, Object>> parseOption(String qlScript) {
        if (!qlScript.startsWith("/*")) {
            return Optional.empty();
        }
        int endIndex = qlScript.indexOf("*/");
        if (endIndex == -1) {
            return Optional.empty();
        }
        String configJson = qlScript.substring(2, endIndex);
        try {
            Map<String, Object> scriptOptions = (Map<String, Object>)CONFIG_RUNNER
                .execute(configJson, Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS)
                .getResult();
            return Optional.of(scriptOptions);
        }
        catch (JSONException e) {
            return Optional.empty();
        }
    }
    
    @Test
    public void assertTest() {
        Map<String, Object> attachment = new HashMap<>();
        attachment.put(TEST_PATH_ATT, "a/b.ql");
        
        QLOptions attachOptions = QLOptions.builder().attachments(attachment).build();
        Express4Runner express4Runner = prepareRunner(InitOptions.DEFAULT_OPTIONS);
        express4Runner.execute("assert(true)", Collections.emptyMap(), attachOptions);
        assertErrCodeAndReason(express4Runner, "assert(false)", attachOptions, "BIZ_EXCEPTION", "a/b.ql: assert fail");
        assertErrCodeAndReason(express4Runner,
            "assert(false, 'my test')",
            attachOptions,
            "BIZ_EXCEPTION",
            "a/b.ql: my test");
        // variable can be the same name with function
        express4Runner.execute("assert = 4;assert(assert == 4)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS);
    }
    
    private Path getTestSuiteRoot()
        throws URISyntaxException {
        return Paths.get(getClass().getClassLoader().getResource("testsuite").toURI());
    }
    
    private void assertErrCodeAndReason(Express4Runner express4Runner, String script, QLOptions qlOptions,
        String errCode, String reason) {
        try {
            express4Runner.execute(script, Collections.emptyMap(), qlOptions);
        }
        catch (QLException e) {
            assertEquals(errCode, e.getErrorCode());
            assertEquals(reason, e.getReason());
        }
    }
    
    private static class PrintFunction implements CustomFunction {
        @Override
        public Object call(QContext qContext, Parameters parameters)
            throws Exception {
            for (int i = 0; i < parameters.size(); i++) {
                System.out.println(parameters.get(i).get());
            }
            return null;
        }
    }
    
    private static class AssertFunction implements CustomFunction {
        @Override
        public Object call(QContext qContext, Parameters parameters)
            throws Exception {
            int pSize = parameters.size();
            switch (pSize) {
                case 1:
                    Boolean b = (Boolean)parameters.getValue(0);
                    if (b == null || !b) {
                        throw new UserDefineException(wrap(qContext.attachment(), "assert fail"));
                    }
                    return null;
                case 2:
                    Boolean b0 = (Boolean)parameters.getValue(0);
                    if (b0 == null || !b0) {
                        throw new UserDefineException(wrap(qContext.attachment(), (String)parameters.getValue(1)));
                    }
                    return null;
                default:
                    throw new UserDefineException("invalid parameter size");
            }
        }
        
        private String wrap(Map<String, Object> attachments, String originErrInfo) {
            return attachments.get(TEST_PATH_ATT) + ": " + originErrInfo;
        }
    }
    
    private static class AssertFalseFunction implements CustomFunction {
        @Override
        public Object call(QContext qContext, Parameters parameters)
            throws Exception {
            int pSize = parameters.size();
            switch (pSize) {
                case 1:
                    Boolean b = (Boolean)parameters.getValue(0);
                    if (b == null || b) {
                        throw new UserDefineException(wrap(qContext.attachment(), "assert fail"));
                    }
                    return null;
                case 2:
                    Boolean b0 = (Boolean)parameters.getValue(0);
                    if (b0 == null || b0) {
                        throw new UserDefineException(wrap(qContext.attachment(), (String)parameters.getValue(1)));
                    }
                    return null;
                default:
                    throw new UserDefineException("invalid parameter size");
            }
        }
        
        private String wrap(Map<String, Object> attachments, String originErrInfo) {
            return attachments.get(TEST_PATH_ATT) + ": " + originErrInfo;
        }
    }
    
    private static class AssertErrorCodeFunction implements CustomFunction {
        @Override
        public Object call(QContext qContext, Parameters parameters)
            throws Throwable {
            int pSize = parameters.size();
            if (pSize != 2) {
                throw new UserDefineException(String.format("invalid pSize:%s, expected 2 parameters", pSize));
            }
            
            Value value = parameters.get(0);
            Value value1 = parameters.get(1);
            QLambda qLambda = (QLambda)value.get();
            try {
                qLambda.run();
                throw new UserDefineException(String.format("expectedErrorCode:%s, but no error", value1.get()));
            }
            catch (QLRuntimeException e) {
                if (!Objects.equals(value1.get(), e.getErrorCode())) {
                    throw new UserDefineException(
                        String.format("expectedErrorCode:%s, actualErrorCode:%s", value1.get(), e.getErrorCode()));
                }
                return null;
            }
        }
    }
}
