/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.asterix.testframework.context;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Pattern;

import org.apache.asterix.testframework.template.TemplateHelper;
import org.apache.asterix.testframework.xml.CategoryEnum;
import org.apache.asterix.testframework.xml.TestCase;
import org.apache.asterix.testframework.xml.TestCase.CompilationUnit;
import org.apache.asterix.testframework.xml.TestGroup;
import org.apache.asterix.testframework.xml.TestSuite;
import org.apache.asterix.testframework.xml.TestSuiteParser;

import com.google.common.collect.Sets;

public class TestCaseContext {

    public static final String DEFAULT_TESTSUITE_XML_NAME = "testsuite.xml";
    public static final String ONLY_TESTSUITE_XML_NAME = "only.xml";
    public static final String DEFAULT_REPEATED_TESTSUITE_XML_NAME = "repeatedtestsuite.xml";

    private File tsRoot;
    private TestSuite testSuite;
    private TestGroup[] testGroups;
    private TestCase testCase;
    private Map<String, Object> kv;
    public int numOfErrors;
    public List<String> expectedErrors;
    public BitSet expectedWarnings;

    public TestCaseContext(File tsRoot, TestSuite testSuite, TestGroup[] testGroups, TestCase testCase) {
        this.tsRoot = tsRoot;
        this.testSuite = testSuite;
        this.testGroups = testGroups;
        this.testCase = testCase;
    }

    public File getTsRoot() {
        return tsRoot;
    }

    public TestSuite getTestSuite() {
        return testSuite;
    }

    public TestGroup[] getTestGroups() {
        return testGroups;
    }

    public TestCase getTestCase() {
        return testCase;
    }

    public int getRepeat() {
        return testCase.getRepeat().intValue();
    }

    public void put(String key, Object object) {
        if (kv == null) {
            kv = new HashMap<>();
        }
        kv.put(key, object);
    }

    @SuppressWarnings("unchecked")
    public <T> T get(String key) {
        return (T) kv.get(key);
    }

    public List<TestFileContext> getFilesInDir(String basePath, String dirName, boolean withType) {
        List<TestFileContext> testFileCtxs = new ArrayList<>();

        File path = tsRoot;
        path = new File(path, basePath);
        path = new File(path, testCase.getFilePath());
        path = new File(path, dirName);

        if (path.isDirectory()) {
            String fileNames[] = path.list();
            for (String fName : fileNames) {
                if (fName.startsWith(".")) {
                    continue;
                }

                File testFile = new File(path, fName);
                if (fName.endsWith(".template")) {
                    try {
                        testFile = TemplateHelper.INSTANCE.resolveTemplateFile(testFile);
                    } catch (IOException e) {
                        throw new IllegalArgumentException(e);
                    }
                    fName = testFile.getName();
                }
                TestFileContext tfsc = new TestFileContext(testFile);
                String[] nameSplits = fName.split("\\.");
                if (nameSplits.length < 3) {
                    throw new IllegalArgumentException("Test file '" + dirName + File.separatorChar + fName
                            + "' does not have the proper test file name format.");
                }
                if (withType) {
                    tfsc.setSeqNum(nameSplits[nameSplits.length - 3]);
                    tfsc.setType(nameSplits[nameSplits.length - 2]);
                } else {
                    tfsc.setSeqNum(nameSplits[nameSplits.length - 2]);
                }
                testFileCtxs.add(tfsc);
            }
        }
        Collections.sort(testFileCtxs);
        return testFileCtxs;
    }

    public List<TestFileContext> getTestFiles(CompilationUnit cUnit) {
        return getFilesInDir(testSuite.getQueryOffsetPath(), cUnit.getName(), true);
    }

    public List<TestFileContext> getExpectedResultFiles(CompilationUnit cUnit) {
        return getFilesInDir(testSuite.getResultOffsetPath(), cUnit.getOutputDir().getValue(), false);
    }

    public List<TestFileContext> getExpectedResultsAndDelta(CompilationUnit cUnit, String deltaBasePath) {
        Comparator<TestFileContext> compOnFileName = Comparator.comparing(o -> o.getFile().getName());
        Set<TestFileContext> deltaSet = new TreeSet<>(compOnFileName);
        deltaSet.addAll(getFilesInDir(deltaBasePath, cUnit.getOutputDir().getValue(), false));
        if (deltaSet.isEmpty()) {
            return getExpectedResultFiles(cUnit);
        }
        Set<TestFileContext> baseSet = new TreeSet<>(compOnFileName);
        baseSet.addAll(getExpectedResultFiles(cUnit));
        Set<TestFileContext> diff = Sets.difference(baseSet, deltaSet);
        List<TestFileContext> expectedWithDelta = new ArrayList<>();
        expectedWithDelta.addAll(diff);
        expectedWithDelta.addAll(deltaSet);
        Collections.sort(expectedWithDelta);
        return expectedWithDelta;
    }

    public File getActualResultFile(CompilationUnit cUnit, File expectedFile, File actualResultsBase) {
        File path = actualResultsBase;
        String resultOffsetPath = removeUpward(testSuite.getResultOffsetPath());
        path = new File(path, resultOffsetPath);
        String testCaseFilePath = removeUpward(testCase.getFilePath());
        String expectedFilePath = removeUpward(expectedFile.getName());
        path = new File(path, testCaseFilePath);
        return new File(path, cUnit.getOutputDir().getValue() + File.separator + expectedFilePath);
    }

    private String removeUpward(String filePath) {
        String evil = ".." + File.separatorChar;
        while (filePath.contains(evil)) {
            filePath = filePath.replace(evil, ""); // NOSONAR
        }
        return filePath;
    }

    public boolean isSourceLocationExpected(CompilationUnit cUnit) {
        Boolean v = cUnit.isSourceLocation();
        return v != null ? v : testSuite.isSourceLocation();
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder(testCase.getFilePath());
        sb.append(':');
        for (CompilationUnit cu : testCase.getCompilationUnit()) {
            sb.append(' ');
            sb.append(cu.getName());
        }
        return sb.toString();
    }

    public static class Builder {
        private final boolean doSlow;
        private final Pattern re;

        public Builder() {
            doSlow = System.getProperty("runSlowAQLTests", "false").equals("true");
            String testre = System.getProperty("testre");
            if (testre == null) {
                this.re = null;
            } else {
                this.re = Pattern.compile(testre);
            }
        }

        public List<TestCaseContext> build(File tsRoot) throws Exception {
            return build(tsRoot, DEFAULT_TESTSUITE_XML_NAME);
        }

        public List<TestCaseContext> build(File tsRoot, String tsXMLFilePath) throws Exception {
            return build(tsRoot, new File(tsRoot, tsXMLFilePath));
        }

        public List<TestCaseContext> build(File tsRoot, File tsFile) throws Exception {
            TestSuiteParser tsp = new TestSuiteParser();
            TestSuite ts = tsp.parse(tsFile);
            return build(tsRoot, ts);
        }

        public List<TestCaseContext> build(File tsRoot, TestSuite ts) throws Exception {
            List<TestCaseContext> tccs = new ArrayList<>();
            List<TestGroup> tgPath = new ArrayList<>();
            addContexts(tsRoot, ts, tgPath, ts.getTestGroup(), tccs);
            return tccs;
        }

        private void addContexts(File tsRoot, TestSuite ts, List<TestGroup> tgPath, List<TestGroup> testGroups,
                List<TestCaseContext> tccs) {
            for (TestGroup tg : testGroups) {
                tgPath.add(tg);
                addContexts(tsRoot, ts, tgPath, tccs);
                tgPath.remove(tgPath.size() - 1);
            }
        }

        private void addContexts(File tsRoot, TestSuite ts, List<TestGroup> tgPath, List<TestCaseContext> tccs) {
            TestGroup tg = tgPath.get(tgPath.size() - 1);
            for (TestCase tc : tg.getTestCase()) {
                if (doSlow || tc.getCategory() != CategoryEnum.SLOW) {
                    boolean matches = false;
                    if (re != null) {
                        // Check all compilation units for matching
                        // name. If ANY match, add the test.
                        for (TestCase.CompilationUnit cu : tc.getCompilationUnit()) {
                            if (re.matcher(cu.getName()).find()) {
                                matches = true;
                                break;
                            }
                        }
                    } else {
                        // No regex == everything matches
                        matches = true;
                    }

                    if (matches) {
                        tccs.add(new TestCaseContext(tsRoot, ts, tgPath.toArray(new TestGroup[tgPath.size()]), tc));
                    }
                }
            }
            addContexts(tsRoot, ts, tgPath, tg.getTestGroup(), tccs);
        }
    }

    /**
     * For specifying the desired output formatting of results.
     */
    public enum OutputFormat {
        NONE("", ""),
        ADM("adm", "application/x-adm"),
        LOSSLESS_JSON("json", "application/json; lossless=true"),
        LOSSLESS_ADM_JSON("json", "application/json; lossless-adm=true"),
        CLEAN_JSON("json", "application/json"),
        CSV("csv", "text/csv"),
        CSV_HEADER("csv-header", "text/csv; header=present"),
        AST("ast", "application/x-ast"),
        PLAN("plan", "application/x-plan"),
        BINARY("", "application/octet-stream");

        private final String extension;
        private final String mimetype;

        OutputFormat(String ext, String mime) {
            this.extension = ext;
            this.mimetype = mime;
        }

        public String extension() {
            return extension;
        }

        public String mimeType() {
            return mimetype;
        }

        //
        public static OutputFormat forCompilationUnit(CompilationUnit cUnit) {
            switch (cUnit.getOutputDir().getCompare()) {
                case TEXT:
                    return OutputFormat.ADM;
                case LOSSLESS_JSON:
                    return OutputFormat.LOSSLESS_JSON;
                case LOSSLESS_ADM_JSON:
                    return OutputFormat.LOSSLESS_ADM_JSON;
                case CLEAN_JSON:
                    return OutputFormat.CLEAN_JSON;
                case CSV:
                    return OutputFormat.CSV;
                case CSV_HEADER:
                    return OutputFormat.CSV_HEADER;
                case BINARY:
                    return OutputFormat.BINARY;
                case INSPECT:
                case IGNORE:
                    return OutputFormat.NONE;
                case AST:
                    return OutputFormat.AST;
                case PLAN:
                    return OutputFormat.PLAN;
                default:
                    assert false : "Unknown ComparisonEnum!";
                    return OutputFormat.NONE;
            }
        }
    }
}
