/*
 * Copyright 2016 Red Hat, Inc. and/or its affiliates.
 *
 * 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 org.uberfire.ext.preferences.processors;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.annotation.processing.Processor;
import javax.tools.Diagnostic;
import javax.tools.Diagnostic.Kind;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;

import org.uberfire.annotations.processors.AbstractErrorAbsorbingProcessor;

import static org.junit.Assert.*;

/**
 * Base miscfeatures to generate source code with an Annotation Processor
 * <p>
 * Due to a IntelliJ bug (IDEA-126596 and IDEA-93855), this class had to be copied from
 * uberfire-workbench-processors to here.
 */
public abstract class AbstractProcessorTest {

    private static final String SOURCE_FILETYPE = ".java";

    /**
     * Compile a unit of source code with the specified annotation processor
     * @param annotationProcessor
     * @param compilationUnits
     * @return
     */
    public List<Diagnostic<? extends JavaFileObject>> compile(final Processor annotationProcessor,
                                                              final String... compilationUnits) {

        final DiagnosticCollector<JavaFileObject> diagnosticListener = new DiagnosticCollector<JavaFileObject>();

        try {

            final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
            final StandardJavaFileManager fileManager = compiler.getStandardFileManager(diagnosticListener,
                                                                                        null,
                                                                                        null);

            String[] convertedCompilationUnits = convertCompilationUnitToFilePaths(compilationUnits);
            final Iterable<? extends JavaFileObject> compilationUnitsJavaObjects =
                    fileManager.getJavaFileObjects(convertedCompilationUnits);

            //Compile with provide annotation processor
            final CompilationTask task = compiler.getTask(null,
                                                          fileManager,
                                                          diagnosticListener,
                                                          null,
                                                          null,
                                                          compilationUnitsJavaObjects);
            task.setProcessors(Arrays.asList(annotationProcessor));
            task.call();
            fileManager.close();
        } catch (IOException ioe) {
            fail(ioe.getMessage());
        }

        return diagnosticListener.getDiagnostics();
    }

    private String[] convertCompilationUnitToFilePaths(String[] compilationUnits) {
        List<String> convertedCompilationUnits = new ArrayList<String>();
        for (String compilationUnit : compilationUnits) {
            convertedCompilationUnits.add(this.getClass().getResource("/" + compilationUnit + SOURCE_FILETYPE).getPath());
        }
        return convertedCompilationUnits.toArray(new String[convertedCompilationUnits.size()]);
    }

    /**
     * Retrieve the expected source code for a compilation unit
     * @param compilationUnit
     * @return
     * @throws FileNotFoundException
     */
    public String getExpectedSourceCode(final String compilationUnit) throws FileNotFoundException {
        StringBuilder sb = new StringBuilder();
        try {
            final String path = this.getClass().getResource("/" + compilationUnit).getPath();
            final FileReader fr = new FileReader(path);
            final BufferedReader input = new BufferedReader(fr);
            try {
                String line = null;
                while ((line = input.readLine()) != null) {
                    sb.append(line);
                    sb.append(System.getProperty("line.separator"));
                }
            } finally {
                input.close();
            }
        } catch (FileNotFoundException fnfe) {
            throw fnfe;
        } catch (IOException ioe) {
            fail(ioe.getMessage());
        }
        return sb.toString();
    }

    /**
     * Assert that compilation was successful
     * @param diagnostics
     */
    public void assertSuccessfulCompilation(final List<Diagnostic<? extends JavaFileObject>> diagnostics) {
        assertFalse(diagnostics.toString(),
                    hasErrors(diagnostics));
    }

    /**
     * Assert that compilation failed
     * @param diagnostics
     */
    public void assertFailedCompilation(final List<Diagnostic<? extends JavaFileObject>> diagnostics) {
        assertTrue(hasErrors(diagnostics));
    }

    private boolean hasErrors(final List<Diagnostic<? extends JavaFileObject>> diagnostics) {
        for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics) {
            if (diagnostic.getKind().equals(Kind.ERROR)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Assert that the given error message is contained in the compilation
     * diagnostics.
     * @param diagnostics the list of diagnostic messages from the compiler. Must not be null.
     * @param kind the kind of message to search for, or null to search messages of
     * any kind.
     * @param line the line number that must be attached to the message, or
     * {@link Diagnostic#NOPOS} if line number is not important.
     * @param col the column number that must be attached to the message, or
     * {@link Diagnostic#NOPOS} if column number is not important.
     * @param message the message to search for. If any otherwise matching message in
     * the given list contains this string, the assertion passes. Must not be null.
     */
    public void assertCompilationMessage(List<Diagnostic<? extends JavaFileObject>> diagnostics,
                                         Kind kind,
                                         long line,
                                         long col,
                                         final String message) {
        StringBuilder sb = new StringBuilder(100);
        for (Diagnostic<? extends JavaFileObject> msg : diagnostics) {
            sb.append(msg.getKind())
                    .append(" ")
                    .append(msg.getLineNumber())
                    .append(":")
                    .append(msg.getColumnNumber())
                    .append(": ")
                    .append(msg.getMessage(null))
                    .append("\n");
            if ((kind == null || msg.getKind().equals(kind))
                    && (line == Diagnostic.NOPOS || msg.getLineNumber() == line)
                    && (col == Diagnostic.NOPOS || msg.getColumnNumber() == col)
                    && msg.getMessage(null).contains(message)) {
                return;
            }
        }

        fail("Compiler diagnostics did not contain " + kind + " message " + line + ":" + col + ": " + message + "\n" +
                     "Dump of all " + diagnostics.size() + " actual messages:\n" +
                     sb);
    }

    /**
     * Returns the annotation processor being tested by the current test. This processor should be
     * created with a GenerationCompleteCallback that will capture the output of the processor so it can be examined
     * by test assertions.
     */
    protected abstract AbstractErrorAbsorbingProcessor getProcessorUnderTest();

    /**
     * Container for miscfeatures results.
     */
    public class Result {

        private String expectedCode;
        private String actualCode;

        public String getExpectedCode() {
            return expectedCode;
        }

        public void setExpectedCode(final String expectedCode) {
            this.expectedCode = expectedCode;
        }

        public String getActualCode() {
            return actualCode;
        }

        public void setActualCode(final String actualCode) {
            this.actualCode = actualCode;
        }
    }
}
