/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed 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 com.android.manifmerger;

import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.utils.FileUtils;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.io.Closeables;
import com.google.common.io.Files;

import org.junit.runner.RunWith;
import org.w3c.dom.Document;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ManifestMergerTestUtil {

    /**
     * Delimiter that indicates the test must fail. An XML output and errors are still generated and
     * checked.
     */
    private static final String DELIM_FAILS = "fails";

    /**
     * Delimiter that starts a library XML content. The delimiter name must be in the form {@code
     * @libSomeName} and it will be used as the base for the test file name. Using separate lib
     * names is encouraged since it makes the error output easier to read.
     */
    private static final String DELIM_LIB = "lib";

    /**
     * Delimiter that starts the main manifest XML content.
     */
    private static final String DELIM_MAIN = "main";

    /**
     * Delimiter that starts an overlay XML content. The delimiter must follow the same rules as
     * {@link #DELIM_LIB}
     */
    private static final String DELIM_OVERLAY = "overlay";

    /**
     * Delimiter that starts the resulting XML content, whatever is generated by the merge.
     */
    private static final String DELIM_RESULT = "result";

    /**
     * Delimiter that starts the SdkLog output. The logger prints each entry on its lines, prefixed
     * with E for errors, W for warnings and P for regular printfs.
     */
    private static final String DELIM_ERRORS = "errors";

    /**
     * Delimiter for starts a section that declares how to inject an attribute. The section is
     * composed of one or more lines with the syntax: "/node/node|attr-URI attrName=attrValue". This
     * is essentially a pseudo XPath-like expression.
     */
    private static final String DELIM_INJECT_ATTR = "inject";

    /**
     * Delimiter for a section that declares how to toggle a ManifMerger option. The section is
     * composed of one or more lines with the syntax: "functionName=false|true".
     */
    private static final String DELIM_FEATURES = "features";

    /**
     * Delimiter for a section that declares how to override the package. The section is composed of
     * one line containing the new package name.
     */
    private static final String DELIM_PACKAGE = "package";

    /**
     * Loads test data for a given test case.
     * The input (main + libs) are stored in temp files.
     * A new destination temp file is created to store the actual result output.
     * The expected result is actually kept in a string.
     * <p>
     * Data File Syntax:
     * <ul>
     * <li> Lines starting with # are ignored (anywhere, as long as # is the first char).
     * <li> Lines before the first {@code @delimiter} are ignored.
     * <li> Empty lines just after the {@code @delimiter}
     *      and before the first &lt; XML line are ignored.
     * <li> Valid delimiters are {@code @main} for the XML of the main app manifest.
     * <li> Following delimiters are {@code @libXYZ}, read in the order of definition.
     *      The name can be anything as long as it starts with "{@code @lib}".
     * </ul>
     *
     * @param testDataDirectory The resource directory name the data file is located in.
     * @param filename The test data filename. If no extension is provided, this will
     *   try with .xml or .txt. Must not be null.
     * @param className The simple name of the test class,
     *                  the manifest files include it in their output.
     * @return A new {@link ManifestMergerTestUtil.TestFiles} instance. Must not be null.
     * @throws Exception when things fail to load properly.
     */
    @NonNull
    static TestFiles loadTestData(
            @NonNull String testDataDirectory,
            @NonNull String filename,
            @NonNull String className) throws Exception {

        String resName = testDataDirectory + File.separator + filename;
        InputStream is = null;
        BufferedReader reader = null;
        BufferedWriter writer = null;

        try {
            is = ManifestMergerTestUtil.class.getResourceAsStream(resName);
            if (is == null && !filename.endsWith(".xml")) {
                String resName2 = resName + ".xml";
                is = ManifestMergerTestUtil.class.getResourceAsStream(resName2);
                if (is != null) {
                    filename = resName2;
                }
            }
            if (is == null && !filename.endsWith(".txt")) {
                String resName3 = resName + ".txt";
                is = ManifestMergerTestUtil.class.getResourceAsStream(resName3);
                if (is != null) {
                    filename = resName3;
                }
            }
            assertNotNull("Test data file not found for " + filename, is);

            reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));

            final File tempDir = Files.createTempDir();
            tempDir.deleteOnExit();
            Runtime.getRuntime().addShutdownHook(new Thread() {
                @Override
                public void run() {
                    try {
                        FileUtils.deletePath(tempDir);
                    } catch (IOException e) {
                        throw new AssertionError(e);
                    }
                }
            });
            tempDir.deleteOnExit();

            String line = null;
            String delimiter = null;
            boolean skipEmpty = true;

            boolean shouldFail = false;
            Map<String, Boolean> features = new HashMap<String, Boolean>();
            String packageOverride = null;
            Map<String, String> injectAttributes = new HashMap<String, String>();
            StringBuilder expectedResult = new StringBuilder();
            StringBuilder expectedErrors = new StringBuilder();
            File mainFile = null;
            File actualResultFile = null;
            List<File> libFiles = new ArrayList<File>();
            List<File> overlayFiles = new ArrayList<File>();
            int tempIndex = 0;

            while ((line = reader.readLine()) != null) {
                if (skipEmpty && line.trim().isEmpty()) {
                    continue;
                }
                if (!line.isEmpty() && line.charAt(0) == '#') {
                    continue;
                }
                if (!line.isEmpty() && line.charAt(0) == '@') {
                    delimiter = line.substring(1);
                    assertTrue(
                            "Unknown delimiter @" + delimiter + " in " + filename,
                            delimiter.startsWith(DELIM_OVERLAY) ||
                                    delimiter.startsWith(DELIM_LIB) ||
                                    delimiter.equals(DELIM_MAIN)    ||
                                    delimiter.equals(DELIM_RESULT)  ||
                                    delimiter.equals(DELIM_ERRORS)  ||
                                    delimiter.equals(DELIM_FAILS)   ||
                                    delimiter.equals(DELIM_FEATURES) ||
                                    delimiter.equals(DELIM_INJECT_ATTR) ||
                                    delimiter.equals(DELIM_PACKAGE));

                    skipEmpty = true;

                    if (writer != null) {
                        writer.close();
                        writer = null;
                    }

                    if (delimiter.equals(DELIM_FAILS)) {
                        shouldFail = true;

                    } else if (!delimiter.equals(DELIM_ERRORS) &&
                            !delimiter.equals(DELIM_FEATURES) &&
                            !delimiter.equals(DELIM_INJECT_ATTR) &&
                            !delimiter.equals(DELIM_PACKAGE)) {
                        File tempFile = new File(tempDir, String.format("%1$s%2$d_%3$s.xml",
                                className,
                                tempIndex++,
                                delimiter.replaceAll("[^a-zA-Z0-9_-]", "")
                        ));
                        tempFile.deleteOnExit();

                        if (delimiter.startsWith(DELIM_OVERLAY)) {
                            overlayFiles.add(tempFile);
                        } else if (delimiter.startsWith(DELIM_LIB)) {
                            libFiles.add(tempFile);

                        } else if (delimiter.equals(DELIM_MAIN)) {
                            mainFile = tempFile;

                        } else if (delimiter.equals(DELIM_RESULT)) {
                            actualResultFile = tempFile;

                        } else {
                            fail("Unexpected data file delimiter @" + delimiter +
                                    " in " + filename);
                        }

                        if (!delimiter.equals(DELIM_RESULT)) {
                            writer = new BufferedWriter(new FileWriter(tempFile));
                        }
                    }

                    continue;
                }
                if (delimiter != null &&
                        skipEmpty &&
                        !line.isEmpty() &&
                        line.charAt(0) != '#' &&
                        line.charAt(0) != '@') {
                    skipEmpty = false;
                }
                if (writer != null) {
                    writer.write(line);
                    writer.write('\n');
                } else if (DELIM_RESULT.equals(delimiter)) {
                    expectedResult.append(line).append('\n');
                } else if (DELIM_ERRORS.equals(delimiter)) {
                    expectedErrors.append(line).append('\n');
                } else if (DELIM_INJECT_ATTR.equals(delimiter)) {
                    String[] in = line.split("=");
                    if (in != null && in.length == 2) {
                        injectAttributes.put(in[0], "null".equals(in[1]) ? null : in[1]);
                    }
                } else if (DELIM_FEATURES.equals(delimiter)) {
                    String[] in = line.split("=");
                    if (in != null && in.length == 2) {
                        features.put(in[0], Boolean.parseBoolean(in[1]));
                    }
                } else if (DELIM_PACKAGE.equals(delimiter)) {
                    if (packageOverride == null) {
                        packageOverride = line;
                    }
                }
            }

            assertNotNull("Missing @" + DELIM_MAIN + " in " + filename, mainFile);

            assert mainFile != null;

            Collections.sort(libFiles);

            return new ManifestMergerTestUtil.TestFiles(
                    shouldFail,
                    overlayFiles.toArray(new File[overlayFiles.size()]),
                    mainFile,
                    libFiles.toArray(new File[libFiles.size()]),
                    features,
                    injectAttributes,
                    packageOverride,
                    actualResultFile,
                    expectedResult.toString(),
                    expectedErrors.toString());

        } finally {
            Closeables.closeQuietly(reader);
            Closeables.closeQuietly(is);
            if (writer != null) {
                writer.close();
            }
        }
    }


    static class TestFiles {
        private final File[] mOverlayFiles;
        private final File mMain;
        private final File[] mLibs;
        private final Map<String, String> mInjectAttributes;
        private final String mPackageOverride;
        private final File mActualResult;
        private final String mExpectedResult;
        private final String mExpectedErrors;
        private final boolean mShouldFail;
        private final Map<String, Boolean> mFeatures;

        /** Files used by a given test case. */
        public TestFiles(
                boolean shouldFail,
                @NonNull File[] overlayFiles,
                @NonNull File main,
                @NonNull File[] libs,
                @NonNull Map<String, Boolean> features,
                @NonNull Map<String, String> injectAttributes,
                @Nullable String packageOverride,
                @Nullable File actualResult,
                @NonNull String expectedResult,
                @NonNull String expectedErrors) {
            mShouldFail = shouldFail;
            mMain = main;
            mLibs = libs;
            mFeatures = features;
            mPackageOverride = packageOverride;
            mInjectAttributes = injectAttributes;
            mActualResult = actualResult;
            mExpectedResult = expectedResult;
            mExpectedErrors = expectedErrors;
            mOverlayFiles = overlayFiles;
        }

        public boolean getShouldFail() {
            return mShouldFail;
        }

        @NonNull
        public File[] getOverlayFiles() {
            return mOverlayFiles;
        }

        @NonNull
        public File getMain() {
            return mMain;
        }

        @NonNull
        public File[] getLibs() {
            return mLibs;
        }

        @NonNull
        public Map<String, Boolean> getFeatures() {
            return mFeatures;
        }

        @NonNull
        public Map<String, String> getInjectAttributes() {
            return mInjectAttributes;
        }

        @Nullable
        public String getPackageOverride() {
            return mPackageOverride;
        }

        @Nullable
        public File getActualResult() {
            return mActualResult;
        }

        @NonNull
        public String getExpectedResult() {
            return mExpectedResult;
        }

        public String getExpectedErrors() {
            return mExpectedErrors;
        }

        // Try to delete any temp file potentially created.
        public void cleanup() {
            try {
                if (mMain != null && mMain.isFile()) {
                    FileUtils.delete(mMain);
                }

                if (mActualResult != null && mActualResult.isFile()) {
                    FileUtils.delete(mActualResult);
                }

                for (File f : mLibs) {
                    if (f != null && f.isFile()) {
                        FileUtils.delete(f);
                    }
                }
            } catch (IOException e) {
                throw new AssertionError(e);
            }
        }
    }

    static Collection<Object[]> transformParameters(String[] params) {

        return ImmutableList.copyOf(
                Iterables.transform(Arrays.asList(params),
                        new Function<Object, Object[]>() {
                            @Override
                            public Object[] apply(Object input) {
                                return new Object[]{input};
                            }
                        }));
    }
}
