/*
 * Contributions to FindBugs
 * Copyright (C) 2009, Tomás Pollak
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package de.tobject.findbugs.test;

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

import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import javax.annotation.Nonnull;

import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.Path;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.ui.IMarkerResolution;
import org.eclipse.ui.IMarkerResolutionGenerator2;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;

import de.tobject.findbugs.FindbugsPlugin;
import de.tobject.findbugs.FindbugsTestPlugin;
import de.tobject.findbugs.reporter.MarkerUtil;
import edu.umd.cs.findbugs.BugPattern;
import edu.umd.cs.findbugs.plugin.eclipse.quickfix.BugResolution;
import edu.umd.cs.findbugs.plugin.eclipse.quickfix.BugResolutionGenerator;

/**
 * Base class for FindBugs quickfix tests.
 *
 * @author Tomás Pollak
 */
public abstract class AbstractQuickfixTest extends AbstractPluginTest {

    private IMarkerResolutionGenerator2 resolutionGenerator;

    @Override
    @Before
    public void setUp() throws Exception {
        super.setUp();

        resolutionGenerator = new BugResolutionGenerator();

        // We need to enable project settings, because some tests need to modify
        // the reporting preferences
        FindbugsPlugin.setProjectSettingsEnabled(getProject(), null, true);
    }

    @Override
    @After
    public void tearDown() throws CoreException {
        resolutionGenerator = null;

        super.tearDown();
    }

    protected void doTestQuickfixResolution(String classFileName, Class<? extends IMarkerResolution> resolutionClass, String... expectedPatterns)
            throws CoreException, IOException {
        QuickFixTestPackager packager = new QuickFixTestPackager();
        packager.addBugPatterns(expectedPatterns);

        doTestQuickfixResolution(classFileName, resolutionClass, packager.asList());
    }

    protected void doTestQuickfixResolution(String classFileName, String... expectedPatterns) throws CoreException, IOException {
        doTestQuickfixResolution(classFileName, null, expectedPatterns);
    }

    protected void doTestQuickfixResolution(String classFileName, List<QuickFixTestPackage> packages) throws CoreException, IOException {
        doTestQuickfixResolution(classFileName, null, packages);
    }

    protected void doTestQuickfixResolution(String classFileName, Class<? extends IMarkerResolution> resolutionClass,
            List<QuickFixTestPackage> packages) throws CoreException, IOException {
        // Run FindBugs on the input class
        work(createFindBugsWorker(), getInputResource(classFileName));

        // Assert the expected markers are present
        IMarker[] markers = getInputFileMarkers(classFileName);
        assertEquals("Too many or too few markers", packages.size(), markers.length);

        sortMarkers(markers);

        assertPresentBugPatterns(packages, markers);
        assertPresentLabels(packages, markers);
        assertPresentLineNumbers(packages, markers);

        // Assert all markers have resolution
        assertAllMarkersHaveResolutions(markers);

        // Apply resolution to each marker
        if (resolutionClass != null) {
            applySpecificResolutionForAllMarkers(markers, resolutionClass);
        } else {
            applySingleResolutionForAllMarkers(markers);
        }

        // Assert output file
        assertEqualFiles(getExpectedOutputFile(classFileName), getInputCompilationUnit(classFileName));
        assertEquals(0, getInputFileMarkers(classFileName).length);
    }

    protected void sortMarkers(IMarker[] markers) {
        Arrays.sort(markers, new Comparator<IMarker>() {

            @Override
            public int compare(IMarker marker1, IMarker marker2) {
                String pattern1 = MarkerUtil.getBugPatternString(marker1);
                String pattern2 = MarkerUtil.getBugPatternString(marker2);
                if (pattern1 != null) {
                    if (pattern1.equals(pattern2)) {
                        return MarkerUtil.findPrimaryLineForMaker(marker1) -
                                MarkerUtil.findPrimaryLineForMaker(marker2);
                    }
                    return pattern1.compareTo(pattern2);
                }
                //else, perhaps fail because markers don't have bugPatternStrings?
                else if (pattern2 == null) {
                    return 0; //neither is a bugPattern?
                }
                return MarkerUtil.findPrimaryLineForMaker(marker1) - MarkerUtil.findPrimaryLineForMaker(marker2);
            }
        });
    }

    protected void enableBugCategory(String category) {
        getProjectPreferences().getFilterSettings().addCategory(category);
    }

    @Override
    protected TestScenario getTestScenario() {
        return TestScenario.QUICKFIX;
    }

    private void applySingleResolutionForAllMarkers(IMarker[] markers) {
        for (int i = 0; i < markers.length; i++) {
            IMarkerResolution[] resolutions = getResolutionGenerator().getResolutions(markers[i]);
            assertEquals(1, resolutions.length);
            resolutions[0].run(markers[i]);
        }
    }

    private void applySpecificResolutionForAllMarkers(IMarker[] markers, Class<? extends IMarkerResolution> resolutionClass) {
        for (int i = 0; i < markers.length; i++) {
            IMarkerResolution[] resolutions = getResolutionGenerator().getResolutions(markers[i]);
            for (int j = 0; j < resolutions.length; j++) {
                if (resolutionClass.isInstance(resolutions[j])) {
                    resolutions[j].run(markers[i]);
                    return;
                }
            }
        }
        Assert.fail("No resolution of class " + resolutionClass);
    }

    protected void assertAllMarkersHaveResolutions(IMarker[] markers) {
        for (int i = 0; i < markers.length; i++) {
            IMarker marker = markers[i];
            boolean hasResolutions = getResolutionGenerator().hasResolutions(marker);
            if (!hasResolutions) {
                String pattern = MarkerUtil.getBugPatternString(marker);
                fail("no resolution for: " + pattern);
            }
            assertTrue(hasResolutions);
        }
    }

    protected void assertEqualFiles(URL expectedFile, ICompilationUnit compilationUnit) throws IOException, JavaModelException {
        String expectedSource = readFileContents(expectedFile);
        assertEquals(expectedSource, compilationUnit.getSource());
    }

    @Deprecated
    protected void assertPresentBugPattern(@Nonnull String bugPatternType, IMarker[] markers) {
        for (int i = 0; i < markers.length; i++) {
            BugPattern pattern = MarkerUtil.findBugPatternForMarker(markers[i]);
            if (pattern != null && bugPatternType.equals(pattern.getType())) {
                return;
            }
        }
        fail("Couldn't find pattern " + bugPatternType);
    }

    protected void assertPresentBugPatterns(List<QuickFixTestPackage> packages, IMarker[] markers) {
        for (int i = 0; i < packages.size(); i++) {
            String actualBugpattern = MarkerUtil.getBugPatternString(markers[i]);
            assertEquals("Bug Pattern should match", packages.get(i).expectedPattern, actualBugpattern);
        }
    }

    protected void assertPresentLineNumbers(List<QuickFixTestPackage> packages, IMarker[] markers) {
        for (int i = 0; i < packages.size(); i++) {
            int lineNumber = MarkerUtil.findPrimaryLineForMaker(markers[i]);
            if (packages.get(i).lineNumber != QuickFixTestPackage.LINE_NUMBER_NOT_SPECIFIED) {
                assertEquals("Line number should match", packages.get(i).lineNumber, lineNumber);
            }
        }
    }

    protected void assertPresentLabels(List<QuickFixTestPackage> packages, IMarker[] markers) {
        for (int i = 0; i < packages.size(); i++) {
            if (packages.get(i).expectedLabels == null) {
                continue; //TODO migrate older tests to specify their labels
            }
            IMarker marker = markers[i];
            List<String> expectedLabels = new ArrayList<>(packages.get(i).expectedLabels);
            IMarkerResolution[] resolutions = getResolutionGenerator().getResolutions(marker);

            assertEquals("The expected number of resolutions available was wrong", expectedLabels.size(), resolutions.length);

            for (int j = 0; j < resolutions.length; j++) {
                BugResolution resolution = (BugResolution) resolutions[j];
                String label = resolution.getLabel();
                assertTrue("Should have seen label: " + label, expectedLabels.contains(label));
                expectedLabels.remove(label);
            }
        }
    }

    protected URL getExpectedOutputFile(String filename) {
        return FindbugsTestPlugin.getDefault().getBundle().getEntry(getOutputFolderName() + filename);
    }

    protected abstract String getOutputFolderName();

    protected ICompilationUnit getInputCompilationUnit(String classFileName) throws JavaModelException {
        return (ICompilationUnit) getJavaProject().findElement(new Path(classFileName));
    }

    private IMarker[] getInputFileMarkers(String classFileName) throws JavaModelException {
        return MarkerUtil.getAllMarkers(getInputResource(classFileName));
    }

    private IResource getInputResource(String classFileName) throws JavaModelException {
        return getInputCompilationUnit(classFileName).getResource();
    }

    protected IMarkerResolutionGenerator2 getResolutionGenerator() {
        return resolutionGenerator;
    }

    private String readFileContents(URL url) throws IOException {
        StringWriter writer = new StringWriter();
        InputStream input = null;
        try {
            input = url.openStream();
            int nextChar;
            while ((nextChar = input.read()) != -1) {
                writer.write(nextChar);
            }
        } finally {
            if (input != null) {
                input.close();
            }
        }
        return writer.toString();
    }

    public static class QuickFixTestPackage {

        public static final int LINE_NUMBER_NOT_SPECIFIED = -1; //TODO remove this after updating current tests
        public String expectedPattern = null;
        public List<String> expectedLabels = null;
        public int lineNumber = LINE_NUMBER_NOT_SPECIFIED;

        @Override
        public String toString() {
            return "QuickFixTestPackage [expectedPattern=" + expectedPattern + ", expectedLabels=" + expectedLabels
                    + ", lineNumber=" + lineNumber + "]";
        }
    }

    protected static class QuickFixTestPackager {

        private final List<QuickFixTestPackage> packages = new ArrayList<>();

        public QuickFixTestPackager() {
            //made public to be seen by subclasses
        }

        public void addBugPatterns(String... expectedPatterns) {
            for (int i = 0; i < expectedPatterns.length; i++) {
                String pattern = expectedPatterns[i];
                if (packages.size() <= i) {
                    packages.add(new QuickFixTestPackage());
                }
                packages.get(i).expectedPattern = pattern;
            }
        }

        /**
         *
         * @return a sorted list of QuickFixTestPackages to be used in assertions.
         */
        public List<QuickFixTestPackage> asList() {
            Collections.sort(packages, new Comparator<QuickFixTestPackage>() {

                @Override
                public int compare(QuickFixTestPackage o1, QuickFixTestPackage o2) {
                    if (o1.expectedPattern.equals(o2.expectedPattern)) {
                        return o1.lineNumber - o2.lineNumber;
                    }
                    return o1.expectedPattern.compareTo(o2.expectedPattern);
                }
            });
            return Collections.unmodifiableList(packages);
        }

        /*
         * Could be more than one at a given index, so they need to be specified individually
         */
        public void setExpectedLabels(int index, String... expectedLabels) {
            while (packages.size() <= index) {
                packages.add(new QuickFixTestPackage());
            }
            packages.get(index).expectedLabels = Arrays.asList(expectedLabels);

        }

        public void addExpectedLines(int... lineNumbers) {
            for (int i = 0; i < lineNumbers.length; i++) {
                int lineNumber = lineNumbers[i];
                if (packages.size() <= i) {
                    packages.add(new QuickFixTestPackage());
                }
                packages.get(i).lineNumber = lineNumber;
            }
        }

    }
}